sdtk-brain-kit 0.1.1 → 0.2.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 CHANGED
@@ -6,7 +6,7 @@ layer (`wiki/`) that you — and your AI agent — can search, lint, and browse
6
6
  a graph. The agent reading the vault (per its `CLAUDE.md` contract) is the
7
7
  knowledge engine; this CLI provides the deterministic rails only.
8
8
 
9
- Package version in this source snapshot: `0.1.1`
9
+ Package version in this source snapshot: `0.2.1`
10
10
  CLI command: `sdtk-brain`
11
11
 
12
12
  Standalone by design: not part of the `sdtk-kit` umbrella, installs no agent
@@ -38,6 +38,8 @@ sdtk-brain open # docs view + graph view (local viewer, no
38
38
  sdtk-brain init [--vault <path>] scaffold a vault (refuses non-empty non-vault targets)
39
39
  sdtk-brain ingest <file|dir> semantic extraction over raw sources (report-first)
40
40
  sdtk-brain compile --mode safe [--apply] compile extraction into wiki/ pages
41
+ --skip-conflicts (with --apply) apply only the non-conflicting remainder;
42
+ drifted pages stay untouched + reported
41
43
  sdtk-brain search [--json] "<q>" deterministic search over the vault wiki
42
44
  sdtk-brain lint | maintain report-first hygiene (orphans, links, stale, contradictions)
43
45
  sdtk-brain discover --plan gap-analysis plan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-brain-kit",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Standalone local-first second-brain vault CLI: immutable raw/ sources compiled into a markdown wiki/ knowledge layer, maintained by rails (ingest, compile, lint, search, graph viewer) while your agent is the knowledge engine.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -23,6 +23,7 @@ const COMPILE_FLAG_DEFS = {
23
23
  "project-path": { type: "string" },
24
24
  mode: { type: "string" },
25
25
  apply: { type: "boolean" },
26
+ "skip-conflicts": { type: "boolean" },
26
27
  };
27
28
 
