nogrep 1.0.0

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.
Files changed (48) hide show
  1. package/README.md +91 -0
  2. package/commands/init.md +241 -0
  3. package/commands/off.md +11 -0
  4. package/commands/on.md +21 -0
  5. package/commands/query.md +13 -0
  6. package/commands/status.md +15 -0
  7. package/commands/update.md +89 -0
  8. package/dist/chunk-SMUAF6SM.js +12 -0
  9. package/dist/chunk-SMUAF6SM.js.map +1 -0
  10. package/dist/query.d.ts +12 -0
  11. package/dist/query.js +272 -0
  12. package/dist/query.js.map +1 -0
  13. package/dist/settings.d.ts +6 -0
  14. package/dist/settings.js +75 -0
  15. package/dist/settings.js.map +1 -0
  16. package/dist/signals.d.ts +9 -0
  17. package/dist/signals.js +174 -0
  18. package/dist/signals.js.map +1 -0
  19. package/dist/trim.d.ts +3 -0
  20. package/dist/trim.js +266 -0
  21. package/dist/trim.js.map +1 -0
  22. package/dist/types.d.ts +141 -0
  23. package/dist/types.js +7 -0
  24. package/dist/types.js.map +1 -0
  25. package/dist/validate.d.ts +10 -0
  26. package/dist/validate.js +143 -0
  27. package/dist/validate.js.map +1 -0
  28. package/dist/write.d.ts +8 -0
  29. package/dist/write.js +267 -0
  30. package/dist/write.js.map +1 -0
  31. package/docs/ARCHITECTURE.md +239 -0
  32. package/docs/CLAUDE.md +161 -0
  33. package/docs/CONVENTIONS.md +162 -0
  34. package/docs/SPEC.md +803 -0
  35. package/docs/TASKS.md +216 -0
  36. package/hooks/hooks.json +35 -0
  37. package/hooks/pre-tool-use.sh +37 -0
  38. package/hooks/prompt-submit.sh +26 -0
  39. package/hooks/session-start.sh +21 -0
  40. package/package.json +24 -0
  41. package/scripts/query.ts +290 -0
  42. package/scripts/settings.ts +98 -0
  43. package/scripts/signals.ts +237 -0
  44. package/scripts/trim.ts +379 -0
  45. package/scripts/types.ts +186 -0
  46. package/scripts/validate.ts +181 -0
  47. package/scripts/write.ts +346 -0
  48. package/templates/claude-md-patch.md +8 -0
