sourcebook 0.1.0 → 0.3.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 CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="sourcebook" width="120" />
3
+ </p>
4
+
1
5
  # sourcebook
2
6
 
3
7
  Generate AI context files from your codebase's actual conventions. Not what agents already know — what they keep missing.
package/dist/cli.js CHANGED
@@ -1,17 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { init } from "./commands/init.js";
4
+ import { update } from "./commands/update.js";
5
+ import { diff } from "./commands/diff.js";
4
6
  const program = new Command();
5
7
  program
6
8
  .name("sourcebook")
7
9
  .description("Extract the conventions, constraints, and architectural truths your AI coding agents keep missing.")
8
- .version("0.1.0");
10
+ .version("0.3.0");
9
11
  program
10
12
  .command("init")
11
13
  .description("Analyze a codebase and generate agent context files")
12
14
  .option("-d, --dir <path>", "Target directory to analyze", ".")
13
- .option("-f, --format <formats>", "Output formats (claude,cursor,copilot,agents,json)", "claude")
15
+ .option("-f, --format <formats>", "Output formats (claude,cursor,copilot,all)", "claude")
14
16
  .option("--budget <tokens>", "Max token budget for generated context", "4000")
15
17
  .option("--dry-run", "Preview findings without writing files")
16
18
  .action(init);
19
+ program
20
+ .command("update")
21
+ .description("Re-analyze and update context files while preserving manual edits")
22
+ .option("-d, --dir <path>", "Target directory to analyze", ".")
23
+ .option("-f, --format <formats>", "Output formats (claude,cursor,copilot,all)", "claude")
24
+ .option("--budget <tokens>", "Max token budget for generated context", "4000")
25
+ .action(update);
26
+ program
27
+ .command("diff")
28
+ .description("Show what would change if context files were regenerated")
29
+ .option("-d, --dir <path>", "Target directory to analyze", ".")
30
+ .option("-f, --format <formats>", "Output format to diff (claude,cursor,copilot)", "claude")
31
+ .option("--budget <tokens>", "Max token budget for generated context", "4000")
32
+ .action(diff);
17
33
  program.parse();
