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
|
@@ -1,119 +1,104 @@
|
|
|
1
|
+
import { hasCommands, categorizeFindings, enforceTokenBudget, } from "./shared.js";
|
|
1
2
|
/**
|
|
2
3
|
* Generate GitHub Copilot instructions from scan results.
|
|
3
|
-
*
|
|
4
|
-
* Copilot supports:
|
|
5
|
-
* - `.github/copilot-instructions.md` — repo-level instructions (always loaded)
|
|
6
|
-
* - `.instructions.md` — per-directory instructions (loaded when files in that dir are referenced)
|
|
7
|
-
*
|
|
8
|
-
* We generate the repo-level file. Copilot's format is plain markdown with
|
|
9
|
-
* natural language instructions — more conversational than Cursor's directive style.
|
|
4
|
+
* Outputs .github/copilot-instructions.md — conversational style.
|
|
10
5
|
*/
|
|
11
6
|
export function generateCopilot(scan, budget) {
|
|
12
|
-
const critical = scan.findings
|
|
13
|
-
const important = scan.findings.filter((f) => f.confidence === "high" && !isCritical(f));
|
|
14
|
-
const supplementary = scan.findings.filter((f) => f.confidence === "medium");
|
|
7
|
+
const { critical, important, supplementary } = categorizeFindings(scan.findings);
|
|
15
8
|
const sections = [];
|
|
16
|
-
sections.push(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
sections.push({
|
|
10
|
+
key: "header",
|
|
11
|
+
content: [
|
|
12
|
+
"# Copilot Instructions",
|
|
13
|
+
"",
|
|
14
|
+
"These instructions were generated by [sourcebook](https://github.com/maroondlabs/sourcebook). Review and edit — the best context comes from human + machine together.",
|
|
15
|
+
"",
|
|
16
|
+
].join("\n"),
|
|
17
|
+
priority: 100,
|
|
18
|
+
});
|
|
20
19
|
// Commands
|
|
21
20
|
if (hasCommands(scan.commands)) {
|
|
22
|
-
|
|
23
|
-
sections.push("");
|
|
21
|
+
const lines = ["## Development Commands", ""];
|
|
24
22
|
if (scan.commands.dev)
|
|
25
|
-
|
|
23
|
+
lines.push(`- Dev server: \`${scan.commands.dev}\``);
|
|
26
24
|
if (scan.commands.build)
|
|
27
|
-
|
|
25
|
+
lines.push(`- Build: \`${scan.commands.build}\``);
|
|
28
26
|
if (scan.commands.test)
|
|
29
|
-
|
|
27
|
+
lines.push(`- Tests: \`${scan.commands.test}\``);
|
|
30
28
|
if (scan.commands.lint)
|
|
31
|
-
|
|
29
|
+
lines.push(`- Lint: \`${scan.commands.lint}\``);
|
|
32
30
|
for (const [name, cmd] of Object.entries(scan.commands)) {
|
|
33
31
|
if (cmd && !["dev", "build", "test", "lint", "start"].includes(name)) {
|
|
34
|
-
|
|
32
|
+
lines.push(`- ${name}: \`${cmd}\``);
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
|
-
|
|
35
|
+
lines.push("");
|
|
36
|
+
sections.push({ key: "commands", content: lines.join("\n"), priority: 95 });
|
|
38
37
|
}
|
|
39
38
|
// Critical constraints
|
|
40
39
|
if (critical.length > 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const lines = [
|
|
41
|
+
"## Important Constraints",
|
|
42
|
+
"",
|
|
43
|
+
"Follow these rules when modifying this codebase:",
|
|
44
|
+
"",
|
|
45
|
+
];
|
|
45
46
|
for (const finding of critical) {
|
|
46
|
-
|
|
47
|
+
lines.push(`- ${finding.description}`);
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
lines.push("");
|
|
50
|
+
sections.push({ key: "critical", content: lines.join("\n"), priority: 90 });
|
|
49
51
|
}
|
|
50
52
|
// Stack
|
|
51
53
|
if (scan.frameworks.length > 0) {
|
|
52
|
-
sections.push(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
sections.push({
|
|
55
|
+
key: "stack",
|
|
56
|
+
content: [
|
|
57
|
+
"## Technology Stack",
|
|
58
|
+
"",
|
|
59
|
+
`This project uses: ${scan.frameworks.join(", ")}.`,
|
|
60
|
+
"",
|
|
61
|
+
].join("\n"),
|
|
62
|
+
priority: 50,
|
|
63
|
+
});
|
|
56
64
|
}
|
|
57
65
|
// Core modules
|
|
58
66
|
if (scan.rankedFiles && scan.rankedFiles.length > 0) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const lines = [
|
|
68
|
+
"## High-Impact Files",
|
|
69
|
+
"",
|
|
70
|
+
"These files are imported by many others. Changes here have wide blast radius:",
|
|
71
|
+
"",
|
|
72
|
+
];
|
|
73
|
+
for (const { file } of scan.rankedFiles.slice(0, 5)) {
|
|
74
|
+
lines.push(`- \`${file}\``);
|
|
66
75
|
}
|
|
67
|
-
|
|
76
|
+
lines.push("");
|
|
77
|
+
sections.push({ key: "core_modules", content: lines.join("\n"), priority: 60 });
|
|
68
78
|
}
|
|
69
79
|
// Conventions
|
|
70
80
|
if (important.length > 0) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
const lines = [
|
|
82
|
+
"## Code Conventions",
|
|
83
|
+
"",
|
|
84
|
+
"This project follows these patterns:",
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
75
87
|
for (const finding of important) {
|
|
76
|
-
|
|
88
|
+
lines.push(`- ${finding.description}`);
|
|
77
89
|
}
|
|
78
|
-
|
|
90
|
+
lines.push("");
|
|
91
|
+
sections.push({ key: "conventions", content: lines.join("\n"), priority: 30 });
|
|
79
92
|
}
|
|
80
93
|
// Additional context
|
|
81
94
|
if (supplementary.length > 0) {
|
|
82
|
-
|
|
83
|
-
sections.push("");
|
|
95
|
+
const lines = ["## Additional Notes", ""];
|
|
84
96
|
for (const finding of supplementary) {
|
|
85
|
-
|
|
97
|
+
lines.push(`- ${finding.description}`);
|
|
86
98
|
}
|
|
87
|
-
|
|
99
|
+
lines.push("");
|
|
100
|
+
sections.push({ key: "supplementary", content: lines.join("\n"), priority: 20 });
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const charBudget = budget * 4;
|
|
92
|
-
if (output.length > charBudget) {
|
|
93
|
-
output = output.slice(0, charBudget);
|
|
94
|
-
const lastNewline = output.lastIndexOf("\n");
|
|
95
|
-
output = output.slice(0, lastNewline) + "\n";
|
|
96
|
-
}
|
|
97
|
-
return output;
|
|
98
|
-
}
|
|
99
|
-
function isCritical(finding) {
|
|
100
|
-
const criticalCategories = new Set([
|
|
101
|
-
"Hidden dependencies",
|
|
102
|
-
"Circular dependencies",
|
|
103
|
-
"Core modules",
|
|
104
|
-
"Fragile code",
|
|
105
|
-
"Git history",
|
|
106
|
-
"Commit conventions",
|
|
107
|
-
]);
|
|
108
|
-
const criticalKeywords = [
|
|
109
|
-
"breaking", "blast radius", "deprecated", "don't", "must",
|
|
110
|
-
"never", "revert", "fragile", "hidden", "invisible", "coupling",
|
|
111
|
-
];
|
|
112
|
-
if (criticalCategories.has(finding.category))
|
|
113
|
-
return true;
|
|
114
|
-
const desc = finding.description.toLowerCase();
|
|
115
|
-
return criticalKeywords.some((kw) => desc.includes(kw));
|
|
116
|
-
}
|
|
117
|
-
function hasCommands(commands) {
|
|
118
|
-
return Object.values(commands).some((v) => v !== undefined);
|
|
102
|
+
const kept = enforceTokenBudget(sections, budget);
|
|
103
|
+
return kept.join("\n");
|
|
119
104
|
}
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import type { ProjectScan } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Generate Cursor rules from scan results.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Each .mdc file has YAML frontmatter (description, globs, alwaysApply) + markdown body.
|
|
7
|
-
*
|
|
8
|
-
* We generate a single `sourcebook.mdc` with alwaysApply: true containing
|
|
9
|
-
* the same non-discoverable findings as the Claude generator, formatted for
|
|
10
|
-
* Cursor's conventions (shorter, more directive).
|
|
4
|
+
* Outputs .cursor/rules/sourcebook.mdc (modular format with YAML frontmatter)
|
|
5
|
+
* and legacy .cursorrules (same content, no frontmatter).
|
|
11
6
|
*/
|
|
12
7
|
export declare function generateCursor(scan: ProjectScan, budget: number): string;
|
|
13
8
|
/**
|
|
14
|
-
*
|
|
15
|
-
* Same content as the .mdc but without the frontmatter.
|
|
9
|
+
* Legacy .cursorrules format — same content without YAML frontmatter.
|
|
16
10
|
*/
|
|
17
11
|
export declare function generateCursorLegacy(scan: ProjectScan, budget: number): string;
|
|
@@ -1,123 +1,93 @@
|
|
|
1
|
+
import { hasCommands, categorizeFindings, enforceTokenBudget, } from "./shared.js";
|
|
1
2
|
/**
|
|
2
3
|
* Generate Cursor rules from scan results.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Each .mdc file has YAML frontmatter (description, globs, alwaysApply) + markdown body.
|
|
6
|
-
*
|
|
7
|
-
* We generate a single `sourcebook.mdc` with alwaysApply: true containing
|
|
8
|
-
* the same non-discoverable findings as the Claude generator, formatted for
|
|
9
|
-
* Cursor's conventions (shorter, more directive).
|
|
4
|
+
* Outputs .cursor/rules/sourcebook.mdc (modular format with YAML frontmatter)
|
|
5
|
+
* and legacy .cursorrules (same content, no frontmatter).
|
|
10
6
|
*/
|
|
11
7
|
export function generateCursor(scan, budget) {
|
|
12
|
-
const critical = scan.findings
|
|
13
|
-
const important = scan.findings.filter((f) => f.confidence === "high" && !isCritical(f));
|
|
14
|
-
const supplementary = scan.findings.filter((f) => f.confidence === "medium");
|
|
8
|
+
const { critical, important, supplementary } = categorizeFindings(scan.findings);
|
|
15
9
|
const sections = [];
|
|
16
10
|
// MDC frontmatter
|
|
17
|
-
sections.push(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
sections.push({
|
|
12
|
+
key: "frontmatter",
|
|
13
|
+
content: [
|
|
14
|
+
"---",
|
|
15
|
+
"description: Project conventions and constraints extracted by sourcebook",
|
|
16
|
+
"alwaysApply: true",
|
|
17
|
+
"---",
|
|
18
|
+
"",
|
|
19
|
+
].join("\n"),
|
|
20
|
+
priority: 100,
|
|
21
|
+
});
|
|
22
22
|
// Commands
|
|
23
23
|
if (hasCommands(scan.commands)) {
|
|
24
|
-
|
|
25
|
-
sections.push("");
|
|
24
|
+
const lines = ["## Commands", ""];
|
|
26
25
|
if (scan.commands.dev)
|
|
27
|
-
|
|
26
|
+
lines.push(`- Dev: \`${scan.commands.dev}\``);
|
|
28
27
|
if (scan.commands.build)
|
|
29
|
-
|
|
28
|
+
lines.push(`- Build: \`${scan.commands.build}\``);
|
|
30
29
|
if (scan.commands.test)
|
|
31
|
-
|
|
30
|
+
lines.push(`- Test: \`${scan.commands.test}\``);
|
|
32
31
|
if (scan.commands.lint)
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
lines.push(`- Lint: \`${scan.commands.lint}\``);
|
|
33
|
+
lines.push("");
|
|
34
|
+
sections.push({ key: "commands", content: lines.join("\n"), priority: 95 });
|
|
35
35
|
}
|
|
36
|
-
// Critical constraints
|
|
36
|
+
// Critical constraints
|
|
37
37
|
if (critical.length > 0) {
|
|
38
|
-
|
|
39
|
-
sections.push("");
|
|
38
|
+
const lines = ["## Constraints", ""];
|
|
40
39
|
for (const finding of critical) {
|
|
41
|
-
|
|
40
|
+
lines.push(`- ${finding.description}`);
|
|
42
41
|
}
|
|
43
|
-
|
|
42
|
+
lines.push("");
|
|
43
|
+
sections.push({ key: "critical", content: lines.join("\n"), priority: 90 });
|
|
44
44
|
}
|
|
45
|
-
// Stack
|
|
45
|
+
// Stack
|
|
46
46
|
if (scan.frameworks.length > 0) {
|
|
47
|
-
sections.push(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
sections.push({
|
|
48
|
+
key: "stack",
|
|
49
|
+
content: ["## Stack", "", scan.frameworks.join(", "), ""].join("\n"),
|
|
50
|
+
priority: 50,
|
|
51
|
+
});
|
|
51
52
|
}
|
|
52
53
|
// Core modules
|
|
53
54
|
if (scan.rankedFiles && scan.rankedFiles.length > 0) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const { file } of top5) {
|
|
58
|
-
sections.push(`- \`${file}\``);
|
|
55
|
+
const lines = ["## Core Modules", ""];
|
|
56
|
+
for (const { file } of scan.rankedFiles.slice(0, 5)) {
|
|
57
|
+
lines.push(`- \`${file}\``);
|
|
59
58
|
}
|
|
60
|
-
|
|
59
|
+
lines.push("");
|
|
60
|
+
sections.push({ key: "core_modules", content: lines.join("\n"), priority: 60 });
|
|
61
61
|
}
|
|
62
62
|
// Conventions
|
|
63
63
|
if (important.length > 0) {
|
|
64
|
-
|
|
65
|
-
sections.push("");
|
|
64
|
+
const lines = ["## Conventions", ""];
|
|
66
65
|
for (const finding of important) {
|
|
67
|
-
|
|
66
|
+
lines.push(`- ${finding.description}`);
|
|
68
67
|
}
|
|
69
|
-
|
|
68
|
+
lines.push("");
|
|
69
|
+
sections.push({ key: "conventions", content: lines.join("\n"), priority: 30 });
|
|
70
70
|
}
|
|
71
71
|
// Additional context
|
|
72
72
|
if (supplementary.length > 0) {
|
|
73
|
-
|
|
74
|
-
sections.push("");
|
|
73
|
+
const lines = ["## Additional Context", ""];
|
|
75
74
|
for (const finding of supplementary) {
|
|
76
|
-
|
|
75
|
+
lines.push(`- ${finding.description}`);
|
|
77
76
|
}
|
|
78
|
-
|
|
77
|
+
lines.push("");
|
|
78
|
+
sections.push({ key: "supplementary", content: lines.join("\n"), priority: 20 });
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const charBudget = budget * 4;
|
|
83
|
-
if (output.length > charBudget) {
|
|
84
|
-
output = output.slice(0, charBudget);
|
|
85
|
-
const lastNewline = output.lastIndexOf("\n");
|
|
86
|
-
output = output.slice(0, lastNewline) + "\n";
|
|
87
|
-
}
|
|
88
|
-
return output;
|
|
80
|
+
const kept = enforceTokenBudget(sections, budget);
|
|
81
|
+
return kept.join("\n");
|
|
89
82
|
}
|
|
90
83
|
/**
|
|
91
|
-
*
|
|
92
|
-
* Same content as the .mdc but without the frontmatter.
|
|
84
|
+
* Legacy .cursorrules format — same content without YAML frontmatter.
|
|
93
85
|
*/
|
|
94
86
|
export function generateCursorLegacy(scan, budget) {
|
|
95
87
|
const mdc = generateCursor(scan, budget);
|
|
96
|
-
// Strip the YAML frontmatter
|
|
97
88
|
const endOfFrontmatter = mdc.indexOf("---", 4);
|
|
98
89
|
if (endOfFrontmatter !== -1) {
|
|
99
90
|
return mdc.slice(endOfFrontmatter + 4).trimStart();
|
|
100
91
|
}
|
|
101
92
|
return mdc;
|
|
102
93
|
}
|
|
103
|
-
function isCritical(finding) {
|
|
104
|
-
const criticalCategories = new Set([
|
|
105
|
-
"Hidden dependencies",
|
|
106
|
-
"Circular dependencies",
|
|
107
|
-
"Core modules",
|
|
108
|
-
"Fragile code",
|
|
109
|
-
"Git history",
|
|
110
|
-
"Commit conventions",
|
|
111
|
-
]);
|
|
112
|
-
const criticalKeywords = [
|
|
113
|
-
"breaking", "blast radius", "deprecated", "don't", "must",
|
|
114
|
-
"never", "revert", "fragile", "hidden", "invisible", "coupling",
|
|
115
|
-
];
|
|
116
|
-
if (criticalCategories.has(finding.category))
|
|
117
|
-
return true;
|
|
118
|
-
const desc = finding.description.toLowerCase();
|
|
119
|
-
return criticalKeywords.some((kw) => desc.includes(kw));
|
|
120
|
-
}
|
|
121
|
-
function hasCommands(commands) {
|
|
122
|
-
return Object.values(commands).some((v) => v !== undefined);
|
|
123
|
-
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Finding } from "../types.js";
|
|
2
|
+
export declare function isCritical(finding: Finding): boolean;
|
|
3
|
+
export declare function groupByCategory(findings: Finding[]): Map<string, Finding[]>;
|
|
4
|
+
export declare function hasCommands(commands: Record<string, string | undefined>): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Estimate token count for a string (rough: 1 token ≈ 4 chars).
|
|
7
|
+
*/
|
|
8
|
+
export declare function estimateTokens(text: string): number;
|
|
9
|
+
/**
|
|
10
|
+
* Categorize findings into priority tiers for budget enforcement.
|
|
11
|
+
*/
|
|
12
|
+
export declare function categorizeFindings(findings: Finding[]): {
|
|
13
|
+
critical: Finding[];
|
|
14
|
+
important: Finding[];
|
|
15
|
+
supplementary: Finding[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Smart budget enforcement. Instead of truncating at a character boundary,
|
|
19
|
+
* drop lower-priority sections first (middle of context = worst retention).
|
|
20
|
+
*
|
|
21
|
+
* Priority order (highest to lowest):
|
|
22
|
+
* 1. Commands (always keep)
|
|
23
|
+
* 2. Critical constraints (always keep)
|
|
24
|
+
* 3. Core modules (keep if budget allows)
|
|
25
|
+
* 4. Stack (keep if budget allows)
|
|
26
|
+
* 5. Conventions/important findings (drop first from middle)
|
|
27
|
+
* 6. Supplementary findings (drop first)
|
|
28
|
+
* 7. Footer/manual section (always keep — end of context = high retention)
|
|
29
|
+
*/
|
|
30
|
+
export declare function enforceTokenBudget(sections: {
|
|
31
|
+
key: string;
|
|
32
|
+
content: string;
|
|
33
|
+
priority: number;
|
|
34
|
+
}[], budget: number): string[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for all generators.
|
|
3
|
+
* Extracted to avoid duplicating criticality logic and budget enforcement.
|
|
4
|
+
*/
|
|
5
|
+
const CRITICAL_CATEGORIES = new Set([
|
|
6
|
+
"Hidden dependencies",
|
|
7
|
+
"Circular dependencies",
|
|
8
|
+
"Core modules",
|
|
9
|
+
"Fragile code",
|
|
10
|
+
"Git history",
|
|
11
|
+
"Commit conventions",
|
|
12
|
+
"Anti-patterns",
|
|
13
|
+
]);
|
|
14
|
+
const CRITICAL_KEYWORDS = [
|
|
15
|
+
"breaking", "blast radius", "deprecated", "don't", "must",
|
|
16
|
+
"never", "revert", "fragile", "hidden", "invisible", "coupling",
|
|
17
|
+
];
|
|
18
|
+
export function isCritical(finding) {
|
|
19
|
+
if (CRITICAL_CATEGORIES.has(finding.category))
|
|
20
|
+
return true;
|
|
21
|
+
const desc = finding.description.toLowerCase();
|
|
22
|
+
return CRITICAL_KEYWORDS.some((kw) => desc.includes(kw));
|
|
23
|
+
}
|
|
24
|
+
export function groupByCategory(findings) {
|
|
25
|
+
const grouped = new Map();
|
|
26
|
+
for (const finding of findings) {
|
|
27
|
+
const existing = grouped.get(finding.category) || [];
|
|
28
|
+
existing.push(finding);
|
|
29
|
+
grouped.set(finding.category, existing);
|
|
30
|
+
}
|
|
31
|
+
return grouped;
|
|
32
|
+
}
|
|
33
|
+
export function hasCommands(commands) {
|
|
34
|
+
return Object.values(commands).some((v) => v !== undefined);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Estimate token count for a string (rough: 1 token ≈ 4 chars).
|
|
38
|
+
*/
|
|
39
|
+
export function estimateTokens(text) {
|
|
40
|
+
return Math.ceil(text.length / 4);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Categorize findings into priority tiers for budget enforcement.
|
|
44
|
+
*/
|
|
45
|
+
export function categorizeFindings(findings) {
|
|
46
|
+
return {
|
|
47
|
+
critical: findings.filter((f) => f.confidence === "high" && isCritical(f)),
|
|
48
|
+
important: findings.filter((f) => f.confidence === "high" && !isCritical(f)),
|
|
49
|
+
supplementary: findings.filter((f) => f.confidence === "medium"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Smart budget enforcement. Instead of truncating at a character boundary,
|
|
54
|
+
* drop lower-priority sections first (middle of context = worst retention).
|
|
55
|
+
*
|
|
56
|
+
* Priority order (highest to lowest):
|
|
57
|
+
* 1. Commands (always keep)
|
|
58
|
+
* 2. Critical constraints (always keep)
|
|
59
|
+
* 3. Core modules (keep if budget allows)
|
|
60
|
+
* 4. Stack (keep if budget allows)
|
|
61
|
+
* 5. Conventions/important findings (drop first from middle)
|
|
62
|
+
* 6. Supplementary findings (drop first)
|
|
63
|
+
* 7. Footer/manual section (always keep — end of context = high retention)
|
|
64
|
+
*/
|
|
65
|
+
export function enforceTokenBudget(sections, budget) {
|
|
66
|
+
// Sort by priority descending (highest priority = keep)
|
|
67
|
+
const sorted = [...sections].sort((a, b) => b.priority - a.priority);
|
|
68
|
+
let totalTokens = sorted.reduce((sum, s) => sum + estimateTokens(s.content), 0);
|
|
69
|
+
if (totalTokens <= budget) {
|
|
70
|
+
// Everything fits — return in original order
|
|
71
|
+
return sections.map((s) => s.content);
|
|
72
|
+
}
|
|
73
|
+
// Drop lowest-priority sections until we fit
|
|
74
|
+
const dropped = new Set();
|
|
75
|
+
const byPriority = [...sections].sort((a, b) => a.priority - b.priority);
|
|
76
|
+
for (const section of byPriority) {
|
|
77
|
+
if (totalTokens <= budget)
|
|
78
|
+
break;
|
|
79
|
+
if (section.priority >= 90)
|
|
80
|
+
continue; // Never drop critical sections
|
|
81
|
+
totalTokens -= estimateTokens(section.content);
|
|
82
|
+
dropped.add(section.key);
|
|
83
|
+
}
|
|
84
|
+
return sections
|
|
85
|
+
.filter((s) => !dropped.has(s.key))
|
|
86
|
+
.map((s) => s.content);
|
|
87
|
+
}
|
package/dist/scanner/build.js
CHANGED
|
@@ -52,5 +52,33 @@ export async function detectBuildCommands(dir) {
|
|
|
52
52
|
// can't read
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
// Check for go.mod
|
|
56
|
+
const goModPath = path.join(dir, "go.mod");
|
|
57
|
+
if (fs.existsSync(goModPath)) {
|
|
58
|
+
if (!commands.build)
|
|
59
|
+
commands.build = "go build ./...";
|
|
60
|
+
if (!commands.test)
|
|
61
|
+
commands.test = "go test ./...";
|
|
62
|
+
// Check for cmd/ entry points
|
|
63
|
+
const cmdDir = path.join(dir, "cmd");
|
|
64
|
+
if (fs.existsSync(cmdDir)) {
|
|
65
|
+
try {
|
|
66
|
+
const entries = fs.readdirSync(cmdDir);
|
|
67
|
+
if (entries.length === 1) {
|
|
68
|
+
commands.dev = `go run ./cmd/${entries[0]}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (!commands.dev)
|
|
75
|
+
commands.dev = "go run .";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Check for requirements.txt / pyproject.toml Python commands
|
|
79
|
+
const hasRequirements = fs.existsSync(path.join(dir, "requirements.txt"));
|
|
80
|
+
if (hasRequirements && !commands.test) {
|
|
81
|
+
commands.test = "pytest";
|
|
82
|
+
}
|
|
55
83
|
return commands;
|
|
56
84
|
}
|