Browse Source

cartography-1

Alvin Unreal 6 months ago
parent
commit
3f5fde46de
5 changed files with 81 additions and 32 deletions
  1. 3 3
      README.md
  2. 5 5
      cartography.md
  3. 58 21
      scripts/cartography.ts
  4. 11 0
      src/tools/cartography/tool.ts
  5. 4 3
      src/tools/skill/builtin.ts

+ 3 - 3
README.md

@@ -703,7 +703,7 @@ Use after major refactors or before finalizing PRs. Identifies unnecessary compl
 **Codebase mapping and structure documentation.**
 
 - **Hierarchical Mapping**: Generate `codemap.md` files at each folder level to document code organization.
-- **Hash-Based Change Detection**: Uses `.codemap.json` per folder and only re-maps files that have changed since last run.
+- **Hash-Based Change Detection**: Uses a root `.codemap.json` and only re-maps files that have changed since last run.
 - **Flow Documentation**: Captures purpose, exports, dependencies, and data flows for each file.
 - **Parallel Exploration**: Uses multiple Explorer agents to map large codebases efficiently.
 
@@ -716,10 +716,10 @@ cartography scan <folder> --extensions ts,tsx,js
 cartography hash <folder> --extensions ts,tsx,js
 
 # Generate/update .codemap.json
-cartography update <folder> --extensions ts,tsx,js
+cartography update <folder> --extensions ts,tsx,js --exclude tests
 
 # Report changed files without writing hashes
