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.
- package/README.md +91 -0
- package/commands/init.md +241 -0
- package/commands/off.md +11 -0
- package/commands/on.md +21 -0
- package/commands/query.md +13 -0
- package/commands/status.md +15 -0
- package/commands/update.md +89 -0
- package/dist/chunk-SMUAF6SM.js +12 -0
- package/dist/chunk-SMUAF6SM.js.map +1 -0
- package/dist/query.d.ts +12 -0
- package/dist/query.js +272 -0
- package/dist/query.js.map +1 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +75 -0
- package/dist/settings.js.map +1 -0
- package/dist/signals.d.ts +9 -0
- package/dist/signals.js +174 -0
- package/dist/signals.js.map +1 -0
- package/dist/trim.d.ts +3 -0
- package/dist/trim.js +266 -0
- package/dist/trim.js.map +1 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.js +143 -0
- package/dist/validate.js.map +1 -0
- package/dist/write.d.ts +8 -0
- package/dist/write.js +267 -0
- package/dist/write.js.map +1 -0
- package/docs/ARCHITECTURE.md +239 -0
- package/docs/CLAUDE.md +161 -0
- package/docs/CONVENTIONS.md +162 -0
- package/docs/SPEC.md +803 -0
- package/docs/TASKS.md +216 -0
- package/hooks/hooks.json +35 -0
- package/hooks/pre-tool-use.sh +37 -0
- package/hooks/prompt-submit.sh +26 -0
- package/hooks/session-start.sh +21 -0
- package/package.json +24 -0
- package/scripts/query.ts +290 -0
- package/scripts/settings.ts +98 -0
- package/scripts/signals.ts +237 -0
- package/scripts/trim.ts +379 -0
- package/scripts/types.ts +186 -0
- package/scripts/validate.ts +181 -0
- package/scripts/write.ts +346 -0
- 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
|