Browse Source

refactor codemap skill naming and legacy state migration

Alvin Unreal 3 months ago
parent
commit
7946e9c152

+ 3 - 3
README.md

@@ -63,7 +63,7 @@ The default generated configuration looks like this:
       "orchestrator": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "openai/gpt-5.4-mini", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] }
     }
@@ -458,7 +458,7 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[Interview](docs/interview.md)** | Turn rough ideas into a structured markdown spec through a browser-based Q&A flow |
 | **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux or Zellij panes |
 | **[Todo Continuation](docs/todo-continuation.md)** | Auto-continue orchestrator sessions with cooldowns and safety checks |
-| **[Cartography](docs/cartography.md)** | Generate hierarchical codemaps to understand large codebases faster |
+| **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 
 ### ⚙️ Config & Reference
 
@@ -466,7 +466,7 @@ Use this section as a map: start with installation, then jump to features, confi
 |-----|----------------|
 | **[Configuration](docs/configuration.md)** | Config file locations, JSONC support, prompt overrides, and full option reference |
 | **[Maintainer Guide](docs/maintainers.md)** | Issue triage rules, label meanings, support routing, and repo maintenance workflow |
-| **[Skills](docs/skills.md)** | Built-in and recommended skills such as `simplify`, `agent-browser`, and `cartography` |
+| **[Skills](docs/skills.md)** | Built-in and recommended skills such as `simplify`, `agent-browser`, and `codemap` |
 | **[MCPs](docs/mcps.md)** | `websearch`, `context7`, `grep_app`, and how MCP permissions work per agent |
 | **[Tools](docs/tools.md)** | Built-in tool capabilities like background tasks, `webfetch`, LSP tools, code search, and formatters |
 

+ 11 - 10
docs/cartography.md → docs/codemap.md

@@ -1,20 +1,21 @@
-# Cartography Skill
+# Codemap Skill
 
-Cartography is a **custom skill** bundled with this repo.
+Codemap is a **custom skill** bundled with this repo.
 
 It helps agents quickly build a high-quality mental model of an unfamiliar codebase by generating a structured *codemap* and tracking changes over time.
 
 ## What it does
 
-Cartography is designed for repository understanding and hierarchical codemap generation:
+Codemap is designed for repository understanding and hierarchical codemap generation:
 
 1. Selects relevant code/config files using LLM judgment
-2. Creates `.slim/cartography.json` for change tracking
-3. Generates `codemap.md` templates (per folder) for explorers to fill in
+2. Creates `.slim/codemap.json` for change tracking
+3. Generates `codemap.md` templates (per folder) for fixers to fill in
+4. Migrates legacy `.slim/cartography.json` state to `.slim/codemap.json`
 
 ## How to use
 
-Cartography is installed automatically by the `oh-my-opencode-slim` installer when custom skills are enabled.
+Codemap is installed automatically by the `oh-my-opencode-slim` installer when custom skills are enabled.
 
 ### Run it (manual / local)
 
@@ -33,13 +34,13 @@ python3 cartographer.py update --root /repo
 
 ## Outputs
 
-### `.slim/cartography.json`
+### `.slim/codemap.json`
 
 A change-tracking file with hashes for files/folders.
 
 ### `codemap.md` (per folder)
 
-Empty templates created in each folder so an Explorer-style agent can fill in:
+Empty templates created in each folder so a Fixer-style agent can fill in:
 
 - Responsibility
 - Design patterns
@@ -50,9 +51,9 @@ Empty templates created in each folder so an Explorer-style agent can fill in:
 
 The existing screenshot lives in `img/cartography.png`.
 
-![Cartography screenshot](../img/cartography.png)
+![Codemap screenshot](../img/cartography.png)
 
 ## Related
 
-- `src/skills/cartography/README.md` and `src/skills/cartography/SKILL.md` contain the skill’s internal docs.
+- `src/skills/codemap/README.md` and `src/skills/codemap/SKILL.md` contain the skill’s internal docs.
 - `codemap.md` at the repo root is an example output/starting point.

+ 1 - 1
docs/installation.md

@@ -258,5 +258,5 @@ See the [Multiplexer Integration Guide](multiplexer-integration.md) for more det
    ```bash
    npx skills remove agent-browser
    rm -rf ~/.config/opencode/skills/simplify
-   rm -rf ~/.config/opencode/skills/cartography
+   rm -rf ~/.config/opencode/skills/codemap
    ```

+ 5 - 5
docs/provider-configurations.md

@@ -18,7 +18,7 @@ The installer generates this configuration automatically:
       "orchestrator": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "openai/gpt-5.4-mini", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] }
     }
