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 +4 -2
- package/assets/atlas/doc_atlas_viewer_template.html +1 -3
- package/package.json +1 -1
- package/src/commands/enrich.js +6 -0
- package/src/commands/init.js +4 -0
- package/src/commands/open.js +22 -6
- package/src/commands/operations.js +30 -3
- package/src/commands/search.js +6 -0
- package/src/commands/status.js +1 -1
- package/src/lib/wiki-build.js +95 -29
- package/src/lib/wiki-compile.js +32 -3
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.
|
|
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 (
|
|
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.
|
|
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": {
|
package/src/commands/enrich.js
CHANGED
|
@@ -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.
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
package/src/commands/open.js
CHANGED
|
@@ -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
|
|
5
|
-
//
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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.");
|
package/src/commands/search.js
CHANGED
|
@@ -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.
|
package/src/commands/status.js
CHANGED
|
@@ -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());
|
package/src/lib/wiki-build.js
CHANGED
|
@@ -7,7 +7,7 @@ const crypto = require("crypto");
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Constants
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
const ATLAS_STATE_VERSION =
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
|
765
|
-
// string
|
|
766
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
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 =
|
|
877
|
-
const provenance_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
|
|
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,
|
package/src/lib/wiki-compile.js
CHANGED
|
@@ -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),
|