sdtk-brain-kit 0.1.0 → 0.2.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 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.0`
9
+ Package version in this source snapshot: `0.2.0`
10
10
  CLI command: `sdtk-brain`
11
11
 
12
12
  Standalone by design: not part of the `sdtk-kit` umbrella, installs no agent
@@ -29,7 +29,7 @@ cp ~/some-article.md raw/inbox/
29
29
  sdtk-brain ingest raw/inbox
30
30
  sdtk-brain compile --mode safe --apply
31
31
  sdtk-brain search "topic"
32
- sdtk-brain open # docs view + graph view (self-contained HTML)
32
+ sdtk-brain open # docs view + graph view (local viewer, no server)
33
33
  ```
34
34
 
35
35
  ## Commands
@@ -38,6 +38,8 @@ sdtk-brain open # docs view + graph view (self-contained HT
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
@@ -1011,10 +1011,8 @@ body.panel-resizing [data-resizable]{transition:none!important;}
1011
1011
  </nav>
1012
1012
 
1013
1013
  <script src="./mermaid.min.js"></script>
1014
+ <script src="./viewer-data.js"></script>
1014
1015
  <script>
1015
- const INDEX = __ATLAS_INDEX_JSON__;
1016
- const GRAPH = __ATLAS_GRAPH_JSON__;
1017
- const FAMILY_COLORS = __ATLAS_FAMILY_COLORS_JSON__;
1018
1016
  const FAMILY_GLYPHS = {
1019
1017
  'governance': 'G', 'product': 'P', 'spec': 'S', 'architecture': 'A', 'database': 'D',
1020
1018
  'api': 'I', 'qa': 'Q', 'design': 'L', 'dev': 'V', 'skill': 'K', 'template': 'T',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-brain-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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": {
@@ -24,6 +24,12 @@ Scope:
24
24
  Enriches EXTERNAL tool/repo candidates surfaced by the ingest/compile pipeline.
25
25
  Not related to your own project docs — those are indexed by "sdtk-brain atlas build".
26
26
 
27
+ Options:
28
+ --source <source> Required. Only "github" is supported in R1.
29
+ --mode <mode> Required. Only "review" is supported in R1.
30
+ --project-path <path> Project root. Defaults to the current directory.
31
+ -h, --help Show this help.
32
+
27
33
  Safety:
28
34
  Review mode only in R1.
29
35
  No network fetch, page rewrite, source mutation, apply, entitlement change, or .brain-legacy-unused mutation.
@@ -38,6 +38,10 @@ Scaffolds a second-brain vault:
38
38
  CLAUDE.md the agent operating contract (how the vault is maintained)
39
39
  README.md human orientation
40
40
 
41
+ Options:
42
+ --vault <path> Vault directory (defaults to the current directory).
43
+ -h, --help Show this help.
44
+
41
45
  Safety:
42
46
  Defaults to the current directory as the vault. Refuses a target that is
43
47
  not empty unless it already looks like a vault (re-runs are idempotent and
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  // `sdtk-brain open` — build the vault knowledge graph (shared atlas builder,
4
- // L-5) and open the self-contained viewer. The viewer is a single HTML file
5
- // with the graph JSON inlined, so file:// works without any server.
4
+ // L-5) and open the viewer. The graph directory is self-contained
5
+ // (viewer.html + viewer-data.js + mermaid.min.js, loaded by relative src),
6
+ // so file:// works without any server (BK-321: data is a sibling script,
7
+ // no longer inlined into a 28MB single file).
6
8
 
7
9
  const path = require("path");
8
10
  const { parseFlags } = require("../lib/args");
@@ -23,10 +25,16 @@ Usage:
23
25
  sdtk-brain open [--vault <path>] [--no-open]
24
26
 
25
27
  Builds the vault knowledge graph over wiki/ and raw/ (shared SDTK atlas
26
- builder) into .brain/graph and opens the self-contained viewer.html
27
- (docs view + graph view). --no-open builds without launching a browser.
28
+ builder) into .brain/graph and opens the viewer (docs view + graph view).
29
+ The output directory is self-contained: viewer.html loads its data from
30
+ the sibling viewer-data.js. --no-open builds without launching a browser.
28
31
 
29
- Local-only: no server is required — the viewer inlines its data.`);
32
+ Options:
33
+ --vault <path> Vault directory (defaults to the current directory).
34
+ --no-open Build without launching a browser.
35
+ -h, --help Show this help.
36
+
37
+ Local-only: no server is required — the viewer directory carries its data.`);
30
38
  return 0;