@@ -38,7 +38,7 @@ To use Kimi, add a `kimi` preset and set it as active:
       "orchestrator": { "model": "kimi-for-coding/k2p5", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "kimi-for-coding/k2p5", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "kimi-for-coding/k2p5", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "kimi-for-coding/k2p5", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "kimi-for-coding/k2p5", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "kimi-for-coding/k2p5", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "kimi-for-coding/k2p5", "variant": "low", "skills": [], "mcps": [] }
     }
@@ -64,7 +64,7 @@ To use GitHub Copilot with Grok Code Fast:
       "orchestrator": { "model": "github-copilot/claude-opus-4.6", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "github-copilot/claude-opus-4.6", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "github-copilot/grok-code-fast-1", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "github-copilot/grok-code-fast-1", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "github-copilot/grok-code-fast-1", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "github-copilot/gemini-3.1-pro-preview", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "github-copilot/claude-sonnet-4.6", "variant": "low", "skills": [], "mcps": [] }
     }
@@ -90,7 +90,7 @@ To use ZAI Coding Plan with GLM 5:
       "orchestrator": { "model": "zai-coding-plan/glm-5", "variant": "high", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "zai-coding-plan/glm-5", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "zai-coding-plan/glm-5", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "zai-coding-plan/glm-5", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "zai-coding-plan/glm-5", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "zai-coding-plan/glm-5", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "zai-coding-plan/glm-5", "variant": "low", "skills": [], "mcps": [] }
     }
@@ -116,7 +116,7 @@ You can mix models from different providers across agents. Create a custom prese
       "orchestrator": { "model": "openai/gpt-5.4", "skills": ["*"], "mcps": ["*", "!context7"] },
       "oracle": { "model": "openai/gpt-5.4", "variant": "high", "skills": ["simplify"], "mcps": [] },
       "librarian": { "model": "kimi-for-coding/k2p5", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
-      "explorer": { "model": "github-copilot/grok-code-fast-1", "variant": "low", "skills": ["cartography"], "mcps": [] },
+      "explorer": { "model": "github-copilot/grok-code-fast-1", "variant": "low", "skills": [], "mcps": [] },
       "designer": { "model": "kimi-for-coding/k2p5", "variant": "medium", "skills": ["agent-browser"], "mcps": [] },
       "fixer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] }
     }

+ 2 - 2
docs/quick-reference.md

@@ -17,13 +17,13 @@
 | [Interview](interview.md) | `/interview` command, browser UI, dashboard mode, multi-session coordination |
 | [Multiplexer Integration](multiplexer-integration.md) | Real-time pane monitoring, layouts, troubleshooting |
 | [Todo Continuation](todo-continuation.md) | `auto_continue`, `/auto-continue`, cooldowns, safety gates |
-| [Cartography Skill](cartography.md) | Hierarchical codemap generation |
+| [Codemap Skill](codemap.md) | Hierarchical codemap generation |
 
 ## ⚙️ Config & Reference
 
 | Doc | Contents |
 |-----|----------|
-| [Skills](skills.md) | `simplify`, `agent-browser`, `cartography` — skills assignment syntax |
+| [Skills](skills.md) | `simplify`, `agent-browser`, `codemap` — skills assignment syntax |
 | [MCPs](mcps.md) | `websearch`, `context7`, `grep_app` — permissions per agent, global disable |
 | [Tools](tools.md) | Background tasks, LSP, code search (`ast_grep`), formatters |
 | [Configuration](configuration.md) | Config files, prompt overriding, JSONC, full option reference table |

+ 6 - 6
docs/skills.md

