sourcebook 0.1.0 → 0.4.1
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/LICENSE +65 -21
- package/README.md +100 -40
- package/dist/auth/license.d.ts +25 -0
- package/dist/auth/license.js +130 -0
- package/dist/cli.js +23 -2
- package/dist/commands/activate.d.ts +1 -0
- package/dist/commands/activate.js +38 -0
- package/dist/commands/diff.d.ts +12 -0
- package/dist/commands/diff.js +97 -0
- package/dist/commands/init.js +10 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +179 -0
- package/dist/generators/agents.d.ts +7 -0
- package/dist/generators/agents.js +119 -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 +345 -2
- package/package.json +7 -6
|
@@ -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,179 @@
|
|
|
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
|
+
import { requirePro } from "../auth/license.js";
|
|
10
|
+
// Headers that sourcebook generates — anything else is user-added
|
|
11
|
+
const SOURCEBOOK_HEADERS = new Set([
|
|
12
|
+
"CLAUDE.md",
|
|
13
|
+
"Commands",
|
|
14
|
+
"Critical Constraints",
|
|
15
|
+
"Stack",
|
|
16
|
+
"Project Structure",
|
|
17
|
+
"Core Modules (by structural importance)",
|
|
18
|
+
"Core Modules",
|
|
19
|
+
"Conventions & Patterns",
|
|
20
|
+
"Conventions",
|
|
21
|
+
"Additional Context",
|
|
22
|
+
"Additional Notes",
|
|
23
|
+
"What to Add Manually",
|
|
24
|
+
"Copilot Instructions",
|
|
25
|
+
"Development Commands",
|
|
26
|
+
"Important Constraints",
|
|
27
|
+
"Technology Stack",
|
|
28
|
+
"High-Impact Files",
|
|
29
|
+
"Code Conventions",
|
|
30
|
+
"Constraints",
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Re-analyze and regenerate context files while preserving manual edits.
|
|
34
|
+
*
|
|
35
|
+
* Strategy:
|
|
36
|
+
* 1. Read existing output file
|
|
37
|
+
* 2. Parse into sections (split on ## headers)
|
|
38
|
+
* 3. Identify manual sections (headers not in SOURCEBOOK_HEADERS)
|
|
39
|
+
* 4. Re-run scan and generate fresh content
|
|
40
|
+
* 5. Replace sourcebook sections, keep manual sections in their original positions
|
|
41
|
+
*/
|
|
42
|
+
export async function update(options) {
|
|
43
|
+
await requirePro("sourcebook update");
|
|
44
|
+
const targetDir = path.resolve(options.dir);
|
|
45
|
+
const formats = options.format.split(",").map((f) => f.trim());
|
|
46
|
+
const budget = parseInt(options.budget, 10);
|
|
47
|
+
console.log(chalk.bold("\nsourcebook update"));
|
|
48
|
+
console.log(chalk.dim("Re-analyzing while preserving your edits...\n"));
|
|
49
|
+
const scan = await scanProject(targetDir);
|
|
50
|
+
console.log(chalk.green("✓") + " Scanned project structure");
|
|
51
|
+
console.log(chalk.dim(` ${scan.files.length} files, ${scan.frameworks.length} frameworks detected`));
|
|
52
|
+
console.log(chalk.green("✓") + ` Extracted ${scan.findings.length} findings\n`);
|
|
53
|
+
for (const format of formats) {
|
|
54
|
+
switch (format) {
|
|
55
|
+
case "claude": {
|
|
56
|
+
const fresh = generateClaude(scan, budget);
|
|
57
|
+
const existing = readExisting(targetDir, "CLAUDE.md");
|
|
58
|
+
const merged = existing ? mergeContent(existing, fresh) : fresh;
|
|
59
|
+
await writeOutput(targetDir, "CLAUDE.md", merged);
|
|
60
|
+
const preserved = existing ? countManualSections(existing) : 0;
|
|
61
|
+
console.log(chalk.green("✓") +
|
|
62
|
+
` Updated CLAUDE.md` +
|
|
63
|
+
(preserved > 0 ? chalk.dim(` (${preserved} manual section${preserved > 1 ? "s" : ""} preserved)`) : ""));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "cursor": {
|
|
67
|
+
const fresh = generateCursor(scan, budget);
|
|
68
|
+
await writeOutput(targetDir, ".cursor/rules/sourcebook.mdc", fresh);
|
|
69
|
+
const legacyFresh = generateCursorLegacy(scan, budget);
|
|
70
|
+
await writeOutput(targetDir, ".cursorrules", legacyFresh);
|
|
71
|
+
console.log(chalk.green("✓") + " Updated .cursor/rules/sourcebook.mdc");
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "copilot": {
|
|
75
|
+
const fresh = generateCopilot(scan, budget);
|
|
76
|
+
const existing = readExisting(targetDir, ".github/copilot-instructions.md");
|
|
77
|
+
const merged = existing ? mergeContent(existing, fresh) : fresh;
|
|
78
|
+
await writeOutput(targetDir, ".github/copilot-instructions.md", merged);
|
|
79
|
+
const preserved = existing ? countManualSections(existing) : 0;
|
|
80
|
+
console.log(chalk.green("✓") +
|
|
81
|
+
` Updated .github/copilot-instructions.md` +
|
|
82
|
+
(preserved > 0 ? chalk.dim(` (${preserved} manual section${preserved > 1 ? "s" : ""} preserved)`) : ""));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "all": {
|
|
86
|
+
// Recurse for each format
|
|
87
|
+
for (const f of ["claude", "cursor", "copilot"]) {
|
|
88
|
+
await update({ ...options, format: f });
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
console.log(chalk.yellow(`⚠ Format "${format}" not yet supported`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log(chalk.dim("\nDone. Your manual sections were preserved.\n"));
|
|
97
|
+
}
|
|
98
|
+
function readExisting(dir, filename) {
|
|
99
|
+
const filePath = path.join(dir, filename);
|
|
100
|
+
try {
|
|
101
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse markdown into sections split by ## headers.
|
|
109
|
+
*/
|
|
110
|
+
function parseSections(content) {
|
|
111
|
+
const sections = [];
|
|
112
|
+
const lines = content.split("\n");
|
|
113
|
+
let currentHeader = "";
|
|
114
|
+
let currentBody = [];
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const headerMatch = line.match(/^##\s+(.+)/);
|
|
117
|
+
if (headerMatch) {
|
|
118
|
+
if (currentHeader || currentBody.length > 0) {
|
|
119
|
+
sections.push({ header: currentHeader, body: currentBody.join("\n") });
|
|
120
|
+
}
|
|
121
|
+
currentHeader = headerMatch[1].trim();
|
|
122
|
+
currentBody = [];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
currentBody.push(line);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (currentHeader || currentBody.length > 0) {
|
|
129
|
+
sections.push({ header: currentHeader, body: currentBody.join("\n") });
|
|
130
|
+
}
|
|
131
|
+
return sections;
|
|
132
|
+
}
|
|
133
|
+
function isSourcebookSection(header) {
|
|
134
|
+
return SOURCEBOOK_HEADERS.has(header) || header === "";
|
|
135
|
+
}
|
|
136
|
+
function countManualSections(content) {
|
|
137
|
+
const sections = parseSections(content);
|
|
138
|
+
return sections.filter((s) => s.header && !isSourcebookSection(s.header)).length;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Merge fresh sourcebook output with existing content,
|
|
142
|
+
* preserving any manually added sections.
|
|
143
|
+
*/
|
|
144
|
+
function mergeContent(existing, fresh) {
|
|
145
|
+
const existingSections = parseSections(existing);
|
|
146
|
+
const freshSections = parseSections(fresh);
|
|
147
|
+
// Extract manual sections from existing content
|
|
148
|
+
const manualSections = existingSections.filter((s) => s.header && !isSourcebookSection(s.header));
|
|
149
|
+
if (manualSections.length === 0) {
|
|
150
|
+
// No manual sections — just use the fresh content
|
|
151
|
+
return fresh;
|
|
152
|
+
}
|
|
153
|
+
// Find where "What to Add Manually" or last section is in fresh content
|
|
154
|
+
// Insert manual sections before the footer
|
|
155
|
+
const result = [];
|
|
156
|
+
let insertedManual = false;
|
|
157
|
+
for (const section of freshSections) {
|
|
158
|
+
// Insert manual sections before the "What to Add Manually" footer
|
|
159
|
+
if (section.header === "What to Add Manually" && !insertedManual) {
|
|
160
|
+
for (const manual of manualSections) {
|
|
161
|
+
result.push(`## ${manual.header}`);
|
|
162
|
+
result.push(manual.body);
|
|
163
|
+
}
|
|
164
|
+
insertedManual = true;
|
|
165
|
+
}
|
|
166
|
+
if (section.header) {
|
|
167
|
+
result.push(`## ${section.header}`);
|
|
168
|
+
}
|
|
169
|
+
result.push(section.body);
|
|
170
|
+
}
|
|
171
|
+
// If we never found the footer, append manual sections at the end
|
|
172
|
+
if (!insertedManual) {
|
|
173
|
+
for (const manual of manualSections) {
|
|
174
|
+
result.push(`## ${manual.header}`);
|
|
175
|
+
result.push(manual.body);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result.join("\n");
|
|
179
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProjectScan } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate an AGENTS.md file from scan results.
|
|
4
|
+
* Used by GitHub Copilot, OpenAI Codex, and other AGENTS.md-aware tools.
|
|
5
|
+
* Format follows the AGENTS.md spec: markdown with directives for agent behavior.
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateAgents(scan: ProjectScan, budget: number): string;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { hasCommands, categorizeFindings, enforceTokenBudget, } from "./shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate an AGENTS.md file from scan results.
|
|
4
|
+
* Used by GitHub Copilot, OpenAI Codex, and other AGENTS.md-aware tools.
|
|
5
|
+
* Format follows the AGENTS.md spec: markdown with directives for agent behavior.
|
|
6
|
+
*/
|
|
7
|
+
export function generateAgents(scan, budget) {
|
|
8
|
+
const { critical, important, supplementary } = categorizeFindings(scan.findings);
|
|
9
|
+
const sections = [];
|
|
10
|
+
sections.push({
|
|
11
|
+
key: "header",
|
|
12
|
+
content: [
|
|
13
|
+
"# AGENTS.md",
|
|
14
|
+
"",
|
|
15
|
+
"Agent instructions for this repository.",
|
|
16
|
+
"Generated by [sourcebook](https://github.com/maroondlabs/sourcebook). Review and edit — the best context comes from human + machine together.",
|
|
17
|
+
"",
|
|
18
|
+
].join("\n"),
|
|
19
|
+
priority: 100,
|
|
20
|
+
});
|
|
21
|
+
// Commands
|
|
22
|
+
if (hasCommands(scan.commands)) {
|
|
23
|
+
const lines = ["## Commands", ""];
|
|
24
|
+
if (scan.commands.dev)
|
|
25
|
+
lines.push(`- **Dev:** \`${scan.commands.dev}\``);
|
|
26
|
+
if (scan.commands.build)
|
|
27
|
+
lines.push(`- **Build:** \`${scan.commands.build}\``);
|
|
28
|
+
if (scan.commands.test)
|
|
29
|
+
lines.push(`- **Test:** \`${scan.commands.test}\``);
|
|
30
|
+
if (scan.commands.lint)
|
|
31
|
+
lines.push(`- **Lint:** \`${scan.commands.lint}\``);
|
|
32
|
+
for (const [name, cmd] of Object.entries(scan.commands)) {
|
|
33
|
+
if (cmd && !["dev", "build", "test", "lint", "start"].includes(name)) {
|
|
34
|
+
lines.push(`- **${name}:** \`${cmd}\``);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lines.push("");
|
|
38
|
+
sections.push({ key: "commands", content: lines.join("\n"), priority: 95 });
|
|
39
|
+
}
|
|
40
|
+
// Critical constraints as agent directives
|
|
41
|
+
if (critical.length > 0) {
|
|
42
|
+
const lines = [
|
|
43
|
+
"## Constraints",
|
|
44
|
+
"",
|
|
45
|
+
"These constraints MUST be followed when modifying this codebase:",
|
|
46
|
+
"",
|
|
47
|
+
];
|
|
48
|
+
for (const finding of critical) {
|
|
49
|
+
lines.push(`- **${finding.category}:** ${finding.description}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push("");
|
|
52
|
+
sections.push({ key: "critical", content: lines.join("\n"), priority: 90 });
|
|
53
|
+
}
|
|
54
|
+
// Stack
|
|
55
|
+
if (scan.frameworks.length > 0) {
|
|
56
|
+
sections.push({
|
|
57
|
+
key: "stack",
|
|
58
|
+
content: [
|
|
59
|
+
"## Stack",
|
|
60
|
+
"",
|
|
61
|
+
scan.frameworks.join(", "),
|
|
62
|
+
"",
|
|
63
|
+
].join("\n"),
|
|
64
|
+
priority: 50,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Core modules
|
|
68
|
+
if (scan.rankedFiles && scan.rankedFiles.length > 0) {
|
|
69
|
+
const lines = [
|
|
70
|
+
"## Core Modules (by structural importance)",
|
|
71
|
+
"",
|
|
72
|
+
];
|
|
73
|
+
for (const { file, score } of scan.rankedFiles.slice(0, 5)) {
|
|
74
|
+
lines.push(`- \`${file}\``);
|
|
75
|
+
}
|
|
76
|
+
lines.push("");
|
|
77
|
+
sections.push({ key: "core_modules", content: lines.join("\n"), priority: 60 });
|
|
78
|
+
}
|
|
79
|
+
// Conventions
|
|
80
|
+
if (important.length > 0) {
|
|
81
|
+
const lines = [
|
|
82
|
+
"## Conventions",
|
|
83
|
+
"",
|
|
84
|
+
];
|
|
85
|
+
for (const finding of important) {
|
|
86
|
+
lines.push(`- **${finding.category}:** ${finding.description}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push("");
|
|
89
|
+
sections.push({ key: "conventions", content: lines.join("\n"), priority: 30 });
|
|
90
|
+
}
|
|
91
|
+
// Additional context
|
|
92
|
+
if (supplementary.length > 0) {
|
|
93
|
+
const lines = ["## Additional Context", ""];
|
|
94
|
+
for (const finding of supplementary) {
|
|
95
|
+
lines.push(`- ${finding.description}`);
|
|
96
|
+
}
|
|
97
|
+
lines.push("");
|
|
98
|
+
sections.push({ key: "supplementary", content: lines.join("\n"), priority: 20 });
|
|
99
|
+
}
|
|
100
|
+
// Manual section prompt
|
|
101
|
+
sections.push({
|
|
102
|
+
key: "manual",
|
|
103
|
+
content: [
|
|
104
|
+
"## What to Add Manually",
|
|
105
|
+
"",
|
|
106
|
+
"The most valuable context is what only you know. Add:",
|
|
107
|
+
"",
|
|
108
|
+
"- Architectural decisions and why they were made",
|
|
109
|
+
"- Past incidents that shaped current conventions",
|
|
110
|
+
"- Deprecated patterns to avoid in new code",
|
|
111
|
+
"- Domain-specific rules or terminology",
|
|
112
|
+
"- Environment setup beyond what .env.example shows",
|
|
113
|
+
"",
|
|
114
|
+
].join("\n"),
|
|
115
|
+
priority: 10,
|
|
116
|
+
});
|
|
117
|
+
const kept = enforceTokenBudget(sections, budget);
|
|
118
|
+
return kept.join("\n");
|
|
119
|
+
}
|
|
@@ -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;
|