31
39
  }
32
40
 
@@ -46,7 +54,15 @@ async function cmdOpen(args) {
46
54
  return 1;
47
55
  }
48
56
 
49
- const result = await buildAtlas({ projectRoot: vault, outputDir, scanRoots, excludes });
57
+ // BK-326: point the builder's pages/provenance workspace at .brain so no
58
+ // stray .sdtk tree ever lands in the vault root.
59
+ const result = await buildAtlas({
60
+ projectRoot: vault,
61
+ outputDir,
62
+ scanRoots,
63
+ excludes,
64
+ workspaceRoot: path.join(vault, ".brain"),
65
+ });
50
66
  const viewer = path.join(outputDir, "viewer.html");
51
67
  console.log(`[brain] Graph: ${result.node_count} nodes, ${result.edge_count} edges`);
52
68
  console.log(`[brain] Viewer: ${viewer}`);
@@ -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 = {
@@ -110,7 +111,9 @@ Safety:
110
111
  No source mutation, local wiki page writes, graph rebuild, web fetch, Ask, raw/provenance mutation, or .brain-legacy-unused mutation.
111
112
 
112
113
  Next:
113
- sdtk-brain compile --mode safe`);
114
+ sdtk-brain compile --mode safe
115
+
116
+ Pass -h / --help on any verb to show its help.`);
114
117
  return 0;
115
118
  }
116
119
 
