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.
- si_cartographer-0.1.0/PKG-INFO +71 -0
- si_cartographer-0.1.0/README.md +59 -0
- si_cartographer-0.1.0/pyproject.toml +26 -0
- si_cartographer-0.1.0/setup.cfg +4 -0
- si_cartographer-0.1.0/src/cartographer/__init__.py +23 -0
- si_cartographer-0.1.0/src/cartographer/chart_maker.py +296 -0
- si_cartographer-0.1.0/src/cartographer/chronicler.py +54 -0
- si_cartographer-0.1.0/src/cartographer/cli.py +120 -0
- si_cartographer-0.1.0/src/cartographer/coastline.py +129 -0
- si_cartographer-0.1.0/src/cartographer/graph.py +195 -0
- si_cartographer-0.1.0/src/cartographer/navigator.py +43 -0
- si_cartographer-0.1.0/src/si_cartographer.egg-info/PKG-INFO +71 -0
- si_cartographer-0.1.0/src/si_cartographer.egg-info/SOURCES.txt +18 -0
- si_cartographer-0.1.0/src/si_cartographer.egg-info/dependency_links.txt +1 -0
- si_cartographer-0.1.0/src/si_cartographer.egg-info/entry_points.txt +2 -0
- si_cartographer-0.1.0/src/si_cartographer.egg-info/requires.txt +3 -0
- si_cartographer-0.1.0/src/si_cartographer.egg-info/top_level.txt +1 -0
- si_cartographer-0.1.0/tests/test_chart_maker.py +128 -0
- si_cartographer-0.1.0/tests/test_graph.py +110 -0
- si_cartographer-0.1.0/tests/test_navigator.py +72 -0
|
@@ -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,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())
|