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 +4 -0
- package/dist/cli.js +18 -2
- package/dist/commands/diff.d.ts +12 -0
- package/dist/commands/diff.js +97 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +177 -0
- package/dist/generators/claude.js +61 -111
- package/dist/generators/copilot.d.ts +1 -7
- package/dist/generators/copilot.js +65 -80
- package/dist/generators/cursor.d.ts +3 -9
- package/dist/generators/cursor.js +49 -79
- package/dist/generators/shared.d.ts +34 -0
- package/dist/generators/shared.js +87 -0
- package/dist/scanner/build.js +28 -0
- package/dist/scanner/frameworks.js +141 -0
- package/dist/scanner/git.js +69 -0
- package/dist/scanner/index.js +2 -0
- package/dist/scanner/patterns.js +87 -2
- package/package.json +1 -1
package/README.md
CHANGED
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
sections.push("");
|
|
28
|
+
const lines = ["## Commands", ""];
|
|
29
29
|
if (scan.commands.dev)
|
|
30
|
-
|
|
30
|
+
lines.push(`- **Dev:** \`${scan.commands.dev}\``);
|
|
31
31
|
if (scan.commands.build)
|
|
32
|
-
|
|
32
|
+
lines.push(`- **Build:** \`${scan.commands.build}\``);
|
|
33
33
|
if (scan.commands.test)
|
|
34
|
-
|
|
34
|
+
lines.push(`- **Test:** \`${scan.commands.test}\``);
|
|
35
35
|
if (scan.commands.lint)
|
|
36
|
-
|
|
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
|
-
|
|
39
|
+
lines.push(`- **${name}:** \`${cmd}\``);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
lines.push("");
|
|
43
|
+
sections.push({ key: "commands", content: lines.join("\n"), priority: 95 });
|
|
43
44
|
}
|
|
44
|
-
// Critical warnings/constraints near the top
|
|
45
|
+
// Critical warnings/constraints near the top
|
|
45
46
|
if (critical.length > 0) {
|
|
46
|
-
|
|
47
|
-
sections.push("");
|
|
47
|
+
const lines = ["## Critical Constraints", ""];
|
|
48
48
|
for (const finding of critical) {
|
|
49
|
-
|
|
49
|
+
lines.push(`- **${finding.category}:** ${finding.description}`);
|
|
50
50
|
}
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
sections.push("");
|
|
67
|
+
const lines = ["## Project Structure", ""];
|
|
70
68
|
for (const [dir, purpose] of nonObvious) {
|
|
71
|
-
|
|
69
|
+
lines.push(`- \`${dir}/\` — ${purpose}`);
|
|
72
70
|
}
|
|
73
|
-
|
|
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
|
-
|
|
80
|
-
sections.push("");
|
|
78
|
+
const lines = ["## Core Modules (by structural importance)", ""];
|
|
81
79
|
for (const { file } of top5) {
|
|
82
|
-
|
|
80
|
+
lines.push(`- \`${file}\``);
|
|
83
81
|
}
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
lines.push(`- **${category}:** ${findings[0].description}`);
|
|
94
92
|
}
|
|
95
93
|
else {
|
|
96
|
-
|
|
94
|
+
lines.push(`- **${category}:**`);
|
|
97
95
|
for (const f of findings) {
|
|
98
|
-
|
|
96
|
+
lines.push(` - ${f.description}`);
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
lines.push(`- **${category}:** ${findings[0].description}`);
|
|
112
110
|
}
|
|
113
111
|
else {
|
|
114
|
-
|
|
112
|
+
lines.push(`- **${category}:**`);
|
|
115
113
|
for (const f of findings) {
|
|
116
|
-
|
|
114
|
+
lines.push(` - ${f.description}`);
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|