si-cartographer 0.1.0__tar.gz

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,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: si-cartographer
3
+ Version: 0.1.0
4
+ Summary: Charts the workspace as graphed knowledge. The agent that thinks with its pen.
5
+ Author: SuperInstance
6
+ License-Expression: MIT
7
+ Keywords: knowledge-graph,procedural-memory,wiki,chart,exo-filed
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.0; extra == "dev"
12
+
13
+ # cartographer
14
+
15
+ *The agent that charts the workspace as graphed knowledge.*
16
+
17
+ ---
18
+
19
+ The **cartographer** doesn't run tasks. The cartographer **charts** them.
20
+
21
+ Every solution becomes a reusable script. Every dead-end becomes a hazard note. Every cross-repo connection becomes an edge on the knowledge graph. The graph is exo-filed (markdown + JSON) so humans and agents can both read it.
22
+
23
+ ## The maritime role
24
+
25
+ The **deckhand** (sibling: [`able-bodied-crew`](https://github.com/SuperInstance/able-bodied-crew)) runs tasks — indexes, retrieves, batches, executes.
26
+
27
+ The **cartographer** charts — documents solutions, maps hazards, draws coastlines, writes chronicles. Pen-on-paper. Thinks with its pen.
28
+
29
+ ```
30
+ Deckhand runs the task → solves the problem → calls Cartographer
31
+ Cartographer charts the solution → writes the script → notes hazards
32
+ Next time the same problem appears → Cartographer finds the chart → Deckhand runs the known-good script
33
+ ```
34
+
35
+ ## What it does
36
+
37
+ - **KnowledgeGraph** — nodes (repos, concepts, scripts, hazards) and edges (references, dependencies, solved-by). Stored as JSON + markdown. No database.
38
+ - **ChartMaker** — writes reusable scripts, hazard notes (what NOT to do), and procedure docs. Tests scripts automatically.
39
+ - **CoastlineBuilder** — generates maps at three zoom levels: 1000ft (fleet at a glance), 100ft (each repo in detail), 10ft (file-level for one repo)
40
+ - **Navigator** — given a problem, finds the nearest documented solution or hazard
41
+ - **Chronicler** — writes daily chronicles (the thinking-with-pen record)
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install si-cartographer
47
+ ```
48
+
49
+ ## Use
50
+
51
+ ```bash
52
+ # Build knowledge graph from workspace
53
+ cartographer graph /path/to/workspace
54
+
55
+ # Generate coastline maps
56
+ cartographer coastline /path/to/workspace
57
+
58
+ # Find a solution
59
+ cartographer navigate "How do I publish to PyPI?"
60
+
61
+ # Write to the chronicle
62
+ cartographer chronicle --section "PyPI" --message "Published si-cartographer v0.1.0"
63
+ ```
64
+
65
+ ## License
66
+
67
+ MIT
68
+
69
+ ---
70
+
71
+ *Built by SuperInstance. The cartographer charts the rocks so the deckhand doesn't hit them.*
@@ -0,0 +1,59 @@
1
+ # cartographer
2
+
3
+ *The agent that charts the workspace as graphed knowledge.*
4
+
5
+ ---
6
+
7
+ The **cartographer** doesn't run tasks. The cartographer **charts** them.
8
+
9
+ Every solution becomes a reusable script. Every dead-end becomes a hazard note. Every cross-repo connection becomes an edge on the knowledge graph. The graph is exo-filed (markdown + JSON) so humans and agents can both read it.
10
+
11
+ ## The maritime role
12
+
13
+ The **deckhand** (sibling: [`able-bodied-crew`](https://github.com/SuperInstance/able-bodied-crew)) runs tasks — indexes, retrieves, batches, executes.
14
+
15
+ The **cartographer** charts — documents solutions, maps hazards, draws coastlines, writes chronicles. Pen-on-paper. Thinks with its pen.
16
+
17
+ ```
18
+ Deckhand runs the task → solves the problem → calls Cartographer
19
+ Cartographer charts the solution → writes the script → notes hazards
20
+ Next time the same problem appears → Cartographer finds the chart → Deckhand runs the known-good script
21
+ ```
22
+
23
+ ## What it does
24
+
25
+ - **KnowledgeGraph** — nodes (repos, concepts, scripts, hazards) and edges (references, dependencies, solved-by). Stored as JSON + markdown. No database.
26
+ - **ChartMaker** — writes reusable scripts, hazard notes (what NOT to do), and procedure docs. Tests scripts automatically.
27
+ - **CoastlineBuilder** — generates maps at three zoom levels: 1000ft (fleet at a glance), 100ft (each repo in detail), 10ft (file-level for one repo)
28
+ - **Navigator** — given a problem, finds the nearest documented solution or hazard
29
+ - **Chronicler** — writes daily chronicles (the thinking-with-pen record)
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install si-cartographer
35
+ ```
36
+
37
+ ## Use
38
+
39
+ ```bash
40
+ # Build knowledge graph from workspace
41
+ cartographer graph /path/to/workspace
42
+
43
+ # Generate coastline maps
44
+ cartographer coastline /path/to/workspace
45
+
46
+ # Find a solution
47
+ cartographer navigate "How do I publish to PyPI?"
48
+
49
+ # Write to the chronicle
50
+ cartographer chronicle --section "PyPI" --message "Published si-cartographer v0.1.0"
51
+ ```
52
+
53
+ ## License
54
+
55
+ MIT
56
+
57
+ ---
58
+
59
+ *Built by SuperInstance. The cartographer charts the rocks so the deckhand doesn't hit them.*
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "si-cartographer"
7
+ version = "0.1.0"
8
+ description = "Charts the workspace as graphed knowledge. The agent that thinks with its pen."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "SuperInstance" }]
13
+ keywords = ["knowledge-graph", "procedural-memory", "wiki", "chart", "exo-filed"]
14
+ dependencies = []
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=7.0"]
18
+
19
+ [project.scripts]
20
+ cartographer = "cartographer.cli:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """
2
+ cartographer — the agent that charts the workspace as graphed knowledge.
3
+
4
+ The deckhand runs tasks. The cartographer charts them.
5
+ Pen-on-paper. Exo-filed. Every solution a script. Every dead-end a hazard.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = [
12
+ "KnowledgeGraph",
13
+ "ChartMaker",
14
+ "CoastlineBuilder",
15
+ "Navigator",
16
+ "Chronicler",
17
+ ]
18
+
19
+ from cartographer.graph import KnowledgeGraph, Node, Edge
20
+ from cartographer.chart_maker import ChartMaker, Script, HazardNote
21
+ from cartographer.coastline import CoastlineBuilder
22
+ from cartographer.navigator import Navigator
23
+ from cartographer.chronicler import Chronicler
@@ -0,0 +1,296 @@
1
+ """ChartMaker — writes reusable scripts + hazard notes + procedure docs.
2
+
3
+ Moved from deckhand. The cartographer's pen.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import time
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ @dataclass
20
+ class Script:
21
+ """A reusable solution script."""
22
+ name: str
23
+ description: str
24
+ language: str
25
+ code: str
26
+ problem: str
27
+ success: bool = False
28
+ tested_at: float = 0.0
29
+ test_output: str = ""
30
+ tags: list[str] = field(default_factory=list)
31
+ author: str = "cartographer"
32
+
33
+ def to_metadata(self) -> dict[str, Any]:
34
+ return {
35
+ "name": self.name,
36
+ "description": self.description,
37
+ "language": self.language,
38
+ "problem": self.problem,
39
+ "success": self.success,
40
+ "tested_at": self.tested_at,
41
+ "tags": self.tags,
42
+ "author": self.author,
43
+ "code_sha256": hashlib.sha256(self.code.encode()).hexdigest()[:12],
44
+ }
45
+
46
+
47
+ @dataclass
48
+ class HazardNote:
49
+ """A rock on the chart — what NOT to do."""
50
+ title: str
51
+ approach: str
52
+ failure: str
53
+ show_stopper: bool
54
+ workaround: str = ""
55
+ discovered_at: float = 0.0
56
+ context: str = ""
57
+
58
+
59
+ class ChartMaker:
60
+ """The cartographer's pen. Writes scripts, hazards, procedures."""
61
+
62
+ def __init__(self, root_dir: str) -> None:
63
+ self.root = Path(root_dir)
64
+ self.scripts_dir = self.root / "scripts"
65
+ self.hazards_dir = self.root / "hazards"
66
+ self.procedures_dir = self.root / "procedures"
67
+ self.logs_dir = self.root / "chronicles"
68
+
69
+ for d in [self.scripts_dir, self.hazards_dir, self.procedures_dir, self.logs_dir]:
70
+ d.mkdir(parents=True, exist_ok=True)
71
+
72
+ def write_script(
73
+ self,
74
+ name: str,
75
+ description: str,
76
+ code: str,
77
+ problem: str,
78
+ language: str = "bash",
79
+ tags: list[str] | None = None,
80
+ run_immediately: bool = False,
81
+ ) -> Script:
82
+ """Write a reusable script and optionally test it."""
83
+ script = Script(
84
+ name=name, description=description, code=code,
85
+ problem=problem, language=language, tags=tags or [],
86
+ )
87
+
88
+ suffix = ".sh" if language == "bash" else f".{language}"
89
+ script_path = self.scripts_dir / f"{name}{suffix}"
90
+ script_path.write_text(code)
91
+ script_path.chmod(0o755)
92
+
93
+ # Procedure doc
94
+ proc_lines = [
95
+ f"# Procedure: {name}",
96
+ f"",
97
+ f"**Problem:** {problem}",
98
+ f"**Solution:** {description}",
99
+ f"**Tags:** {', '.join(tags) if tags else 'none'}",
100
+ f"",
101
+ f"## The Code",
102
+ f"",
103
+ f"```{language}",
104
+ code,
105
+ f"```",
106
+ f"",
107
+ f"## When to Use",
108
+ f"",
109
+ f"{problem}",
110
+ f"",
111
+ ]
112
+ (self.procedures_dir / f"{name}.md").write_text("\n".join(proc_lines))
113
+
114
+ if run_immediately:
115
+ self._test_script(script)
116
+
117
+ # Metadata
118
+ meta_path = self.scripts_dir / f"{name}.meta.json"
119
+ meta_path.write_text(json.dumps(script.to_metadata(), indent=2))
120
+
121
+ self._log(f"Wrote script `{name}` — {description}")
122
+ return script
123
+
124
+ def _test_script(self, script: Script) -> bool:
125
+ """Run a script and record result."""
126
+ suffix = ".sh" if script.language == "bash" else f".{script.language}"
127
+ script_path = self.scripts_dir / f"{script.name}{suffix}"
128
+
129
+ try:
130
+ runner = "python3" if script.language == "python" else "bash"
131
+ result = subprocess.run(
132
+ [runner, str(script_path)],
133
+ capture_output=True, text=True, timeout=30,
134
+ )
135
+ script.success = result.returncode == 0
136
+ script.tested_at = time.time()
137
+ script.test_output = (result.stdout[:500] + "\n" + result.stderr[:500]).strip()
138
+
139
+ status = "✓" if script.success else "✗"
140
+ self._log(f"{status} Script `{script.name}` tested")
141
+
142
+ if not script.success:
143
+ self.write_hazard(
144
+ title=f"Script {script.name} failed",
145
+ approach=f"Ran {script.name}",
146
+ failure=result.stderr[:300] or result.stdout[:300],
147
+ show_stopper=False,
148
+ workaround="Debug and retry",
149
+ )
150
+ return script.success
151
+
152
+ except Exception as e:
153
+ script.success = False
154
+ script.test_output = f"EXCEPTION: {e}"
155
+ self._log(f"✗ Script `{script.name}` EXCEPTION: {e}")
156
+ return False
157
+
158
+ def write_hazard(
159
+ self,
160
+ title: str,
161
+ approach: str,
162
+ failure: str,
163
+ show_stopper: bool,
164
+ workaround: str = "",
165
+ context: str = "",
166
+ ) -> HazardNote:
167
+ """Chart a rock — what NOT to do."""
168
+ hazard = HazardNote(
169
+ title=title, approach=approach, failure=failure,
170
+ show_stopper=show_stopper, workaround=workaround,
171
+ discovered_at=time.time(), context=context,
172
+ )
173
+
174
+ safe = re.sub(r"[^a-z0-9_-]", "_", title.lower())[:60]
175
+ path = self.hazards_dir / f"{safe}.md"
176
+
177
+ lines = [
178
+ f"# ⚠️ Hazard: {title}",
179
+ f"",
180
+ f"**Discovered:** {time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime(hazard.discovered_at))}",
181
+ f"**Show-stopper:** {'🚫 YES' if show_stopper else '⚠️ No — workaround exists'}",
182
+ f"",
183
+ f"## What Was Tried",
184
+ f"",
185
+ f"```\n{approach}\n```",
186
+ f"",
187
+ f"## Why It Failed",
188
+ f"",
189
+ f"{failure}",
190
+ f"",
191
+ ]
192
+ if workaround:
193
+ lines += [f"## Workaround\n", f"{workaround}\n"]
194
+ if context:
195
+ lines += [f"## Context\n", f"{context}\n"]
196
+ lines += [f"## Lesson\n", f"**Do NOT do this.**\n"]
197
+
198
+ path.write_text("\n".join(lines))
199
+ self._log(f"⚠️ Hazard charted: {title}")
200
+ return hazard
201
+
202
+ def find_script(self, problem: str) -> Script | None:
203
+ """Find a previously written script for a similar problem."""
204
+ problem_words = set(re.findall(r"\w+", problem.lower()))
205
+ best_match = None
206
+ best_score = 0
207
+
208
+ for meta_file in self.scripts_dir.glob("*.meta.json"):
209
+ try:
210
+ meta = json.loads(meta_file.read_text())
211
+ except Exception:
212
+ continue
213
+ meta_text = (
214
+ meta.get("problem", "").lower() + " "
215
+ + meta.get("description", "").lower() + " "
216
+ + " ".join(meta.get("tags", []))
217
+ )
218
+ overlap = len(problem_words & set(re.findall(r"\w+", meta_text)))
219
+ if overlap > best_score:
220
+ best_score = overlap
221
+ best_match = meta
222
+
223
+ if best_match and best_score >= 2:
224
+ name = best_match["name"]
225
+ suffix = ".sh" if best_match.get("language", "bash") == "bash" else f".{best_match['language']}"
226
+ script_path = self.scripts_dir / f"{name}{suffix}"
227
+ if script_path.exists():
228
+ return Script(
229
+ name=name, description=best_match.get("description", ""),
230
+ code=script_path.read_text(), problem=best_match.get("problem", ""),
231
+ language=best_match.get("language", "bash"),
232
+ success=best_match.get("success", False),
233
+ tested_at=best_match.get("tested_at", 0),
234
+ tags=best_match.get("tags", []),
235
+ )
236
+ return None
237
+
238
+ def has_hazard_for(self, approach: str) -> bool:
239
+ """Check if there's a hazard for this approach."""
240
+ approach_words = set(re.findall(r"\w+", approach.lower()))
241
+ for hazard_file in self.hazards_dir.glob("*.md"):
242
+ content = hazard_file.read_text().lower()
243
+ content_words = set(re.findall(r"\w+", content))
244
+ if len(approach_words & content_words) >= 3:
245
+ return True
246
+ return False
247
+
248
+ def list_scripts(self) -> list[dict[str, Any]]:
249
+ """List all scripts."""
250
+ scripts = []
251
+ for meta_file in sorted(self.scripts_dir.glob("*.meta.json")):
252
+ try:
253
+ scripts.append(json.loads(meta_file.read_text()))
254
+ except Exception:
255
+ pass
256
+ return scripts
257
+
258
+ def list_hazards(self) -> list[str]:
259
+ """List all hazard titles."""
260
+ return [f.stem for f in sorted(self.hazards_dir.glob("*.md"))]
261
+
262
+ def _log(self, message: str) -> None:
263
+ """Write to the daily chronicle."""
264
+ log_path = self.logs_dir / f"{time.strftime('%Y-%m-%d')}.md"
265
+ timestamp = time.strftime("%H:%M:%S UTC", time.gmtime())
266
+ with open(log_path, "a") as fh:
267
+ fh.write(f"- [{timestamp}] {message}\n")
268
+
269
+ def generate_index(self) -> str:
270
+ """Generate PROCEDURES.md index."""
271
+ lines = [
272
+ "# Procedures & Hazards",
273
+ "",
274
+ f"*The cartographer's procedural memory. Generated {time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime())}.*",
275
+ "",
276
+ "## Scripts",
277
+ "",
278
+ "| Script | Problem | Tested | Tags |",
279
+ "|--------|---------|--------|------|",
280
+ ]
281
+ for s in self.list_scripts():
282
+ tested = "✓" if s.get("success") else "✗"
283
+ tags = ", ".join(s.get("tags", [])) or "—"
284
+ lines.append(f"| `{s['name']}` | {s.get('problem', '—')[:50]} | {tested} | {tags} |")
285
+
286
+ lines += ["", "## Hazards", ""]
287
+ hazards = self.list_hazards()
288
+ if hazards:
289
+ for h in hazards:
290
+ lines.append(f"- ⚠️ {h}")
291
+ else:
292
+ lines.append("*No hazards charted. Waters clear.*")
293
+
294
+ path = self.root / "PROCEDURES.md"
295
+ path.write_text("\n".join(lines))
296
+ return str(path)
@@ -0,0 +1,54 @@
1
+ """Chronicler — writes the daily chronicle (the thinking-with-pen record)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ class Chronicler:
11
+ """Writes daily chronicles from workspace activity."""
12
+
13
+ def __init__(self, chronicle_dir: str) -> None:
14
+ self.dir = Path(chronicle_dir)
15
+ self.dir.mkdir(parents=True, exist_ok=True)
16
+
17
+ def write_entry(self, section: str, message: str) -> None:
18
+ """Append an entry to today's chronicle."""
19
+ log_path = self.dir / f"{time.strftime('%Y-%m-%d')}.md"
20
+ timestamp = time.strftime("%H:%M:%S UTC", time.gmtime())
21
+
22
+ # Ensure file has header
23
+ if not log_path.exists():
24
+ log_path.write_text(
25
+ f"# Chronicle — {time.strftime('%Y-%m-%d')}\n\n"
26
+ f"*The cartographer's daily log. Thinking with the pen.*\n\n"
27
+ )
28
+
29
+ with open(log_path, "a") as fh:
30
+ fh.write(f"### [{timestamp}] {section}\n\n{message}\n\n")
31
+
32
+ def read_today(self) -> str:
33
+ """Read today's chronicle."""
34
+ path = self.dir / f"{time.strftime('%Y-%m-%d')}.md"
35
+ if path.exists():
36
+ return path.read_text()
37
+ return ""
38
+
39
+ def summarize(self) -> dict[str, Any]:
40
+ """Summarize today's chronicle."""
41
+ content = self.read_today()
42
+ sections = {}
43
+ current = None
44
+ for line in content.split("\n"):
45
+ if line.startswith("### ["):
46
+ current = line
47
+ sections[current] = []
48
+ elif current and line.strip():
49
+ sections[current].append(line.strip())
50
+ return {
51
+ "date": time.strftime("%Y-%m-%d"),
52
+ "entries": len(sections),
53
+ "sections": list(sections.keys()),
54
+ }
@@ -0,0 +1,120 @@
1
+ """CLI for cartographer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+
9
+ def main(argv: list[str] | None = None) -> int:
10
+ parser = argparse.ArgumentParser(
11
+ prog="cartographer",
12
+ description="🗺️ Charts the workspace as graphed knowledge.",
13
+ )
14
+
15
+ sub = parser.add_subparsers(dest="command")
16
+
17
+ # graph
18
+ p_graph = sub.add_parser("graph", help="Build knowledge graph from workspace")
19
+ p_graph.add_argument("workspace", help="Workspace root")
20
+
21
+ # chart
22
+ p_chart = sub.add_parser("chart", help="Write a script/hazard")
23
+ p_chart.add_argument("--type", choices=["script", "hazard"], required=True)
24
+ p_chart.add_argument("--name", required=True)
25
+
26
+ # coastline
27
+ p_coast = sub.add_parser("coastline", help="Generate coastline maps")
28
+ p_coast.add_argument("workspace")
29
+ p_coast.add_argument("--output", default="charts")
30
+
31
+ # navigate
32
+ p_nav = sub.add_parser("navigate", help="Find nearest solution")
33
+ p_nav.add_argument("problem", help="The problem to solve")
34
+
35
+ # chronicle
36
+ p_chron = sub.add_parser("chronicle", help="Write or read chronicle")
37
+ p_chron.add_argument("--section", default="General")
38
+ p_chron.add_argument("--message", default="")
39
+ p_chron.add_argument("--read", action="store_true")
40
+
41
+ args = parser.parse_args(argv)
42
+
43
+ if not args.command:
44
+ parser.print_help()
45
+ return 0
46
+
47
+ if args.command == "graph":
48
+ from cartographer.graph import KnowledgeGraph
49
+ from pathlib import Path
50
+
51
+ kg = KnowledgeGraph()
52
+
53
+ # Scan repos in workspace
54
+ workspace = Path(args.workspace)
55
+ for entry in sorted(workspace.iterdir()):
56
+ if entry.is_dir() and not entry.name.startswith("."):
57
+ has_git = (entry / ".git").exists()
58
+ has_readme = any((entry / f).exists() for f in ["README.md", "readme.md"])
59
+ file_count = sum(1 for _ in entry.rglob("*") if _.is_file() and ".git" not in str(_))
60
+ health = "ok" if has_readme else "no-readme"
61
+ kg.add_node(entry.name, type="repo", health=health, file_count=file_count, has_git=has_git)
62
+
63
+ out = workspace / "cartographer-charts"
64
+ kg.save(str(out))
65
+ print(f"Graph: {kg.stats()}")
66
+ print(f"Saved to {out}")
67
+
68
+ elif args.command == "coastline":
69
+ from cartographer.graph import KnowledgeGraph
70
+ from cartographer.coastline import CoastlineBuilder
71
+
72
+ workspace = args.workspace
73
+ out = args.output
74
+ charts_path = workspace.rstrip("/") + "/cartographer-charts"
75
+
76
+ kg = KnowledgeGraph.load(charts_path)
77
+ if not kg.nodes:
78
+ print("No graph found. Run `cartographer graph <workspace>` first.")
79
+ return 1
80
+
81
+ cb = CoastlineBuilder(kg, workspace)
82
+ files = cb.build_all(out)
83
+ print(f"Coastline maps generated:")
84
+ for f in files:
85
+ print(f" {f}")
86
+
87
+ elif args.command == "navigate":
88
+ from cartographer.chart_maker import ChartMaker
89
+ from cartographer.navigator import Navigator
90
+
91
+ cm = ChartMaker(".")
92
+ kg = KnowledgeGraph()
93
+ nav = Navigator(kg, cm)
94
+ result = nav.navigate(args.problem)
95
+ print(f"Problem: {result['problem']}")
96
+ if result["script"]:
97
+ print(f"Found script: {result['script']['name']}")
98
+ if result["hazards"]:
99
+ print(f"Hazards: {result['hazards']}")
100
+ if result["graph_neighbors"]:
101
+ print(f"Related: {result['graph_neighbors']}")
102
+
103
+ elif args.command == "chronicle":
104
+ from cartographer.chronicler import Chronicler
105
+
106
+ chron = Chronicler("chronicles")
107
+ if args.read:
108
+ print(chron.read_today())
109
+ elif args.message:
110
+ chron.write_entry(args.section, args.message)
111
+ print("Written.")
112
+ else:
113
+ summary = chron.summarize()
114
+ print(f"Today: {summary['entries']} entries")
115
+
116
+ return 0
117
+
118
+
119
+ if __name__ == "__main__":
120
+ sys.exit(main())