@@ -0,0 +1,12 @@
1
+ interface DiffOptions {
2
+ dir: string;
3
+ format: string;
4
+ budget: string;
5
+ }
6
+ /**
7
+ * Show what would change if sourcebook regenerated the context files.
8
+ * Does not write any files — pure comparison.
9
+ * Exit code: 0 if no changes, 1 if changes found (useful for CI).
10
+ */
11
+ export declare function diff(options: DiffOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { scanProject } from "../scanner/index.js";
5
+ import { generateClaude } from "../generators/claude.js";
6
+ import { generateCursor } from "../generators/cursor.js";
7
+ import { generateCopilot } from "../generators/copilot.js";
8
+ /**
9
+ * Show what would change if sourcebook regenerated the context files.
10
+ * Does not write any files — pure comparison.
11
+ * Exit code: 0 if no changes, 1 if changes found (useful for CI).
12
+ */
13
+ export async function diff(options) {
14
+ const targetDir = path.resolve(options.dir);
15
+ const format = options.format.split(",")[0].trim(); // diff one format at a time
16
+ const budget = parseInt(options.budget, 10);
17
+ console.log(chalk.bold("\nsourcebook diff"));
18
+ console.log(chalk.dim("Comparing current context with fresh analysis...\n"));
19
+ const scan = await scanProject(targetDir);
20
+ const formatMap = {
21
+ claude: {
22
+ generator: () => generateClaude(scan, budget),
23
+ file: "CLAUDE.md",
24
+ },
25
+ cursor: {
26
+ generator: () => generateCursor(scan, budget),
27
+ file: ".cursor/rules/sourcebook.mdc",
28
+ },
29
+ copilot: {
30
+ generator: () => generateCopilot(scan, budget),
31
+ file: ".github/copilot-instructions.md",
32
+ },
33
+ };
34
+ const config = formatMap[format];
35
+ if (!config) {
36
+ console.log(chalk.yellow(`⚠ Format "${format}" not supported for diff`));
37
+ process.exit(1);
38
+ }
39
+ const filePath = path.join(targetDir, config.file);
40
+ let existing;
41
+ try {
42
+ existing = fs.readFileSync(filePath, "utf-8");
43
+ }
44
+ catch {
45
+ console.log(chalk.yellow(`⚠ ${config.file} does not exist yet. Run \`sourcebook init\` first.`));
46
+ process.exit(1);
47
+ }
48
+ const fresh = config.generator();
49
+ if (existing.trim() === fresh.trim()) {
50
+ console.log(chalk.green("✓") + ` ${config.file} is up to date. No changes needed.\n`);
51
+ process.exit(0);
52
+ }
53
+ // Line-by-line diff
54
+ const existingLines = existing.split("\n");
55
+ const freshLines = fresh.split("\n");
56
+ let hasChanges = false;
57
+ console.log(chalk.bold(` ${config.file}\n`));
58
+ // Simple diff: show removed and added lines
59
+ const maxLen = Math.max(existingLines.length, freshLines.length);
60
+ // Build sets for quick lookup
61
+ const existingSet = new Set(existingLines.map((l) => l.trim()).filter(Boolean));
62
+ const freshSet = new Set(freshLines.map((l) => l.trim()).filter(Boolean));
63
+ // Lines only in existing (removed)
64
+ const removed = existingLines.filter((l) => l.trim() && !freshSet.has(l.trim()));
65
+ // Lines only in fresh (added)
66
+ const added = freshLines.filter((l) => l.trim() && !existingSet.has(l.trim()));
67
+ if (removed.length > 0) {
68
+ hasChanges = true;
69
+ console.log(chalk.red(" Removed:"));
70
+ for (const line of removed.slice(0, 20)) {
71
+ console.log(chalk.red(` - ${line}`));
72
+ }
73
+ if (removed.length > 20) {
74
+ console.log(chalk.dim(` ... and ${removed.length - 20} more`));
75
+ }
76
+ console.log("");
77
+ }
78
+ if (added.length > 0) {
79
+ hasChanges = true;
80
+ console.log(chalk.green(" Added:"));
81
+ for (const line of added.slice(0, 20)) {
82
+ console.log(chalk.green(` + ${line}`));
83
+ }
84
+ if (added.length > 20) {
85
+ console.log(chalk.dim(` ... and ${added.length - 20} more`));
86
+ }
87
+ console.log("");
88
+ }
89
+ if (hasChanges) {
90
+ console.log(chalk.dim(" Run `sourcebook update` to apply these changes while preserving your edits.\n"));
91
+ process.exit(1);
92
+ }
93
+ else {
94
+ console.log(chalk.green("✓") + " No meaningful changes detected.\n");
95
+ process.exit(0);
96
+ }
97
+ }
@@ -0,0 +1,17 @@
1
+ interface UpdateOptions {
2
+ dir: string;
3
+ format: string;
4
+ budget: string;
5
+ }
6
+ /**
7
+ * Re-analyze and regenerate context files while preserving manual edits.
8
+ *
9
+ * Strategy:
10
+ * 1. Read existing output file
11
+ * 2. Parse into sections (split on ## headers)
12
+ * 3. Identify manual sections (headers not in SOURCEBOOK_HEADERS)
13
+ * 4. Re-run scan and generate fresh content
14
+ * 5. Replace sourcebook sections, keep manual sections in their original positions
15
+ */
16
+ export declare function update(options: UpdateOptions): Promise<void>;
17
+ export {};
@@ -0,0 +1,177 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { scanProject } from "../scanner/index.js";
5
+ import { generateClaude } from "../generators/claude.js";
6
+ import { generateCursor, generateCursorLegacy } from "../generators/cursor.js";
7
+ import { generateCopilot } from "../generators/copilot.js";
8
+ import { writeOutput } from "../utils/output.js";
9
+ // Headers that sourcebook generates — anything else is user-added
10
+ const SOURCEBOOK_HEADERS = new Set([
11
+ "CLAUDE.md",
12
+ "Commands",
13
+ "Critical Constraints",
14
+ "Stack",
15
+ "Project Structure",
16
+ "Core Modules (by structural importance)",
17
+ "Core Modules",
18
+ "Conventions & Patterns",
19
+ "Conventions",
20
+ "Additional Context",
21
+ "Additional Notes",
22
+ "What to Add Manually",
23
+ "Copilot Instructions",
24
+ "Development Commands",
25
+ "Important Constraints",
26
+ "Technology Stack",
27
+ "High-Impact Files",
28
+ "Code Conventions",
29
+ "Constraints",
30
+ ]);
31
+ /**
32
+ * Re-analyze and regenerate context files while preserving manual edits.
33
+ *
34
+ * Strategy:
35
+ * 1. Read existing output file
36
+ * 2. Parse into sections (split on ## headers)
37
+ * 3. Identify manual sections (headers not in SOURCEBOOK_HEADERS)
38
+ * 4. Re-run scan and generate fresh content
39
+ * 5. Replace sourcebook sections, keep manual sections in their original positions
40
+ */
41
+ export async function update(options) {
42
+ const targetDir = path.resolve(options.dir);
43
+ const formats = options.format.split(",").map((f) => f.trim());
44
+ const budget = parseInt(options.budget, 10);
45
+ console.log(chalk.bold("\nsourcebook update"));
46
+ console.log(chalk.dim("Re-analyzing while preserving your edits...\n"));
47
+ const scan = await scanProject(targetDir);
48
+ console.log(chalk.green("✓") + " Scanned project structure");
49
+ console.log(chalk.dim(` ${scan.files.length} files, ${scan.frameworks.length} frameworks detected`));
50
+ console.log(chalk.green("✓") + ` Extracted ${scan.findings.length} findings\n`);
51
+ for (const format of formats) {
52
+ switch (format) {
53
+ case "claude": {
54
+ const fresh = generateClaude(scan, budget);
55
+ const existing = readExisting(targetDir, "CLAUDE.md");
56
+ const merged = existing ? mergeContent(existing, fresh) : fresh;
57
+ await writeOutput(targetDir, "CLAUDE.md", merged);
58
+ const preserved = existing ? countManualSections(existing) : 0;
59
+ console.log(chalk.green("✓") +
60
+ ` Updated CLAUDE.md` +
61
+ (preserved > 0 ? chalk.dim(` (${preserved} manual section${preserved > 1 ? "s" : ""} preserved)`) : ""));
62
+ break;
63
+ }
64
+ case "cursor": {
65
+ const fresh = generateCursor(scan, budget);
66
+ await writeOutput(targetDir, ".cursor/rules/sourcebook.mdc", fresh);
67
+ const legacyFresh = generateCursorLegacy(scan, budget);
68
+ await writeOutput(targetDir, ".cursorrules", legacyFresh);
69
+ console.log(chalk.green("✓") + " Updated .cursor/rules/sourcebook.mdc");
70
+ break;
71
+ }
72
+ case "copilot": {
73
+ const fresh = generateCopilot(scan, budget);
74
+ const existing = readExisting(targetDir, ".github/copilot-instructions.md");
75
+ const merged = existing ? mergeContent(existing, fresh) : fresh;
76
+ await writeOutput(targetDir, ".github/copilot-instructions.md", merged);
77
+ const preserved = existing ? countManualSections(existing) : 0;
78
+ console.log(chalk.green("✓") +
79
+ ` Updated .github/copilot-instructions.md` +
80
+ (preserved > 0 ? chalk.dim(` (${preserved} manual section${preserved > 1 ? "s" : ""} preserved)`) : ""));
81
+ break;
82
+ }
83
+ case "all": {
84
+ // Recurse for each format
85
+ for (const f of ["claude", "cursor", "copilot"]) {
86
+ await update({ ...options, format: f });
87
+ }
88
+ return;
89
+ }
90
+ default:
91
+ console.log(chalk.yellow(`⚠ Format "${format}" not yet supported`));
92
+ }
93
+ }
94
+ console.log(chalk.dim("\nDone. Your manual sections were preserved.\n"));
95
+ }
96
+ function readExisting(dir, filename) {
97
+ const filePath = path.join(dir, filename);
98
+ try {
99
+ return fs.readFileSync(filePath, "utf-8");
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /**
106
+ * Parse markdown into sections split by ## headers.
107
+ */
108
+ function parseSections(content) {
109
+ const sections = [];
110
+ const lines = content.split("\n");
111
+ let currentHeader = "";
112
+ let currentBody = [];
113
+ for (const line of lines) {
114
+ const headerMatch = line.match(/^##\s+(.+)/);
115
+ if (headerMatch) {
116
+ if (currentHeader || currentBody.length > 0) {
117
+ sections.push({ header: currentHeader, body: currentBody.join("\n") });
118
+ }
119
+ currentHeader = headerMatch[1].trim();
120
+ currentBody = [];
121
+ }
122
+ else {
123
+ currentBody.push(line);
124
+ }
125
+ }
126
+ if (currentHeader || currentBody.length > 0) {
127
+ sections.push({ header: currentHeader, body: currentBody.join("\n") });
128
+ }
129
+ return sections;
130
+ }
131
+ function isSourcebookSection(header) {
132
+ return SOURCEBOOK_HEADERS.has(header) || header === "";
133
+ }
134
+ function countManualSections(content) {
135
+ const sections = parseSections(content);
136
+ return sections.filter((s) => s.header && !isSourcebookSection(s.header)).length;
137
+ }
138
+ /**
139
+ * Merge fresh sourcebook output with existing content,
140
+ * preserving any manually added sections.
141
+ */
142
+ function mergeContent(existing, fresh) {
143
+ const existingSections = parseSections(existing);
144
+ const freshSections = parseSections(fresh);
145
+ // Extract manual sections from existing content
146
+ const manualSections = existingSections.filter((s) => s.header && !isSourcebookSection(s.header));
147
+ if (manualSections.length === 0) {
148
+ // No manual sections — just use the fresh content
149
+ return fresh;
150
+ }
151
+ // Find where "What to Add Manually" or last section is in fresh content
152
+ // Insert manual sections before the footer
153
+ const result = [];
154
+ let insertedManual = false;
155
+ for (const section of freshSections) {
156
+ // Insert manual sections before the "What to Add Manually" footer
157
+ if (section.header === "What to Add Manually" && !insertedManual) {
158
+ for (const manual of manualSections) {
159
+ result.push(`## ${manual.header}`);
160
+ result.push(manual.body);
161
+ }
162
+ insertedManual = true;
163
+ }
164
+ if (section.header) {
165
+ result.push(`## ${section.header}`);
166
+ }
167
+ result.push(section.body);
168
+ }
169
+ // If we never found the footer, append manual sections at the end
170
+ if (!insertedManual) {
171
+ for (const manual of manualSections) {
172
+ result.push(`## ${manual.header}`);
173
+ result.push(manual.body);
174
+ }
175
+ }
176
+ return result.join("\n");
177
+ }
@@ -1,3 +1,4 @@
1
+ import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, } from "./shared.js";
1
2
  /**
2
3
  * Generate a CLAUDE.md file from scan results.
3
4
  *
@@ -8,47 +9,47 @@
8
9
  * 3. Karpathy's program.md pattern: constraints, gotchas, and autonomy boundaries
9
10
  */
10
11
  export function generateClaude(scan, budget) {
11
- // Separate findings by importance for context-rot-aware placement
12
- const critical = scan.findings.filter((f) => f.confidence === "high" && isCritical(f));
13
- const important = scan.findings.filter((f) => f.confidence === "high" && !isCritical(f));
14
- const supplementary = scan.findings.filter((f) => f.confidence === "medium");
12
+ const { critical, important, supplementary } = categorizeFindings(scan.findings);
15
13
  const sections = [];
16
14
  // ============================================
17
15
  // BEGINNING: Most critical info goes here
18
16
  // (LLMs retain start of context best)
19
17
  // ============================================
20
- sections.push("# CLAUDE.md");
21
- sections.push("");
22
- sections.push("This file provides guidance to Claude Code when working with this codebase.");
23
- sections.push("Generated by [sourcebook](https://github.com/maroondlabs/sourcebook). Review and edit the best context comes from human + machine together.");
24
- sections.push("");
18
+ const header = [
19
+ "# CLAUDE.md",
20
+ "",
21
+ "This file provides guidance to Claude Code when working with this codebase.",
22
+ "Generated by [sourcebook](https://github.com/maroondlabs/sourcebook). Review and edit — the best context comes from human + machine together.",
23
+ "",
24
+ ].join("\n");
25
+ sections.push({ key: "header", content: header, priority: 100 });
25
26
  // Commands first -- most immediately actionable
26
27
  if (hasCommands(scan.commands)) {
27
- sections.push("## Commands");
28
- sections.push("");
28
+ const lines = ["## Commands", ""];
29
29
  if (scan.commands.dev)
30
- sections.push(`- **Dev:** \`${scan.commands.dev}\``);
30
+ lines.push(`- **Dev:** \`${scan.commands.dev}\``);
31
31
  if (scan.commands.build)
32
- sections.push(`- **Build:** \`${scan.commands.build}\``);
32
+ lines.push(`- **Build:** \`${scan.commands.build}\``);
33
33
  if (scan.commands.test)
34
- sections.push(`- **Test:** \`${scan.commands.test}\``);
34
+ lines.push(`- **Test:** \`${scan.commands.test}\``);
35
35
  if (scan.commands.lint)
36
- sections.push(`- **Lint:** \`${scan.commands.lint}\``);
36
+ lines.push(`- **Lint:** \`${scan.commands.lint}\``);
37
37
  for (const [name, cmd] of Object.entries(scan.commands)) {
38
38
  if (cmd && !["dev", "build", "test", "lint", "start"].includes(name)) {
39
- sections.push(`- **${name}:** \`${cmd}\``);
39
+ lines.push(`- **${name}:** \`${cmd}\``);
40
40
  }
41
41
  }
42
- sections.push("");
42
+ lines.push("");
43
+ sections.push({ key: "commands", content: lines.join("\n"), priority: 95 });
43
44
  }
44
- // Critical warnings/constraints near the top (danger zone, fragile code, hidden deps)
45
+ // Critical warnings/constraints near the top
45
46
  if (critical.length > 0) {
46
- sections.push("## Critical Constraints");
47
- sections.push("");
47
+ const lines = ["## Critical Constraints", ""];
48
48
  for (const finding of critical) {
49
- sections.push(`- **${finding.category}:** ${finding.description}`);
49
+ lines.push(`- **${finding.category}:** ${finding.description}`);
50
50
  }
51
- sections.push("");
51
+ lines.push("");
52
+ sections.push({ key: "critical", content: lines.join("\n"), priority: 90 });
52
53
  }
53
54
  // ============================================
54
55
  // MIDDLE: Less critical but useful info
@@ -56,136 +57,85 @@ export function generateClaude(scan, budget) {
56
57
  // ============================================
57
58
  // Stack (brief)
58
59
  if (scan.frameworks.length > 0) {
59
- sections.push("## Stack");
60
- sections.push("");
61
- sections.push(scan.frameworks.join(", "));
62
- sections.push("");
60
+ const content = ["## Stack", "", scan.frameworks.join(", "), ""].join("\n");
61
+ sections.push({ key: "stack", content, priority: 50 });
63
62
  }
64
63
  // Key directories (only non-obvious ones)
65
64
  if (Object.keys(scan.structure.directories).length > 0) {
66
65
  const nonObvious = Object.entries(scan.structure.directories).filter(([dir]) => !["src", "public", "node_modules", "dist", "build"].includes(dir));
67
66
  if (nonObvious.length > 0) {
68
- sections.push("## Project Structure");
69
- sections.push("");
67
+ const lines = ["## Project Structure", ""];
70
68
  for (const [dir, purpose] of nonObvious) {
71
- sections.push(`- \`${dir}/\` — ${purpose}`);
69
+ lines.push(`- \`${dir}/\` — ${purpose}`);
72
70
  }
73
- sections.push("");
71
+ lines.push("");
72
+ sections.push({ key: "structure", content: lines.join("\n"), priority: 40 });
74
73
  }
75
74
  }
76
75
  // Core modules (from PageRank)
77
76
  if (scan.rankedFiles && scan.rankedFiles.length > 0) {
78
77
  const top5 = scan.rankedFiles.slice(0, 5);
79
- sections.push("## Core Modules (by structural importance)");
80
- sections.push("");
78
+ const lines = ["## Core Modules (by structural importance)", ""];
81
79
  for (const { file } of top5) {
82
- sections.push(`- \`${file}\``);
80
+ lines.push(`- \`${file}\``);
83
81
  }
84
- sections.push("");
82
+ lines.push("");
83
+ sections.push({ key: "core_modules", content: lines.join("\n"), priority: 60 });
85
84
  }
86
85
  // Important findings (high confidence, non-critical)
87
86
  if (important.length > 0) {
88
- sections.push("## Conventions & Patterns");
89
- sections.push("");
87
+ const lines = ["## Conventions & Patterns", ""];
90
88
  const grouped = groupByCategory(important);
91
89
  for (const [category, findings] of grouped) {
92
90
  if (findings.length === 1) {
93
- sections.push(`- **${category}:** ${findings[0].description}`);
91
+ lines.push(`- **${category}:** ${findings[0].description}`);
94
92
  }
95
93
  else {
96
- sections.push(`- **${category}:**`);
94
+ lines.push(`- **${category}:**`);
97
95
  for (const f of findings) {
98
- sections.push(` - ${f.description}`);
96
+ lines.push(` - ${f.description}`);
99
97
  }
100
98
  }
101
99
  }
102
- sections.push("");
100
+ lines.push("");
101
+ sections.push({ key: "conventions", content: lines.join("\n"), priority: 30 });
103
102
  }
104
103
  // Supplementary findings (medium confidence)
105
104
  if (supplementary.length > 0) {
106
- sections.push("## Additional Context");
107
- sections.push("");
105
+ const lines = ["## Additional Context", ""];
108
106
  const grouped = groupByCategory(supplementary);
109
107
  for (const [category, findings] of grouped) {
110
108
  if (findings.length === 1) {
111
- sections.push(`- **${category}:** ${findings[0].description}`);
109
+ lines.push(`- **${category}:** ${findings[0].description}`);
112
110
  }
113
111
  else {
114
- sections.push(`- **${category}:**`);
112
+ lines.push(`- **${category}:**`);
115
113
  for (const f of findings) {
116
- sections.push(` - ${f.description}`);
114
+ lines.push(` - ${f.description}`);
117
115
  }
118
116
  }
119
117
  }
120
- sections.push("");
118
+ lines.push("");
119
+ sections.push({ key: "supplementary", content: lines.join("\n"), priority: 20 });
121
120
  }
122
121
  // ============================================
123
122
  // END: Important reminders go here
124
123
  // (LLMs retain end of context second-best)
125
124
  // ============================================
126
- // "What to add" section -- prompts human to add non-discoverable context
127
- sections.push("## What to Add Manually");
128
- sections.push("");
129
- sections.push("The most valuable context is what only you know. Add:");
130
- sections.push("");
131
- sections.push("- Architectural decisions and why they were made");
132
- sections.push("- Past incidents that shaped current conventions");
133
- sections.push("- Deprecated patterns to avoid in new code");
134
- sections.push("- Domain-specific rules or terminology");
135
- sections.push("- Environment setup beyond what .env.example shows");
136
- sections.push("");
137
- let output = sections.join("\n");
138
- // Token budget enforcement (rough: 1 token 4 chars)
139
- const charBudget = budget * 4;
140
- if (output.length > charBudget) {
141
- output = output.slice(0, charBudget);
142
- const lastNewline = output.lastIndexOf("\n");
143
- output =
144
- output.slice(0, lastNewline) +
145
- "\n\n<!-- truncated to fit token budget -->\n";
146
- }
147
- return output;
148
- }
149
- /**
150
- * Determine if a finding is "critical" -- things that can cause real damage
151
- * if an agent gets them wrong. These go at the TOP of the file.
152
- */
153
- function isCritical(finding) {
154
- const criticalCategories = new Set([
155
- "Hidden dependencies",
156
- "Circular dependencies",
157
- "Core modules",
158
- "Fragile code",
159
- "Git history",
160
- "Commit conventions",
161
- ]);
162
- const criticalKeywords = [
163
- "breaking",
164
- "blast radius",
165
- "deprecated",
166
- "don't",
167
- "must",
168
- "never",
169
- "revert",
170
- "fragile",
171
- "hidden",
172
- "invisible",
173
- "coupling",
174
- ];
175
- if (criticalCategories.has(finding.category))
176
- return true;
177
- const desc = finding.description.toLowerCase();
178
- return criticalKeywords.some((kw) => desc.includes(kw));
179
- }
180
- function groupByCategory(findings) {
181
- const grouped = new Map();
182
- for (const finding of findings) {
183
- const existing = grouped.get(finding.category) || [];
184
- existing.push(finding);
185
- grouped.set(finding.category, existing);
186
- }
187
- return grouped;
188
- }
189
- function hasCommands(commands) {
190
- return Object.values(commands).some((v) => v !== undefined);
125
+ const footer = [
126
+ "## What to Add Manually",
127
+ "",
128
+ "The most valuable context is what only you know. Add:",
129
+ "",
130
+ "- Architectural decisions and why they were made",
131
+ "- Past incidents that shaped current conventions",
132
+ "- Deprecated patterns to avoid in new code",
133
+ "- Domain-specific rules or terminology",
134
+ "- Environment setup beyond what .env.example shows",
135
+ "",
136
+ ].join("\n");
137
+ sections.push({ key: "footer", content: footer, priority: 90 });
138
+ // Apply smart budget enforcement
139
+ const kept = enforceTokenBudget(sections, budget);
140
+ return kept.join("\n");
191
141
  }
@@ -1,12 +1,6 @@
1
1
  import type { ProjectScan } from "../types.js";
2
2
  /**
3
3
  * Generate GitHub Copilot instructions from scan results.
4
- *
5
- * Copilot supports:
6
- * - `.github/copilot-instructions.md` — repo-level instructions (always loaded)
7
- * - `.instructions.md` — per-directory instructions (loaded when files in that dir are referenced)
8
- *
9
- * We generate the repo-level file. Copilot's format is plain markdown with
10
- * natural language instructions — more conversational than Cursor's directive style.
4
+ * Outputs .github/copilot-instructions.md — conversational style.
11
5
  */
12
6
  export declare function generateCopilot(scan: ProjectScan, budget: number): string;