brainclaw 1.9.0 → 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 +631 -499
- 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/harvest.js +1 -1
- package/dist/commands/hooks.js +73 -73
- package/dist/commands/init.js +1 -1
- package/dist/commands/install-hooks.js +78 -78
- package/dist/commands/mcp-read-handlers.js +57 -14
- package/dist/commands/mcp.js +200 -13
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/switch.js +125 -93
- package/dist/commands/version.js +1 -1
- package/dist/core/agent-capability.js +19 -4
- package/dist/core/agent-files.js +131 -119
- 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-prompts.js +38 -38
- package/dist/core/codev-rounds.js +4 -0
- package/dist/core/default-profiles/doctor.yaml +11 -11
- package/dist/core/default-profiles/janitor.yaml +11 -11
- package/dist/core/default-profiles/onboarder.yaml +11 -11
- package/dist/core/default-profiles/reviewer.yaml +13 -13
- package/dist/core/dispatcher.js +1 -1
- package/dist/core/entity-operations.js +29 -3
- package/dist/core/execution-adapters.js +11 -10
- package/dist/core/execution-profile.js +58 -0
- package/dist/core/execution.js +1 -1
- package/dist/core/facade-schema.js +9 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/verbs.js +0 -1
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/messaging.js +2 -2
- package/dist/core/protocol-skills.js +164 -164
- package/dist/core/runtime-signals.js +1 -1
- package/dist/core/search.js +19 -2
- package/dist/core/security-guard.js +207 -207
- package/dist/core/spawn-check.js +16 -2
- package/dist/core/staleness.js +1 -1
- package/dist/core/store-resolution.js +67 -11
- package/dist/core/worktree.js +18 -18
- 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/PROTOCOL.md +1 -1
- package/docs/adapters/openclaw.md +43 -43
- package/docs/architecture/project-refs.md +328 -328
- package/docs/cli.md +2131 -2093
- package/docs/code-map.md +198 -0
- package/docs/concepts/coordination.md +52 -52
- package/docs/concepts/coordinator-runbook.md +129 -129
- package/docs/concepts/dispatch-lifecycle.md +245 -245
- package/docs/concepts/event-log-store.md +928 -928
- package/docs/concepts/ideation-loop.md +317 -317
- package/docs/concepts/loop-engine.md +520 -511
- package/docs/concepts/mcp-governance.md +268 -268
- package/docs/concepts/memory.md +84 -84
- package/docs/concepts/multi-agent-workflows.md +167 -167
- package/docs/concepts/observer-protocol.md +361 -361
- package/docs/concepts/plans-and-claims.md +217 -217
- package/docs/concepts/project-md-convention.md +35 -35
- package/docs/concepts/runtime-notes.md +38 -38
- package/docs/concepts/troubleshooting.md +254 -254
- package/docs/concepts/workspace-bootstrapping.md +142 -142
- package/docs/context-format-changelog.md +35 -35
- package/docs/context-format.md +48 -48
- package/docs/index.md +65 -65
- package/docs/integrations/agents.md +158 -158
- package/docs/integrations/claude-code.md +23 -23
- package/docs/integrations/cline.md +77 -77
- package/docs/integrations/continue.md +55 -55
- package/docs/integrations/copilot.md +68 -68
- package/docs/integrations/cursor.md +23 -23
- package/docs/integrations/kilocode.md +72 -72
- package/docs/integrations/mcp.md +385 -378
- package/docs/integrations/mistral-vibe.md +122 -122
- package/docs/integrations/openclaw.md +92 -92
- package/docs/integrations/opencode.md +84 -84
- package/docs/integrations/overview.md +115 -115
- package/docs/integrations/roo.md +71 -71
- package/docs/integrations/windsurf.md +77 -77
- package/docs/mcp-schema-changelog.md +364 -356
- package/docs/playbooks/integration/index.md +121 -121
- package/docs/playbooks/orchestration.md +37 -0
- package/docs/playbooks/productivity/index.md +99 -99
- package/docs/playbooks/team/index.md +117 -117
- package/docs/product/agent-first-model.md +184 -184
- package/docs/product/entity-model-audit.md +462 -462
- package/docs/product/positioning.md +86 -86
- package/docs/quickstart-existing-project.md +107 -107
- package/docs/quickstart.md +183 -183
- package/docs/release-maintenance.md +79 -79
- package/docs/reputation.md +52 -52
- package/docs/review.md +45 -45
- package/docs/security.md +212 -212
- package/docs/server-operations.md +118 -118
- package/docs/storage.md +106 -106
- package/package.json +86 -66
- package/docs/concepts/event-log-store-critique-A.md +0 -333
- package/docs/concepts/event-log-store-critique-B.md +0 -353
- package/docs/concepts/event-log-store-phase0-measurements.md +0 -58
- package/docs/concepts/event-log-store-proposal-A.md +0 -365
- package/docs/concepts/event-log-store-proposal-B.md +0 -404
- package/docs/concepts/identity-model-proposal.md +0 -371
|
@@ -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
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map P1a — CORE finalizer (spec §3, §6; dec#108 #1, dec#109 P0 #4 / P1 #5/#6/#7).
|
|
3
|
+
*
|
|
4
|
+
* The finalizer is the ONE AND ONLY identity authority. Providers hand it typed,
|
|
5
|
+
* id-free {@link ExtractionDraft}s; this module reproduces TODAY'S exact legacy
|
|
6
|
+
* `extractor.ts` output: byte-identical node/edge IDs (via `ids.ts`), spans,
|
|
7
|
+
* confidence, the `exported` flag, and — critically — the LEGACY SOURCE-APPEND
|
|
8
|
+
* ORDER. Nothing here sorts node/edge content: draft items carry an `ordinal`
|
|
9
|
+
* (their traversal position) and the finalizer replays them in that order.
|
|
10
|
+
*
|
|
11
|
+
* Emission contract (mirrors the legacy `addSymbol` / `addModule` / `markOrAddExport`):
|
|
12
|
+
* - File node FIRST.
|
|
13
|
+
* - Per definition (ascending ordinal): symbol node, `contains` edge, `defines` edge.
|
|
14
|
+
* - Per import / re-export source: `module` node, `imports` edge.
|
|
15
|
+
* - Per export clause / default-id (`markOrAddExport`): if the name matches an
|
|
16
|
+
* already-emitted symbol, set that node `exported=true` + emit ONE `exports`
|
|
17
|
+
* edge to it (no new node); otherwise fabricate an `export`-subtype symbol
|
|
18
|
+
* (node + `contains` + `defines`) then emit the `exports` edge to it.
|
|
19
|
+
*
|
|
20
|
+
* The `exported` FLAG is NOT an `exports` EDGE: in-place exported declarations set
|
|
21
|
+
* only `node.exported=true`; only export clauses / default-identifier exports emit
|
|
22
|
+
* an `exports` edge (and they ALSO flip the referenced node's flag, matching legacy).
|
|
23
|
+
*
|
|
24
|
+
* Output nodes/edges are validated against the `types.ts` zod schemas before return.
|
|
25
|
+
*/
|
|
26
|
+
import { edgeId, fileNodeId, nodeId } from './ids.js';
|
|
27
|
+
import { EdgeSchema, NodeSchema } from './types.js';
|
|
28
|
+
/** Compute the legacy `sym:<hash>` node id (mirrors `extractor.ts:symId`). */
|
|
29
|
+
function symNodeId(projectId, path, lang, subtype, name, span) {
|
|
30
|
+
return `sym:${nodeId({
|
|
31
|
+
projectId,
|
|
32
|
+
path,
|
|
33
|
+
lang,
|
|
34
|
+
kind: 'symbol',
|
|
35
|
+
subtype,
|
|
36
|
+
name,
|
|
37
|
+
startLine: span.start_line,
|
|
38
|
+
startCol: span.start_col,
|
|
39
|
+
})}`;
|
|
40
|
+
}
|
|
41
|
+
/** Compute the legacy `module:<hash>` node id (mirrors `extractor.ts:addModule`). */
|
|
42
|
+
function moduleNodeId(projectId, path, lang, source, span) {
|
|
43
|
+
return `module:${nodeId({
|
|
44
|
+
projectId,
|
|
45
|
+
path,
|
|
46
|
+
lang,
|
|
47
|
+
kind: 'module',
|
|
48
|
+
subtype: null,
|
|
49
|
+
name: source,
|
|
50
|
+
startLine: span.start_line,
|
|
51
|
+
startCol: span.start_col,
|
|
52
|
+
})}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Turn a provider draft into the final {@link ExtractResult}. The ONLY identity
|
|
56
|
+
* authority — reproduces the legacy extractor output exactly (see file header).
|
|
57
|
+
*
|
|
58
|
+
* `input` supplies `projectId`/`path`/`lang` (the id inputs). `parseStatus`
|
|
59
|
+
* defaults to `'parsed'`; provider diagnostics ride on `draft.facts`.
|
|
60
|
+
*/
|
|
61
|
+
export function finalize(draft, input) {
|
|
62
|
+
const { projectId, path, lang } = input;
|
|
63
|
+
const fileNode = fileNodeId(projectId, path, lang);
|
|
64
|
+
const nodes = [
|
|
65
|
+
{
|
|
66
|
+
id: fileNode,
|
|
67
|
+
kind: 'file',
|
|
68
|
+
subtype: null,
|
|
69
|
+
lang,
|
|
70
|
+
name: path,
|
|
71
|
+
path,
|
|
72
|
+
span: null,
|
|
73
|
+
exported: false,
|
|
74
|
+
confidence: 1.0,
|
|
75
|
+
related_memory_ids: [],
|
|
76
|
+
imported_names: [],
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
const edges = [];
|
|
80
|
+
// symbol name -> node id, mirroring the legacy `ctx.byName` (used by export
|
|
81
|
+
// clauses to mark-or-add). Last writer per name wins, exactly like legacy.
|
|
82
|
+
const byName = new Map();
|
|
83
|
+
// node id -> index in `nodes`, so an export clause can flip `exported` in place.
|
|
84
|
+
const nodeIndexById = new Map();
|
|
85
|
+
const pushSymbol = (subtype, name, span, exported, confidence) => {
|
|
86
|
+
const id = symNodeId(projectId, path, lang, subtype, name, span);
|
|
87
|
+
nodeIndexById.set(id, nodes.length);
|
|
88
|
+
nodes.push({
|
|
89
|
+
id,
|
|
90
|
+
kind: 'symbol',
|
|
91
|
+
subtype,
|
|
92
|
+
lang,
|
|
93
|
+
name,
|
|
94
|
+
path,
|
|
95
|
+
span,
|
|
96
|
+
exported,
|
|
97
|
+
confidence,
|
|
98
|
+
related_memory_ids: [],
|
|
99
|
+
imported_names: [],
|
|
100
|
+
});
|
|
101
|
+
byName.set(name, id);
|
|
102
|
+
edges.push({
|
|
103
|
+
id: edgeId({ projectId, from: fileNode, to: id, kind: 'contains' }),
|
|
104
|
+
from: fileNode,
|
|
105
|
+
to: id,
|
|
106
|
+
kind: 'contains',
|
|
107
|
+
confidence: 1.0,
|
|
108
|
+
source: { path, line: span.start_line },
|
|
109
|
+
});
|
|
110
|
+
edges.push({
|
|
111
|
+
id: edgeId({ projectId, from: fileNode, to: id, kind: 'defines' }),
|
|
112
|
+
from: fileNode,
|
|
113
|
+
to: id,
|
|
114
|
+
kind: 'defines',
|
|
115
|
+
confidence: 1.0,
|
|
116
|
+
source: { path, line: span.start_line },
|
|
117
|
+
});
|
|
118
|
+
return id;
|
|
119
|
+
};
|
|
120
|
+
// Build a single ordinal-ordered stream across all draft kinds so the finalizer
|
|
121
|
+
// replays the legacy source-append order without ever sorting node/edge content.
|
|
122
|
+
const items = [
|
|
123
|
+
...draft.definitions.map((ref) => ({ kind: 'def', ordinal: ref.ordinal, ref })),
|
|
124
|
+
...draft.imports.map((ref) => ({ kind: 'import', ordinal: ref.ordinal, ref })),
|
|
125
|
+
...draft.exports.map((ref) => ({ kind: 'export', ordinal: ref.ordinal, ref })),
|
|
126
|
+
].sort((a, b) => a.ordinal - b.ordinal);
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
if (item.kind === 'def') {
|
|
129
|
+
const d = item.ref;
|
|
130
|
+
pushSymbol(d.subtype, d.name, d.span, d.exported === true, d.confidence ?? 1.0);
|
|
131
|
+
}
|
|
132
|
+
else if (item.kind === 'import') {
|
|
133
|
+
const im = item.ref;
|
|
134
|
+
const id = moduleNodeId(projectId, path, lang, im.source, im.span);
|
|
135
|
+
nodeIndexById.set(id, nodes.length);
|
|
136
|
+
nodes.push({
|
|
137
|
+
id,
|
|
138
|
+
kind: 'module',
|
|
139
|
+
subtype: null,
|
|
140
|
+
lang,
|
|
141
|
+
name: im.source,
|
|
142
|
+
path,
|
|
143
|
+
span: im.span,
|
|
144
|
+
exported: false,
|
|
145
|
+
confidence: im.confidence ?? 1.0,
|
|
146
|
+
related_memory_ids: [],
|
|
147
|
+
imported_names: [...im.importedNames],
|
|
148
|
+
});
|
|
149
|
+
edges.push({
|
|
150
|
+
id: edgeId({ projectId, from: fileNode, to: id, kind: 'imports' }),
|
|
151
|
+
from: fileNode,
|
|
152
|
+
to: id,
|
|
153
|
+
kind: 'imports',
|
|
154
|
+
confidence: 1.0,
|
|
155
|
+
source: { path, line: im.span.start_line },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// export clause / default-identifier — legacy `markOrAddExport`.
|
|
160
|
+
const ex = item.ref;
|
|
161
|
+
const existing = byName.get(ex.name);
|
|
162
|
+
let target;
|
|
163
|
+
if (existing) {
|
|
164
|
+
const idx = nodeIndexById.get(existing);
|
|
165
|
+
if (idx !== undefined)
|
|
166
|
+
nodes[idx] = { ...nodes[idx], exported: true };
|
|
167
|
+
target = existing;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
target = pushSymbol('export', ex.name, ex.span, true, ex.confidence ?? 1.0);
|
|
171
|
+
}
|
|
172
|
+
edges.push({
|
|
173
|
+
id: edgeId({ projectId, from: fileNode, to: target, kind: 'exports' }),
|
|
174
|
+
from: fileNode,
|
|
175
|
+
to: target,
|
|
176
|
+
kind: 'exports',
|
|
177
|
+
confidence: 1.0,
|
|
178
|
+
source: { path, line: ex.span.start_line },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const parseStatus = draft.attributes?.parseStatus ?? 'parsed';
|
|
183
|
+
const diagnostics = draft.facts.map((f) => ({ ...f }));
|
|
184
|
+
// Validate the finalized output against the durable schemas (spec §6).
|
|
185
|
+
for (const n of nodes)
|
|
186
|
+
NodeSchema.parse(n);
|
|
187
|
+
for (const e of edges)
|
|
188
|
+
EdgeSchema.parse(e);
|
|
189
|
+
return { parseStatus, nodes, edges, diagnostics };
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=finalizer.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-side freshness hashing + per-shard freshness classification
|
|
3
|
+
* (spec §5.1, §6.2, §12.4).
|
|
4
|
+
*
|
|
5
|
+
* The READ-path lazy freshness check (§6.1) is Sprint 3 — NOT implemented here.
|
|
6
|
+
* This module owns only:
|
|
7
|
+
* - `computeExtractorConfigHash` — sha256 of a stable serialization of
|
|
8
|
+
* extractor_config + the active language set + (P1a) the registry's
|
|
9
|
+
* `configHashInputs()` (provider versions + every query-asset hash). Changing
|
|
10
|
+
* ignore rules, size caps, supported extensions, query budget, active langs, a
|
|
11
|
+
* provider version, OR a tags/imports `.scm` => stale_extractor.
|
|
12
|
+
* NOTE: grammar/engine hashes are deliberately NOT folded in (spec §6.2):
|
|
13
|
+
* stale_grammar (changed parse binary) is kept separable from stale_extractor.
|
|
14
|
+
* - `shardFreshnessStatus` — classify a stored shard against the current
|
|
15
|
+
* extractor_config_hash + per-language grammar hashes.
|
|
16
|
+
*/
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
/** Stable serialization: sort object keys recursively so hashing is order-independent. */
|
|
19
|
+
function stableStringify(value) {
|
|
20
|
+
if (value === null || typeof value !== 'object')
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
24
|
+
const obj = value;
|
|
25
|
+
const keys = Object.keys(obj).sort();
|
|
26
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* spec §5.1 / §9 — sha256 of (extractor_config + active language set + the
|
|
30
|
+
* registry's provider/query-asset fingerprint). The active language set is the
|
|
31
|
+
* sorted list of enabled languages, so enabling/disabling a language invalidates
|
|
32
|
+
* affected shards as stale_extractor. `registryInputs` (from
|
|
33
|
+
* `registry.configHashInputs()`) folds in provider `version` + every tags/imports
|
|
34
|
+
* `.scm` hash, so editing a query asset flips affected shards to stale_extractor
|
|
35
|
+
* (dec#109 P0#3). Optional + omitted-vs-undefined hash the same so legacy callers
|
|
36
|
+
* (config-only) keep a stable hash for that input combination.
|
|
37
|
+
*/
|
|
38
|
+
export function computeExtractorConfigHash(config, activeLanguages, registryInputs) {
|
|
39
|
+
const payload = {
|
|
40
|
+
extractor_config: config,
|
|
41
|
+
active_languages: [...activeLanguages].sort(),
|
|
42
|
+
registry: registryInputs ?? null,
|
|
43
|
+
};
|
|
44
|
+
return `sha256:${crypto.createHash('sha256').update(stableStringify(payload)).digest('hex')}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* spec §12.4 — classify a stored shard:
|
|
48
|
+
* - extractor_config_hash mismatch => stale_extractor
|
|
49
|
+
* - tree_sitter_grammar_hash mismatch => stale_grammar
|
|
50
|
+
* - otherwise fresh (content/path drift is the §6.1 read-path concern, Sprint 3)
|
|
51
|
+
*
|
|
52
|
+
* Precedence: extractor first, then grammar — both are "the binary/logic that
|
|
53
|
+
* produced this shard changed", and the badge only needs to surface one reason;
|
|
54
|
+
* extractor-config drift is the cheaper, more common cause.
|
|
55
|
+
*/
|
|
56
|
+
export function shardFreshnessStatus(input) {
|
|
57
|
+
const { shard } = input;
|
|
58
|
+
if (shard.extractor_config_hash !== input.currentExtractorConfigHash) {
|
|
59
|
+
return 'stale_extractor';
|
|
60
|
+
}
|
|
61
|
+
const expectedGrammar = input.grammarHashFor(shard.lang);
|
|
62
|
+
if (expectedGrammar !== undefined &&
|
|
63
|
+
shard.tree_sitter_grammar_hash != null &&
|
|
64
|
+
shard.tree_sitter_grammar_hash !== expectedGrammar) {
|
|
65
|
+
return 'stale_grammar';
|
|
66
|
+
}
|
|
67
|
+
return 'fresh';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Roll per-shard freshness up into a manifest-level freshness summary
|
|
71
|
+
* (spec §5.1). `missing_index` when nothing parsed; otherwise the dominant
|
|
72
|
+
* stale reason, else fresh.
|
|
73
|
+
*/
|
|
74
|
+
export function summarizeFreshness(shards) {
|
|
75
|
+
if (shards.length === 0) {
|
|
76
|
+
return { status: 'missing_index', stale_file_count: 0, partial_reason: null };
|
|
77
|
+
}
|
|
78
|
+
let staleExtractor = 0;
|
|
79
|
+
let staleGrammar = 0;
|
|
80
|
+
let staleChanged = 0;
|
|
81
|
+
for (const s of shards) {
|
|
82
|
+
switch (s.freshness.status) {
|
|
83
|
+
case 'stale_extractor':
|
|
84
|
+
staleExtractor++;
|
|
85
|
+
break;
|
|
86
|
+
case 'stale_grammar':
|
|
87
|
+
staleGrammar++;
|
|
88
|
+
break;
|
|
89
|
+
case 'stale_changed_files':
|
|
90
|
+
staleChanged++;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const staleTotal = staleExtractor + staleGrammar + staleChanged;
|
|
97
|
+
if (staleTotal === 0) {
|
|
98
|
+
return { status: 'fresh', stale_file_count: 0, partial_reason: null };
|
|
99
|
+
}
|
|
100
|
+
// Surface the dominant reason for the manifest badge.
|
|
101
|
+
let status = 'stale_changed_files';
|
|
102
|
+
if (staleExtractor >= staleGrammar && staleExtractor >= staleChanged)
|
|
103
|
+
status = 'stale_extractor';
|
|
104
|
+
else if (staleGrammar >= staleChanged)
|
|
105
|
+
status = 'stale_grammar';
|
|
106
|
+
return { status, stale_file_count: staleTotal, partial_reason: null };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=freshness.js.map
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defaultImportableSymbol } from './lang/provider.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build `name -> importable symbol candidates` for one target file. `nodes` is the
|
|
4
|
+
* target shard's node list; only `kind: 'symbol'` nodes are considered, and the full
|
|
5
|
+
* symbol set is passed to the provider hook (Python needs it for top-level span
|
|
6
|
+
* containment).
|
|
7
|
+
*/
|
|
8
|
+
export function buildImportableIndex(nodes, provider) {
|
|
9
|
+
const symbols = nodes.filter((n) => n.kind === 'symbol');
|
|
10
|
+
const predicate = provider?.isImportableSymbol
|
|
11
|
+
? (n) => provider.isImportableSymbol(n, symbols)
|
|
12
|
+
: defaultImportableSymbol;
|
|
13
|
+
const byName = new Map();
|
|
14
|
+
for (const n of symbols) {
|
|
15
|
+
if (!predicate(n))
|
|
16
|
+
continue;
|
|
17
|
+
const arr = byName.get(n.name);
|
|
18
|
+
if (arr)
|
|
19
|
+
arr.push(n);
|
|
20
|
+
else
|
|
21
|
+
byName.set(n.name, [n]);
|
|
22
|
+
}
|
|
23
|
+
return byName;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve one imported name to its UNAMBIGUOUS importable symbol, or null when the
|
|
27
|
+
* name is absent OR matches more than one importable candidate (ambiguous → skip).
|
|
28
|
+
*/
|
|
29
|
+
export function lookupImportable(index, name) {
|
|
30
|
+
const cands = index.get(name);
|
|
31
|
+
if (!cands || cands.length !== 1)
|
|
32
|
+
return null;
|
|
33
|
+
return cands[0];
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=importable.js.map
|