@@ -120,6 +123,16 @@ function cmdCompileHelp() {
120
123
  Usage:
121
124
  sdtk-brain compile --mode safe [--project-path <path>]
122
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.
123
136
 
124
137
  Purpose:
125
138
  Use the latest semantic extraction report to write a compile preview and JSON sidecar, then optionally apply the sidecar.
@@ -131,7 +144,8 @@ Scope:
131
144
 
132
145
  Safety:
133
146
  Safe mode is the only R1 mode.
134
- --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.
135
149
  No delete, archive, rewrite, raw/provenance mutation, source mutation, web fetch, Ask, or .brain-legacy-unused mutation.`);
136
150
  return 0;
137
151
  }
@@ -249,6 +263,9 @@ function cmdCompile(args) {
249
263
  if (flags.help) return cmdCompileHelp();
250
264
  ensureSafeMode(flags.mode, "compile");
251
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
+ }
252
269
 
253
270
  if (!flags.apply) {
254
271
  const result = runCompileDryRunFromLatestExtraction(projectPath);
@@ -268,13 +285,23 @@ function cmdCompile(args) {
268
285
  }
269
286
 
270
287
  const sidecar = ensureApplySidecar(projectPath);
271
- const result = runWikiCompileApply({ projectPath, planArg: sidecar.sidecarPath });
288
+ const result = runWikiCompileApply({
289
+ projectPath,
290
+ planArg: sidecar.sidecarPath,
291
+ skipConflicts: Boolean(flags["skip-conflicts"]),
292
+ });
272
293
  console.log(`[brain] Compile apply plan: ${result.planPath}`);
273
294
  if (sidecar.createdFreshSidecar) {
274
295
  console.log(`[brain] Refreshed compile dry-run preview: ${sidecar.dryRun.reportPath}`);
275
296
  }
276
297
  console.log(`[brain] Created files: ${result.created.length}`);
277
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
+ }
278
305
  console.log(`[brain] Scaffold created/skipped: ${result.scaffoldCreated.length}/${result.scaffoldSkipped.length}`);
279
306
  console.log(`[brain] Source-quality warnings: ${result.sourceQualityWarningCount}`);
280
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.");
@@ -30,6 +30,12 @@ Inputs:
30
30
  Duplicate coverage of the same source is de-duplicated; the atlas version wins.
31
31
  Each match reports its store (atlas | wiki).
32
32
 
33
+ Options:
34
+ --project-path <path> Vault directory (--vault works as an alias).
35
+ --json Emit the full result object as JSON.
36
+ --limit <n> Maximum matches to display (default 10, max 50).
37
+ -h, --help Show this help.
38
+
33
39
  Behavior:
34
40
  Read-only and non-premium.
35
41
  No wiki.ask entitlement is required.
@@ -35,7 +35,7 @@ function countFiles(root, ext) {
35
35
  function cmdStatus(args) {
36
36
  const { flags } = parseFlags(args || [], STATUS_FLAG_DEFS);
37
37
  if (flags.help) {
38
- console.log("Usage: sdtk-brain status [--vault <path>] — read-only vault status.");
38
+ console.log("Usage: sdtk-brain status [--vault <path>] [-h|--help] — read-only vault status.");
39
39
  return 0;
40
40
  }
41
41
  const vault = resolveProjectPath(flags.vault || process.cwd());
@@ -7,7 +7,7 @@ const crypto = require("crypto");
7
7
  // ---------------------------------------------------------------------------
8
8
  // Constants
9
9
  // ---------------------------------------------------------------------------
10
- const ATLAS_STATE_VERSION = 6;
10
+ const ATLAS_STATE_VERSION = 7;
11
11
  const WIKI_PAGE_SCHEMA_VERSION = 1;
12
12
  const WIKI_PROVENANCE_SCHEMA_VERSION = 1;
13
13
 
@@ -514,13 +514,38 @@ function load_atlas_state(atlas_dir) {
514
514
  }
515
515
  }
516
516
 
517
+ // BK-324: the cache exists to skip re-PARSING (reference extraction etc.),
518
+ // not to duplicate content — doc bodies are dropped at save time and
519
+ // rehydrated from the source file on reuse (safe: mtime/hash verified the
520
+ // file is unchanged). Cut the state file ~80% on real corpora.
521
+ function _slim_state_for_save(state) {
522
+ const documents = {};
523
+ for (const [rel, record] of Object.entries(state.documents || {})) {
524
+ if (record && typeof record === "object" && record.doc && typeof record.doc === "object") {
525
+ const doc = { ...record.doc };
526
+ delete doc.body_markdown;
527
+ documents[rel] = { ...record, doc };
528
+ } else {
529
+ documents[rel] = record;
530
+ }
531
+ }
532
+ return { ...state, documents };
533
+ }
534
+
517
535
  function save_atlas_state(state, atlas_dir) {
518
536
  fs.mkdirSync(atlas_dir, { recursive: true });
519
537
  const state_path = _atlas_state_path(atlas_dir);
520
- _write_text_lf(state_path, _escapeNonAscii(JSON.stringify(state, null, 2)));
538
+ _write_text_lf(state_path, _escapeNonAscii(JSON.stringify(_slim_state_for_save(state), null, 2)));
521
539
  return state_path;
522
540
  }
523
541
 
542
+ function _rehydrate_doc_body(doc, md_file) {
543
+ if (doc.body_markdown != null) return doc;
544
+ const text = fs.readFileSync(md_file, { encoding: "utf8" });
545
+ doc.body_markdown = _parse_frontmatter(text)[1];
546
+ return doc;
547
+ }
548
+
524
549
  function build_docs_incremental(root, atlas_dir, generated, scan_roots, exclude_frags) {
525
550
  const prior_state = load_atlas_state(atlas_dir);
526
551
  const prior_documents = prior_state.documents || {};
@@ -545,6 +570,7 @@ function build_docs_incremental(root, atlas_dir, generated, scan_roots, exclude_
545
570
 
546
571
  if (prior_record && typeof prior_record === "object" && prior_doc &&
547
572
  prior_record.mtime === current_mtime) {
573
+ _rehydrate_doc_body(prior_doc, md_file);
548
574
  next_documents[rel] = prior_record;
549
575
  reused_count++;
550
576
  continue;
@@ -553,6 +579,7 @@ function build_docs_incremental(root, atlas_dir, generated, scan_roots, exclude_
553
579
  const current_hash = _compute_file_hash(md_file);
554
580
  if (prior_record && typeof prior_record === "object" && prior_doc &&
555
581
  prior_record.hash === current_hash) {
582
+ _rehydrate_doc_body(prior_doc, md_file);
556
583
  next_documents[rel] = {
557
584
  mtime: current_mtime,
558
585
  hash: current_hash,
@@ -753,24 +780,37 @@ function build_summary(docs, graph, generated, stats, root, scan_roots, exclude_
753
780
  // ---------------------------------------------------------------------------
754
781
  // Viewer builder
755
782
  // ---------------------------------------------------------------------------
756
- function build_viewer(index, graph, generated) {
757
- if (!fs.existsSync(_VIEWER_TEMPLATE_PATH)) {
758
- throw new Error(`Viewer template not found: ${_VIEWER_TEMPLATE_PATH}`);
759
- }
783
+ // BK-321: the doc index/graph are written to a sibling viewer-data.js instead
784
+ // of being inlined into viewer.html (which grew to ~28MB on real corpora).
785
+ // The viewer already loads ./mermaid.min.js by relative src, so a sibling
786
+ // data script works identically under file:// and the atlas server. Top-level
787
+ // const bindings in a classic script are visible to later classic scripts,
788
+ // so the template's main script keeps using INDEX/GRAPH/FAMILY_COLORS as-is.
789
+ function build_viewer_data(index, graph) {
790
+ // String concatenation, not String.replace: replace's string form treats
791
+ // "$&", "$`", "$'", "$$", "$<n>" in doc content as special patterns and
792
+ // spliced garbage into the script (BK-317). Concatenation has no such
793
+ // semantics, so "$"-bearing content is inherently safe here.
760
794
  const index_json = _json_for_inline_script(index);
761
795
  const graph_json = _json_for_inline_script(graph);
762
796
  const family_colors_json = _json_for_inline_script(_FAMILY_COLORS);
797
+ return [
798
+ "// Generated by the SDTK atlas builder — regenerable via `atlas build`, do not edit.",
799
+ "const INDEX = " + index_json + ";",
800
+ "const GRAPH = " + graph_json + ";",
801
+ "const FAMILY_COLORS = " + family_colors_json + ";",
802
+ "",
803
+ ].join("\n");
804
+ }
805
+
806
+ function build_viewer(generated) {
807
+ if (!fs.existsSync(_VIEWER_TEMPLATE_PATH)) {
808
+ throw new Error(`Viewer template not found: ${_VIEWER_TEMPLATE_PATH}`);
809
+ }
763
810
  const template = fs.readFileSync(_VIEWER_TEMPLATE_PATH, "utf8");
764
- // Replacer functions, not plain strings: String.prototype.replace treats a
765
- // string replacement's "$&", "$`", "$'", "$$", "$<n>" sequences as special
766
- // patterns. Indexed doc content routinely contains "$" (shell docs, prices,
767
- // template-literal examples), which previously spliced stray template/JSON
768
- // fragments into the inline script and broke it (BK-317).
769
- return template
770
- .replace(/__ATLAS_GENERATED__/g, () => generated)
771
- .replace(/__ATLAS_INDEX_JSON__/g, () => index_json)
772
- .replace(/__ATLAS_GRAPH_JSON__/g, () => graph_json)
773
- .replace(/__ATLAS_FAMILY_COLORS_JSON__/g, () => family_colors_json);
811
+ // Replacer function per BK-317 discipline (timestamp is "$"-free today, but
812
+ // the string form's special-pattern hazard is not worth re-litigating).
813
+ return template.replace(/__ATLAS_GENERATED__/g, () => generated);
774
814
  }
