sdtk-brain-kit 0.1.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.
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+
5
+ const WIKI_WORKSPACE_RELATIVE = ".brain";
6
+ const WIKI_GRAPH_RELATIVE = path.join(".brain", "graph");
7
+ const WIKI_MANIFEST_RELATIVE = path.join(".brain", "manifest.json");
8
+ const WIKI_PAGES_RELATIVE = path.join(".brain", "pages");
9
+ const WIKI_RAW_RELATIVE = path.join(".brain", "raw");
10
+ const WIKI_RAW_DESCRIPTORS_RELATIVE = path.join(".brain", "raw", "descriptors");
11
+ const WIKI_RAW_SOURCES_RELATIVE = path.join(".brain", "raw", "sources.json");
12
+ const WIKI_PROVENANCE_RELATIVE = path.join(".brain", "provenance");
13
+ const WIKI_PROVENANCE_INGEST_EVENTS_RELATIVE = path.join(".brain", "provenance", "ingest-events.json");
14
+ const WIKI_PROVENANCE_SOURCES_RELATIVE = path.join(".brain", "provenance", "sources.json");
15
+ const WIKI_QUERIES_RELATIVE = path.join(".brain", "queries");
16
+ const WIKI_REPORTS_RELATIVE = path.join(".brain", "reports");
17
+ const WIKI_LOGS_RELATIVE = path.join(".brain", "logs");
18
+ const CANONICAL_WIKI_RELATIVE = "wiki";
19
+ const LEGACY_PERSONAL_BRAIN_RELATIVE = path.join(".brain", "personal-brain");
20
+ const LEGACY_ATLAS_RELATIVE = path.join(".sdtk", "atlas");
21
+
22
+ function resolveProjectPath(projectPath) {
23
+ return path.resolve(projectPath || process.cwd());
24
+ }
25
+
26
+ function normalizeComparablePath(targetPath) {
27
+ const resolved = path.resolve(targetPath);
28
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
29
+ }
30
+
31
+ function isPathInsideOrEqual(targetPath, rootPath) {
32
+ const comparableTarget = normalizeComparablePath(targetPath);
33
+ const comparableRoot = normalizeComparablePath(rootPath);
34
+ return (
35
+ comparableTarget === comparableRoot ||
36
+ comparableTarget.startsWith(comparableRoot + path.sep)
37
+ );
38
+ }
39
+
40
+ function assertPathInsideOrEqual(
41
+ targetPath,
42
+ rootPath,
43
+ message = "Refusing to access a path outside the allowed root"
44
+ ) {
45
+ const resolvedTarget = path.resolve(targetPath);
46
+ if (!isPathInsideOrEqual(resolvedTarget, rootPath)) {
47
+ throw new Error(`${message}: ${resolvedTarget}`);
48
+ }
49
+ return resolvedTarget;
50
+ }
51
+
52
+ function getWikiWorkspacePath(projectPath) {
53
+ return path.join(resolveProjectPath(projectPath), WIKI_WORKSPACE_RELATIVE);
54
+ }
55
+
56
+ function getCanonicalWikiPath(projectPath) {
57
+ return path.join(resolveProjectPath(projectPath), CANONICAL_WIKI_RELATIVE);
58
+ }
59
+
60
+ function getLegacyPersonalBrainPath(projectPath) {
61
+ return path.join(resolveProjectPath(projectPath), LEGACY_PERSONAL_BRAIN_RELATIVE);
62
+ }
63
+
64
+ function getPreferredWikiContentPath(projectPath) {
65
+ const canonical = getCanonicalWikiPath(projectPath);
66
+ const legacy = getLegacyPersonalBrainPath(projectPath);
67
+ try {
68
+ const fs = require("fs");
69
+ if (fs.existsSync(canonical) && fs.statSync(canonical).isDirectory()) {
70
+ return {
71
+ path: canonical,
72
+ mode: "canonical_project_wiki",
73
+ relative: CANONICAL_WIKI_RELATIVE,
74
+ };
75
+ }
76
+ if (fs.existsSync(legacy) && fs.statSync(legacy).isDirectory()) {
77
+ return {
78
+ path: legacy,
79
+ mode: "legacy_personal_brain_fallback",
80
+ relative: LEGACY_PERSONAL_BRAIN_RELATIVE,
81
+ };
82
+ }
83
+ } catch (_) {
84
+ // Callers perform their own existence validation and error reporting.
85
+ }
86
+ return {
87
+ path: canonical,
88
+ mode: "canonical_project_wiki",
89
+ relative: CANONICAL_WIKI_RELATIVE,
90
+ };
91
+ }
92
+
93
+ // BK-318: ordered content roots for the unified query surface.
94
+ // The atlas pages mirror (full original content, built by `atlas build`) comes
95
+ // first; the compiled canonical wiki (or its legacy personal-brain fallback)
96
+ // second. Union of both is the one logical corpus query/search/ask ground on.
97
+ function getWikiContentRoots(projectPath) {
98
+ const fs = require("fs");
99
+ const roots = [];
100
+ const atlasPages = getWikiPagesPath(projectPath);
101
+ try {
102
+ if (
103
+ fs.existsSync(atlasPages) &&
104
+ fs.statSync(atlasPages).isDirectory() &&
105
+ fs.readdirSync(atlasPages).length > 0
106
+ ) {
107
+ // An empty pages dir (scaffolded by `init --no-build`) is not a store yet.
108
+ roots.push({ path: atlasPages, store: "atlas", mode: "atlas_pages" });
109
+ }
110
+ } catch (_) {
111
+ // fall through — callers validate emptiness themselves
112
+ }
113
+ const preferred = getPreferredWikiContentPath(projectPath);
114
+ try {
115
+ if (fs.existsSync(preferred.path) && fs.statSync(preferred.path).isDirectory()) {
116
+ roots.push({ path: preferred.path, store: "wiki", mode: preferred.mode });
117
+ }
118
+ } catch (_) {
119
+ // fall through
120
+ }
121
+ return roots;
122
+ }
123
+
124
+ function getWikiGraphPath(projectPath) {
125
+ return path.join(resolveProjectPath(projectPath), WIKI_GRAPH_RELATIVE);
126
+ }
127
+
128
+ function getWikiManifestPath(projectPath) {
129
+ return path.join(resolveProjectPath(projectPath), WIKI_MANIFEST_RELATIVE);
130
+ }
131
+
132
+ function getWikiPagesPath(projectPath) {
133
+ return path.join(resolveProjectPath(projectPath), WIKI_PAGES_RELATIVE);
134
+ }
135
+
136
+ function getWikiRawPath(projectPath) {
137
+ return path.join(resolveProjectPath(projectPath), WIKI_RAW_RELATIVE);
138
+ }
139
+
140
+ function getWikiRawDescriptorsPath(projectPath) {
141
+ return path.join(resolveProjectPath(projectPath), WIKI_RAW_DESCRIPTORS_RELATIVE);
142
+ }
143
+
144
+ function getWikiRawSourcesPath(projectPath) {
145
+ return path.join(resolveProjectPath(projectPath), WIKI_RAW_SOURCES_RELATIVE);
146
+ }
147
+
148
+ function getWikiProvenancePath(projectPath) {
149
+ return path.join(resolveProjectPath(projectPath), WIKI_PROVENANCE_RELATIVE);
150
+ }
151
+
152
+ function getWikiProvenanceIngestEventsPath(projectPath) {
153
+ return path.join(resolveProjectPath(projectPath), WIKI_PROVENANCE_INGEST_EVENTS_RELATIVE);
154
+ }
155
+
156
+ function getWikiProvenanceSourcesPath(projectPath) {
157
+ return path.join(resolveProjectPath(projectPath), WIKI_PROVENANCE_SOURCES_RELATIVE);
158
+ }
159
+
160
+ function getWikiQueriesPath(projectPath) {
161
+ return path.join(resolveProjectPath(projectPath), WIKI_QUERIES_RELATIVE);
162
+ }
163
+
164
+ function getWikiReportsPath(projectPath) {
165
+ return path.join(resolveProjectPath(projectPath), WIKI_REPORTS_RELATIVE);
166
+ }
167
+
168
+ function getWikiLogsPath(projectPath) {
169
+ return path.join(resolveProjectPath(projectPath), WIKI_LOGS_RELATIVE);
170
+ }
171
+
172
+ function getLegacyAtlasPath(projectPath) {
173
+ return path.join(resolveProjectPath(projectPath), LEGACY_ATLAS_RELATIVE);
174
+ }
175
+
176
+ function assertWikiWorkspaceWritePath(targetPath, projectPath) {
177
+ return assertPathInsideOrEqual(
178
+ targetPath,
179
+ getWikiWorkspacePath(projectPath),
180
+ "Refusing to write outside project-local .brain workspace"
181
+ );
182
+ }
183
+
184
+ function assertCanonicalWikiWritePath(targetPath, projectPath) {
185
+ return assertPathInsideOrEqual(
186
+ targetPath,
187
+ getCanonicalWikiPath(projectPath),
188
+ "Refusing to write outside project-local wiki output"
189
+ );
190
+ }
191
+
192
+ function describeWikiPaths(projectPath) {
193
+ return {
194
+ projectPath: resolveProjectPath(projectPath),
195
+ canonicalWikiPath: getCanonicalWikiPath(projectPath),
196
+ legacyPersonalBrainPath: getLegacyPersonalBrainPath(projectPath),
197
+ wikiWorkspacePath: getWikiWorkspacePath(projectPath),
198
+ wikiGraphPath: getWikiGraphPath(projectPath),
199
+ wikiManifestPath: getWikiManifestPath(projectPath),
200
+ wikiPagesPath: getWikiPagesPath(projectPath),
201
+ wikiRawPath: getWikiRawPath(projectPath),
202
+ wikiRawDescriptorsPath: getWikiRawDescriptorsPath(projectPath),
203
+ wikiRawSourcesPath: getWikiRawSourcesPath(projectPath),
204
+ wikiProvenancePath: getWikiProvenancePath(projectPath),
205
+ wikiProvenanceIngestEventsPath: getWikiProvenanceIngestEventsPath(projectPath),
206
+ wikiProvenanceSourcesPath: getWikiProvenanceSourcesPath(projectPath),
207
+ wikiQueriesPath: getWikiQueriesPath(projectPath),
208
+ wikiReportsPath: getWikiReportsPath(projectPath),
209
+ wikiLogsPath: getWikiLogsPath(projectPath),
210
+ legacyAtlasPath: getLegacyAtlasPath(projectPath),
211
+ };
212
+ }
213
+
214
+ module.exports = {
215
+ CANONICAL_WIKI_RELATIVE,
216
+ LEGACY_ATLAS_RELATIVE,
217
+ LEGACY_PERSONAL_BRAIN_RELATIVE,
218
+ WIKI_GRAPH_RELATIVE,
219
+ WIKI_LOGS_RELATIVE,
220
+ WIKI_MANIFEST_RELATIVE,
221
+ WIKI_PAGES_RELATIVE,
222
+ WIKI_RAW_DESCRIPTORS_RELATIVE,
223
+ WIKI_RAW_RELATIVE,
224
+ WIKI_RAW_SOURCES_RELATIVE,
225
+ WIKI_PROVENANCE_INGEST_EVENTS_RELATIVE,
226
+ WIKI_PROVENANCE_RELATIVE,
227
+ WIKI_PROVENANCE_SOURCES_RELATIVE,
228
+ WIKI_QUERIES_RELATIVE,
229
+ WIKI_REPORTS_RELATIVE,
230
+ WIKI_WORKSPACE_RELATIVE,
231
+ assertPathInsideOrEqual,
232
+ assertCanonicalWikiWritePath,
233
+ assertWikiWorkspaceWritePath,
234
+ describeWikiPaths,
235
+ getCanonicalWikiPath,
236
+ getLegacyPersonalBrainPath,
237
+ getLegacyAtlasPath,
238
+ getPreferredWikiContentPath,
239
+ getWikiContentRoots,
240
+ getWikiGraphPath,
241
+ getWikiLogsPath,
242
+ getWikiManifestPath,
243
+ getWikiPagesPath,
244
+ getWikiProvenanceIngestEventsPath,
245
+ getWikiProvenancePath,
246
+ getWikiProvenanceSourcesPath,
247
+ getWikiQueriesPath,
248
+ getWikiRawDescriptorsPath,
249
+ getWikiRawPath,
250
+ getWikiRawSourcesPath,
251
+ getWikiReportsPath,
252
+ getWikiWorkspacePath,
253
+ isPathInsideOrEqual,
254
+ normalizeComparablePath,
255
+ resolveProjectPath,
256
+ };
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ // Shared deterministic relevance scorer (BK-318).
4
+ //
5
+ // One implementation used by BOTH surfaces that rank content against a query:
6
+ // - `sdtk-wiki search` / `query` (lib/wiki-search.js) over markdown files
7
+ // - `sdtk-wiki ask` source selection (lib/wiki-ask.js) over atlas index docs
8
+ // Extracted from wiki-search.js so the two can never drift apart.
9
+ // Exact-phrase + token-overlap scoring; no LLM, no network, fully offline.
10
+
11
+ function normalizeText(value) {
12
+ return String(value || "").toLowerCase();
13
+ }
14
+
15
+ function tokenize(query) {
16
+ return normalizeText(query)
17
+ .split(/[^a-z0-9À-ỹ_]+/i)
18
+ .map((part) => part.trim())
19
+ .filter((part) => part.length >= 2);
20
+ }
21
+
22
+ function scoreFile({ text, title, relativePath, query, tokens }) {
23
+ const lowerText = normalizeText(text);
24
+ const lowerTitle = normalizeText(title);
25
+ const lowerPath = normalizeText(relativePath);
26
+ const phrase = normalizeText(query);
27
+ const reasons = [];
28
+ let score = 0;
29
+
30
+ if (phrase && lowerText.includes(phrase)) {
31
+ score += 50;
32
+ reasons.push("exact phrase match in page content");
33
+ }
34
+ if (phrase && lowerTitle.includes(phrase)) {
35
+ score += 30;
36
+ reasons.push("exact phrase match in title");
37
+ }
38
+ if (phrase && lowerPath.includes(phrase)) {
39
+ score += 20;
40
+ reasons.push("exact phrase match in path");
41
+ }
42
+
43
+ let matchedTokens = 0;
44
+ for (const token of tokens) {
45
+ const inText = lowerText.includes(token);
46
+ const inTitle = lowerTitle.includes(token);
47
+ const inPath = lowerPath.includes(token);
48
+ if (inText || inTitle || inPath) {
49
+ matchedTokens += 1;
50
+ score += inTitle ? 12 : inPath ? 8 : 5;
51
+ }
52
+ }
53
+ if (matchedTokens > 0) {
54
+ reasons.push(`matched ${matchedTokens}/${tokens.length} query token(s)`);
55
+ }
56
+
57
+ return { score, reasons };
58
+ }
59
+
60
+ module.exports = {
61
+ normalizeText,
62
+ tokenize,
63
+ scoreFile,
64
+ };
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { ValidationError } = require("./errors");
6
+ const {
7
+ getWikiContentRoots,
8
+ isPathInsideOrEqual,
9
+ resolveProjectPath,
10
+ } = require("./wiki-paths");
11
+ const { normalizeText, tokenize, scoreFile } = require("./wiki-score");
12
+
13
+ const DEFAULT_LIMIT = 10;
14
+ const CANONICAL_WIKI_RELATIVE = "wiki";
15
+ const LEGACY_PERSONAL_BRAIN_RELATIVE = path.join(".sdtk", "wiki", "personal-brain");
16
+
17
+ function toPosix(value) {
18
+ return String(value || "").replace(/\\/g, "/");
19
+ }
20
+
21
+ function collectMarkdownFiles(rootPath) {
22
+ const files = [];
23
+ function visit(current) {
24
+ const stat = fs.statSync(current);
25
+ if (stat.isDirectory()) {
26
+ for (const child of fs.readdirSync(current).sort()) {
27
+ visit(path.join(current, child));
28
+ }
29
+ return;
30
+ }
31
+ if (stat.isFile() && /\.md(?:arkdown)?$/i.test(current)) {
32
+ files.push(current);
33
+ }
34
+ }
35
+ visit(rootPath);
36
+ return files.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
37
+ }
38
+
39
+ function extractTitle(text, filePath) {
40
+ const heading = text.match(/^#\s+(.+?)\s*$/m);
41
+ if (heading) return heading[1].trim();
42
+ return path.basename(filePath, path.extname(filePath)).replace(/[-_]+/g, " ").trim();
43
+ }
44
+
45
+ function snippetFor(text, query, tokens) {
46
+ const lower = normalizeText(text);
47
+ const phrase = normalizeText(query);
48
+ let index = phrase ? lower.indexOf(phrase) : -1;
49
+ if (index < 0) {
50
+ for (const token of tokens) {
51
+ index = lower.indexOf(token);
52
+ if (index >= 0) break;
53
+ }
54
+ }
55
+ if (index < 0) {
56
+ return text.replace(/\s+/g, " ").trim().slice(0, 180);
57
+ }
58
+ const start = Math.max(0, index - 80);
59
+ const end = Math.min(text.length, index + 180);
60
+ return text.slice(start, end).replace(/\s+/g, " ").trim();
61
+ }
62
+
63
+ // BK-318 union de-dup: identify the underlying source a page describes.
64
+ // Atlas pages carry `source_path:` (repo-relative). Compiled wiki source pages
65
+ // carry `aliases: ["<logical path>"]` (relative to their ingest root). A
66
+ // compiled page is a duplicate of an atlas page when the atlas source_path
67
+ // equals — or path-suffix-matches — one of its aliases (atlas wins).
68
+ function extractFrontmatterBlock(text) {
69
+ const match = String(text || "").match(/^---\n([\s\S]*?)\n---/);
70
+ return match ? match[1] : "";
71
+ }
72
+
73
+ function extractSourceKey(text) {
74
+ const fm = extractFrontmatterBlock(text);
75
+ const sourcePath = fm.match(/^source_path:\s*"?([^"\n]+)"?\s*$/m);
76
+ if (sourcePath) {
77
+ return { kind: "source_path", value: toPosix(sourcePath[1]).toLowerCase() };
78
+ }
79
+ const aliases = fm.match(/^aliases:\s*\[\s*"([^"\n]+)"/m);
80
+ if (aliases) {
81
+ return { kind: "alias", value: toPosix(aliases[1]).toLowerCase() };
82
+ }
83
+ return null;
84
+ }
85
+
86
+ function isDuplicateOfAtlas(aliasValue, atlasSourcePaths) {
87
+ if (atlasSourcePaths.has(aliasValue)) return true;
88
+ for (const sourcePath of atlasSourcePaths) {
89
+ if (sourcePath.endsWith(`/${aliasValue}`)) return true;
90
+ }
91
+ return false;
92
+ }
93
+
94
+ function runWikiSearch({ projectPath, query, limit = DEFAULT_LIMIT }) {
95
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
96
+ if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
97
+ throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}`);
98
+ }
99
+
100
+ const normalizedQuery = String(query || "").trim();
101
+ if (!normalizedQuery) {
102
+ throw new ValidationError('sdtk-brain search requires a query, for example: sdtk-brain search --project-path <path> "multi-agent".');
103
+ }
104
+
105
+ const parsedLimit = Number.parseInt(limit, 10);
106
+ const safeLimit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 50) : DEFAULT_LIMIT;
107
+
108
+ const contentRoots = getWikiContentRoots(resolvedProjectPath);
109
+ for (const root of contentRoots) {
110
+ if (!isPathInsideOrEqual(root.path, resolvedProjectPath)) {
111
+ throw new ValidationError("Refusing to search outside the project root.");
112
+ }
113
+ }
114
+ if (contentRoots.length === 0) {
115
+ throw new ValidationError(
116
+ `No SDTK-BRAIN local wiki found under ${resolvedProjectPath}. ` +
117
+ 'Run "sdtk-brain init" / "sdtk-brain atlas build" to index your project docs (searched at .brain/pages), ' +
118
+ 'or "sdtk-brain ingest <source-root>" and "sdtk-brain compile --mode safe --apply" for the compiled wiki. ' +
119
+ "Legacy .brain/personal-brain workspaces are still readable when present."
120
+ );
121
+ }
122
+
123
+ const tokens = tokenize(normalizedQuery);
124
+
125
+ // Pass 1 — read every candidate once, keyed by root; collect atlas source paths.
126
+ const candidates = [];
127
+ const atlasSourcePaths = new Set();
128
+ let scannedFiles = 0;
129
+ for (const root of contentRoots) {
130
+ for (const filePath of collectMarkdownFiles(root.path)) {
131
+ scannedFiles += 1;
132
+ const text = fs.readFileSync(filePath, "utf-8");
133
+ const sourceKey = extractSourceKey(text);
134
+ if (root.store === "atlas" && sourceKey && sourceKey.kind === "source_path") {
135
+ atlasSourcePaths.add(sourceKey.value);
136
+ }
137
+ candidates.push({ filePath, text, store: root.store, sourceKey });
138
+ }
139
+ }
140
+
141
+ // Pass 2 — score, suppressing compiled-wiki duplicates of atlas sources.
142
+ const matches = [];
143
+ for (const candidate of candidates) {
144
+ if (
145
+ candidate.store === "wiki" &&
146
+ candidate.sourceKey &&
147
+ isDuplicateOfAtlas(candidate.sourceKey.value, atlasSourcePaths)
148
+ ) {
149
+ continue;
150
+ }
151
+ const relativePath = toPosix(path.relative(resolvedProjectPath, candidate.filePath));
152
+ const title = extractTitle(candidate.text, candidate.filePath);
153
+ const scored = scoreFile({
154
+ text: candidate.text,
155
+ title,
156
+ relativePath,
157
+ query: normalizedQuery,
158
+ tokens,
159
+ });
160
+ if (scored.score <= 0) continue;
161
+ matches.push({
162
+ path: relativePath,
163
+ title,
164
+ store: candidate.store,
165
+ score: scored.score,
166
+ why: scored.reasons.join("; "),
167
+ snippet: snippetFor(candidate.text, normalizedQuery, tokens),
168
+ });
169
+ }
170
+
171
+ matches.sort((a, b) => {
172
+ if (b.score !== a.score) return b.score - a.score;
173
+ if (a.store !== b.store) return a.store === "atlas" ? -1 : 1;
174
+ return a.path.localeCompare(b.path);
175
+ });
176
+
177
+ const hasAtlas = contentRoots.some((root) => root.store === "atlas");
178
+ const wikiRoot = contentRoots.find((root) => root.store === "wiki");
179
+ const primaryRoot = contentRoots[0];
180
+
181
+ return {
182
+ query: normalizedQuery,
183
+ projectPath: resolvedProjectPath,
184
+ wikiContentPath: primaryRoot.path,
185
+ wikiContentMode: primaryRoot.mode,
186
+ contentRoots: contentRoots.map((root) => ({
187
+ path: root.path,
188
+ store: root.store,
189
+ mode: root.mode,
190
+ })),
191
+ personalBrainPath: primaryRoot.path,
192
+ scannedFiles,
193
+ matches: matches.slice(0, safeLimit),
194
+ totalMatches: matches.length,
195
+ limit: safeLimit,
196
+ searchMode: hasAtlas
197
+ ? (wikiRoot
198
+ ? "local_deterministic_union_atlas_and_wiki_markdown"
199
+ : "local_deterministic_atlas_pages_markdown")
200
+ : (primaryRoot.mode === "canonical_project_wiki"
201
+ ? "local_deterministic_project_wiki_markdown"
202
+ : "local_deterministic_legacy_personal_brain_markdown"),
203
+ premiumRequired: false,
204
+ mutated: false,
205
+ };
206
+ }
207
+
208
+ module.exports = {
209
+ CANONICAL_WIKI_RELATIVE,
210
+ LEGACY_PERSONAL_BRAIN_RELATIVE,
211
+ runWikiSearch,
212
+ tokenize,
213
+ };
@@ -0,0 +1,39 @@
1
+ # CLAUDE.md — Vault Operating Contract
2
+
3
+ This folder is a **second-brain vault** managed with `sdtk-brain` (rails) and an
4
+ AI agent (the knowledge engine — you). Read this contract before real work.
5
+
6
+ ## Two layers that never blur
7
+
8
+ - **`raw/`** — immutable source documents (articles, papers, repos, notes,
9
+ meeting transcripts). You read from here; you never edit meaning here.
10
+ - **`wiki/`** — the compiled knowledge layer (sources, concepts, entities,
11
+ comparisons, syntheses, decisions, queries, dashboards). You maintain this
12
+ freely, in markdown, with relative links (Obsidian-compatible).
13
+ - **`workspace/`** — scratch space; nothing here is permanent knowledge.
14
+ - **`.brain/`** — machine state written by `sdtk-brain` (reports, graph,
15
+ provenance). Never edit it by hand.
16
+
17
+ ## Core workflow
18
+
19
+ ```
20
+ add source → raw/inbox/<file>
21
+ ingest → sdtk-brain ingest raw/inbox
22
+ compile → sdtk-brain compile --mode safe --apply
23
+ ask a question → YOU read wiki/, synthesize the answer, cite pages
24
+ file the answer → wiki/queries/<page> or wiki/syntheses/<page>
25
+ lint periodically → sdtk-brain lint (fix orphans, broken links, stale claims)
26
+ view the graph → sdtk-brain open
27
+ ```
28
+
29
+ ## Rules for the agent
30
+
31
+ 1. Answers must be grounded in `wiki/` (and `raw/` when needed) with page
32
+ citations — label anything unverified as unverified.
33
+ 2. New knowledge goes into `wiki/` as pages with frontmatter; keep reciprocal
34
+ `related_pages` links so the graph stays connected.
35
+ 3. Never mutate `raw/` content; corrections live in `wiki/` with a note.
36
+ 4. External claims (stars, licenses, activity of repos) stay unverified until
37
+ an explicit enrichment step confirms them.
38
+ 5. `sdtk-brain` commands are the only writers of `.brain/`; you are the only
39
+ writer of prose in `wiki/`.
@@ -0,0 +1,18 @@
1
+ # Second-Brain Vault
2
+
3
+ A local-first, markdown-first knowledge vault: `raw/` holds immutable sources,
4
+ `wiki/` holds the compiled knowledge layer, and your AI agent (reading
5
+ `CLAUDE.md`) is the knowledge engine. `sdtk-brain` provides the deterministic
6
+ rails: ingest bookkeeping, compile, search, lint, and a docs/graph viewer.
7
+
8
+ Quick start:
9
+
10
+ ```
11
+ sdtk-brain ingest raw/inbox
12
+ sdtk-brain compile --mode safe --apply
13
+ sdtk-brain search "<topic>"
14
+ sdtk-brain open
15
+ ```
16
+
17
+ Open this folder in your agent (or Obsidian) and ask questions — everything is
18
+ local: no LLM in the CLI, no network, no telemetry.