@@ -19,7 +19,7 @@ Skills are installed via the `oh-my-opencode-slim` installer or manually with `n
 | Skill | Description | Assigned to by default |
 |-------|-------------|----------------------|
 | [`simplify`](#simplify) | Behavior-preserving code simplification | `oracle` |
-| [`cartography`](#cartography) | Repository codemap generation | `orchestrator`, `explorer` |
+| [`codemap`](#codemap) | Repository codemap generation | `orchestrator` |
 
 ---
 
@@ -43,13 +43,13 @@ Source: adapted from Addy Osmani's `code-simplification` skill and bundled local
 
 ---
 
-## cartography
+## codemap
 
 **Automated repository mapping through hierarchical codemaps.**
 
-`cartography` empowers the Orchestrator to build and maintain a deep architectural understanding of any codebase. Instead of reading thousands of lines of code on every task, agents refer to hierarchical `codemap.md` files describing the *why* and *how* of each directory.
+`codemap` empowers the Orchestrator to build and maintain a deep architectural understanding of any codebase. Instead of reading thousands of lines of code on every task, agents refer to hierarchical `codemap.md` files describing the *why* and *how* of each directory.
 
-**How to use:** Ask the Orchestrator to `run cartography`. It automatically detects whether to initialize a new map or update an existing one.
+**How to use:** Ask the Orchestrator to `run codemap`. It automatically detects whether to initialize a new map or update an existing one.
 
 **Why it's useful:**
 - **Instant onboarding** — understand unfamiliar codebases in seconds
@@ -57,7 +57,7 @@ Source: adapted from Addy Osmani's `code-simplification` skill and bundled local
 - **Change detection** — only modified folders are re-analyzed
 - **Timeless documentation** — focuses on high-level design, not implementation details
 
-See **[Cartography Skill](cartography.md)** for full documentation including manual commands and technical details.
+See **[Codemap Skill](codemap.md)** for full documentation including manual commands and technical details.
 
 ---
 
@@ -85,7 +85,7 @@ Control which skills each agent can use in `~/.config/opencode/oh-my-opencode-sl
   "presets": {
     "my-preset": {
       "orchestrator": {
-        "skills": ["cartography"]
+        "skills": ["codemap"]
       },
       "oracle": {
         "skills": ["simplify"]

+ 1 - 1
scripts/verify-release-artifact.ts

@@ -30,7 +30,7 @@ const packagedRequiredFiles = [
   'dist/cli/index.js',
   'oh-my-opencode-slim.schema.json',
   'src/skills/simplify/SKILL.md',
-  'src/skills/cartography/SKILL.md',
+  'src/skills/codemap/SKILL.md',
 ];
 
 function fail(message: string): never {

+ 8 - 8
src/agents/index.test.ts

@@ -219,7 +219,7 @@ describe('per-model variant in array config', () => {
 });
 
 describe('skill permissions', () => {
-  test('orchestrator gets cartography skill allowed by default', () => {
+  test('orchestrator gets codemap skill allowed by default', () => {
     const agents = createAgents();
     const orchestrator = agents.find((a) => a.name === 'orchestrator');
     expect(orchestrator).toBeDefined();
@@ -228,17 +228,17 @@ describe('skill permissions', () => {
     )?.skill as Record<string, string>;
     // orchestrator gets wildcard allow (from RECOMMENDED_SKILLS wildcard entry)
     expect(skillPerm?.['*']).toBe('allow');
-    // CUSTOM_SKILLS loop must also add a named cartography entry for orchestrator
-    expect(skillPerm?.cartography).toBe('allow');
+    // CUSTOM_SKILLS loop must also add a named codemap entry for orchestrator
+    expect(skillPerm?.codemap).toBe('allow');
   });
 
-  test('explorer gets cartography skill allowed by default', () => {
+  test('fixer does not get codemap skill allowed by default', () => {
     const agents = createAgents();
-    const explorer = agents.find((a) => a.name === 'explorer');
-    expect(explorer).toBeDefined();
-    const skillPerm = (explorer?.config.permission as Record<string, unknown>)
+    const fixer = agents.find((a) => a.name === 'fixer');
+    expect(fixer).toBeDefined();
+    const skillPerm = (fixer?.config.permission as Record<string, unknown>)
       ?.skill as Record<string, string>;
-    expect(skillPerm?.cartography).toBe('allow');
+    expect(skillPerm?.codemap).not.toBe('allow');
   });
 
   test('oracle gets requesting-code-review skill allowed by default', () => {

+ 1 - 1
src/cli/codemap.md

@@ -400,7 +400,7 @@ InstallConfig (preferences)
 
 | Name | Source | Agents | Description |
 |------|--------|--------|-------------|
-| `cartography` | src/skills/cartography | orchestrator, explorer | Repository understanding and hierarchical codemap generation |
+| `codemap` | src/skills/codemap | orchestrator | Repository understanding and hierarchical codemap generation |
 
 ### Permission-Only Skills (external, not installed)
 

+ 3 - 3
src/cli/custom-skills.ts

@@ -35,10 +35,10 @@ export const CUSTOM_SKILLS: CustomSkill[] = [
     sourcePath: 'src/skills/simplify',
   },
   {
-    name: 'cartography',
+    name: 'codemap',
     description: 'Repository understanding and hierarchical codemap generation',
-    allowedAgents: ['orchestrator', 'explorer'],
-    sourcePath: 'src/skills/cartography',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/codemap',
   },
 ];
 

+ 6 - 3
src/cli/providers.test.ts

@@ -79,13 +79,16 @@ describe('providers', () => {
     // Oracle should have bundled simplify
     expect(agents.oracle.skills).toContain('simplify');
 
-    // Explorer should have bundled cartography
-    expect(agents.explorer.skills).toContain('cartography');
+    // Orchestrator should implicitly cover bundled codemap via '*'
+    expect(agents.orchestrator.skills).toContain('*');
 
     // Designer should have 'agent-browser'
     expect(agents.designer.skills).toContain('agent-browser');
 
-    // Fixer should have no skills by default (empty recommended list)
+    // Explorer should have no bundled skills by default
+    expect(agents.explorer.skills).toEqual([]);
+
+    // Fixer should have no bundled skills by default
     expect(agents.fixer.skills).toEqual([]);
   });
 

+ 8 - 6
src/skills/cartography/README.md → src/skills/codemap/README.md

@@ -1,14 +1,16 @@
-# Cartography Skill
+# Codemap Skill
 
 Repository understanding and hierarchical codemap generation.
 
 ## Overview
 
-Cartography helps orchestrators map and understand codebases by:
+Codemap helps orchestrators map and understand codebases by:
 
 1. Selecting relevant code/config files using LLM judgment
-2. Creating `.slim/cartography.json` for change tracking
-3. Generating empty `codemap.md` templates for explorers to fill in
+2. Creating `.slim/codemap.json` for change tracking
+3. Generating empty `codemap.md` templates for fixers to fill in
+
+Legacy `.slim/cartography.json` state is migrated to `.slim/codemap.json` automatically.
 
 ## Commands
 
@@ -25,7 +27,7 @@ python3 cartographer.py update --root /repo
 
 ## Outputs
 
-### .slim/cartography.json
+### .slim/codemap.json
 
 ```json
 {
@@ -46,7 +48,7 @@ python3 cartographer.py update --root /repo
 
 ### codemap.md (per folder)
 
-Empty templates created in each folder for explorers to fill with:
+Empty templates created in each folder for fixers to fill with:
 - Responsibility
 - Design patterns
 - Data/control flow

+ 17 - 14
src/skills/cartography/SKILL.md → src/skills/codemap/SKILL.md

@@ -1,9 +1,9 @@
 ---
-name: cartography
+name: codemap
 description: Generate comprehensive hierarchical codemaps for UNFAMILIAR repositories. Expensive operation - only use when explicitly asked for codebase documentation or initial repository mapping
 ---
 
-# Cartography Skill
+# Codemap Skill
 
 You help users understand and map repositories by creating hierarchical codemaps.
 
@@ -17,11 +17,15 @@ You help users understand and map repositories by creating hierarchical codemaps
 
 ### Step 1: Check for Existing State
 
-**First, check if `.slim/cartography.json` exists in the repo root.**
+**First, check if `.slim/codemap.json` exists in the repo root.**
 
-If it **exists**: Skip to Step 3 (Detect Changes) - no need to re-initialize.
+If it does not exist, check for legacy state at `.slim/cartography.json`.
 
-If it **doesn't exist**: Continue to Step 2 (Initialize).
+If legacy state exists: move `.slim/cartography.json` to `.slim/codemap.json`, then continue with change detection.
+
+If `.slim/codemap.json` exists: Skip to Step 3 (Detect Changes) - no need to re-initialize.
+
+If neither file exists: Continue to Step 2 (Initialize).
 
 ### Step 2: Initialize (Only if no state exists)
 
@@ -36,24 +40,24 @@ If it **doesn't exist**: Continue to Step 2 (Initialize).
 3. **Run cartographer.py init**:
 
 ```bash
-python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py init \
+python3 ~/.config/opencode/skills/codemap/scripts/cartographer.py init \
   --root ./ \
   --include "src/**/*.ts" \
   --exclude "**/*.test.ts" --exclude "dist/**" --exclude "node_modules/**"
 ```
 
 This creates:
-- `.slim/cartography.json` - File and folder hashes for change detection
+- `.slim/codemap.json` - File and folder hashes for change detection
 - Empty `codemap.md` files in all relevant subdirectories
 
-4. **Delegate to Explorer agents** - Spawn one explorer per folder to read code and fill in its specific `codemap.md` file.
+4. **Delegate codemap writing to Fixer agents** - Spawn one fixer per folder to read code and create or update its specific `codemap.md` file.
 
 ### Step 3: Detect Changes (If state already exists)
 
 1. **Run cartographer.py changes** to see what changed:
 
 ```bash
-python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py changes \
+python3 ~/.config/opencode/skills/codemap/scripts/cartographer.py changes \
   --root ./
 ```
 
@@ -63,11 +67,11 @@ python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py changes \
    - Modified files
    - Affected folders
 
-3. **Only update affected codemaps** - Spawn one explorer per affected folder to update its `codemap.md`.
+3. **Only update affected codemaps** - Spawn one fixer per affected folder to update its `codemap.md`.
 4. **Run update** to save new state:
 
 ```bash
-python3 ~/.config/opencode/skills/cartography/scripts/cartographer.py update \
+python3 ~/.config/opencode/skills/codemap/scripts/cartographer.py update \
   --root ./
 ```
 
@@ -100,12 +104,11 @@ Before working on any task, read `codemap.md` to understand:
 For deep work on a specific folder, also read that folder's `codemap.md`.
 ```
 
-This is idempotent — repeated cartography runs will detect the existing section and skip. No duplication.
-
+This is idempotent — repeated codemap runs will detect the existing section and skip. No duplication.
 
 ## Codemap Content
 
-Explorers are granted write permissions for `codemap.md` files during this workflow. Use precise technical terminology to document the implementation:
+Fixers are responsible for writing `codemap.md` files during this workflow. Use precise technical terminology to document the implementation:
 
 - **Responsibility** - Define the specific role of this directory using standard software engineering terms (e.g., "Service Layer", "Data Access Object", "Middleware").
 - **Design Patterns** - Identify and name specific patterns used (e.g., "Observer", "Singleton", "Factory", "Strategy"). Detail the abstractions and interfaces.

+ 88 - 103
src/skills/cartography/scripts/cartographer.py → src/skills/codemap/scripts/cartographer.py

@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-Cartographer - Repository mapping and change detection tool.
+Codemap cartographer - repository mapping and change detection tool.
 
 Commands:
   init     Initialize mapping (create hashes + empty codemaps)
@@ -20,12 +20,13 @@ import os
 import re
 import sys
 from datetime import datetime, timezone
-from pathlib import Path, PurePath
-from typing import Dict, List, Optional, Set, Tuple
+from pathlib import Path
+from typing import Dict, List, Optional, Set
 
 VERSION = "1.0.0"
 STATE_DIR = ".slim"
-STATE_FILE = "cartography.json"
+STATE_FILE = "codemap.json"
+LEGACY_STATE_FILE = "cartography.json"
 CODEMAP_FILE = "codemap.md"
 
 
@@ -52,11 +53,10 @@ class PatternMatcher:
 
         regex_parts = []
         for pattern in patterns:
-            # Regex conversion logic
             reg = re.escape(pattern)
-            reg = reg.replace(r'\*\*/', '(?:.*/)?')  # Recursive glob
+            reg = reg.replace(r'\*\*/', '(?:.*/)?')
             reg = reg.replace(r'\*\*', '.*')
-            reg = reg.replace(r'\*', '[^/]*')  # Single level glob
+            reg = reg.replace(r'\*', '[^/]*')
             reg = reg.replace(r'\?', '.')
 
             if pattern.endswith('/'):
@@ -66,10 +66,9 @@ class PatternMatcher:
                 reg = '^' + reg[1:]
             else:
                 reg = '(?:^|.*/)' + reg
-            
+
             regex_parts.append(f'(?:{reg}$)')
-        
-        # Combine all patterns into a single regex for speed
+
         combined_regex = '|'.join(regex_parts)
         self.regex = re.compile(combined_regex)
 
@@ -89,42 +88,35 @@ def select_files(
 ) -> List[Path]:
     """Select files based on include/exclude patterns and exceptions."""
     selected = []
-    
-    # Pre-compile matchers
+
     include_matcher = PatternMatcher(include_patterns)
     exclude_matcher = PatternMatcher(exclude_patterns)
     gitignore_matcher = PatternMatcher(gitignore_patterns)
     exception_set = set(exceptions)
-    
+
     root_str = str(root)
-    
+
     for dirpath, dirnames, filenames in os.walk(root_str):
-        # Skip hidden directories early by modifying dirnames in-place
         dirnames[:] = [d for d in dirnames if not d.startswith(".")]
-        
+
         rel_dir = os.path.relpath(dirpath, root_str)
         if rel_dir == ".":
             rel_dir = ""
-        
+
         for filename in filenames:
             rel_path = os.path.join(rel_dir, filename).replace("\\", "/")
             if rel_path.startswith("./"):
                 rel_path = rel_path[2:]
-            
-            # Skip if ignored by .gitignore
+
             if gitignore_matcher.matches(rel_path):
                 continue
-            
-            # Check explicit exclusions first
-            if exclude_matcher.matches(rel_path):
-                # Unless it's an exception
-                if rel_path not in exception_set:
-                    continue
-            
-            # Check inclusions
+
+            if exclude_matcher.matches(rel_path) and rel_path not in exception_set:
+                continue
+
             if include_matcher.matches(rel_path) or rel_path in exception_set:
                 selected.append(root / rel_path)
-    
+
     return sorted(selected)
 
 
@@ -142,17 +134,15 @@ def compute_file_hash(filepath: Path) -> str:
 
 def compute_folder_hash(folder: str, file_hashes: Dict[str, str]) -> str:
     """Compute a stable hash for a folder based on its files."""
-    # Get all files in this folder
     folder_files = sorted(
         (path, hash_val)
         for path, hash_val in file_hashes.items()
         if path.startswith(folder + "/") or (folder == "." and "/" not in path)
     )
-    
+
     if not folder_files:
         return ""
-    
-    # Hash the concatenation of path:hash pairs
+
     hasher = hashlib.md5()
     for path, hash_val in folder_files:
         hasher.update(f"{path}:{hash_val}\n".encode())
@@ -164,16 +154,31 @@ def get_folders_with_files(files: List[Path], root: Path) -> Set[str]:
     folders = set()
     for f in files:
         rel = f.relative_to(root)
-        # Add all parent directories
-        parts = rel.parts[:-1]  # Exclude filename
+        parts = rel.parts[:-1]
         for i in range(len(parts)):
             folders.add("/".join(parts[: i + 1]))
-    folders.add(".")  # Always include root
+    folders.add(".")
     return folders
 
 
+def migrate_legacy_state(root: Path) -> bool:
+    """Move legacy cartography state to codemap state if needed."""
+    state_dir = root / STATE_DIR
+    legacy_path = state_dir / LEGACY_STATE_FILE
+    state_path = state_dir / STATE_FILE
+
+    if state_path.exists() or not legacy_path.exists():
+        return False
+
+    state_dir.mkdir(parents=True, exist_ok=True)
+    legacy_path.replace(state_path)
+    print(f"Migrated {STATE_DIR}/{LEGACY_STATE_FILE} -> {STATE_DIR}/{STATE_FILE}")
+    return True
+
+
 def load_state(root: Path) -> Optional[dict]:
-    """Load the current cartography state."""
+    """Load the current codemap state, migrating legacy state if needed."""
+    migrate_legacy_state(root)
     state_path = root / STATE_DIR / STATE_FILE
     if state_path.exists():
         try:
@@ -185,10 +190,10 @@ def load_state(root: Path) -> Optional[dict]:
 
 
 def save_state(root: Path, state: dict) -> None:
-    """Save the cartography state."""
+    """Save the codemap state."""
     state_dir = root / STATE_DIR
     state_dir.mkdir(parents=True, exist_ok=True)
-    
+
     state_path = state_dir / STATE_FILE
     with open(state_path, "w", encoding="utf-8") as f:
         json.dump(state, f, indent=2)
@@ -200,7 +205,7 @@ def create_empty_codemap(folder_path: Path, folder_name: str) -> None:
     if not codemap_path.exists():
         content = f"""# {folder_name}/
 
-<!-- Explorer: Fill in this section with architectural understanding -->
+<!-- Fixer: Fill in this section with architectural understanding -->
 
 ## Responsibility
 
@@ -225,42 +230,37 @@ def create_empty_codemap(folder_path: Path, folder_name: str) -> None:
 def cmd_init(args: argparse.Namespace) -> int:
     """Initialize mapping: create hashes and empty codemaps."""
     root = Path(args.root).resolve()
-    
+
     if not root.is_dir():
         print(f"Error: {root} is not a directory", file=sys.stderr)
         return 1
-    
-    # Load patterns
+
     gitignore = load_gitignore(root)
     include_patterns = args.include or ["**/*"]
     exclude_patterns = args.exclude or []
     exceptions = args.exception or []
-    
+
     print(f"Scanning {root}...")
     print(f"Include patterns: {include_patterns}")
     print(f"Exclude patterns: {exclude_patterns}")
     print(f"Exceptions: {exceptions}")
-    
-    # Select files
+
     selected_files = select_files(
         root, include_patterns, exclude_patterns, exceptions, gitignore
     )
-    
+
     print(f"Selected {len(selected_files)} files")
-    
-    # Compute file hashes
+
     file_hashes: Dict[str, str] = {}
     for f in selected_files:
         rel_path = str(f.relative_to(root))
         file_hashes[rel_path] = compute_file_hash(f)
-    
-    # Get folders and compute folder hashes
+
     folders = get_folders_with_files(selected_files, root)
     folder_hashes: Dict[str, str] = {}
     for folder in folders:
         folder_hashes[folder] = compute_folder_hash(folder, file_hashes)
-    
-    # Create state
+
     state = {
         "metadata": {
             "version": VERSION,
@@ -273,12 +273,10 @@ def cmd_init(args: argparse.Namespace) -> int:
         "file_hashes": file_hashes,
         "folder_hashes": folder_hashes,
     }
-    
-    # Save state
+
     save_state(root, state)
     print(f"Created {STATE_DIR}/{STATE_FILE}")
-    
-    # Create empty codemaps
+
     for folder in folders:
         if folder == ".":
             folder_path = root
@@ -286,45 +284,41 @@ def cmd_init(args: argparse.Namespace) -> int:
         else:
             folder_path = root / folder
             folder_name = folder
-        
+
         create_empty_codemap(folder_path, folder_name)
-    
+
     print(f"Created {len(folders)} empty codemap.md files")
-    
+
     return 0
 
 
 def cmd_changes(args: argparse.Namespace) -> int:
     """Show what changed since last update."""
     root = Path(args.root).resolve()
-    
+
     state = load_state(root)
     if not state:
-        print("No cartography state found. Run 'init' first.", file=sys.stderr)
+        print("No codemap state found. Run 'init' first.", file=sys.stderr)
         return 1
-    
-    # Get patterns from saved state
+
     metadata = state.get("metadata", {})
     include_patterns = metadata.get("include_patterns", ["**/*"])
     exclude_patterns = metadata.get("exclude_patterns", [])
     exceptions = metadata.get("exceptions", [])
-    
+
     gitignore = load_gitignore(root)
-    
-    # Select current files
+
     current_files = select_files(
         root, include_patterns, exclude_patterns, exceptions, gitignore
     )
-    
-    # Compute current hashes
+
     current_hashes: Dict[str, str] = {}
     for f in current_files:
         rel_path = str(f.relative_to(root))
         current_hashes[rel_path] = compute_file_hash(f)
-    
+
     saved_hashes = state.get("file_hashes", {})
-    
-    # Find changes
+
     added = set(current_hashes.keys()) - set(saved_hashes.keys())
     removed = set(saved_hashes.keys()) - set(current_hashes.keys())
     modified = {
@@ -332,93 +326,86 @@ def cmd_changes(args: argparse.Namespace) -> int:
         for path in current_hashes.keys() & saved_hashes.keys()
         if current_hashes[path] != saved_hashes[path]
     }
-    
+
     if not added and not removed and not modified:
         print("No changes detected.")
         return 0
-    
+
     if added:
         print(f"\n{len(added)} added:")
         for path in sorted(added):
             print(f"  + {path}")
-    
+
     if removed:
         print(f"\n{len(removed)} removed:")
         for path in sorted(removed):
             print(f"  - {path}")
-    
+
     if modified:
         print(f"\n{len(modified)} modified:")
         for path in sorted(modified):
             print(f"  ~ {path}")
-    
-    # Show affected folders
+
     affected_folders = set()
     for path in added | removed | modified:
         parts = Path(path).parts[:-1]
         for i in range(len(parts)):
             affected_folders.add("/".join(parts[: i + 1]))
         affected_folders.add(".")
-    
+
     print(f"\n{len(affected_folders)} folders affected:")
     for folder in sorted(affected_folders):
         print(f"  {folder}/")
-    
+
     return 0
 
 
 def cmd_update(args: argparse.Namespace) -> int:
     """Update hashes and save state."""
     root = Path(args.root).resolve()
-    
+
     state = load_state(root)
     if not state:
-        print("No cartography state found. Run 'init' first.", file=sys.stderr)
+        print("No codemap state found. Run 'init' first.", file=sys.stderr)
         return 1
-    
-    # Get patterns from saved state
+
     metadata = state.get("metadata", {})
     include_patterns = metadata.get("include_patterns", ["**/*"])
     exclude_patterns = metadata.get("exclude_patterns", [])
     exceptions = metadata.get("exceptions", [])
-    
+
     gitignore = load_gitignore(root)
-    
-    # Select current files
+
     selected_files = select_files(
         root, include_patterns, exclude_patterns, exceptions, gitignore
     )
-    
-    # Compute new hashes
+
     file_hashes: Dict[str, str] = {}
     for f in selected_files:
         rel_path = str(f.relative_to(root))
         file_hashes[rel_path] = compute_file_hash(f)
-    
-    # Compute folder hashes
+
     folders = get_folders_with_files(selected_files, root)
     folder_hashes: Dict[str, str] = {}
     for folder in folders:
         folder_hashes[folder] = compute_folder_hash(folder, file_hashes)
-    
-    # Update state
+
     state["metadata"]["last_run"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
     state["file_hashes"] = file_hashes
     state["folder_hashes"] = folder_hashes
-    
+
     save_state(root, state)
     print(f"Updated {STATE_DIR}/{STATE_FILE} with {len(file_hashes)} files")
-    
+
     return 0
 
 
 def main() -> int:
     parser = argparse.ArgumentParser(
-        description="Cartographer - Repository mapping and change detection"
+        description="Codemap cartographer - repository mapping and change detection"
     )
     subparsers = parser.add_subparsers(dest="command", help="Available commands")
-    
-    # Init command
+
     init_parser = subparsers.add_parser("init", help="Initialize mapping")
     init_parser.add_argument("--root", required=True, help="Repository root path")
     init_parser.add_argument(
@@ -430,17 +417,15 @@ def main() -> int:
     init_parser.add_argument(
         "--exception", action="append", help="Explicit file paths to include despite exclusions"
     )
-    
-    # Changes command
+
     changes_parser = subparsers.add_parser("changes", help="Show what changed")
     changes_parser.add_argument("--root", required=True, help="Repository root path")
-    
-    # Update command
+
     update_parser = subparsers.add_parser("update", help="Update hashes")
     update_parser.add_argument("--root", required=True, help="Repository root path")
-    
+
     args = parser.parse_args()
-    
+
     if args.command == "init":
         return cmd_init(args)
     elif args.command == "changes":

+ 35 - 22
src/skills/cartography/scripts/test_cartographer.py → src/skills/codemap/scripts/test_cartographer.py

@@ -1,44 +1,40 @@
-import unittest
-import os
-import shutil
+import hashlib
 import json
+import os
 import tempfile
-import hashlib
+import unittest
 from pathlib import Path
-from cartographer import PatternMatcher, compute_file_hash, compute_folder_hash, select_files
+
+from cartographer import (PatternMatcher, compute_file_hash,
+                          compute_folder_hash, load_state, select_files)
+
 
 class TestCartographer(unittest.TestCase):
     def test_pattern_matcher(self):
         patterns = ["node_modules/", "dist/", "*.log", "src/**/*.ts"]
         matcher = PatternMatcher(patterns)
-        
-        # Directory patterns
+
         self.assertTrue(matcher.matches("node_modules/foo.js"))
         self.assertTrue(matcher.matches("vendor/node_modules/bar.js"))
         self.assertTrue(matcher.matches("dist/main.js"))
         self.assertTrue(matcher.matches("src/dist/output.js"))
-        
-        # Glob patterns
+
         self.assertTrue(matcher.matches("error.log"))
         self.assertTrue(matcher.matches("logs/access.log"))
-        
-        # Recursive glob patterns
+
         self.assertTrue(matcher.matches("src/index.ts"))
         self.assertTrue(matcher.matches("src/utils/helper.ts"))
-        
-        # Non-matches
+
         self.assertFalse(matcher.matches("README.md"))
         self.assertFalse(matcher.matches("tests/test.py"))
 
     def test_compute_file_hash(self):
-        # Use binary mode to avoid any newline translation issues
-        with tempfile.NamedTemporaryFile(mode='wb', delete=False) as f:
+        with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
             f.write(b"test content")
             f_path = f.name
-        
+
         try:
             h1 = compute_file_hash(Path(f_path))
-            # md5 of b"test content" is 9473fdd0d880a43c21b7778d34872157
             expected = hashlib.md5(b"test content").hexdigest()
             self.assertEqual(h1, expected)
             self.assertEqual(h1, "9473fdd0d880a43c21b7778d34872157")
@@ -52,11 +48,11 @@ class TestCartographer(unittest.TestCase):
             "src/b.ts": "hash-b",
             "tests/test.ts": "hash-test"
         }
-        
+
         h1 = compute_folder_hash("src", file_hashes)
         h2 = compute_folder_hash("src", file_hashes)
         self.assertEqual(h1, h2)
-        
+
         file_hashes_alt = {
             "src/a.ts": "hash-a-modified",
             "src/b.ts": "hash-b"
@@ -73,15 +69,32 @@ class TestCartographer(unittest.TestCase):
             (root / "src" / "index.test.ts").write_text("test")
             (root / "node_modules" / "foo.js").write_text("dep")
             (root / "package.json").write_text("{}")
-            
+
             includes = ["src/**/*.ts", "package.json"]
             excludes = ["**/*.test.ts", "node_modules/"]
             exceptions = []
-            
+
             selected = select_files(root, includes, excludes, exceptions, [])
-            
+
             rel_selected = sorted([os.path.relpath(f, root) for f in selected])
             self.assertEqual(rel_selected, ["package.json", "src/index.ts"])
 
+    def test_load_state_migrates_legacy_cartography_json(self):
+        with tempfile.TemporaryDirectory() as tmpdir:
+            root = Path(tmpdir)
+            slim_dir = root / ".slim"
+            slim_dir.mkdir()
+
+            legacy_path = slim_dir / "cartography.json"
+            legacy_state = {"metadata": {"version": "1.0.0"}}
+            legacy_path.write_text(json.dumps(legacy_state), encoding="utf-8")
+
+            loaded = load_state(root)
+
+            self.assertEqual(loaded, legacy_state)
+            self.assertFalse(legacy_path.exists())
+            self.assertTrue((slim_dir / "codemap.json").exists())
+
+
 if __name__ == "__main__":
     unittest.main()