brainclaw 1.9.1 → 1.10.1
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 +78 -25
- 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/dispatch-watch.js +1 -1
- package/dist/commands/doctor.js +3 -5
- package/dist/commands/loops-handlers.js +4 -1
- package/dist/commands/mcp-read-handlers.js +8 -0
- package/dist/commands/mcp.js +121 -1
- package/dist/commands/metrics.js +0 -1
- package/dist/commands/release-claims.js +1 -1
- package/dist/commands/run-profile.js +3 -2
- package/dist/commands/sequence.js +1 -1
- package/dist/commands/switch.js +100 -89
- package/dist/commands/sync.js +1 -1
- package/dist/commands/upgrade.js +0 -7
- package/dist/core/agent-context.js +1 -1
- package/dist/core/agent-files.js +13 -2
- package/dist/core/agent-integrations.js +3 -3
- package/dist/core/agent-registry.js +2 -2
- package/dist/core/assignments.js +12 -0
- package/dist/core/brainclaw-version.js +2 -2
- package/dist/core/code-map/backend.js +176 -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 +144 -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 +599 -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 +293 -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/context.js +1 -1
- package/dist/core/cross-project.js +1 -1
- package/dist/core/dispatcher.js +0 -2
- package/dist/core/entity-operations.js +0 -3
- 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/ids.js +1 -1
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/instructions.js +0 -1
- package/dist/core/loops/lock.js +0 -3
- package/dist/core/mcp-command-resolution.js +3 -1
- package/dist/core/protocol-skills.js +5 -3
- package/dist/core/security-detectors.js +2 -2
- package/dist/core/security-extract.js +2 -2
- 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 +209 -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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map P1c — whole-project import RESOLUTION pass (cadrage v2; Codex R1).
|
|
3
|
+
*
|
|
4
|
+
* The FIRST cross-file pass in Code Map. P0..langs are strictly per-file: the
|
|
5
|
+
* finalizer turns one file's draft into nodes/edges with no knowledge of sibling
|
|
6
|
+
* files. Resolution is inherently cross-file (`./utils` in `src/a.ts` →
|
|
7
|
+
* `src/utils.ts`), so it runs as a core pass over the whole shard set AFTER
|
|
8
|
+
* extraction + compaction + freshness reclassification, but BEFORE the index /
|
|
9
|
+
* materialized / stats writes (so everything downstream sees the same edge set).
|
|
10
|
+
*
|
|
11
|
+
* For each `module` node (an import) it asks the owning provider's `resolveImport`
|
|
12
|
+
* to map the specifier → a project-internal target FILE path, then emits
|
|
13
|
+
* `module --resolves_to--> <target file node>` (A, file-level). The PROVIDER returns
|
|
14
|
+
* paths only; the CORE mints the edge + target file-node id (dec#108/#109 boundary).
|
|
15
|
+
*
|
|
16
|
+
* P1c-B (symbol-level, additive): for that SAME-RUN resolved target file, each NAMED
|
|
17
|
+
* imported symbol (`mod.imported_names`, excluding `'default'`/`'*'`) is bound to its
|
|
18
|
+
* definition via `module --imports_symbol--> <def symbol node>` — but only when the
|
|
19
|
+
* target file has exactly ONE importable symbol of that name (ambiguous/absent → no
|
|
20
|
+
* edge). "Importable" is the TARGET provider's `isImportableSymbol` (TS: exported &&
|
|
21
|
+
* not a synthetic export; Python: top-level). B's confidence INHERITS the A
|
|
22
|
+
* file-resolution confidence (never higher than the resolution it rides on).
|
|
23
|
+
*
|
|
24
|
+
* Idempotency (Codex R1 #4): edge ids are deterministic by (project, from, to,
|
|
25
|
+
* kind) only, so each shard is rewritten by KEEPING every non-pass-owned node/edge
|
|
26
|
+
* byte-identical (same order), FILTERING old pass-owned edges (BOTH `resolves_to`
|
|
27
|
+
* AND `imports_symbol` — else a renamed/deleted symbol leaves a stale B edge), then
|
|
28
|
+
* appending the freshly computed A+B edges in a single deterministic order. A shard
|
|
29
|
+
* is only written when its edge array actually changed — so a project with no
|
|
30
|
+
* resolver is a pure no-op, and re-runs are byte-identical.
|
|
31
|
+
*
|
|
32
|
+
* Recompute-all (Codex R1): the pass resolves over ALL shards every refresh (even
|
|
33
|
+
* `--changed`) and rewrites only the shards whose pass-owned set changed. That
|
|
34
|
+
* removes the moved/deleted-target stale-edge problem without a reverse-dependency
|
|
35
|
+
* scheduler — the pass already holds the full current file set.
|
|
36
|
+
*/
|
|
37
|
+
import { edgeId, fileNodeId } from './ids.js';
|
|
38
|
+
import { writeShard } from './store.js';
|
|
39
|
+
import { buildImportableIndex, lookupImportable } from './importable.js';
|
|
40
|
+
import { EdgeSchema } from './types.js';
|
|
41
|
+
/** Edge kinds this pass OWNS — stale-stripped + recomputed every run (A + B). */
|
|
42
|
+
const PASS_OWNED_EDGE_KINDS = new Set(['resolves_to', 'imports_symbol']);
|
|
43
|
+
function toPosix(p) {
|
|
44
|
+
return p.replace(/\\/g, '/');
|
|
45
|
+
}
|
|
46
|
+
function clampConfidence(c) {
|
|
47
|
+
if (typeof c !== 'number' || !Number.isFinite(c))
|
|
48
|
+
return 1.0;
|
|
49
|
+
return Math.max(0, Math.min(1, c));
|
|
50
|
+
}
|
|
51
|
+
/** Order-and-content equality for an edge array (decides whether a shard rewrite is needed). */
|
|
52
|
+
function edgesArrayEqual(a, b) {
|
|
53
|
+
if (a.length !== b.length)
|
|
54
|
+
return false;
|
|
55
|
+
for (let i = 0; i < a.length; i++) {
|
|
56
|
+
const x = a[i];
|
|
57
|
+
const y = b[i];
|
|
58
|
+
if (x.id !== y.id || x.from !== y.from || x.to !== y.to || x.kind !== y.kind || x.confidence !== y.confidence) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve every import in the project to a `resolves_to` edge (file-level v1).
|
|
66
|
+
* Mutates + persists only the shards whose `resolves_to` set changed; returns
|
|
67
|
+
* whether any shard was rewritten so the caller can re-list before index build.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveProjectImports(input) {
|
|
70
|
+
const { projectId, registry, shards } = input;
|
|
71
|
+
const persist = input.persistShard ?? writeShard;
|
|
72
|
+
// Project file-set: indexed path -> lang. Resolution targets MUST be indexed
|
|
73
|
+
// files — this map is the ONLY project knowledge providers get (via ctx).
|
|
74
|
+
const fileLang = new Map();
|
|
75
|
+
for (const s of shards)
|
|
76
|
+
fileLang.set(toPosix(s.path), s.lang);
|
|
77
|
+
const ctx = {
|
|
78
|
+
fileExists: (rel) => fileLang.has(toPosix(rel)),
|
|
79
|
+
langOfFile: (rel) => fileLang.get(toPosix(rel)),
|
|
80
|
+
};
|
|
81
|
+
// Target-file lookups for B: path -> shard, and a memoized importable index
|
|
82
|
+
// (name -> importable symbols) per target file (built lazily, reused across importers).
|
|
83
|
+
const shardByPath = new Map();
|
|
84
|
+
for (const s of shards)
|
|
85
|
+
shardByPath.set(toPosix(s.path), s);
|
|
86
|
+
const importableCache = new Map();
|
|
87
|
+
const importableFor = (targetPath, targetLang) => {
|
|
88
|
+
let idx = importableCache.get(targetPath);
|
|
89
|
+
if (!idx) {
|
|
90
|
+
const targetShard = shardByPath.get(targetPath);
|
|
91
|
+
idx = buildImportableIndex(targetShard?.nodes ?? [], registry.providerForLang(targetLang));
|
|
92
|
+
importableCache.set(targetPath, idx);
|
|
93
|
+
}
|
|
94
|
+
return idx;
|
|
95
|
+
};
|
|
96
|
+
let shardsRewritten = 0;
|
|
97
|
+
let edgesEmitted = 0;
|
|
98
|
+
let symbolEdgesEmitted = 0;
|
|
99
|
+
for (const shard of shards) {
|
|
100
|
+
const provider = registry.providerForLang(shard.lang);
|
|
101
|
+
// Non-pass-owned edges are preserved byte-identical (same order). Filtering BOTH
|
|
102
|
+
// pass-owned kinds also strips any stale A/B edge from a prior run (idempotency).
|
|
103
|
+
const kept = shard.edges.filter((e) => !PASS_OWNED_EDGE_KINDS.has(e.kind));
|
|
104
|
+
// Fresh pass-owned set (A resolves_to + B imports_symbol). Empty when no resolver.
|
|
105
|
+
const fresh = [];
|
|
106
|
+
let freshSymbolCount = 0;
|
|
107
|
+
if (provider?.resolveImport) {
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
for (const mod of shard.nodes) {
|
|
110
|
+
if (mod.kind !== 'module')
|
|
111
|
+
continue;
|
|
112
|
+
const resolutions = await provider.resolveImport({
|
|
113
|
+
source: mod.name,
|
|
114
|
+
fromPath: toPosix(shard.path),
|
|
115
|
+
importedNames: mod.imported_names ?? [],
|
|
116
|
+
span: mod.span ?? undefined,
|
|
117
|
+
}, ctx);
|
|
118
|
+
for (const r of resolutions) {
|
|
119
|
+
if (!r.resolvedPath)
|
|
120
|
+
continue;
|
|
121
|
+
const targetPath = toPosix(r.resolvedPath);
|
|
122
|
+
const targetLang = fileLang.get(targetPath);
|
|
123
|
+
if (!targetLang)
|
|
124
|
+
continue; // target not an indexed file → no edge
|
|
125
|
+
const confidence = clampConfidence(r.confidence);
|
|
126
|
+
if (confidence <= 0)
|
|
127
|
+
continue; // preserves the resolves_to confidence invariant: (0, 1]
|
|
128
|
+
const source = { path: toPosix(shard.path), line: mod.span?.start_line ?? null };
|
|
129
|
+
// A — file-level resolves_to.
|
|
130
|
+
const to = fileNodeId(projectId, targetPath, targetLang);
|
|
131
|
+
const id = edgeId({ projectId, from: mod.id, to, kind: 'resolves_to' });
|
|
132
|
+
if (!seen.has(id)) {
|
|
133
|
+
seen.add(id);
|
|
134
|
+
fresh.push({ id, from: mod.id, to, kind: 'resolves_to', confidence, source });
|
|
135
|
+
}
|
|
136
|
+
// B — symbol-level imports_symbol, off the SAME-RUN resolved target file.
|
|
137
|
+
// Skip default/namespace (no single named symbol). Bind a name only when the
|
|
138
|
+
// target has exactly ONE importable symbol with it (ambiguous/absent → skip).
|
|
139
|
+
const idx = importableFor(targetPath, targetLang);
|
|
140
|
+
for (const name of mod.imported_names ?? []) {
|
|
141
|
+
if (name === 'default' || name === '*')
|
|
142
|
+
continue;
|
|
143
|
+
const target = lookupImportable(idx, name);
|
|
144
|
+
if (!target)
|
|
145
|
+
continue;
|
|
146
|
+
const symId = edgeId({ projectId, from: mod.id, to: target.id, kind: 'imports_symbol' });
|
|
147
|
+
if (seen.has(symId))
|
|
148
|
+
continue; // dedup (duplicate names / re-imports)
|
|
149
|
+
seen.add(symId);
|
|
150
|
+
fresh.push({ id: symId, from: mod.id, to: target.id, kind: 'imports_symbol', confidence, source });
|
|
151
|
+
freshSymbolCount++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Single deterministic order over A+B so re-runs are byte-identical (from, to, kind).
|
|
157
|
+
fresh.sort((a, b) => {
|
|
158
|
+
if (a.from !== b.from)
|
|
159
|
+
return a.from.localeCompare(b.from);
|
|
160
|
+
if (a.to !== b.to)
|
|
161
|
+
return a.to.localeCompare(b.to);
|
|
162
|
+
return a.kind.localeCompare(b.kind);
|
|
163
|
+
});
|
|
164
|
+
const nextEdges = [...kept, ...fresh];
|
|
165
|
+
if (!edgesArrayEqual(shard.edges, nextEdges)) {
|
|
166
|
+
for (const e of fresh)
|
|
167
|
+
EdgeSchema.parse(e); // loud on a malformed pass-owned edge
|
|
168
|
+
shard.edges = nextEdges;
|
|
169
|
+
persist(shard, input.cwd, input.preferredDirName);
|
|
170
|
+
shardsRewritten++;
|
|
171
|
+
}
|
|
172
|
+
edgesEmitted += fresh.length;
|
|
173
|
+
symbolEdgesEmitted += freshSymbolCount;
|
|
174
|
+
}
|
|
175
|
+
return { rewroteAny: shardsRewritten > 0, shardsRewritten, edgesEmitted, symbolEdgesEmitted };
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Map durable store — init/read/write for manifest, profiler, and shards
|
|
3
|
+
* (spec §4, §5). All writes go through `writeFileAtomic` (io.ts), which is the
|
|
4
|
+
* spec's NTFS-safe atomic temp+rename with EPERM/EBUSY backoff.
|
|
5
|
+
*
|
|
6
|
+
* Queries must be answerable from `files/**` + `indexes/**` alone; the
|
|
7
|
+
* `materialized/` cache is rebuildable and never required (spec §4).
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { writeFileAtomic } from '../io.js';
|
|
12
|
+
import { CODE_MAP_SCHEMA_VERSION, FileShardSchema, ImportsIndexSchema, ManifestSchema, ProfilerSchema, ResolutionIndexSchema, SymbolsIndexSchema, } from './types.js';
|
|
13
|
+
import { codeMapDir, filesDir, importsIndexPath, indexesDir, manifestPath, profilerPath, resolutionIndexPath, shardPath, symbolsIndexPath, } from './paths.js';
|
|
14
|
+
function ensureDir(dir) {
|
|
15
|
+
if (!fs.existsSync(dir))
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
/** Read whole JSON file into memory before parsing (spec §6 — NTFS handle contention). */
|
|
19
|
+
function readJsonFile(filepath) {
|
|
20
|
+
if (!fs.existsSync(filepath))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(filepath, 'utf-8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Create the on-disk skeleton: code/, files/, indexes/ (materialized/ is lazy). */
|
|
31
|
+
export function ensureStoreDirs(cwd, preferredDirName) {
|
|
32
|
+
ensureDir(codeMapDir(cwd, preferredDirName));
|
|
33
|
+
ensureDir(filesDir(cwd, preferredDirName));
|
|
34
|
+
ensureDir(indexesDir(cwd, preferredDirName));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Initialize a fresh Code Map store. A brand-new store starts in
|
|
38
|
+
* `missing_index` freshness (nothing parsed yet — spec §5.1).
|
|
39
|
+
*/
|
|
40
|
+
export function initStore(input) {
|
|
41
|
+
ensureStoreDirs(input.cwd, input.preferredDirName);
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const manifest = ManifestSchema.parse({
|
|
44
|
+
schema_version: CODE_MAP_SCHEMA_VERSION,
|
|
45
|
+
project_id: input.projectId,
|
|
46
|
+
code_map_enabled: true,
|
|
47
|
+
project_root: input.projectRoot,
|
|
48
|
+
code_map_version: 1,
|
|
49
|
+
store_created_at: now,
|
|
50
|
+
updated_at: now,
|
|
51
|
+
active_backend: 'jsonl',
|
|
52
|
+
extractor_version: input.extractorVersion,
|
|
53
|
+
extractor_config_hash: input.extractorConfigHash,
|
|
54
|
+
engine_glue_hash: input.engineGlueHash ?? null,
|
|
55
|
+
extractor_config: input.extractorConfig,
|
|
56
|
+
languages: input.languages ?? {},
|
|
57
|
+
git: { head: null, branch: null, dirty: false, ...input.git },
|
|
58
|
+
worktree: { worktree_id: null, path: null, ...input.worktree },
|
|
59
|
+
stats: {
|
|
60
|
+
files_indexed: 0,
|
|
61
|
+
nodes: 0,
|
|
62
|
+
edges: 0,
|
|
63
|
+
last_full_refresh_ms: null,
|
|
64
|
+
last_changed_refresh_ms: null,
|
|
65
|
+
},
|
|
66
|
+
freshness: { status: 'missing_index', stale_file_count: 0, partial_reason: null },
|
|
67
|
+
});
|
|
68
|
+
writeManifest(manifest, input.cwd, input.preferredDirName);
|
|
69
|
+
return manifest;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read the manifest. Tolerates a missing store: returns `null` so callers can
|
|
73
|
+
* surface a `missing_index` freshness badge instead of throwing (spec §6.1).
|
|
74
|
+
*/
|
|
75
|
+
export function readManifest(cwd, preferredDirName) {
|
|
76
|
+
const raw = readJsonFile(manifestPath(cwd, preferredDirName));
|
|
77
|
+
if (raw === null)
|
|
78
|
+
return null;
|
|
79
|
+
const parsed = ManifestSchema.safeParse(raw);
|
|
80
|
+
return parsed.success ? parsed.data : null;
|
|
81
|
+
}
|
|
82
|
+
export function writeManifest(manifest, cwd, preferredDirName) {
|
|
83
|
+
ensureDir(codeMapDir(cwd, preferredDirName));
|
|
84
|
+
const next = { ...manifest, updated_at: new Date().toISOString() };
|
|
85
|
+
writeFileAtomic(manifestPath(cwd, preferredDirName), JSON.stringify(next, null, 2));
|
|
86
|
+
}
|
|
87
|
+
export function readProfiler(cwd, preferredDirName) {
|
|
88
|
+
const raw = readJsonFile(profilerPath(cwd, preferredDirName));
|
|
89
|
+
if (raw === null)
|
|
90
|
+
return null;
|
|
91
|
+
const parsed = ProfilerSchema.safeParse(raw);
|
|
92
|
+
return parsed.success ? parsed.data : null;
|
|
93
|
+
}
|
|
94
|
+
export function writeProfiler(profiler, cwd, preferredDirName) {
|
|
95
|
+
ensureDir(codeMapDir(cwd, preferredDirName));
|
|
96
|
+
const next = { ...profiler, updated_at: new Date().toISOString() };
|
|
97
|
+
writeFileAtomic(profilerPath(cwd, preferredDirName), JSON.stringify(next, null, 2));
|
|
98
|
+
}
|
|
99
|
+
/** Read one shard by file id. Returns null on missing/corrupt (readers tolerate). */
|
|
100
|
+
export function readShard(fileIdHash, cwd, preferredDirName) {
|
|
101
|
+
const raw = readJsonFile(shardPath(fileIdHash, cwd, preferredDirName));
|
|
102
|
+
if (raw === null)
|
|
103
|
+
return null;
|
|
104
|
+
const parsed = FileShardSchema.safeParse(raw);
|
|
105
|
+
return parsed.success ? parsed.data : null;
|
|
106
|
+
}
|
|
107
|
+
/** Write one shard. Ensures the hash-prefix dir exists, then atomic write. */
|
|
108
|
+
export function writeShard(shard, cwd, preferredDirName) {
|
|
109
|
+
const target = shardPath(shard.file_id, cwd, preferredDirName);
|
|
110
|
+
ensureDir(path.dirname(target));
|
|
111
|
+
writeFileAtomic(target, JSON.stringify(shard, null, 2));
|
|
112
|
+
}
|
|
113
|
+
export function deleteShard(fileIdHash, cwd, preferredDirName) {
|
|
114
|
+
const target = shardPath(fileIdHash, cwd, preferredDirName);
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(target))
|
|
117
|
+
fs.unlinkSync(target);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* best effort */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Enumerate every shard under files/** (used by index rebuild + queries). */
|
|
124
|
+
export function listShards(cwd, preferredDirName) {
|
|
125
|
+
const root = filesDir(cwd, preferredDirName);
|
|
126
|
+
if (!fs.existsSync(root))
|
|
127
|
+
return [];
|
|
128
|
+
const shards = [];
|
|
129
|
+
for (const prefix of fs.readdirSync(root)) {
|
|
130
|
+
const prefixDir = path.join(root, prefix);
|
|
131
|
+
let stat;
|
|
132
|
+
try {
|
|
133
|
+
stat = fs.statSync(prefixDir);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!stat.isDirectory())
|
|
139
|
+
continue;
|
|
140
|
+
for (const entry of fs.readdirSync(prefixDir)) {
|
|
141
|
+
// Readers never consume a temp file (spec §6 rule 6).
|
|
142
|
+
if (!entry.endsWith('.json') || entry.includes('.tmp'))
|
|
143
|
+
continue;
|
|
144
|
+
const raw = readJsonFile(path.join(prefixDir, entry));
|
|
145
|
+
if (raw === null)
|
|
146
|
+
continue;
|
|
147
|
+
const parsed = FileShardSchema.safeParse(raw);
|
|
148
|
+
if (parsed.success)
|
|
149
|
+
shards.push(parsed.data);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return shards;
|
|
153
|
+
}
|
|
154
|
+
export function readSymbolsIndex(cwd, preferredDirName) {
|
|
155
|
+
const raw = readJsonFile(symbolsIndexPath(cwd, preferredDirName));
|
|
156
|
+
if (raw === null)
|
|
157
|
+
return null;
|
|
158
|
+
const parsed = SymbolsIndexSchema.safeParse(raw);
|
|
159
|
+
if (!parsed.success)
|
|
160
|
+
return null;
|
|
161
|
+
// Re-home entries onto a null-proto object so a token lookup like
|
|
162
|
+
// entries['constructor'] / entries['toString'] cannot resolve to an inherited
|
|
163
|
+
// Object.prototype member (a non-iterable function) on a JSON-parsed object.
|
|
164
|
+
parsed.data.entries = Object.assign(Object.create(null), parsed.data.entries);
|
|
165
|
+
return parsed.data;
|
|
166
|
+
}
|
|
167
|
+
export function writeSymbolsIndex(index, cwd, preferredDirName) {
|
|
168
|
+
ensureDir(indexesDir(cwd, preferredDirName));
|
|
169
|
+
writeFileAtomic(symbolsIndexPath(cwd, preferredDirName), JSON.stringify(index, null, 2));
|
|
170
|
+
}
|
|
171
|
+
export function readImportsIndex(cwd, preferredDirName) {
|
|
172
|
+
const raw = readJsonFile(importsIndexPath(cwd, preferredDirName));
|
|
173
|
+
if (raw === null)
|
|
174
|
+
return null;
|
|
175
|
+
const parsed = ImportsIndexSchema.safeParse(raw);
|
|
176
|
+
if (!parsed.success)
|
|
177
|
+
return null;
|
|
178
|
+
parsed.data.entries = Object.assign(Object.create(null), parsed.data.entries);
|
|
179
|
+
return parsed.data;
|
|
180
|
+
}
|
|
181
|
+
export function writeImportsIndex(index, cwd, preferredDirName) {
|
|
182
|
+
ensureDir(indexesDir(cwd, preferredDirName));
|
|
183
|
+
writeFileAtomic(importsIndexPath(cwd, preferredDirName), JSON.stringify(index, null, 2));
|
|
184
|
+
}
|
|
185
|
+
export function readResolutionIndex(cwd, preferredDirName) {
|
|
186
|
+
const raw = readJsonFile(resolutionIndexPath(cwd, preferredDirName));
|
|
187
|
+
if (raw === null)
|
|
188
|
+
return null;
|
|
189
|
+
const parsed = ResolutionIndexSchema.safeParse(raw);
|
|
190
|
+
if (!parsed.success)
|
|
191
|
+
return null;
|
|
192
|
+
// Re-home both maps onto null-proto objects so a key like 'constructor' /
|
|
193
|
+
// 'toString' (a path or node id) can't resolve to an inherited prototype member.
|
|
194
|
+
parsed.data.dependents_by_file = Object.assign(Object.create(null), parsed.data.dependents_by_file);
|
|
195
|
+
parsed.data.dependents_by_symbol = Object.assign(Object.create(null), parsed.data.dependents_by_symbol);
|
|
196
|
+
return parsed.data;
|
|
197
|
+
}
|
|
198
|
+
export function writeResolutionIndex(index, cwd, preferredDirName) {
|
|
199
|
+
ensureDir(indexesDir(cwd, preferredDirName));
|
|
200
|
+
writeFileAtomic(resolutionIndexPath(cwd, preferredDirName), JSON.stringify(index, null, 2));
|
|
201
|
+
}
|
|
202
|
+
/** True when a Code Map store has been initialized for this project. */
|
|
203
|
+
export function storeExists(cwd, preferredDirName) {
|
|
204
|
+
return fs.existsSync(manifestPath(cwd, preferredDirName));
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brainclaw Code Map — durable store schemas (spec §5).
|
|
3
|
+
*
|
|
4
|
+
* Every persisted object carries `schema_version`. zod schemas are the runtime
|
|
5
|
+
* source of truth; the exported TS types are inferred from them so the two can
|
|
6
|
+
* never drift.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { NAMESPACED_VOCAB_RE, UniversalEdgeKinds, UniversalNodeSubtypes, } from './vocabulary.js';
|
|
10
|
+
/** Current schema version for all Code Map objects in P0. */
|
|
11
|
+
export const CODE_MAP_SCHEMA_VERSION = 1;
|
|
12
|
+
const Sha256Hash = z.string();
|
|
13
|
+
// --- Enums (spec §5) ---
|
|
14
|
+
/** spec §5.3 — per-file shard parse outcome. */
|
|
15
|
+
export const ParseStatusSchema = z.enum([
|
|
16
|
+
'parsed',
|
|
17
|
+
'skipped_too_large',
|
|
18
|
+
'skipped_unsupported',
|
|
19
|
+
'parse_error',
|
|
20
|
+
]);
|
|
21
|
+
/** spec §5.4 — node kinds. */
|
|
22
|
+
export const NodeKindSchema = z.enum(['file', 'module', 'symbol']);
|
|
23
|
+
/**
|
|
24
|
+
* spec §5 (dec#108 #4) — symbol subtypes.
|
|
25
|
+
*
|
|
26
|
+
* No longer a closed `z.enum`: a subtype is EITHER a universal value
|
|
27
|
+
* ({@link UniversalNodeSubtypes}) OR a provider-namespaced value matching
|
|
28
|
+
* `^[a-z]+\.[a-z_]+$` (e.g. `rust.trait`). P1a emits only universal values.
|
|
29
|
+
* The TS type is sourced from `vocabulary.ts` so the static + runtime surfaces
|
|
30
|
+
* cannot drift.
|
|
31
|
+
*/
|
|
32
|
+
const UNIVERSAL_NODE_SUBTYPES = new Set(UniversalNodeSubtypes);
|
|
33
|
+
export const NodeSubtypeSchema = z.custom((v) => typeof v === 'string' && (UNIVERSAL_NODE_SUBTYPES.has(v) || NAMESPACED_VOCAB_RE.test(v)), { error: 'Invalid node subtype: expected a universal subtype or a namespaced `provider.subtype` value' });
|
|
34
|
+
/**
|
|
35
|
+
* spec §5 (dec#108 #4) — edge kinds. Same constrained-string contract as
|
|
36
|
+
* {@link NodeSubtypeSchema}: a universal value ({@link UniversalEdgeKinds}) or a
|
|
37
|
+
* provider-namespaced value matching `^[a-z]+\.[a-z_]+$`.
|
|
38
|
+
*/
|
|
39
|
+
const UNIVERSAL_EDGE_KINDS = new Set(UniversalEdgeKinds);
|
|
40
|
+
export const EdgeKindSchema = z.custom((v) => typeof v === 'string' && (UNIVERSAL_EDGE_KINDS.has(v) || NAMESPACED_VOCAB_RE.test(v)), { error: 'Invalid edge kind: expected a universal edge kind or a namespaced `provider.kind` value' });
|
|
41
|
+
/** spec §5.1 — freshness status enum, shared by manifest + read responses. */
|
|
42
|
+
export const FreshnessStatusSchema = z.enum([
|
|
43
|
+
'fresh',
|
|
44
|
+
'stale_changed_files',
|
|
45
|
+
'stale_extractor',
|
|
46
|
+
'stale_grammar',
|
|
47
|
+
// Read-path only (trp_42688015): the index was built against a different git
|
|
48
|
+
// commit than the working tree's current HEAD (e.g. after `git checkout`). Kept
|
|
49
|
+
// DISTINCT from `stale_changed_files` (confirmed per-file content drift) so a
|
|
50
|
+
// HEAD move is not misreported as confirmed changes — see applyGitHeadDrift.
|
|
51
|
+
'stale_git_head',
|
|
52
|
+
'partial',
|
|
53
|
+
'missing_index',
|
|
54
|
+
]);
|
|
55
|
+
/** The langs the bundled providers emit today — the fast-path of the validator. */
|
|
56
|
+
const KNOWN_CODE_LANGS = new Set([
|
|
57
|
+
'javascript',
|
|
58
|
+
'typescript',
|
|
59
|
+
'tsx',
|
|
60
|
+
'jsx',
|
|
61
|
+
'python',
|
|
62
|
+
'php',
|
|
63
|
+
'java',
|
|
64
|
+
]);
|
|
65
|
+
/** A well-formed (lowercase) language id a future provider could register. */
|
|
66
|
+
const CODE_LANG_RE = /^[a-z][a-z0-9_]*$/;
|
|
67
|
+
export const CodeLangSchema = z.custom((v) => typeof v === 'string' && (KNOWN_CODE_LANGS.has(v) || CODE_LANG_RE.test(v)), { error: 'Invalid code lang: expected a known or well-formed lowercase language id' });
|
|
68
|
+
// --- Node / Edge / Span (spec §5.4, §5.5) ---
|
|
69
|
+
export const SpanSchema = z.object({
|
|
70
|
+
start_line: z.number().int(),
|
|
71
|
+
start_col: z.number().int(),
|
|
72
|
+
end_line: z.number().int(),
|
|
73
|
+
end_col: z.number().int(),
|
|
74
|
+
});
|
|
75
|
+
export const NodeSchema = z.object({
|
|
76
|
+
id: z.string(),
|
|
77
|
+
kind: NodeKindSchema,
|
|
78
|
+
subtype: NodeSubtypeSchema.nullable().optional(),
|
|
79
|
+
lang: CodeLangSchema,
|
|
80
|
+
name: z.string(),
|
|
81
|
+
path: z.string(),
|
|
82
|
+
span: SpanSchema.nullable().optional(),
|
|
83
|
+
exported: z.boolean().default(false),
|
|
84
|
+
confidence: z.number().default(1.0),
|
|
85
|
+
related_memory_ids: z.array(z.string()).default([]),
|
|
86
|
+
/**
|
|
87
|
+
* For `module` nodes: the named bindings pulled from this import/re-export
|
|
88
|
+
* (e.g. ["useEffect","useMemo"]). Default import → ["default"], namespace
|
|
89
|
+
* import → ["*"]. Feeds index.imports.v1 `imported[]` (spec §5.7). Empty/absent
|
|
90
|
+
* for non-module nodes.
|
|
91
|
+
*/
|
|
92
|
+
imported_names: z.array(z.string()).default([]),
|
|
93
|
+
});
|
|
94
|
+
export const EdgeSchema = z.object({
|
|
95
|
+
id: z.string(),
|
|
96
|
+
from: z.string(),
|
|
97
|
+
to: z.string(),
|
|
98
|
+
kind: EdgeKindSchema,
|
|
99
|
+
confidence: z.number().default(1.0),
|
|
100
|
+
source: z
|
|
101
|
+
.object({
|
|
102
|
+
path: z.string(),
|
|
103
|
+
line: z.number().int().nullable().optional(),
|
|
104
|
+
})
|
|
105
|
+
.nullable()
|
|
106
|
+
.optional(),
|
|
107
|
+
});
|
|
108
|
+
// --- Per-file shard (spec §5.3) ---
|
|
109
|
+
export const ShardFreshnessSchema = z.object({
|
|
110
|
+
status: FreshnessStatusSchema,
|
|
111
|
+
reason: z.string().nullable().default(null),
|
|
112
|
+
});
|
|
113
|
+
export const FileShardSchema = z.object({
|
|
114
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
115
|
+
file_id: Sha256Hash,
|
|
116
|
+
project_id: z.string(),
|
|
117
|
+
worktree_id: z.string().nullable().optional(),
|
|
118
|
+
path: z.string(),
|
|
119
|
+
lang: CodeLangSchema,
|
|
120
|
+
file_hash: Sha256Hash,
|
|
121
|
+
mtime_ms: z.number(),
|
|
122
|
+
size_bytes: z.number().int(),
|
|
123
|
+
parse_status: ParseStatusSchema,
|
|
124
|
+
git_head: z.string().nullable().optional(),
|
|
125
|
+
extractor_version: z.string(),
|
|
126
|
+
extractor_config_hash: Sha256Hash,
|
|
127
|
+
grammar_name: z.string().nullable().optional(),
|
|
128
|
+
grammar_version: z.string().nullable().optional(),
|
|
129
|
+
tree_sitter_grammar_hash: Sha256Hash.nullable().optional(),
|
|
130
|
+
freshness: ShardFreshnessSchema,
|
|
131
|
+
nodes: z.array(NodeSchema).default([]),
|
|
132
|
+
edges: z.array(EdgeSchema).default([]),
|
|
133
|
+
diagnostics: z.array(z.unknown()).default([]),
|
|
134
|
+
});
|
|
135
|
+
// --- manifest.json (spec §5.1) ---
|
|
136
|
+
export const ExtractorConfigSchema = z.object({
|
|
137
|
+
included_extensions: z.array(z.string()),
|
|
138
|
+
ignored_patterns_hash: Sha256Hash,
|
|
139
|
+
max_parse_file_bytes: z.number().int(),
|
|
140
|
+
max_query_wait_ms: z.number().int(),
|
|
141
|
+
});
|
|
142
|
+
export const LanguageEntrySchema = z.object({
|
|
143
|
+
enabled: z.boolean(),
|
|
144
|
+
grammar_name: z.string(),
|
|
145
|
+
grammar_version: z.string(),
|
|
146
|
+
tree_sitter_grammar_hash: Sha256Hash,
|
|
147
|
+
});
|
|
148
|
+
export const ManifestGitSchema = z.object({
|
|
149
|
+
head: z.string().nullable(),
|
|
150
|
+
branch: z.string().nullable(),
|
|
151
|
+
dirty: z.boolean(),
|
|
152
|
+
});
|
|
153
|
+
export const ManifestWorktreeSchema = z.object({
|
|
154
|
+
worktree_id: z.string().nullable(),
|
|
155
|
+
path: z.string().nullable(),
|
|
156
|
+
});
|
|
157
|
+
export const ManifestStatsSchema = z.object({
|
|
158
|
+
files_indexed: z.number().int().default(0),
|
|
159
|
+
nodes: z.number().int().default(0),
|
|
160
|
+
edges: z.number().int().default(0),
|
|
161
|
+
last_full_refresh_ms: z.number().nullable().default(null),
|
|
162
|
+
last_changed_refresh_ms: z.number().nullable().default(null),
|
|
163
|
+
});
|
|
164
|
+
export const ManifestFreshnessSchema = z.object({
|
|
165
|
+
status: FreshnessStatusSchema,
|
|
166
|
+
stale_file_count: z.number().int().default(0),
|
|
167
|
+
partial_reason: z.string().nullable().default(null),
|
|
168
|
+
});
|
|
169
|
+
export const ManifestSchema = z.object({
|
|
170
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
171
|
+
project_id: z.string(),
|
|
172
|
+
code_map_enabled: z.boolean().default(true),
|
|
173
|
+
project_root: z.string(),
|
|
174
|
+
code_map_version: z.number().int().default(1),
|
|
175
|
+
store_created_at: z.string(),
|
|
176
|
+
updated_at: z.string(),
|
|
177
|
+
active_backend: z.string().default('jsonl'),
|
|
178
|
+
extractor_version: z.string(),
|
|
179
|
+
extractor_config_hash: Sha256Hash,
|
|
180
|
+
engine_glue_hash: Sha256Hash.nullable().default(null),
|
|
181
|
+
extractor_config: ExtractorConfigSchema,
|
|
182
|
+
languages: z.record(z.string(), LanguageEntrySchema).default({}),
|
|
183
|
+
git: ManifestGitSchema,
|
|
184
|
+
worktree: ManifestWorktreeSchema,
|
|
185
|
+
stats: ManifestStatsSchema,
|
|
186
|
+
freshness: ManifestFreshnessSchema,
|
|
187
|
+
});
|
|
188
|
+
// --- profiler.json (spec §5.2) ---
|
|
189
|
+
export const ProfilerRootSchema = z.object({
|
|
190
|
+
path: z.string(),
|
|
191
|
+
kind: z.string(),
|
|
192
|
+
manifest_files: z.array(z.string()).default([]),
|
|
193
|
+
source_file_count: z.number().int().default(0),
|
|
194
|
+
estimated_loc: z.number().int().default(0),
|
|
195
|
+
recommended: z.boolean().default(false),
|
|
196
|
+
});
|
|
197
|
+
export const ProfilerSchema = z.object({
|
|
198
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
199
|
+
project_id: z.string(),
|
|
200
|
+
last_profiled_at: z.string(),
|
|
201
|
+
detection_method: z.string().default('manifest-and-source-scan'),
|
|
202
|
+
updated_at: z.string(),
|
|
203
|
+
roots: z.array(ProfilerRootSchema).default([]),
|
|
204
|
+
ignored: z.object({
|
|
205
|
+
patterns: z.array(z.string()).default([]),
|
|
206
|
+
source: z.array(z.string()).default([]),
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
// --- indexes (spec §5.6, §5.7) ---
|
|
210
|
+
export const SymbolIndexEntrySchema = z.object({
|
|
211
|
+
node_id: z.string(),
|
|
212
|
+
name: z.string(),
|
|
213
|
+
kind: NodeKindSchema,
|
|
214
|
+
subtype: NodeSubtypeSchema.nullable().optional(),
|
|
215
|
+
path: z.string(),
|
|
216
|
+
file_id: Sha256Hash,
|
|
217
|
+
score_hint: z.number().default(1.0),
|
|
218
|
+
});
|
|
219
|
+
export const SymbolsIndexSchema = z.object({
|
|
220
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
221
|
+
project_id: z.string(),
|
|
222
|
+
updated_at: z.string(),
|
|
223
|
+
extractor_version: z.string(),
|
|
224
|
+
/** Keys are normalized lowercase tokens. */
|
|
225
|
+
entries: z.record(z.string(), z.array(SymbolIndexEntrySchema)).default({}),
|
|
226
|
+
});
|
|
227
|
+
export const ImportIndexEntrySchema = z.object({
|
|
228
|
+
path: z.string(),
|
|
229
|
+
file_id: Sha256Hash,
|
|
230
|
+
imported: z.array(z.string()).default([]),
|
|
231
|
+
});
|
|
232
|
+
export const ImportsIndexSchema = z.object({
|
|
233
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
234
|
+
project_id: z.string(),
|
|
235
|
+
updated_at: z.string(),
|
|
236
|
+
/** Keys are module specifiers (e.g. "react"). */
|
|
237
|
+
entries: z.record(z.string(), z.array(ImportIndexEntrySchema)).default({}),
|
|
238
|
+
});
|
|
239
|
+
// --- resolution index (P1d) — reverse dependency maps over the P1c graph ---
|
|
240
|
+
/**
|
|
241
|
+
* One DEPENDENT of a target (file or symbol): the importing file + enough metadata
|
|
242
|
+
* to lazy-validate it (file_id) and explain WHY it appears (module specifier the
|
|
243
|
+
* import was written as, source-side imported names, edge confidence).
|
|
244
|
+
*/
|
|
245
|
+
export const DependencyIndexEntrySchema = z.object({
|
|
246
|
+
/** Importer file path (POSIX, store identity). */
|
|
247
|
+
path: z.string(),
|
|
248
|
+
/** Importer shard file id (for read-path freshness validation). */
|
|
249
|
+
file_id: Sha256Hash,
|
|
250
|
+
/** Module specifier the importer wrote (e.g. `./b`, `.core`), for reason text. */
|
|
251
|
+
module: z.string().optional(),
|
|
252
|
+
/** Source-side imported names carried on the importing module node. */
|
|
253
|
+
imported: z.array(z.string()).default([]),
|
|
254
|
+
/** Resolution edge confidence (inherited from the A file resolution). */
|
|
255
|
+
confidence: z.number().optional(),
|
|
256
|
+
});
|
|
257
|
+
/**
|
|
258
|
+
* Reverse dependency index (P1d): "who imports this target". Built at refresh from
|
|
259
|
+
* the in-memory shards' `resolves_to` / `imports_symbol` edges (post-pass), so
|
|
260
|
+
* `bclaw_code_brief` can surface a target's dependents (blast radius) without a
|
|
261
|
+
* read-path scan of every shard. Forward deps are read straight from a target's own
|
|
262
|
+
* shard, so they need no index.
|
|
263
|
+
*/
|
|
264
|
+
export const ResolutionIndexSchema = z.object({
|
|
265
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
266
|
+
project_id: z.string(),
|
|
267
|
+
updated_at: z.string(),
|
|
268
|
+
/** Keys are TARGET file paths (reverse `resolves_to`). */
|
|
269
|
+
dependents_by_file: z.record(z.string(), z.array(DependencyIndexEntrySchema)).default({}),
|
|
270
|
+
/** Keys are TARGET symbol node ids (reverse `imports_symbol`). */
|
|
271
|
+
dependents_by_symbol: z.record(z.string(), z.array(DependencyIndexEntrySchema)).default({}),
|
|
272
|
+
});
|
|
273
|
+
// --- .lock (spec §5.8) ---
|
|
274
|
+
export const CodeLockSchema = z.object({
|
|
275
|
+
schema_version: z.number().int().default(CODE_MAP_SCHEMA_VERSION),
|
|
276
|
+
lock_id: z.string(),
|
|
277
|
+
project_id: z.string().nullable().optional(),
|
|
278
|
+
worktree_id: z.string().nullable().optional(),
|
|
279
|
+
owner_agent: z.string().nullable().optional(),
|
|
280
|
+
owner_agent_id: z.string().nullable().optional(),
|
|
281
|
+
pid: z.number().int(),
|
|
282
|
+
operation: z.string(),
|
|
283
|
+
scope: z.string(),
|
|
284
|
+
created_at: z.string(),
|
|
285
|
+
heartbeat_at: z.string(),
|
|
286
|
+
stale_after_ms: z.number().int(),
|
|
287
|
+
});
|
|
288
|
+
/** Freshness badge attached to every agent-facing read response (spec §9). */
|
|
289
|
+
export const FreshnessBadgeSchema = z.object({
|
|
290
|
+
status: FreshnessStatusSchema,
|
|
291
|
+
details: z.record(z.string(), z.unknown()).default({}),
|
|
292
|
+
});
|
|
293
|
+
//# sourceMappingURL=types.js.map
|