sdtk-brain-kit 0.1.0 → 0.1.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 +2 -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 +3 -1
- package/src/commands/search.js +6 -0
- package/src/commands/status.js +1 -1
- package/src/lib/wiki-build.js +82 -29
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.
|
|
9
|
+
Package version in this source snapshot: `0.1.1`
|
|
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
|
|
@@ -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.
|
|
3
|
+
"version": "0.1.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": {
|
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}`);
|
|
@@ -110,7 +110,9 @@ Safety:
|
|
|
110
110
|
No source mutation, local wiki page writes, graph rebuild, web fetch, Ask, raw/provenance mutation, or .brain-legacy-unused mutation.
|
|
111
111
|
|
|
112
112
|
Next:
|
|
113
|
-
sdtk-brain compile --mode safe
|
|
113
|
+
sdtk-brain compile --mode safe
|
|
114
|
+
|
|
115
|
+
Pass -h / --help on any verb to show its help.`);
|
|
114
116
|
return 0;
|
|
115
117
|
}
|
|
116
118
|
|
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) {
|
|
@@ -871,10 +915,16 @@ function _build_change_set(prior_hashes, current_hashes) {
|
|
|
871
915
|
return { added, changed, unchanged, removed };
|
|
872
916
|
}
|
|
873
917
|
|
|
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 =
|
|
918
|
+
function write_wiki_pages_and_provenance(docs, state, root, generated, scan_roots, workspaceRoot) {
|
|
919
|
+
const workspace_root = _wiki_workspace_root(root, workspaceRoot);
|
|
920
|
+
const pages_root = path.join(workspace_root, "pages");
|
|
921
|
+
const provenance_root = path.join(workspace_root, "provenance");
|
|
922
|
+
// Page paths are recorded relative to the project root (posix separators)
|
|
923
|
+
// so provenance and _index.md stay portable.
|
|
924
|
+
const pages_prefix = path
|
|
925
|
+
.relative(root, pages_root)
|
|
926
|
+
.split(path.sep)
|
|
927
|
+
.join("/");
|
|
878
928
|
const sources_path = path.join(provenance_root, "sources.json");
|
|
879
929
|
const changes_path = path.join(provenance_root, "changes.json");
|
|
880
930
|
|
|
@@ -897,7 +947,7 @@ function write_wiki_pages_and_provenance(docs, state, root, generated, scan_root
|
|
|
897
947
|
source_hash = crypto.createHash("sha256").update(source_path, "utf8").digest("hex");
|
|
898
948
|
}
|
|
899
949
|
const page_id = _stable_page_id(source_path);
|
|
900
|
-
const page_rel = _page_relative_path(doc);
|
|
950
|
+
const page_rel = _page_relative_path(doc, pages_prefix);
|
|
901
951
|
const page_path = path.join(root, page_rel);
|
|
902
952
|
|
|
903
953
|
_assert_inside(workspace_root, page_path);
|
|
@@ -971,7 +1021,7 @@ function write_wiki_pages_and_provenance(docs, state, root, generated, scan_root
|
|
|
971
1021
|
// ---------------------------------------------------------------------------
|
|
972
1022
|
// Top-level buildAtlas
|
|
973
1023
|
// ---------------------------------------------------------------------------
|
|
974
|
-
async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose } = {}) {
|
|
1024
|
+
async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose, workspaceRoot } = {}) {
|
|
975
1025
|
const generated = _now_utc();
|
|
976
1026
|
const root = path.resolve(projectRoot);
|
|
977
1027
|
const atlas_dir = path.resolve(outputDir);
|
|
@@ -1008,7 +1058,7 @@ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose
|
|
|
1008
1058
|
process.stdout.write(`[atlas] Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges.\n`);
|
|
1009
1059
|
|
|
1010
1060
|
process.stdout.write("[atlas] Writing wiki pages and provenance...\n");
|
|
1011
|
-
const wiki_result = write_wiki_pages_and_provenance(docs, state, root, generated, roots);
|
|
1061
|
+
const wiki_result = write_wiki_pages_and_provenance(docs, state, root, generated, roots, workspaceRoot);
|
|
1012
1062
|
process.stdout.write(`[atlas] Wiki pages: ${wiki_result.page_count}\n`);
|
|
1013
1063
|
|
|
1014
1064
|
const index_data = {
|
|
@@ -1036,7 +1086,9 @@ async function buildAtlas({ projectRoot, outputDir, scanRoots, excludes, verbose
|
|
|
1036
1086
|
const summary_path = path.join(atlas_dir, "SDTK_DOC_ATLAS_SUMMARY.md");
|
|
1037
1087
|
_write_text_lf(summary_path, summary_text);
|
|
1038
1088
|
|
|
1039
|
-
const
|
|
1089
|
+
const viewer_data_js = build_viewer_data(index_data, graph_out);
|
|
1090
|
+
_write_text_lf(path.join(atlas_dir, "viewer-data.js"), viewer_data_js);
|
|
1091
|
+
const viewer_html = build_viewer(generated);
|
|
1040
1092
|
const viewer_path = path.join(atlas_dir, "viewer.html");
|
|
1041
1093
|
_write_text_lf(viewer_path, viewer_html);
|
|
1042
1094
|
|
|
@@ -1089,6 +1141,7 @@ module.exports = {
|
|
|
1089
1141
|
build_graph,
|
|
1090
1142
|
build_summary,
|
|
1091
1143
|
build_viewer,
|
|
1144
|
+
build_viewer_data,
|
|
1092
1145
|
write_wiki_pages_and_provenance,
|
|
1093
1146
|
load_atlas_state,
|
|
1094
1147
|
save_atlas_state,
|