package/dist/write.js ADDED
@@ -0,0 +1,267 @@
1
+ // scripts/write.ts
2
+ import { readFile, writeFile, mkdir } from "fs/promises";
3
+ import { join, resolve, dirname } from "path";
4
+ import { execFile } from "child_process";
5
+ import { promisify } from "util";
6
+ import yaml from "js-yaml";
7
+ var execFileAsync = promisify(execFile);
8
+ function extractManualNotes(content) {
9
+ const match = content.match(
10
+ /## Manual Notes\n([\s\S]*?)(?=\n## |\n---|\s*$)/
11
+ );
12
+ return match ? match[1].trim() : "";
13
+ }
14
+ function buildNodeFrontmatter(node) {
15
+ return {
16
+ id: node.id,
17
+ title: node.title,
18
+ category: node.category,
19
+ tags: {
20
+ domain: node.tags.domain,
21
+ layer: node.tags.layer,
22
+ tech: node.tags.tech,
23
+ concern: node.tags.concern,
24
+ type: node.tags.type
25
+ },
26
+ relates_to: node.relatesTo.map((r) => ({ id: r.id, reason: r.reason })),
27
+ inverse_relations: node.inverseRelations.map((r) => ({ id: r.id, reason: r.reason })),
28
+ src_paths: node.srcPaths,
29
+ keywords: node.keywords,
30
+ last_synced: {
31
+ commit: node.lastSynced.commit,
32
+ timestamp: node.lastSynced.timestamp,
33
+ src_hash: node.lastSynced.srcHash
34
+ }
35
+ };
36
+ }
37
+ function buildNodeMarkdown(node, manualNotes) {
38
+ const fm = buildNodeFrontmatter(node);
39
+ const yamlStr = yaml.dump(fm, { lineWidth: -1, quotingType: '"', forceQuotes: false });
40
+ const sections = [];
41
+ sections.push(`---
42
+ ${yamlStr.trimEnd()}
43
+ ---`);
44
+ sections.push(`
45
+ ## Purpose
46
+ ${node.purpose}`);
47
+ if (node.publicSurface.length > 0) {
48
+ sections.push(`
49
+ ## Public Surface
50
+
51
+ \`\`\`
52
+ ${node.publicSurface.join("\n")}
53
+ \`\`\``);
54
+ }
55
+ if (node.doesNotOwn.length > 0) {
56
+ sections.push(`
57
+ ## Does Not Own
58
+ ${node.doesNotOwn.map((d) => `- ${d}`).join("\n")}`);
59
+ }
60
+ if (node.gotchas.length > 0) {
61
+ sections.push(`
62
+ ## Gotchas
63
+ ${node.gotchas.map((g) => `- ${g}`).join("\n")}`);
64
+ }
65
+ const notesContent = manualNotes || "_Human annotations. Never overwritten by nogrep update._";
66
+ sections.push(`
67
+ ## Manual Notes
68
+ ${notesContent}`);
69
+ return sections.join("\n") + "\n";
70
+ }
71
+ function categoryDir(category) {
72
+ switch (category) {
73
+ case "domain":
74
+ return "domains";
75
+ case "architecture":
76
+ return "architecture";
77
+ case "flow":
78
+ return "flows";
79
+ case "entity":
80
+ return "entities";
81
+ }
82
+ }
83
+ async function writeContextNodes(nodes, outputDir) {
84
+ for (const node of nodes) {
85
+ const dir = join(outputDir, categoryDir(node.category));
86
+ await mkdir(dir, { recursive: true });
87
+ const filePath = join(dir, `${node.id}.md`);
88
+ let manualNotes = "";
89
+ try {
90
+ const existing = await readFile(filePath, "utf-8");
91
+ manualNotes = extractManualNotes(existing);
92
+ } catch {
93
+ }
94
+ const content = buildNodeMarkdown(node, manualNotes);
95
+ await writeFile(filePath, content, "utf-8");
96
+ }
97
+ }
98
+ function buildIndex(nodes, stack) {
99
+ const tags = {};
100
+ const keywords = {};
101
+ const paths = {};
102
+ const inverseMap = /* @__PURE__ */ new Map();
103
+ for (const node of nodes) {
104
+ for (const rel of node.relatesTo) {
105
+ const existing = inverseMap.get(rel.id) ?? [];
106
+ existing.push({ fromId: node.id, reason: rel.reason });
107
+ inverseMap.set(rel.id, existing);
108
+ }
109
+ }
110
+ for (const node of nodes) {
111
+ const contextFile = `.nogrep/${categoryDir(node.category)}/${node.id}.md`;
112
+ const inverseEntries = inverseMap.get(node.id) ?? [];
113
+ for (const inv of inverseEntries) {
114
+ if (!node.inverseRelations.some((r) => r.id === inv.fromId)) {
115
+ node.inverseRelations.push({ id: inv.fromId, reason: inv.reason });
116
+ }
117
+ }
118
+ const tagCategories = [
119
+ ["domain", node.tags.domain],
120
+ ["layer", node.tags.layer],
121
+ ["tech", node.tags.tech],
122
+ ["concern", node.tags.concern],
123
+ ["type", node.tags.type]
124
+ ];
125
+ const flatTags = [];
126
+ for (const [cat, values] of tagCategories) {
127
+ for (const val of values) {
128
+ const tagKey = `${cat}:${val}`;
129
+ flatTags.push(tagKey);
130
+ const tagList = tags[tagKey] ?? [];
131
+ if (!tagList.includes(contextFile)) {
132
+ tagList.push(contextFile);
133
+ }
134
+ tags[tagKey] = tagList;
135
+ }
136
+ }
137
+ for (const kw of node.keywords) {
138
+ const kwList = keywords[kw] ?? [];
139
+ if (!kwList.includes(contextFile)) {
140
+ kwList.push(contextFile);
141
+ }
142
+ keywords[kw] = kwList;
143
+ }
144
+ for (const srcPath of node.srcPaths) {
145
+ paths[srcPath] = { context: contextFile, tags: flatTags };
146
+ }
147
+ }
148
+ let commit = "";
149
+ try {
150
+ } catch {
151
+ }
152
+ return {
153
+ version: "1.0",
154
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
155
+ commit,
156
+ stack: {
157
+ primaryLanguage: stack.primaryLanguage,
158
+ frameworks: stack.frameworks,
159
+ architecture: stack.architecture
160
+ },
161
+ tags,
162
+ keywords,
163
+ paths
164
+ };
165
+ }
166
+ function buildRegistry(nodes) {
167
+ const mappings = nodes.flatMap(
168
+ (node) => node.srcPaths.map((srcPath) => ({
169
+ glob: srcPath,
170
+ contextFile: `.nogrep/${categoryDir(node.category)}/${node.id}.md`,
171
+ watch: true
172
+ }))
173
+ );
174
+ return { mappings };
175
+ }
176
+ async function patchClaudeMd(projectRoot) {
177
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
178
+ const patchPath = join(dirname(import.meta.url.replace("file://", "")), "..", "templates", "claude-md-patch.md");
179
+ let patch;
180
+ try {
181
+ patch = await readFile(patchPath, "utf-8");
182
+ } catch {
183
+ patch = [
184
+ "<!-- nogrep -->",
185
+ "## Code Navigation",
186
+ "",
187
+ "This project uses [nogrep](https://github.com/techtulp/nogrep).",
188
+ "Context files in `.nogrep/` are a navigable index of this codebase.",
189
+ "When you see nogrep results injected into your context, trust them \u2014",
190
+ "read those files before exploring source.",
191
+ "<!-- /nogrep -->"
192
+ ].join("\n") + "\n";
193
+ }
194
+ let existing = "";
195
+ try {
196
+ existing = await readFile(claudeMdPath, "utf-8");
197
+ } catch {
198
+ }
199
+ if (existing.includes("<!-- nogrep -->")) {
200
+ return;
201
+ }
202
+ const newContent = existing ? existing.trimEnd() + "\n\n" + patch : patch;
203
+ await writeFile(claudeMdPath, newContent, "utf-8");
204
+ }
205
+ async function writeAll(input, projectRoot) {
206
+ const outputDir = join(projectRoot, ".nogrep");
207
+ await mkdir(outputDir, { recursive: true });
208
+ await writeContextNodes(input.nodes, outputDir);
209
+ const index = buildIndex(input.nodes, input.stack);
210
+ try {
211
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], {
212
+ cwd: projectRoot
213
+ });
214
+ index.commit = stdout.trim();
215
+ } catch {
216
+ }
217
+ await writeFile(
218
+ join(outputDir, "_index.json"),
219
+ JSON.stringify(index, null, 2) + "\n",
220
+ "utf-8"
221
+ );
222
+ const registry = buildRegistry(input.nodes);
223
+ await writeFile(
224
+ join(outputDir, "_registry.json"),
225
+ JSON.stringify(registry, null, 2) + "\n",
226
+ "utf-8"
227
+ );
228
+ await patchClaudeMd(projectRoot);
229
+ }
230
+ async function main() {
231
+ const args = process.argv.slice(2);
232
+ let inputFile;
233
+ let projectRoot = process.cwd();
234
+ for (let i = 0; i < args.length; i++) {
235
+ if (args[i] === "--input" && args[i + 1]) {
236
+ inputFile = args[i + 1];
237
+ i++;
238
+ } else if (args[i] === "--root" && args[i + 1]) {
239
+ projectRoot = resolve(args[i + 1]);
240
+ i++;
241
+ }
242
+ }
243
+ let rawInput;
244
+ if (inputFile) {
245
+ rawInput = await readFile(resolve(inputFile), "utf-8");
246
+ } else {
247
+ const chunks = [];
248
+ for await (const chunk of process.stdin) {
249
+ chunks.push(chunk);
250
+ }
251
+ rawInput = Buffer.concat(chunks).toString("utf-8");
252
+ }
253
+ const input = JSON.parse(rawInput);
254
+ await writeAll(input, projectRoot);
255
+ }
256
+ main().catch((err) => {
257
+ const message = err instanceof Error ? err.message : String(err);
258
+ process.stderr.write(JSON.stringify({ error: message }) + "\n");
259
+ process.exitCode = 1;
260
+ });
261
+ export {
262
+ buildIndex,
263
+ buildRegistry,
264
+ patchClaudeMd,
265
+ writeContextNodes
266
+ };
267
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../scripts/write.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'\nimport { join, resolve, dirname } from 'node:path'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { createHash } from 'node:crypto'\nimport { glob } from 'glob'\nimport matter from 'gray-matter'\nimport yaml from 'js-yaml'\nimport type {\n NodeResult,\n StackResult,\n IndexJson,\n RegistryJson,\n PathEntry,\n NogrepError,\n} from './types.js'\n\nconst execFileAsync = promisify(execFile)\n\n// --- Manual Notes preservation ---\n\nfunction extractManualNotes(content: string): string {\n const match = content.match(\n /## Manual Notes\\n([\\s\\S]*?)(?=\\n## |\\n---|\\s*$)/,\n )\n return match ? match[1]!.trim() : ''\n}\n\n// --- Context node markdown generation ---\n\nfunction buildNodeFrontmatter(node: NodeResult): Record<string, unknown> {\n return {\n id: node.id,\n title: node.title,\n category: node.category,\n tags: {\n domain: node.tags.domain,\n layer: node.tags.layer,\n tech: node.tags.tech,\n concern: node.tags.concern,\n type: node.tags.type,\n },\n relates_to: node.relatesTo.map(r => ({ id: r.id, reason: r.reason })),\n inverse_relations: node.inverseRelations.map(r => ({ id: r.id, reason: r.reason })),\n src_paths: node.srcPaths,\n keywords: node.keywords,\n last_synced: {\n commit: node.lastSynced.commit,\n timestamp: node.lastSynced.timestamp,\n src_hash: node.lastSynced.srcHash,\n },\n }\n}\n\nfunction buildNodeMarkdown(node: NodeResult, manualNotes: string): string {\n const fm = buildNodeFrontmatter(node)\n const yamlStr = yaml.dump(fm, { lineWidth: -1, quotingType: '\"', forceQuotes: false })\n\n const sections: string[] = []\n sections.push(`---\\n${yamlStr.trimEnd()}\\n---`)\n\n sections.push(`\\n## Purpose\\n${node.purpose}`)\n\n if (node.publicSurface.length > 0) {\n sections.push(`\\n## Public Surface\\n\\n\\`\\`\\`\\n${node.publicSurface.join('\\n')}\\n\\`\\`\\``)\n }\n\n if (node.doesNotOwn.length > 0) {\n sections.push(`\\n## Does Not Own\\n${node.doesNotOwn.map(d => `- ${d}`).join('\\n')}`)\n }\n\n if (node.gotchas.length > 0) {\n sections.push(`\\n## Gotchas\\n${node.gotchas.map(g => `- ${g}`).join('\\n')}`)\n }\n\n const notesContent = manualNotes || '_Human annotations. Never overwritten by nogrep update._'\n sections.push(`\\n## Manual Notes\\n${notesContent}`)\n\n return sections.join('\\n') + '\\n'\n}\n\n// --- Write context files ---\n\nfunction categoryDir(category: NodeResult['category']): string {\n switch (category) {\n case 'domain': return 'domains'\n case 'architecture': return 'architecture'\n case 'flow': return 'flows'\n case 'entity': return 'entities'\n }\n}\n\nexport async function writeContextNodes(\n nodes: NodeResult[],\n outputDir: string,\n): Promise<void> {\n for (const node of nodes) {\n const dir = join(outputDir, categoryDir(node.category))\n await mkdir(dir, { recursive: true })\n\n const filePath = join(dir, `${node.id}.md`)\n\n // Preserve existing manual notes\n let manualNotes = ''\n try {\n const existing = await readFile(filePath, 'utf-8')\n manualNotes = extractManualNotes(existing)\n } catch {\n // File doesn't exist yet\n }\n\n const content = buildNodeMarkdown(node, manualNotes)\n await writeFile(filePath, content, 'utf-8')\n }\n}\n\n// --- Build index ---\n\nexport function buildIndex(\n nodes: NodeResult[],\n stack: Pick<StackResult, 'primaryLanguage' | 'frameworks' | 'architecture'>,\n): IndexJson {\n const tags: Record<string, string[]> = {}\n const keywords: Record<string, string[]> = {}\n const paths: Record<string, PathEntry> = {}\n\n // Populate inverse relations\n const inverseMap = new Map<string, Array<{ fromId: string; reason: string }>>()\n for (const node of nodes) {\n for (const rel of node.relatesTo) {\n const existing = inverseMap.get(rel.id) ?? []\n existing.push({ fromId: node.id, reason: rel.reason })\n inverseMap.set(rel.id, existing)\n }\n }\n\n for (const node of nodes) {\n const contextFile = `.nogrep/${categoryDir(node.category)}/${node.id}.md`\n\n // Merge inverse relations from the map\n const inverseEntries = inverseMap.get(node.id) ?? []\n for (const inv of inverseEntries) {\n if (!node.inverseRelations.some(r => r.id === inv.fromId)) {\n node.inverseRelations.push({ id: inv.fromId, reason: inv.reason })\n }\n }\n\n // Build tag index\n const tagCategories: Array<[string, string[]]> = [\n ['domain', node.tags.domain],\n ['layer', node.tags.layer],\n ['tech', node.tags.tech],\n ['concern', node.tags.concern],\n ['type', node.tags.type],\n ]\n\n const flatTags: string[] = []\n for (const [cat, values] of tagCategories) {\n for (const val of values) {\n const tagKey = `${cat}:${val}`\n flatTags.push(tagKey)\n const tagList = tags[tagKey] ?? []\n if (!tagList.includes(contextFile)) {\n tagList.push(contextFile)\n }\n tags[tagKey] = tagList\n }\n }\n\n // Build keyword index\n for (const kw of node.keywords) {\n const kwList = keywords[kw] ?? []\n if (!kwList.includes(contextFile)) {\n kwList.push(contextFile)\n }\n keywords[kw] = kwList\n }\n\n // Build path index\n for (const srcPath of node.srcPaths) {\n paths[srcPath] = { context: contextFile, tags: flatTags }\n }\n }\n\n let commit = ''\n try {\n // Sync exec not ideal but this is a one-time build step\n // We'll set it from the caller if available\n } catch {\n // no git\n }\n\n return {\n version: '1.0',\n generatedAt: new Date().toISOString(),\n commit,\n stack: {\n primaryLanguage: stack.primaryLanguage,\n frameworks: stack.frameworks,\n architecture: stack.architecture,\n },\n tags,\n keywords,\n paths,\n }\n}\n\n// --- Build registry ---\n\nexport function buildRegistry(nodes: NodeResult[]): RegistryJson {\n const mappings = nodes.flatMap(node =>\n node.srcPaths.map(srcPath => ({\n glob: srcPath,\n contextFile: `.nogrep/${categoryDir(node.category)}/${node.id}.md`,\n watch: true,\n })),\n )\n\n return { mappings }\n}\n\n// --- Patch CLAUDE.md ---\n\nexport async function patchClaudeMd(projectRoot: string): Promise<void> {\n const claudeMdPath = join(projectRoot, 'CLAUDE.md')\n const patchPath = join(dirname(import.meta.url.replace('file://', '')), '..', 'templates', 'claude-md-patch.md')\n\n let patch: string\n try {\n patch = await readFile(patchPath, 'utf-8')\n } catch {\n // Fallback: inline the patch content\n patch = [\n '<!-- nogrep -->',\n '## Code Navigation',\n '',\n 'This project uses [nogrep](https://github.com/techtulp/nogrep).',\n 'Context files in `.nogrep/` are a navigable index of this codebase.',\n 'When you see nogrep results injected into your context, trust them —',\n 'read those files before exploring source.',\n '<!-- /nogrep -->',\n ].join('\\n') + '\\n'\n }\n\n let existing = ''\n try {\n existing = await readFile(claudeMdPath, 'utf-8')\n } catch {\n // File doesn't exist\n }\n\n // Check for existing nogrep marker\n if (existing.includes('<!-- nogrep -->')) {\n return\n }\n\n const newContent = existing\n ? existing.trimEnd() + '\\n\\n' + patch\n : patch\n\n await writeFile(claudeMdPath, newContent, 'utf-8')\n}\n\n// --- Write all outputs ---\n\ninterface WriteInput {\n nodes: NodeResult[]\n stack: Pick<StackResult, 'primaryLanguage' | 'frameworks' | 'architecture'>\n}\n\nasync function writeAll(input: WriteInput, projectRoot: string): Promise<void> {\n const outputDir = join(projectRoot, '.nogrep')\n await mkdir(outputDir, { recursive: true })\n\n // Write context node files\n await writeContextNodes(input.nodes, outputDir)\n\n // Build and write index\n const index = buildIndex(input.nodes, input.stack)\n\n // Try to get current git commit\n try {\n const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], {\n cwd: projectRoot,\n })\n index.commit = stdout.trim()\n } catch {\n // no git\n }\n\n await writeFile(\n join(outputDir, '_index.json'),\n JSON.stringify(index, null, 2) + '\\n',\n 'utf-8',\n )\n\n // Build and write registry\n const registry = buildRegistry(input.nodes)\n await writeFile(\n join(outputDir, '_registry.json'),\n JSON.stringify(registry, null, 2) + '\\n',\n 'utf-8',\n )\n\n // Patch CLAUDE.md\n await patchClaudeMd(projectRoot)\n}\n\n// --- CLI interface ---\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2)\n let inputFile: string | undefined\n let projectRoot = process.cwd()\n\n for (let i = 0; i < args.length; i++) {\n if (args[i] === '--input' && args[i + 1]) {\n inputFile = args[i + 1]!\n i++\n } else if (args[i] === '--root' && args[i + 1]) {\n projectRoot = resolve(args[i + 1]!)\n i++\n }\n }\n\n let rawInput: string\n if (inputFile) {\n rawInput = await readFile(resolve(inputFile), 'utf-8')\n } else {\n // Read from stdin\n const chunks: Buffer[] = []\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer)\n }\n rawInput = Buffer.concat(chunks).toString('utf-8')\n }\n\n const input = JSON.parse(rawInput) as WriteInput\n await writeAll(input, projectRoot)\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err)\n process.stderr.write(JSON.stringify({ error: message }) + '\\n')\n process.exitCode = 1\n})\n"],"mappings":"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","names":[]}
@@ -0,0 +1,239 @@
1
+ # nogrep — Internal Architecture
2
+
3
+ ## Module Boundaries
4
+
5
+ ```
6
+ ┌──────────────────────────────────────────────────────┐
7
+ │ CC Plugin (slash commands) │
8
+ │ /init · /update · /query · /status · /on · /off │
9
+ │ Claude orchestrates — AI work happens here │
10
+ └────────────────────────┬─────────────────────────────┘
11
+ │ calls scripts via Bash
12
+ ┌───────────────┼───────────────┐
13
+ ▼ ▼ ▼
14
+ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐
15
+ │ Signals │ │ Query │ │ Settings │
16
+ │ (collect) │ │ (lookup) │ │ (r/w JSON) │
17
+ └─────────────┘ └──────┬──────┘ └──────────────┘
18
+
19
+ ┌─────────────┐ ┌──────┴──────┐
20
+ │ Writer │ │ Validator │
21
+ │ (file I/O) │ │ (freshness) │
22
+ └─────────────┘ └─────────────┘
23
+ ```
24
+
25
+ No AI client module — Claude IS the AI. The slash commands contain the analysis prompts, and Claude executes them directly during the session.
26
+
27
+ ---
28
+
29
+ ## Module Responsibilities
30
+
31
+ ### `commands/` (slash commands)
32
+ Markdown prompts that guide Claude through each operation. `init.md` is the most complex — it orchestrates the full pipeline. Claude reads script output, performs analysis, and writes results.
33
+
34
+ ### `scripts/signals.ts`
35
+ Collects language-agnostic signals from the filesystem. Pure data collection — no AI, no writes to `.nogrep/`.
36
+
37
+ ```typescript
38
+ collectSignals(root, options) → SignalResult
39
+ ```
40
+
41
+ ### `scripts/write.ts`
42
+ All file I/O for the `.nogrep/` directory. Takes structured JSON input (from Claude's analysis), writes files.
43
+
44
+ ```typescript
45
+ writeContextFiles(nodes: NodeResult[], outputDir: string) → void
46
+ buildIndex(nodes: NodeResult[]) → IndexJson
47
+ buildRegistry(nodes: NodeResult[]) → RegistryJson
48
+ patchClaudeMd(projectRoot: string) → void
49
+ ```
50
+
51
+ ### `scripts/query.ts`
52
+ Pure lookup logic. Reads `_index.json`, matches tags/keywords, ranks results. No AI, no file writes. Called by hooks and `/nogrep:query`.
53
+
54
+ ```typescript
55
+ extractTerms(question: string, taxonomy: Taxonomy) → { tags, keywords }
56
+ resolve(terms, index) → RankedResult[]
57
+ ```
58
+
59
+ ### `scripts/validate.ts`
60
+ Computes SHA256 of `src_paths` contents, compares to stored `src_hash` in node frontmatter.
61
+
62
+ ```typescript
63
+ checkFreshness(node: ContextNode, projectRoot: string) → StaleResult
64
+ ```
65
+
66
+ ### `scripts/settings.ts`
67
+ Read/write `.claude/settings.json` and `.claude/settings.local.json`. Handles merge logic (local takes precedence).
68
+
69
+ ---
70
+
71
+ ## Data Flow: `/nogrep:init`
72
+
73
+ > `$PLUGIN` = `${CLAUDE_PLUGIN_ROOT}` — the absolute path to the installed plugin directory.
74
+
75
+ ```
76
+ Slash command: init.md (Claude orchestrates)
77
+
78
+ ├─→ Bash: node $PLUGIN/dist/signals.js → SignalResult (JSON stdout)
79
+
80
+ ├─→ Claude analyzes signals → StackResult
81
+
82
+ ├─→ For each cluster:
83
+ │ Claude reads trimmed source → NodeResult
84
+
85
+ ├─→ Claude detects flows → FlowResult[]
86
+
87
+ └─→ Bash: node $PLUGIN/dist/write.js (receives JSON stdin)
88
+ writes .nogrep/domains/*.md etc
89
+ writes .nogrep/_index.json
90
+ writes .nogrep/_registry.json
91
+ patches CLAUDE.md
92
+ writes .claude/settings.json
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Data Flow: Hooks
98
+
99
+ ```
100
+ User types prompt
101
+
102
+ └─→ prompt-submit.sh
103
+ node $PLUGIN/dist/query.js --question "$PROMPT"
104
+ → injects additionalContext
105
+
106
+ CC decides to run grep
107
+
108
+ └─→ pre-tool-use.sh (PreToolUse hook)
109
+ extracts keywords from grep command
110
+ node $PLUGIN/dist/query.js --keywords "$KEYWORDS"
111
+ → injects additionalContext
112
+
113
+ CC starts session
114
+
115
+ └─→ session-start.sh (SessionStart hook)
116
+ node $PLUGIN/dist/validate.js
117
+ → injects staleness warning if needed
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Key Types (`scripts/types.ts`)
123
+
124
+ ```typescript
125
+ export interface SignalResult {
126
+ directoryTree: DirectoryNode[]
127
+ extensionMap: Record<string, number>
128
+ manifests: ManifestFile[]
129
+ entryPoints: string[]
130
+ gitChurn: ChurnEntry[]
131
+ largeFiles: FileSize[]
132
+ envFiles: string[]
133
+ testFiles: string[]
134
+ }
135
+
136
+ export interface StackResult {
137
+ primaryLanguage: string
138
+ frameworks: string[]
139
+ architecture: 'monolith' | 'monorepo' | 'multi-repo' | 'microservice' | 'library'
140
+ domainClusters: DomainCluster[]
141
+ conventions: StackConventions
142
+ stackHints: string
143
+ dynamicTaxonomy: { domain: string[]; tech: string[] }
144
+ }
145
+
146
+ export interface DomainCluster {
147
+ name: string
148
+ path: string
149
+ confidence: number
150
+ }
151
+
152
+ export interface NodeResult {
153
+ id: string
154
+ title: string
155
+ category: 'domain' | 'architecture' | 'flow' | 'entity'
156
+ tags: TagSet
157
+ relatesTo: Relation[]
158
+ inverseRelations: Relation[]
159
+ srcPaths: string[]
160
+ keywords: string[]
161
+ lastSynced: SyncMeta
162
+ // content fields
163
+ purpose: string
164
+ publicSurface: string[]
165
+ doesNotOwn: string[]
166
+ externalDeps: ExternalDep[]
167
+ gotchas: string[]
168
+ }
169
+
170
+ export interface TagSet {
171
+ domain: string[]
172
+ layer: string[]
173
+ tech: string[]
174
+ concern: string[]
175
+ type: string[]
176
+ }
177
+
178
+ export interface IndexJson {
179
+ version: string
180
+ generatedAt: string
181
+ commit: string
182
+ stack: Pick<StackResult, 'primaryLanguage' | 'frameworks' | 'architecture'>
183
+ tags: Record<string, string[]>
184
+ keywords: Record<string, string[]>
185
+ paths: Record<string, PathEntry>
186
+ }
187
+
188
+ export interface RankedResult {
189
+ contextFile: string
190
+ score: number
191
+ matchedOn: string[]
192
+ summary: string
193
+ }
194
+
195
+ export interface StaleResult {
196
+ file: string
197
+ isStale: boolean
198
+ reason?: string
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Error Handling Strategy
205
+
206
+ - Scripts: throw typed errors (`NogrepError` with `code` field), exit 1 with JSON error on stderr
207
+ - Hooks: fail silently (exit 0) — never block CC session
208
+ - Never swallow errors silently in scripts
209
+
210
+ ```typescript
211
+ export class NogrepError extends Error {
212
+ constructor(
213
+ message: string,
214
+ public code: 'NO_INDEX' | 'NO_GIT' | 'IO_ERROR' | 'STALE'
215
+ ) {
216
+ super(message)
217
+ }
218
+ }
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Testing Strategy
224
+
225
+ ### Unit tests (no filesystem)
226
+ - `query/extractor.test.ts` — NL extraction logic
227
+ - `query/resolver.test.ts` — index lookup ranking
228
+ - `validator/staleness.test.ts` — hash comparison logic
229
+ - `settings/index.test.ts` — merge logic
230
+
231
+ ### Integration tests (real filesystem)
232
+ - `signals.test.ts` — run against fixture projects
233
+ - `writer/*.test.ts` — write to temp dir, verify file contents
234
+
235
+ ### Fixture projects (`tests/fixtures/`)
236
+ Minimal 5-10 file projects, enough for signal detection:
237
+ - `nestjs-project/` — NestJS with billing + auth modules
238
+ - `django-project/` — Django with users + payments apps
239
+ - `react-project/` — React app with auth + dashboard features