28
29
  const QUERY_FLAG_DEFS = {
@@ -122,6 +123,16 @@ function cmdCompileHelp() {
122
123
  Usage:
123
124
  sdtk-brain compile --mode safe [--project-path <path>]
124
125
  sdtk-brain compile --mode safe --apply [--project-path <path>]
126
+ sdtk-brain compile --mode safe --apply --skip-conflicts [--project-path <path>]
127
+
128
+ Options:
129
+ --mode safe Required. Safe mode is the only R1 mode.
130
+ --apply Apply the JSON sidecar (otherwise preview only).
131
+ --skip-conflicts With --apply: apply the non-conflicting remainder,
132
+ leave every existing-but-different file untouched,
133
+ and record the skipped list in .brain/reports.
134
+ --project-path <path> Vault directory (--vault works as an alias).
135
+ -h, --help Show this help.
125
136
 
126
137
  Purpose:
127
138
  Use the latest semantic extraction report to write a compile preview and JSON sidecar, then optionally apply the sidecar.
@@ -133,7 +144,8 @@ Scope:
133
144
 
134
145
  Safety:
135
146
  Safe mode is the only R1 mode.
136
- --apply still uses the existing JSON sidecar contract.
147
+ --apply still uses the existing JSON sidecar contract; without
148
+ --skip-conflicts it refuses atomically when any existing file differs.
137
149
  No delete, archive, rewrite, raw/provenance mutation, source mutation, web fetch, Ask, or .brain-legacy-unused mutation.`);
138
150
  return 0;
139
151
  }
@@ -251,6 +263,9 @@ function cmdCompile(args) {
251
263
  if (flags.help) return cmdCompileHelp();
252
264
  ensureSafeMode(flags.mode, "compile");
253
265
  const projectPath = ensureProjectPath(flags["project-path"]);
266
+ if (flags["skip-conflicts"] && !flags.apply) {
267
+ throw new ValidationError("--skip-conflicts only makes sense with --apply. No project files were changed.");
268
+ }
254
269
 
255
270
  if (!flags.apply) {
256
271
  const result = runCompileDryRunFromLatestExtraction(projectPath);
@@ -270,13 +285,23 @@ function cmdCompile(args) {
270
285
  }
271
286
 
272
287
  const sidecar = ensureApplySidecar(projectPath);
273
- const result = runWikiCompileApply({ projectPath, planArg: sidecar.sidecarPath });
288
+ const result = runWikiCompileApply({
289
+ projectPath,
290
+ planArg: sidecar.sidecarPath,
291
+ skipConflicts: Boolean(flags["skip-conflicts"]),
292
+ });
274
293
  console.log(`[brain] Compile apply plan: ${result.planPath}`);
275
294
  if (sidecar.createdFreshSidecar) {
276
295
  console.log(`[brain] Refreshed compile dry-run preview: ${sidecar.dryRun.reportPath}`);
277
296
  }
278
297
  console.log(`[brain] Created files: ${result.created.length}`);
279
298
  console.log(`[brain] Unchanged files: ${result.unchanged.length}`);
299
+ if (result.skipped && result.skipped.length > 0) {
300
+ console.log(`[brain] Skipped ${result.skipped.length} conflicting file(s) (existing content differs): ${result.skipped.join(", ")}`);
301
+ if (result.skipReportPath) {
302
+ console.log(`[brain] Skip report: ${result.skipReportPath}`);
303
+ }
304
+ }
280
305
  console.log(`[brain] Scaffold created/skipped: ${result.scaffoldCreated.length}/${result.scaffoldSkipped.length}`);
281
306
  console.log(`[brain] Source-quality warnings: ${result.sourceQualityWarningCount}`);
282
307
  console.log("[brain] Canonical local wiki output is <project>/wiki; .brain remains internal state. No source files, raw sources, provenance descriptors, or atlas compatibility files were modified.");
@@ -873,6 +873,7 @@ function _render_generated_page(doc, page_id, source_hash, generated) {
873
873
  `title: ${_yaml_quote(doc.title || "")}`,
874
874
  `family: ${_yaml_quote(doc.family || "")}`,
875
875
  `role: ${_yaml_quote(doc.role || "")}`,
876
+ `trust_zone: ${_yaml_quote(doc.trust_zone || "medium")}`,
876
877
  `generated_at: ${_yaml_quote(generated)}`,
877
878
  "---",
878
879
  "",
@@ -1021,7 +1022,7 @@ function write_wiki_pages_and_provenance(docs, state, root, generated, scan_root
1021
1022
  // ---------------------------------------------------------------------------
1022
1023
  // Top-level buildAtlas
1023
1024
  // ---------------------------------------------------------------------------
1024
- async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose, workspaceRoot } = {}) {
1025
+ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose, workspaceRoot, archiveFrags } = {}) {
1025
1026
  const generated = _now_utc();
1026
1027
  const root = path.resolve(projectRoot);
1027
1028
  const atlas_dir = path.resolve(outputDir);
@@ -1037,6 +1038,18 @@ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose
1037
1038
  process.stdout.write("[atlas] Scanning markdown files...\n");
1038
1039
  const [docs, state, stats] = build_docs_incremental(root, atlas_dir, generated, roots, frags);
1039
1040
 
1041
+ // BK-325: archive tier. Applied to EVERY doc after the incremental pass —
1042
+ // cache-reused docs carry the trust_zone of the build that cached them, so
1043
+ // re-deriving here keeps tiering correct when the config changes without a
1044
+ // reparse. Same path-fragment matching semantics as `excludes`.
1045
+ const archive_frags = Array.isArray(archiveFrags) ? archiveFrags.filter(Boolean) : [];
1046
+ for (const doc of docs) {
1047
+ const abs = path.join(root, doc.path);
1048
+ doc.trust_zone = archive_frags.length > 0 && _match_exclude(abs, root, archive_frags)
1049
+ ? "archive"
1050
+ : "medium";
1051
+ }
1052
+
1040
1053
  process.stdout.write(`[atlas] Indexed ${docs.length} documents.\n`);
1041
1054
  process.stdout.write(
1042
1055
  `[atlas] Scan coverage: scanned ${stats.scanned_count !== undefined ? stats.scanned_count : docs.length}, ` +
@@ -1975,7 +1975,7 @@ function scaffoldCanonicalWikiStructure(projectPath) {
1975
1975
  return { created, skipped };
1976
1976
  }
1977
1977
 
1978
- function runWikiCompileApply({ projectPath, planArg }) {
1978
+ function runWikiCompileApply({ projectPath, planArg, skipConflicts = false }) {
1979
1979
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
1980
1980
  if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
1981
1981
  throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}`);
@@ -2012,8 +2012,8 @@ function runWikiCompileApply({ projectPath, planArg }) {
2012
2012
  }
2013
2013
  }