-cartography changes <folder> --extensions ts,tsx,js
+cartography changes <folder> --extensions ts,tsx,js --exclude tests
 ```
 
 See [cartography.md](cartography.md) for detailed design documentation.

+ 5 - 5
cartography.md

@@ -10,7 +10,7 @@ Cartography operates through an orchestrated "bottom-up" analysis pattern, combi
 A lightweight utility designed for the Orchestrator to handle deterministic file operations.
 - **Scanning**: Discovers directory structures while respecting `.gitignore` and default excludes (node_modules, .git, etc.).
 - **Hashing**: Calculates MD5 hashes for individual files and a composite "Folder Hash" (hash of all valid file hashes in that directory).
-- **Hash File**: Manages a minimal `.codemap.json` file to track state:
+- **Hash File**: Manages a single root `.codemap.json` file to track state:
   ```json
   {
     "h": "[folder_hash]",
@@ -40,7 +40,7 @@ Explorers are tasked with generating the human/AI-readable body of the `codemap.
 ## 🔄 Operational Workflow
 
 1.  **Discovery Phase**: Orchestrator runs the helper script to scan the root and identifies "High Importance" directories.
-2.  **Initial Hash Check**: The script identifies which folders are "Dirty" (hash mismatch or missing `.codemap.json`).
+2.  **Initial Hash Check**: The script identifies which folders are "Dirty" (hash mismatch or missing root `.codemap.json`).
 3.  **Leaf-Node Analysis**: Explorers are dispatched to the deepest sub-folders first.
 4.  **Incremental Update**: 
     - If a file hash changes, the Explorer re-analyzes only that file and updates the Folder Summary.
@@ -73,7 +73,7 @@ The resulting `codemap.md` files serve as a "Pre-flight Checklist" for any futur
 **A:** One `codemap.md` per folder. Sub-folders must be mapped before their parents so the parent can synthesize the sub-folder's high-level purpose into its own map.
 
 **Q: What is the script's specific responsibility?**
-**A:** The script is deterministic. It calculates hashes, manages `.codemap.json`, and scaffolds hash state. It *never* generates the descriptive body; that is reserved for the Explorer agents.
+**A:** The script is deterministic. It calculates hashes, manages root `.codemap.json`, and scaffolds hash state. It *never* generates the descriptive body; that is reserved for the Explorer agents.
 
 **Q: How is parallelism handled?**
 **A:** Explorers run in parallel for all "leaf" folders (folders with no sub-folders). Once a layer is complete, the Orchestrator moves up the tree.
@@ -91,7 +91,7 @@ User: "I need codemaps for this codebase so I can understand the architecture."
 
 **2.1 - Discovery Phase**
 ```
-Orchestrator calls: cartography scan {folder} --extensions {exts}
+Orchestrator calls: cartography scan {folder} --extensions {exts} --exclude tests,docs
 Response: { folder: ".", files: ["src/index.ts", "src/config.ts", ...] }
 ```
 
@@ -176,7 +176,7 @@ User: "I made some changes. Update the codemaps."
 
 Orchestrator: "Checking for changes..."
 
-Orchestrator calls: cartography changes src --extensions ts
+Orchestrator calls: cartography changes src --extensions ts --exclude tests
 
 Response: {
   folder: "src",

+ 58 - 21
scripts/cartography.ts

@@ -1,6 +1,6 @@
 #!/usr/bin/env bun
 import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
-import { join, resolve } from 'node:path';
+import { join, relative, resolve } from 'node:path';
 import { createMD5, md5 } from 'hash-wasm';
 import ignore from 'ignore';
 
@@ -14,6 +14,10 @@ interface CodemapData {
   f: FileEntry[];
 }
 
+interface RootCodemapData {
+  folders: Record<string, CodemapData>;
+}
+
 const DEFAULT_IGNORE = [
   'node_modules',
   '.git',
@@ -27,15 +31,20 @@ const DEFAULT_IGNORE = [
   '.DS_Store',
 ];
 
-function parseGitignore(folder: string): ignore.Ignore {
+function parseGitignore(folder: string, extraIgnores: string[]): ignore.Ignore {
   const gitignorePath = join(folder, '.gitignore');
+  const ig = ignore();
+
+  if (extraIgnores.length > 0) {
+    ig.add(extraIgnores);
+  }
 
   if (existsSync(gitignorePath)) {
     const content = readFileSync(gitignorePath, 'utf-8');
-    return ignore().add(content.split('\n'));
+    ig.add(content.split('\n'));
   }
 
-  return ignore();
+  return ig;
 }
 
 function shouldIgnore(relPath: string, ignorer: ignore.Ignore): boolean {
@@ -117,20 +126,27 @@ async function calculateFolderHash(
   return hasher.digest();
 }
 
-function readCodemapData(codemapPath: string): CodemapData | null {
+function readRootCodemapData(codemapPath: string): RootCodemapData {
   if (!existsSync(codemapPath)) {
-    return null;
+    return { folders: {} };
   }
 
   try {
     const content = readFileSync(codemapPath, 'utf-8');
-    return JSON.parse(content) as CodemapData;
+    const parsed = JSON.parse(content) as RootCodemapData;
+    if (!parsed.folders) {
+      return { folders: {} };
+    }
+    return parsed;
   } catch {
-    return null;
+    return { folders: {} };
   }
 }
 
-function writeCodemapData(codemapPath: string, data: CodemapData): void {
+function writeRootCodemapData(
+  codemapPath: string,
+  data: RootCodemapData,
+): void {
   const content = `${JSON.stringify(data, null, 2)}\n`;
   writeFileSync(codemapPath, content, 'utf-8');
 }
@@ -164,14 +180,18 @@ function diffFiles(
 async function updateCodemap(
   folder: string,
   extensions: string[],
+  extraIgnores: string[],
 ): Promise<{ updated: boolean; fileCount: number; changedFiles: string[] }> {
-  const ignorer = parseGitignore(folder);
+  const ignorer = parseGitignore(folder, extraIgnores);
   const files = getFiles(folder, extensions, ignorer);
   const fileHashes = await calculateHashes(folder, files);
   const folderHash = await calculateFolderHash(fileHashes);
 
-  const codemapPath = join(folder, '.codemap.json');
-  const existing = readCodemapData(codemapPath);
+  const rootPath = process.cwd();
+  const codemapPath = join(rootPath, '.codemap.json');
+  const rootData = readRootCodemapData(codemapPath);
+  const folderKey = relative(rootPath, folder) || '.';
+  const existing = rootData.folders[folderKey];
 
   if (existing?.h === folderHash) {
     return { updated: false, fileCount: files.length, changedFiles: [] };
@@ -183,7 +203,8 @@ async function updateCodemap(
     f: files.map((p) => ({ p, h: fileHashes.get(p)! })),
   };
 
-  writeCodemapData(codemapPath, data);
+  rootData.folders[folderKey] = data;
+  writeRootCodemapData(codemapPath, rootData);
 
   return { updated: true, fileCount: files.length, changedFiles };
 }
@@ -191,17 +212,21 @@ async function updateCodemap(
 async function getChanges(
   folder: string,
   extensions: string[],
+  extraIgnores: string[],
 ): Promise<{
   fileCount: number;
   folderHash: string;
   changedFiles: string[];
 }> {
-  const ignorer = parseGitignore(folder);
+  const ignorer = parseGitignore(folder, extraIgnores);
   const files = getFiles(folder, extensions, ignorer);
   const fileHashes = await calculateHashes(folder, files);
   const folderHash = await calculateFolderHash(fileHashes);
-  const codemapPath = join(folder, '.codemap.json');
-  const existing = readCodemapData(codemapPath);
+  const rootPath = process.cwd();
+  const codemapPath = join(rootPath, '.codemap.json');
+  const rootData = readRootCodemapData(codemapPath);
+  const folderKey = relative(rootPath, folder) || '.';
+  const existing = rootData.folders[folderKey];
   const changedFiles = diffFiles(fileHashes, existing);
 
   return {
@@ -217,7 +242,9 @@ async function main() {
   const folder = folderArg ? resolve(folderArg) : process.cwd();
 
   const extArg = process.argv.find((a) => a.startsWith('--extensions'));
+  const excludeArg = process.argv.find((a) => a.startsWith('--exclude'));
   let extensions: string[];
+  let extraIgnores: string[] = [];
 
   if (extArg) {
     const extList = extArg.split('=')[1];
@@ -232,16 +259,26 @@ async function main() {
     extensions = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs'];
   }
 
+  if (excludeArg) {
+    const excludeList = excludeArg.split('=')[1];
+    if (excludeList) {
+      extraIgnores = excludeList
+        .split(',')
+        .map((e) => e.trim())
+        .filter(Boolean);
+    }
+  }
+
   switch (command) {
     case 'scan': {
-      const ignorer = parseGitignore(folder);
+      const ignorer = parseGitignore(folder, extraIgnores);
       const files = getFiles(folder, extensions, ignorer);
       console.log(JSON.stringify({ folder, files }, null, 2));
       break;
     }
 
     case 'hash': {
-      const ignorer = parseGitignore(folder);
+      const ignorer = parseGitignore(folder, extraIgnores);
       const files = getFiles(folder, extensions, ignorer);
       const fileHashes = await calculateHashes(folder, files);
       const folderHash = await calculateFolderHash(fileHashes);
@@ -259,7 +296,7 @@ async function main() {
     }
 
     case 'update': {
-      const result = await updateCodemap(folder, extensions);
+      const result = await updateCodemap(folder, extensions, extraIgnores);
       if (result.updated) {
         console.log(
           JSON.stringify(
@@ -290,7 +327,7 @@ async function main() {
     }
 
     case 'changes': {
-      const result = await getChanges(folder, extensions);
+      const result = await getChanges(folder, extensions, extraIgnores);
       console.log(
         JSON.stringify(
           {
@@ -309,7 +346,7 @@ async function main() {
 
     default:
       console.error(
-        'Usage: cartography <scan|hash|update|changes> [folder] [--extensions ts,tsx,js]',
+        'Usage: cartography <scan|hash|update|changes> [folder] [--extensions ts,tsx,js] [--exclude tests,dist]',
       );
       process.exit(1);
   }

+ 11 - 0
src/tools/cartography/tool.ts

@@ -25,6 +25,12 @@ export function createCartographyTool(ctx: PluginInput): ToolDefinition {
         .describe(
           'File extensions to map, comma-separated without dots (e.g., "ts,tsx,js")',
         ),
+      exclude: tool.schema
+        .string()
+        .optional()
+        .describe(
+          'Additional ignore patterns, comma-separated (e.g., "tests,**/*.spec.ts")',
+        ),
     },
     execute: async (args, toolContext) => {
       const sessionDir = await getSessionDirectory(ctx, toolContext);
@@ -33,6 +39,7 @@ export function createCartographyTool(ctx: PluginInput): ToolDefinition {
       const scriptPath = join(ctx.directory, 'scripts/cartography.ts');
 
       const extensions = (args.extensions as string) || 'ts,tsx,js,jsx';
+      const exclude = (args.exclude as string) || '';
       const commandArgs = [
         'run',
         scriptPath,
@@ -41,6 +48,10 @@ export function createCartographyTool(ctx: PluginInput): ToolDefinition {
         `--extensions=${extensions}`,
       ];
 
+      if (exclude) {
+        commandArgs.push(`--exclude=${exclude}`);
+      }
+
       const result = await Bun.$`bun ${commandArgs}`.cwd(sessionDir);
 
       try {

+ 4 - 3
src/tools/skill/builtin.ts

@@ -157,7 +157,8 @@ You are a code cartographer. Your mission is to create structured codemaps that
 
 When the user asks for codemaps or updates, you orchestrate the workflow:
 - Call the \`cartography\` tool with \`scan\` to understand folder structure and decide priority folders and extensions.
-- For each target folder, run \`cartography update <folder> --extensions ...\`.
+- Use \`--exclude\` when the user wants to skip folders (e.g., \`tests\`, \`docs\`).
+- For each target folder, run \`cartography update <folder> --extensions ...\` to refresh the root \`.codemap.json\`.
 - If \`updated: false\`, skip analysis for that folder.
 - If \`updated: true\`, use \`changedFiles\` to decide which files need re-analysis.
 - Dispatch Explorer agents to update the body content of \`codemap.md\` (leaf folders first, then parents).
@@ -204,11 +205,11 @@ Use this structure:
 - Avoid listing function parameters (they change often)
 - Document flows and relationships, not signatures
 - Be concise but informative
-- Reference the \`.codemap.json\` hashes for change tracking
+- Reference the root \`.codemap.json\` hashes for change tracking
 
 ## Hash Storage
 
-The helper script manages hashes in \`.codemap.json\`. You only update the body content when needed. Check \`.codemap.json\` or the \`changedFiles\` list to see which files changed since the last update.
+The helper script manages hashes in a root \`.codemap.json\`. You only update the body content when needed. Check \`.codemap.json\` or the \`changedFiles\` list to see which files changed since the last update.
 `;
 
 const PLAYWRIGHT_TEMPLATE = `# Playwright Browser Automation Skill