775
815
 
776
816
  function copy_viewer_assets(atlas_dir) {
@@ -786,9 +826,13 @@ function copy_viewer_assets(atlas_dir) {
786
826
  // ---------------------------------------------------------------------------
787
827
  // Wiki pages + provenance
788
828
  // ---------------------------------------------------------------------------
789
- function _wiki_workspace_root(root) { return path.join(root, ".sdtk", "wiki"); }
790
- function _wiki_pages_root(root) { return path.join(_wiki_workspace_root(root), "pages"); }
791
- function _wiki_provenance_root(root) { return path.join(_wiki_workspace_root(root), "provenance"); }
829
+ // BK-326: the workspace root is overridable so hosts with a different
830
+ // machine-state home (sdtk-brain uses `.brain/`) don't get a stray
831
+ // `.sdtk/wiki` tree leaked into their vault root. sdtk-wiki's default is
832
+ // unchanged.
833
+ function _wiki_workspace_root(root, workspace_root) {
834
+ return workspace_root ? path.resolve(workspace_root) : path.join(root, ".sdtk", "wiki");
835
+ }
792
836
 
793
837
  function _stable_page_id(source_path) {
794
838
  const norm = source_path.replace(/\\/g, "/");
@@ -803,12 +847,12 @@ function _safe_slug(value) {
803
847
  return slug || "page";
804
848
  }
805
849
 
806
- function _page_relative_path(doc) {
850
+ function _page_relative_path(doc, pages_prefix) {
807
851
  const source_path = String(doc.path).replace(/\\/g, "/");
808
852
  const source_digest = crypto.createHash("sha256").update(source_path, "utf8").digest("hex").slice(0, 8);
809
853
  const slug = _safe_slug(String(doc.title || path.parse(source_path).name));
810
854
  const family = _safe_slug(String(doc.family || "other-markdown"));
811
- return `.sdtk/wiki/pages/${family}/${slug}--${source_digest}.md`;
855
+ return `${pages_prefix || ".sdtk/wiki/pages"}/${family}/${slug}--${source_digest}.md`;
812
856
  }
813
857
 
814
858
  function _yaml_quote(value) {
@@ -829,6 +873,7 @@ function _render_generated_page(doc, page_id, source_hash, generated) {
829
873
  `title: ${_yaml_quote(doc.title || "")}`,
830
874
  `family: ${_yaml_quote(doc.family || "")}`,
831
875
  `role: ${_yaml_quote(doc.role || "")}`,
876
+ `trust_zone: ${_yaml_quote(doc.trust_zone || "medium")}`,
832
877
  `generated_at: ${_yaml_quote(generated)}`,
833
878
  "---",
834
879
  "",
@@ -871,10 +916,16 @@ function _build_change_set(prior_hashes, current_hashes) {
871
916
  return { added, changed, unchanged, removed };
872
917
  }
873
918
 
874
- function write_wiki_pages_and_provenance(docs, state, root, generated, scan_roots) {
875
- const workspace_root = _wiki_workspace_root(root);
876
- const pages_root = _wiki_pages_root(root);
877
- const provenance_root = _wiki_provenance_root(root);
919
+ function write_wiki_pages_and_provenance(docs, state, root, generated, scan_roots, workspaceRoot) {
920
+ const workspace_root = _wiki_workspace_root(root, workspaceRoot);
921
+ const pages_root = path.join(workspace_root, "pages");
922
+ const provenance_root = path.join(workspace_root, "provenance");
923
+ // Page paths are recorded relative to the project root (posix separators)
924
+ // so provenance and _index.md stay portable.
925
+ const pages_prefix = path
926
+ .relative(root, pages_root)
927
+ .split(path.sep)
928
+ .join("/");
878
929
  const sources_path = path.join(provenance_root, "sources.json");
879
930
  const changes_path = path.join(provenance_root, "changes.json");
880
931
 
@@ -897,7 +948,7 @@ function write_wiki_pages_and_provenance(docs, state, root, generated, scan_root
897
948
  source_hash = crypto.createHash("sha256").update(source_path, "utf8").digest("hex");
898
949
  }
899
950
  const page_id = _stable_page_id(source_path);
900
- const page_rel = _page_relative_path(doc);
951
+ const page_rel = _page_relative_path(doc, pages_prefix);
901
952
  const page_path = path.join(root, page_rel);
902
953
 
903
954
  _assert_inside(workspace_root, page_path);
@@ -971,7 +1022,7 @@ function write_wiki_pages_and_provenance(docs, state, root, generated, scan_root
971
1022
  // ---------------------------------------------------------------------------
972
1023
  // Top-level buildAtlas
973
1024
  // ---------------------------------------------------------------------------
974
- async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose } = {}) {
1025
+ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose, workspaceRoot, archiveFrags } = {}) {
975
1026
  const generated = _now_utc();
976
1027
  const root = path.resolve(projectRoot);
977
1028
  const atlas_dir = path.resolve(outputDir);
@@ -987,6 +1038,18 @@ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose
987
1038
  process.stdout.write("[atlas] Scanning markdown files...\n");
988
1039
  const [docs, state, stats] = build_docs_incremental(root, atlas_dir, generated, roots, frags);
989
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
+
990
1053
  process.stdout.write(`[atlas] Indexed ${docs.length} documents.\n`);
991
1054
  process.stdout.write(
992
1055
  `[atlas] Scan coverage: scanned ${stats.scanned_count !== undefined ? stats.scanned_count : docs.length}, ` +
@@ -1008,7 +1071,7 @@ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose
1008
1071
  process.stdout.write(`[atlas] Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges.\n`);
1009
1072
 
1010
1073
  process.stdout.write("[atlas] Writing wiki pages and provenance...\n");
1011
- const wiki_result = write_wiki_pages_and_provenance(docs, state, root, generated, roots);
1074
+ const wiki_result = write_wiki_pages_and_provenance(docs, state, root, generated, roots, workspaceRoot);
1012
1075
  process.stdout.write(`[atlas] Wiki pages: ${wiki_result.page_count}\n`);
1013
1076
 
1014
1077
  const index_data = {
@@ -1036,7 +1099,9 @@ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose
1036
1099
  const summary_path = path.join(atlas_dir, "SDTK_DOC_ATLAS_SUMMARY.md");
1037
1100
  _write_text_lf(summary_path, summary_text);
1038
1101
 
1039
- const viewer_html = build_viewer(index_data, graph_out, generated);
1102
+ const viewer_data_js = build_viewer_data(index_data, graph_out);
1103
+ _write_text_lf(path.join(atlas_dir, "viewer-data.js"), viewer_data_js);
1104
+ const viewer_html = build_viewer(generated);
1040
1105
  const viewer_path = path.join(atlas_dir, "viewer.html");
1041
1106
  _write_text_lf(viewer_path, viewer_html);
1042
1107
 
@@ -1089,6 +1154,7 @@ module.exports = {
1089
1154
  build_graph,
1090
1155
  build_summary,
1091
1156
  build_viewer,
1157
+ build_viewer_data,
1092
1158
  write_wiki_pages_and_provenance,
1093
1159
  load_atlas_state,
1094
1160
  save_atlas_state,
@@ -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),