2014
2014
 
2015
- if (conflicts.length > 0) {
2016
- throw new ValidationError(`Compile apply would overwrite ${conflicts.length} existing different file(s): ${conflicts.join(", ")}. No project files were changed.`);
2015
+ if (conflicts.length > 0 && !skipConflicts) {
2016
+ throw new ValidationError(`Compile apply would overwrite ${conflicts.length} existing different file(s): ${conflicts.join(", ")}. No project files were changed. Pass --skip-conflicts to apply the non-conflicting remainder and leave these files untouched.`);
2017
2017
  }
2018
2018
 
2019
2019
  const created = [];
@@ -2025,10 +2025,39 @@ function runWikiCompileApply({ projectPath, planArg }) {
2025
2025
  }
2026
2026
 
2027
2027
  const sourceQualitySummary = payload.source_quality_summary || emptySourceQualitySummary();
2028
+ // BK-327: when --skip-conflicts applied a partial plan, record the
2029
+ // decision durably so the skip list is auditable later.
2030
+ let skipReportPath = null;
2031
+ if (skipConflicts) {
2032
+ const reportsPath = getWikiReportsPath(resolvedProjectPath);
2033
+ assertWikiWorkspaceWritePath(reportsPath, resolvedProjectPath);
2034
+ fs.mkdirSync(reportsPath, { recursive: true });
2035
+ skipReportPath = path.join(reportsPath, `compile-apply-result-${todayStamp()}.json`);
2036
+ assertWikiWorkspaceWritePath(skipReportPath, resolvedProjectPath);
2037
+ fs.writeFileSync(
2038
+ skipReportPath,
2039
+ JSON.stringify(
2040
+ {
2041
+ generated_at: new Date().toISOString(),
2042
+ plan_path: planPath,
2043
+ mode: "skip_conflicts",
2044
+ created,
2045
+ unchanged_count: unchanged.length,
2046
+ skipped: conflicts,
2047
+ },
2048
+ null,
2049
+ 2
2050
+ ) + "\n",
2051
+ "utf-8"
2052
+ );
2053
+ }
2054
+
2028
2055
  return {
2029
2056
  planPath,
2030
2057
  created,
2031
2058
  unchanged,
2059
+ skipped: conflicts,
2060
+ skipReportPath,
2032
2061
  scaffoldCreated: scaffold.created,
2033
2062
  scaffoldSkipped: scaffold.skipped,
2034
2063
  sourceQualityWarningCount: Number(sourceQualitySummary.warning_count || sourceQualitySummary.findings_count || 0),
@@ -418,7 +418,16 @@ function lineOf(text, needle) {
418
418
  function boundedText(value, maxLength = 700) {
419
419
  const text = String(value || "").replace(/\s+/g, " ").trim();
420
420
  if (text.length <= maxLength) return text;
421
- return `${text.slice(0, maxLength - 1).trim()}...`;
421
+ // Never split a surrogate pair (BK-331): a lone high surrogate cannot
422
+ // round-trip through fs.writeFileSync (it lands on disk as U+FFFD), so the
423
+ // regenerated plan content differed from the written page forever and the
424
+ // page flapped as a permanent apply "conflict" on unchanged input.
425
+ let cut = text.slice(0, maxLength - 1);
426
+ const last = cut.charCodeAt(cut.length - 1);
427
+ if (last >= 0xd800 && last <= 0xdbff) {
428
+ cut = cut.slice(0, -1);
429
+ }
430
+ return `${cut.trim()}...`;
422
431
  }
423
432
 
424
433
  function asArray(value) {