builder-assistant-engineer 0.2.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/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/analyst/prompt.js +19 -0
- package/dist/analyst/retry.js +38 -0
- package/dist/artifacts/diff.js +47 -0
- package/dist/artifacts/lessons.js +32 -0
- package/dist/artifacts/merge.js +70 -0
- package/dist/artifacts/write.js +44 -0
- package/dist/backends/agent-cli.js +174 -0
- package/dist/backends/api.js +171 -0
- package/dist/backends/index.js +10 -0
- package/dist/backends/manual.js +52 -0
- package/dist/backends/sse.js +24 -0
- package/dist/backends/types.js +1 -0
- package/dist/bin.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/commands/context.js +12 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/next.js +103 -0
- package/dist/commands/plan.js +71 -0
- package/dist/commands/replan.js +37 -0
- package/dist/commands/review.js +37 -0
- package/dist/commands/shared.js +40 -0
- package/dist/commands/status.js +95 -0
- package/dist/config/commands.js +85 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/settings.js +8 -0
- package/dist/config/store.js +38 -0
- package/dist/core/bash.js +105 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/fs.js +35 -0
- package/dist/core/git.js +104 -0
- package/dist/core/gitignore.js +14 -0
- package/dist/core/json.js +13 -0
- package/dist/core/paths.js +14 -0
- package/dist/core/process.js +56 -0
- package/dist/core/prompt-loader.js +27 -0
- package/dist/core/state.js +27 -0
- package/dist/core/which.js +24 -0
- package/dist/detect/agents.js +17 -0
- package/dist/detect/mode.js +11 -0
- package/dist/digest/baseline.js +192 -0
- package/dist/digest/budget.js +31 -0
- package/dist/digest/files.js +167 -0
- package/dist/digest/format.js +18 -0
- package/dist/digest/git.js +19 -0
- package/dist/digest/index.js +100 -0
- package/dist/digest/manifests.js +133 -0
- package/dist/digest/read.js +28 -0
- package/dist/digest/redact.js +37 -0
- package/dist/digest/render.js +138 -0
- package/dist/digest/select.js +116 -0
- package/dist/digest/todos.js +16 -0
- package/dist/digest/tree.js +55 -0
- package/dist/digest/walk.js +72 -0
- package/dist/gates/capture.js +145 -0
- package/dist/gates/contract.js +339 -0
- package/dist/gates/enforce.js +143 -0
- package/dist/gates/findings.js +17 -0
- package/dist/gates/gate.js +315 -0
- package/dist/gates/ignore-rules.js +104 -0
- package/dist/gates/protected-files.js +311 -0
- package/dist/gates/regression.js +219 -0
- package/dist/gates/reports.js +349 -0
- package/dist/gates/results.js +295 -0
- package/dist/gates/runners.js +189 -0
- package/dist/gates/state-guard.js +56 -0
- package/dist/i18n/en.js +288 -0
- package/dist/i18n/es.js +288 -0
- package/dist/i18n/index.js +18 -0
- package/dist/interview/adaptive.js +69 -0
- package/dist/interview/brief.js +27 -0
- package/dist/interview/questions.js +18 -0
- package/dist/interview/render.js +19 -0
- package/dist/interview/reply.js +34 -0
- package/dist/next/attempts.js +151 -0
- package/dist/next/start.js +179 -0
- package/dist/plan/continuation.js +40 -0
- package/dist/plan/evidence.js +239 -0
- package/dist/plan/filter.js +20 -0
- package/dist/plan/parser.js +283 -0
- package/dist/plan/prior.js +32 -0
- package/dist/plan/questions.js +48 -0
- package/dist/plan/repair.js +33 -0
- package/dist/plan/report.js +37 -0
- package/dist/plan/run.js +191 -0
- package/dist/prompts/analyst.md +101 -0
- package/dist/prompts/continue.md +11 -0
- package/dist/prompts/fix-format.md +13 -0
- package/dist/prompts/fix-paths.md +23 -0
- package/dist/prompts/lesson.md +18 -0
- package/dist/prompts/retry.md +9 -0
- package/dist/prompts/review.md +36 -0
- package/dist/prompts/task.md +9 -0
- package/dist/review/changes.js +177 -0
- package/dist/review/diff.js +115 -0
- package/dist/review/integrity.js +150 -0
- package/dist/review/mechanical.js +62 -0
- package/dist/review/parse.js +40 -0
- package/dist/review/reviewer.js +23 -0
- package/dist/review/run.js +134 -0
- package/dist/review/scope.js +55 -0
- package/dist/review/secrets.js +163 -0
- package/dist/review/snapshot.js +38 -0
- package/dist/review/tests.js +140 -0
- package/dist/tasks/attempts.js +71 -0
- package/dist/tasks/checks.js +181 -0
- package/dist/tasks/frontmatter.js +58 -0
- package/dist/tasks/graph.js +27 -0
- package/dist/tasks/handoff.js +62 -0
- package/dist/tasks/learn.js +128 -0
- package/dist/tasks/load.js +37 -0
- package/dist/tasks/metrics.js +40 -0
- package/dist/tasks/runs.js +12 -0
- package/dist/tasks/schema.js +96 -0
- package/dist/tasks/select.js +15 -0
- package/dist/tasks/shell-words.js +176 -0
- package/dist/tasks/status.js +13 -0
- package/dist/tasks/verify.js +181 -0
- package/dist/ui/clack.js +46 -0
- package/dist/ui/prompter.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { baseName, languageOf } from "./files.js";
|
|
2
|
+
import { findDependency } from "./manifests.js";
|
|
3
|
+
export const BASELINE_CHECKS = ["tests", "lint", "formatter", "typecheck", "ci"];
|
|
4
|
+
const EVIDENCE_SAMPLE = 3;
|
|
5
|
+
const TEST_DEPENDENCIES = [
|
|
6
|
+
["npm", "vitest"],
|
|
7
|
+
["npm", "jest"],
|
|
8
|
+
["npm", "mocha"],
|
|
9
|
+
["npm", "ava"],
|
|
10
|
+
["npm", "jasmine"],
|
|
11
|
+
["npm", "@playwright/test"],
|
|
12
|
+
["npm", "cypress"],
|
|
13
|
+
["python", "pytest"],
|
|
14
|
+
["java", "junit"],
|
|
15
|
+
["java", "testng"],
|
|
16
|
+
["php", "phpunit/phpunit"],
|
|
17
|
+
["php", "pestphp/pest"],
|
|
18
|
+
["ruby", "rspec"],
|
|
19
|
+
["ruby", "minitest"],
|
|
20
|
+
["dotnet", "xunit"],
|
|
21
|
+
["dotnet", "nunit"],
|
|
22
|
+
["dotnet", "mstest"],
|
|
23
|
+
["dart", "flutter_test"],
|
|
24
|
+
];
|
|
25
|
+
const LINT_DEPENDENCIES = [
|
|
26
|
+
["npm", "eslint"],
|
|
27
|
+
["npm", "@biomejs/biome"],
|
|
28
|
+
["npm", "oxlint"],
|
|
29
|
+
["npm", "xo"],
|
|
30
|
+
["npm", "standard"],
|
|
31
|
+
["python", "ruff"],
|
|
32
|
+
["python", "flake8"],
|
|
33
|
+
["python", "pylint"],
|
|
34
|
+
["php", "phpstan/phpstan"],
|
|
35
|
+
["php", "squizlabs/php_codesniffer"],
|
|
36
|
+
["ruby", "rubocop"],
|
|
37
|
+
];
|
|
38
|
+
const FORMAT_DEPENDENCIES = [
|
|
39
|
+
["npm", "prettier"],
|
|
40
|
+
["npm", "@biomejs/biome"],
|
|
41
|
+
["npm", "dprint"],
|
|
42
|
+
["python", "black"],
|
|
43
|
+
["python", "yapf"],
|
|
44
|
+
["python", "autopep8"],
|
|
45
|
+
["php", "friendsofphp/php-cs-fixer"],
|
|
46
|
+
["php", "laravel/pint"],
|
|
47
|
+
];
|
|
48
|
+
const TYPECHECK_DEPENDENCIES = [
|
|
49
|
+
["npm", "typescript"],
|
|
50
|
+
["python", "mypy"],
|
|
51
|
+
["python", "pyright"],
|
|
52
|
+
];
|
|
53
|
+
const TEST_SECTIONS = [["python", "[tool.pytest"]];
|
|
54
|
+
const LINT_SECTIONS = [
|
|
55
|
+
["python", "[tool.ruff"],
|
|
56
|
+
["python", "[tool.pylint"],
|
|
57
|
+
["python", "[flake8]"],
|
|
58
|
+
];
|
|
59
|
+
const FORMAT_SECTIONS = [
|
|
60
|
+
["python", "[tool.black"],
|
|
61
|
+
["python", "[tool.ruff.format"],
|
|
62
|
+
];
|
|
63
|
+
const TYPECHECK_SECTIONS = [
|
|
64
|
+
["python", "[tool.mypy"],
|
|
65
|
+
["python", "[tool.pyright"],
|
|
66
|
+
];
|
|
67
|
+
const FORMAT_TOOLCHAINS = [
|
|
68
|
+
["go", "gofmt (Go toolchain)"],
|
|
69
|
+
["rust", "rustfmt (Rust toolchain)"],
|
|
70
|
+
["dart", "dart format (Dart toolchain)"],
|
|
71
|
+
];
|
|
72
|
+
const TYPECHECK_TOOLCHAINS = [
|
|
73
|
+
["go", "Go compiler"],
|
|
74
|
+
["rust", "Rust compiler"],
|
|
75
|
+
["java", "JVM compiler"],
|
|
76
|
+
["dotnet", ".NET compiler"],
|
|
77
|
+
["swift", "Swift compiler"],
|
|
78
|
+
["dart", "Dart analyzer"],
|
|
79
|
+
];
|
|
80
|
+
const LINT_FILES = [
|
|
81
|
+
/^eslint\.config\.[cm]?[jt]s$/,
|
|
82
|
+
/^\.eslintrc(\..+)?$/,
|
|
83
|
+
/^biome\.jsonc?$/,
|
|
84
|
+
/^\.oxlintrc\.json$/,
|
|
85
|
+
/^\.golangci\.ya?ml$/,
|
|
86
|
+
/^\.?ruff\.toml$/,
|
|
87
|
+
/^\.flake8$/,
|
|
88
|
+
/^\.pylintrc$/,
|
|
89
|
+
/^\.rubocop\.yml$/,
|
|
90
|
+
/^phpstan\.neon(\.dist)?$/,
|
|
91
|
+
/^clippy\.toml$/,
|
|
92
|
+
/^\.swiftlint\.yml$/,
|
|
93
|
+
/^\.credo\.exs$/,
|
|
94
|
+
];
|
|
95
|
+
const FORMAT_FILES = [
|
|
96
|
+
/^\.prettierrc(\..+)?$/,
|
|
97
|
+
/^prettier\.config\.[cm]?[jt]s$/,
|
|
98
|
+
/^biome\.jsonc?$/,
|
|
99
|
+
/^dprint\.jsonc?$/,
|
|
100
|
+
/^\.?rustfmt\.toml$/,
|
|
101
|
+
/^\.clang-format$/,
|
|
102
|
+
/^\.php-cs-fixer(\.dist)?\.php$/,
|
|
103
|
+
/^\.swiftformat$/,
|
|
104
|
+
/^\.formatter\.exs$/,
|
|
105
|
+
];
|
|
106
|
+
const TYPECHECK_FILES = [/^tsconfig(\..+)?\.json$/, /^\.?mypy\.ini$/, /^pyrightconfig\.json$/];
|
|
107
|
+
const CI_PATHS = [
|
|
108
|
+
/^\.github\/workflows\/[^/]+\.ya?ml$/,
|
|
109
|
+
/^\.gitlab-ci\.yml$/,
|
|
110
|
+
/^\.circleci\/config\.ya?ml$/,
|
|
111
|
+
/^azure-pipelines\.ya?ml$/,
|
|
112
|
+
/^Jenkinsfile$/,
|
|
113
|
+
/^bitbucket-pipelines\.yml$/,
|
|
114
|
+
/^\.travis\.yml$/,
|
|
115
|
+
/^\.buildkite\/.+\.ya?ml$/,
|
|
116
|
+
/^\.drone\.yml$/,
|
|
117
|
+
/^\.woodpecker(\.yml|\/.+\.ya?ml)$/,
|
|
118
|
+
];
|
|
119
|
+
const TEST_PATHS = [
|
|
120
|
+
/(^|\/)tests?\.(py|js|ts|mjs|cjs|rb)$/,
|
|
121
|
+
/_(?:unit)?test\.(cc|cpp|cxx|c)$/,
|
|
122
|
+
/(^|\/)(__tests__|tests?|spec|e2e|cypress)\//,
|
|
123
|
+
/\.(test|spec|cy|e2e)\.[a-z0-9]+$/i,
|
|
124
|
+
/(^|\/)test_[^/]+\.py$/,
|
|
125
|
+
/_(test|spec)\.(go|py|exs|rb)$/,
|
|
126
|
+
/Tests?\.(cs|java|kt|swift|php|scala)$/,
|
|
127
|
+
];
|
|
128
|
+
export function isTestFile(path) {
|
|
129
|
+
return languageOf(path) !== undefined && TEST_PATHS.some((pattern) => pattern.test(path));
|
|
130
|
+
}
|
|
131
|
+
export function isCiPath(path) {
|
|
132
|
+
return CI_PATHS.some((pattern) => pattern.test(path));
|
|
133
|
+
}
|
|
134
|
+
export function detectBaseline(paths, manifests) {
|
|
135
|
+
return {
|
|
136
|
+
tests: [
|
|
137
|
+
...dependencyEvidence(manifests, TEST_DEPENDENCIES),
|
|
138
|
+
...sectionEvidence(manifests, TEST_SECTIONS),
|
|
139
|
+
...testFileEvidence(paths),
|
|
140
|
+
],
|
|
141
|
+
lint: [
|
|
142
|
+
...dependencyEvidence(manifests, LINT_DEPENDENCIES),
|
|
143
|
+
...sectionEvidence(manifests, LINT_SECTIONS),
|
|
144
|
+
...fileEvidence(paths.filter((path) => matchesName(path, LINT_FILES))),
|
|
145
|
+
],
|
|
146
|
+
formatter: [
|
|
147
|
+
...dependencyEvidence(manifests, FORMAT_DEPENDENCIES),
|
|
148
|
+
...sectionEvidence(manifests, FORMAT_SECTIONS),
|
|
149
|
+
...fileEvidence(paths.filter((path) => matchesName(path, FORMAT_FILES))),
|
|
150
|
+
...toolchainEvidence(manifests, FORMAT_TOOLCHAINS),
|
|
151
|
+
],
|
|
152
|
+
typecheck: [
|
|
153
|
+
...dependencyEvidence(manifests, TYPECHECK_DEPENDENCIES),
|
|
154
|
+
...sectionEvidence(manifests, TYPECHECK_SECTIONS),
|
|
155
|
+
...fileEvidence(paths.filter((path) => matchesName(path, TYPECHECK_FILES))),
|
|
156
|
+
...toolchainEvidence(manifests, TYPECHECK_TOOLCHAINS),
|
|
157
|
+
],
|
|
158
|
+
ci: fileEvidence(paths.filter(isCiPath)),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function dependencyEvidence(manifests, markers) {
|
|
162
|
+
return markers.flatMap(([ecosystem, name]) => {
|
|
163
|
+
const manifest = findDependency(manifests, ecosystem, name);
|
|
164
|
+
return manifest ? [`${name} (${manifest.path})`] : [];
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function sectionEvidence(manifests, markers) {
|
|
168
|
+
return markers.flatMap(([ecosystem, section]) => {
|
|
169
|
+
const manifest = manifests.find((candidate) => candidate.ecosystem === ecosystem && candidate.text.includes(section));
|
|
170
|
+
return manifest ? [`${section.replace(/\]?$/, "]")} (${manifest.path})`] : [];
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function toolchainEvidence(manifests, markers) {
|
|
174
|
+
return markers
|
|
175
|
+
.filter(([ecosystem]) => manifests.some((manifest) => manifest.ecosystem === ecosystem))
|
|
176
|
+
.map(([, label]) => label);
|
|
177
|
+
}
|
|
178
|
+
function testFileEvidence(paths) {
|
|
179
|
+
const tests = paths.filter(isTestFile);
|
|
180
|
+
if (tests.length === 0)
|
|
181
|
+
return [];
|
|
182
|
+
const noun = tests.length === 1 ? "test file" : "test files";
|
|
183
|
+
return [`${tests.length} ${noun}, e.g. ${tests.slice(0, EVIDENCE_SAMPLE).join(", ")}`];
|
|
184
|
+
}
|
|
185
|
+
function fileEvidence(paths) {
|
|
186
|
+
const extra = paths.length - EVIDENCE_SAMPLE;
|
|
187
|
+
return [...paths.slice(0, EVIDENCE_SAMPLE), ...(extra > 0 ? [`+${extra} more`] : [])];
|
|
188
|
+
}
|
|
189
|
+
function matchesName(path, patterns) {
|
|
190
|
+
const name = baseName(path);
|
|
191
|
+
return patterns.some((pattern) => pattern.test(name));
|
|
192
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { extensionOf } from "./files.js";
|
|
2
|
+
import { codeFence, truncateText } from "./format.js";
|
|
3
|
+
const MIN_PARTIAL = 1_000;
|
|
4
|
+
const FENCE_SLACK = 8;
|
|
5
|
+
export function renderBlock(block) {
|
|
6
|
+
return `### ${block.path}\n\n${codeFence(block.body, extensionOf(block.path).slice(1))}\n\n`;
|
|
7
|
+
}
|
|
8
|
+
export function fitBlocks(blocks, budget) {
|
|
9
|
+
const kept = [];
|
|
10
|
+
const omitted = [];
|
|
11
|
+
let remaining = budget;
|
|
12
|
+
for (const block of [...blocks].sort((a, b) => a.priority - b.priority)) {
|
|
13
|
+
const fitted = fitBlock(block, remaining);
|
|
14
|
+
if (fitted) {
|
|
15
|
+
kept.push(fitted);
|
|
16
|
+
remaining -= renderBlock(fitted).length;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
omitted.push(block.path);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { blocks: kept, omitted };
|
|
23
|
+
}
|
|
24
|
+
function fitBlock(block, remaining) {
|
|
25
|
+
if (renderBlock(block).length <= remaining)
|
|
26
|
+
return block;
|
|
27
|
+
if (remaining < MIN_PARTIAL)
|
|
28
|
+
return undefined;
|
|
29
|
+
const overhead = renderBlock({ ...block, body: "" }).length + FENCE_SLACK;
|
|
30
|
+
return { ...block, body: truncateText(block.body, remaining - overhead) };
|
|
31
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
const BINARY_EXTENSIONS = new Set([
|
|
3
|
+
"png jpg jpeg gif bmp ico icns webp avif tiff psd svgz",
|
|
4
|
+
"pdf doc docx xls xlsx ppt pptx odt",
|
|
5
|
+
"zip gz tgz bz2 xz 7z rar tar jar war ear",
|
|
6
|
+
"exe dll so dylib a o obj lib class pyc pyo wasm bin dat",
|
|
7
|
+
"woff woff2 ttf otf eot",
|
|
8
|
+
"mp3 mp4 m4a wav ogg flac webm mov avi mkv",
|
|
9
|
+
"sqlite sqlite3 db",
|
|
10
|
+
]
|
|
11
|
+
.join(" ")
|
|
12
|
+
.split(" ")
|
|
13
|
+
.map((extension) => `.${extension}`));
|
|
14
|
+
const LOCKFILES = new Set([
|
|
15
|
+
"package-lock.json",
|
|
16
|
+
"npm-shrinkwrap.json",
|
|
17
|
+
"yarn.lock",
|
|
18
|
+
"pnpm-lock.yaml",
|
|
19
|
+
"bun.lock",
|
|
20
|
+
"bun.lockb",
|
|
21
|
+
"deno.lock",
|
|
22
|
+
"Cargo.lock",
|
|
23
|
+
"poetry.lock",
|
|
24
|
+
"Pipfile.lock",
|
|
25
|
+
"uv.lock",
|
|
26
|
+
"pdm.lock",
|
|
27
|
+
"composer.lock",
|
|
28
|
+
"Gemfile.lock",
|
|
29
|
+
"go.sum",
|
|
30
|
+
"mix.lock",
|
|
31
|
+
"pubspec.lock",
|
|
32
|
+
"Podfile.lock",
|
|
33
|
+
"packages.lock.json",
|
|
34
|
+
"flake.lock",
|
|
35
|
+
]);
|
|
36
|
+
const SECRET_NAMES = new Set([
|
|
37
|
+
".envrc",
|
|
38
|
+
".netrc",
|
|
39
|
+
".pgpass",
|
|
40
|
+
"credentials",
|
|
41
|
+
"id_rsa",
|
|
42
|
+
"id_dsa",
|
|
43
|
+
"id_ecdsa",
|
|
44
|
+
"id_ed25519",
|
|
45
|
+
".pypirc",
|
|
46
|
+
".git-credentials",
|
|
47
|
+
"secrets.env",
|
|
48
|
+
]);
|
|
49
|
+
const SECRET_EXTENSIONS = new Set([
|
|
50
|
+
".pem",
|
|
51
|
+
".key",
|
|
52
|
+
".p12",
|
|
53
|
+
".pfx",
|
|
54
|
+
".jks",
|
|
55
|
+
".keystore",
|
|
56
|
+
".tfstate",
|
|
57
|
+
".tfvars",
|
|
58
|
+
".p8",
|
|
59
|
+
".ppk",
|
|
60
|
+
]);
|
|
61
|
+
const LANGUAGES = {
|
|
62
|
+
".ts": "TypeScript",
|
|
63
|
+
".tsx": "TypeScript",
|
|
64
|
+
".mts": "TypeScript",
|
|
65
|
+
".cts": "TypeScript",
|
|
66
|
+
".js": "JavaScript",
|
|
67
|
+
".jsx": "JavaScript",
|
|
68
|
+
".mjs": "JavaScript",
|
|
69
|
+
".cjs": "JavaScript",
|
|
70
|
+
".py": "Python",
|
|
71
|
+
".go": "Go",
|
|
72
|
+
".rs": "Rust",
|
|
73
|
+
".java": "Java",
|
|
74
|
+
".kt": "Kotlin",
|
|
75
|
+
".kts": "Kotlin",
|
|
76
|
+
".scala": "Scala",
|
|
77
|
+
".cs": "C#",
|
|
78
|
+
".fs": "F#",
|
|
79
|
+
".php": "PHP",
|
|
80
|
+
".rb": "Ruby",
|
|
81
|
+
".swift": "Swift",
|
|
82
|
+
".m": "Objective-C",
|
|
83
|
+
".c": "C",
|
|
84
|
+
".h": "C",
|
|
85
|
+
".cc": "C++",
|
|
86
|
+
".cpp": "C++",
|
|
87
|
+
".hpp": "C++",
|
|
88
|
+
".dart": "Dart",
|
|
89
|
+
".ex": "Elixir",
|
|
90
|
+
".exs": "Elixir",
|
|
91
|
+
".erl": "Erlang",
|
|
92
|
+
".hs": "Haskell",
|
|
93
|
+
".lua": "Lua",
|
|
94
|
+
".r": "R",
|
|
95
|
+
".jl": "Julia",
|
|
96
|
+
".zig": "Zig",
|
|
97
|
+
".vue": "Vue",
|
|
98
|
+
".svelte": "Svelte",
|
|
99
|
+
".astro": "Astro",
|
|
100
|
+
".html": "HTML",
|
|
101
|
+
".css": "CSS",
|
|
102
|
+
".scss": "SCSS",
|
|
103
|
+
".sass": "SCSS",
|
|
104
|
+
".less": "Less",
|
|
105
|
+
".sql": "SQL",
|
|
106
|
+
".sh": "Shell",
|
|
107
|
+
".bash": "Shell",
|
|
108
|
+
".ps1": "PowerShell",
|
|
109
|
+
".tf": "Terraform",
|
|
110
|
+
".proto": "Protocol Buffers",
|
|
111
|
+
".graphql": "GraphQL",
|
|
112
|
+
};
|
|
113
|
+
export function extensionOf(path) {
|
|
114
|
+
return posix.extname(path).toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
export function baseName(path) {
|
|
117
|
+
return posix.basename(path);
|
|
118
|
+
}
|
|
119
|
+
export function depthOf(path) {
|
|
120
|
+
return path.split("/").length - 1;
|
|
121
|
+
}
|
|
122
|
+
export function isBinaryPath(path) {
|
|
123
|
+
return BINARY_EXTENSIONS.has(extensionOf(path));
|
|
124
|
+
}
|
|
125
|
+
const BUILD_DIRS = new Set([
|
|
126
|
+
"dist",
|
|
127
|
+
"build",
|
|
128
|
+
"out",
|
|
129
|
+
"target",
|
|
130
|
+
"coverage",
|
|
131
|
+
"node_modules",
|
|
132
|
+
"vendor",
|
|
133
|
+
".venv",
|
|
134
|
+
"venv",
|
|
135
|
+
"__pycache__",
|
|
136
|
+
".pytest_cache",
|
|
137
|
+
".mypy_cache",
|
|
138
|
+
".ruff_cache",
|
|
139
|
+
".next",
|
|
140
|
+
".nuxt",
|
|
141
|
+
".svelte-kit",
|
|
142
|
+
".turbo",
|
|
143
|
+
".gradle",
|
|
144
|
+
]);
|
|
145
|
+
export function isBuildOutput(path) {
|
|
146
|
+
const parts = path.split("/");
|
|
147
|
+
return (parts.slice(0, -1).some((part) => BUILD_DIRS.has(part)) ||
|
|
148
|
+
/\.(pyc|tsbuildinfo)$|\.min\.(js|css|mjs)$|\.(js|css|mjs)\.map$/i.test(path));
|
|
149
|
+
}
|
|
150
|
+
export function isLockfile(path) {
|
|
151
|
+
return LOCKFILES.has(baseName(path));
|
|
152
|
+
}
|
|
153
|
+
export function isSecretPath(path) {
|
|
154
|
+
const name = baseName(path).toLowerCase();
|
|
155
|
+
if (/^\.env([.-].+)?$/.test(name))
|
|
156
|
+
return !/[.-](example|sample|template|dist)$/.test(name);
|
|
157
|
+
return SECRET_NAMES.has(name) || SECRET_EXTENSIONS.has(extensionOf(name));
|
|
158
|
+
}
|
|
159
|
+
export function isReadable(path) {
|
|
160
|
+
return !isBinaryPath(path) && !isLockfile(path) && !isSecretPath(path);
|
|
161
|
+
}
|
|
162
|
+
export function languageOf(path) {
|
|
163
|
+
return LANGUAGES[extensionOf(path)];
|
|
164
|
+
}
|
|
165
|
+
export function byPath(a, b) {
|
|
166
|
+
return depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0);
|
|
167
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const TRUNCATION_MARK = "\n… [truncated]";
|
|
2
|
+
export function formatBytes(bytes) {
|
|
3
|
+
if (bytes < 1024)
|
|
4
|
+
return `${bytes} B`;
|
|
5
|
+
if (bytes < 1024 * 1024)
|
|
6
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
7
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
8
|
+
}
|
|
9
|
+
export function codeFence(body, language = "") {
|
|
10
|
+
const longest = Array.from(body.matchAll(/`+/g)).reduce((max, match) => Math.max(max, match[0].length), 0);
|
|
11
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
12
|
+
return `${fence}${language}\n${body}\n${fence}`;
|
|
13
|
+
}
|
|
14
|
+
export function truncateText(text, max) {
|
|
15
|
+
if (text.length <= max)
|
|
16
|
+
return text;
|
|
17
|
+
return `${text.slice(0, Math.max(0, max - TRUNCATION_MARK.length))}${TRUNCATION_MARK}`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { git, isGitRepo, lines } from "../core/git.js";
|
|
2
|
+
import { redact } from "./redact.js";
|
|
3
|
+
const COMMIT_LIMIT = 10;
|
|
4
|
+
export async function readGitInfo(cwd) {
|
|
5
|
+
if (!(await isGitRepo(cwd)))
|
|
6
|
+
return undefined;
|
|
7
|
+
const [branch, branches, commits, status] = await Promise.all([
|
|
8
|
+
git(cwd, ["branch", "--show-current"]),
|
|
9
|
+
git(cwd, ["branch", "--format=%(refname:short)"]),
|
|
10
|
+
git(cwd, ["log", "-n", String(COMMIT_LIMIT), "--date=short", "--pretty=format:%h %ad %s"]),
|
|
11
|
+
git(cwd, ["status", "--porcelain"]),
|
|
12
|
+
]);
|
|
13
|
+
return {
|
|
14
|
+
branch: (branch ?? "").trim(),
|
|
15
|
+
branches: lines(branches),
|
|
16
|
+
commits: lines(commits).map(redact),
|
|
17
|
+
changes: lines(status).length,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { DEFAULT_DIGEST_MAX_CHARS } from "../config/schema.js";
|
|
3
|
+
import { detectMode } from "../detect/mode.js";
|
|
4
|
+
import { detectBaseline } from "./baseline.js";
|
|
5
|
+
import { fitBlocks } from "./budget.js";
|
|
6
|
+
import { baseName, isReadable, languageOf } from "./files.js";
|
|
7
|
+
import { truncateText } from "./format.js";
|
|
8
|
+
import { readGitInfo } from "./git.js";
|
|
9
|
+
import { detectFrameworks, detectPackageManagers, findManifestPaths, toManifest, } from "./manifests.js";
|
|
10
|
+
import { readText, readTexts } from "./read.js";
|
|
11
|
+
import { redact } from "./redact.js";
|
|
12
|
+
import { renderContents, renderHead, renderOmitted } from "./render.js";
|
|
13
|
+
import { findConfigs, findDocs, findEntryPoints } from "./select.js";
|
|
14
|
+
import { countTodos } from "./todos.js";
|
|
15
|
+
import { DEFAULT_TREE_DEPTH } from "./tree.js";
|
|
16
|
+
import { scanFiles } from "./walk.js";
|
|
17
|
+
export { BASELINE_CHECKS } from "./baseline.js";
|
|
18
|
+
const CONTENT_RESERVE = 3_000;
|
|
19
|
+
const MAX_CONTENT_BYTES = 1_000_000;
|
|
20
|
+
const TODO_MAX_FILES = 5_000;
|
|
21
|
+
const TODO_MAX_BYTES = 256_000;
|
|
22
|
+
const CAPS = { manifest: 8_000, entry: 6_000, readme: 12_000, doc: 6_000, config: 3_000 };
|
|
23
|
+
const GROUPS = {
|
|
24
|
+
manifests: { group: "Manifest contents", priority: 1 },
|
|
25
|
+
entries: { group: "Entry point contents", priority: 2 },
|
|
26
|
+
docs: { group: "Doc contents", priority: 3 },
|
|
27
|
+
configs: { group: "Config contents", priority: 4 },
|
|
28
|
+
};
|
|
29
|
+
export async function buildDigest(root, options = {}) {
|
|
30
|
+
const maxChars = options.maxChars ?? DEFAULT_DIGEST_MAX_CHARS;
|
|
31
|
+
const facts = await gatherFacts(root, options.treeDepth ?? DEFAULT_TREE_DEPTH);
|
|
32
|
+
const head = truncateText(`${renderHead(facts)}\n\n`, maxChars);
|
|
33
|
+
const blocks = await contentBlocks(root, facts);
|
|
34
|
+
const fitted = fitBlocks(blocks, maxChars - head.length - CONTENT_RESERVE);
|
|
35
|
+
const body = renderContents(fitted.blocks) + renderOmitted(fitted.omitted);
|
|
36
|
+
return {
|
|
37
|
+
text: truncateText(head + body, maxChars),
|
|
38
|
+
mode: facts.mode,
|
|
39
|
+
baseline: facts.baseline,
|
|
40
|
+
omitted: fitted.omitted,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function gatherFacts(root, treeDepth) {
|
|
44
|
+
const scan = await scanFiles(root);
|
|
45
|
+
const paths = scan.files.map((file) => file.path);
|
|
46
|
+
const manifests = await readManifests(root, findManifestPaths(paths));
|
|
47
|
+
const [git, todoTexts] = await Promise.all([
|
|
48
|
+
readGitInfo(root),
|
|
49
|
+
readTexts(root, todoCandidates(scan.files)),
|
|
50
|
+
]);
|
|
51
|
+
return {
|
|
52
|
+
name: basename(resolve(root)),
|
|
53
|
+
mode: detectMode(paths),
|
|
54
|
+
scan,
|
|
55
|
+
manifests,
|
|
56
|
+
frameworks: detectFrameworks(manifests),
|
|
57
|
+
packageManagers: detectPackageManagers(paths, manifests),
|
|
58
|
+
baseline: detectBaseline(paths, manifests),
|
|
59
|
+
entryPoints: findEntryPoints(paths, manifests),
|
|
60
|
+
docs: findDocs(paths),
|
|
61
|
+
configs: findConfigs(paths),
|
|
62
|
+
git,
|
|
63
|
+
todos: countTodos(todoTexts),
|
|
64
|
+
treeDepth,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function readManifests(root, paths) {
|
|
68
|
+
const texts = await readTexts(root, paths);
|
|
69
|
+
return paths
|
|
70
|
+
.map((path) => toManifest(path, texts.get(path) ?? ""))
|
|
71
|
+
.filter((manifest) => manifest !== undefined);
|
|
72
|
+
}
|
|
73
|
+
function todoCandidates(files) {
|
|
74
|
+
return files
|
|
75
|
+
.filter((file) => file.size <= TODO_MAX_BYTES && languageOf(file.path) && isReadable(file.path))
|
|
76
|
+
.slice(0, TODO_MAX_FILES)
|
|
77
|
+
.map((file) => file.path);
|
|
78
|
+
}
|
|
79
|
+
async function contentBlocks(root, facts) {
|
|
80
|
+
const sizes = new Map(facts.scan.files.map((file) => [file.path, file.size]));
|
|
81
|
+
const read = async (path, group, cap) => {
|
|
82
|
+
if (!isReadable(path) || (sizes.get(path) ?? 0) > MAX_CONTENT_BYTES)
|
|
83
|
+
return undefined;
|
|
84
|
+
const text = await readText(root, path);
|
|
85
|
+
return text === undefined ? undefined : toBlock(path, text, group, cap);
|
|
86
|
+
};
|
|
87
|
+
const manifests = facts.manifests.map((manifest) => toBlock(manifest.path, manifest.text, GROUPS.manifests, CAPS.manifest));
|
|
88
|
+
const others = await Promise.all([
|
|
89
|
+
...facts.entryPoints.map((path) => read(path, GROUPS.entries, CAPS.entry)),
|
|
90
|
+
...facts.docs.map((path) => read(path, GROUPS.docs, docCap(path))),
|
|
91
|
+
...facts.configs.map((path) => read(path, GROUPS.configs, CAPS.config)),
|
|
92
|
+
]);
|
|
93
|
+
return [...manifests, ...others.filter((block) => block !== undefined)];
|
|
94
|
+
}
|
|
95
|
+
function toBlock(path, text, group, cap) {
|
|
96
|
+
return { ...group, path, body: truncateText(redact(text).trimEnd(), cap) };
|
|
97
|
+
}
|
|
98
|
+
function docCap(path) {
|
|
99
|
+
return /^readme/i.test(baseName(path)) ? CAPS.readme : CAPS.doc;
|
|
100
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { asRecord, parseObject } from "../core/json.js";
|
|
2
|
+
import { baseName, byPath, extensionOf } from "./files.js";
|
|
3
|
+
export const MAX_MANIFESTS = 20;
|
|
4
|
+
const MANIFESTS = new Map([
|
|
5
|
+
["package.json", "npm"],
|
|
6
|
+
["deno.json", "npm"],
|
|
7
|
+
["deno.jsonc", "npm"],
|
|
8
|
+
["pyproject.toml", "python"],
|
|
9
|
+
["setup.py", "python"],
|
|
10
|
+
["setup.cfg", "python"],
|
|
11
|
+
["Pipfile", "python"],
|
|
12
|
+
["go.mod", "go"],
|
|
13
|
+
["Cargo.toml", "rust"],
|
|
14
|
+
["pom.xml", "java"],
|
|
15
|
+
["build.gradle", "java"],
|
|
16
|
+
["build.gradle.kts", "java"],
|
|
17
|
+
["composer.json", "php"],
|
|
18
|
+
["Gemfile", "ruby"],
|
|
19
|
+
["mix.exs", "elixir"],
|
|
20
|
+
["pubspec.yaml", "dart"],
|
|
21
|
+
["Package.swift", "swift"],
|
|
22
|
+
]);
|
|
23
|
+
const DOTNET_EXTENSIONS = new Set([".csproj", ".fsproj", ".vbproj"]);
|
|
24
|
+
const JSON_DEPENDENCY_FIELDS = new Map([
|
|
25
|
+
["package.json", ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]],
|
|
26
|
+
["composer.json", ["require", "require-dev"]],
|
|
27
|
+
]);
|
|
28
|
+
const LOCKFILE_MANAGERS = [
|
|
29
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
30
|
+
["yarn.lock", "yarn"],
|
|
31
|
+
["package-lock.json", "npm"],
|
|
32
|
+
["bun.lock", "bun"],
|
|
33
|
+
["bun.lockb", "bun"],
|
|
34
|
+
["deno.lock", "deno"],
|
|
35
|
+
["poetry.lock", "poetry"],
|
|
36
|
+
["uv.lock", "uv"],
|
|
37
|
+
["Pipfile.lock", "pipenv"],
|
|
38
|
+
["pdm.lock", "pdm"],
|
|
39
|
+
["Cargo.lock", "cargo"],
|
|
40
|
+
["go.sum", "go modules"],
|
|
41
|
+
["composer.lock", "composer"],
|
|
42
|
+
["Gemfile.lock", "bundler"],
|
|
43
|
+
["mix.lock", "mix"],
|
|
44
|
+
["pubspec.lock", "pub"],
|
|
45
|
+
];
|
|
46
|
+
const FRAMEWORKS = [
|
|
47
|
+
["Next.js", "npm", "next"],
|
|
48
|
+
["React", "npm", "react"],
|
|
49
|
+
["React Native", "npm", "react-native"],
|
|
50
|
+
["Expo", "npm", "expo"],
|
|
51
|
+
["Vue", "npm", "vue"],
|
|
52
|
+
["Nuxt", "npm", "nuxt"],
|
|
53
|
+
["Svelte", "npm", "svelte"],
|
|
54
|
+
["SvelteKit", "npm", "@sveltejs/kit"],
|
|
55
|
+
["Angular", "npm", "@angular/core"],
|
|
56
|
+
["Astro", "npm", "astro"],
|
|
57
|
+
["Remix", "npm", "@remix-run/react"],
|
|
58
|
+
["Vite", "npm", "vite"],
|
|
59
|
+
["Express", "npm", "express"],
|
|
60
|
+
["Fastify", "npm", "fastify"],
|
|
61
|
+
["Hono", "npm", "hono"],
|
|
62
|
+
["Koa", "npm", "koa"],
|
|
63
|
+
["NestJS", "npm", "@nestjs/core"],
|
|
64
|
+
["Electron", "npm", "electron"],
|
|
65
|
+
["Prisma", "npm", "prisma"],
|
|
66
|
+
["Drizzle", "npm", "drizzle-orm"],
|
|
67
|
+
["TypeORM", "npm", "typeorm"],
|
|
68
|
+
["Mongoose", "npm", "mongoose"],
|
|
69
|
+
["Django", "python", "django"],
|
|
70
|
+
["Flask", "python", "flask"],
|
|
71
|
+
["FastAPI", "python", "fastapi"],
|
|
72
|
+
["SQLAlchemy", "python", "sqlalchemy"],
|
|
73
|
+
["Gin", "go", "gin-gonic/gin"],
|
|
74
|
+
["Echo", "go", "labstack/echo"],
|
|
75
|
+
["Fiber", "go", "gofiber/fiber"],
|
|
76
|
+
["Axum", "rust", "axum"],
|
|
77
|
+
["Actix Web", "rust", "actix-web"],
|
|
78
|
+
["Rocket", "rust", "rocket"],
|
|
79
|
+
["Tokio", "rust", "tokio"],
|
|
80
|
+
["Spring Boot", "java", "spring-boot"],
|
|
81
|
+
["Laravel", "php", "laravel/framework"],
|
|
82
|
+
["Symfony", "php", "symfony/framework-bundle"],
|
|
83
|
+
["Rails", "ruby", "rails"],
|
|
84
|
+
["ASP.NET Core", "dotnet", "microsoft.aspnetcore"],
|
|
85
|
+
["Phoenix", "elixir", "phoenix"],
|
|
86
|
+
["Flutter", "dart", "flutter"],
|
|
87
|
+
];
|
|
88
|
+
export function ecosystemOf(path) {
|
|
89
|
+
const name = baseName(path);
|
|
90
|
+
if (/^requirements.*\.txt$/.test(name))
|
|
91
|
+
return "python";
|
|
92
|
+
if (DOTNET_EXTENSIONS.has(extensionOf(name)))
|
|
93
|
+
return "dotnet";
|
|
94
|
+
return MANIFESTS.get(name);
|
|
95
|
+
}
|
|
96
|
+
export function findManifestPaths(paths) {
|
|
97
|
+
return paths
|
|
98
|
+
.filter((path) => ecosystemOf(path) !== undefined)
|
|
99
|
+
.sort(byPath)
|
|
100
|
+
.slice(0, MAX_MANIFESTS);
|
|
101
|
+
}
|
|
102
|
+
export function toManifest(path, text) {
|
|
103
|
+
const ecosystem = ecosystemOf(path);
|
|
104
|
+
if (!ecosystem)
|
|
105
|
+
return undefined;
|
|
106
|
+
const fields = JSON_DEPENDENCY_FIELDS.get(baseName(path));
|
|
107
|
+
const json = fields || path.endsWith(".json") ? parseObject(text) : undefined;
|
|
108
|
+
const dependencies = new Set((fields ?? []).flatMap((field) => Object.keys(asRecord(json?.[field]))));
|
|
109
|
+
return { path, ecosystem, text, json, dependencies };
|
|
110
|
+
}
|
|
111
|
+
export function findDependency(manifests, ecosystem, name) {
|
|
112
|
+
return manifests.find((manifest) => manifest.ecosystem === ecosystem && declares(manifest, name));
|
|
113
|
+
}
|
|
114
|
+
export function detectFrameworks(manifests) {
|
|
115
|
+
return FRAMEWORKS.filter(([, ecosystem, name]) => findDependency(manifests, ecosystem, name)).map(([label]) => label);
|
|
116
|
+
}
|
|
117
|
+
export function detectPackageManagers(paths, manifests) {
|
|
118
|
+
const names = new Set(paths.map(baseName));
|
|
119
|
+
const declared = manifests
|
|
120
|
+
.map((manifest) => manifest.json?.packageManager)
|
|
121
|
+
.filter((value) => typeof value === "string");
|
|
122
|
+
const locked = LOCKFILE_MANAGERS.filter(([file]) => names.has(file)).map(([, manager]) => manager);
|
|
123
|
+
return [...new Set([...declared, ...locked])];
|
|
124
|
+
}
|
|
125
|
+
function declares(manifest, name) {
|
|
126
|
+
if (JSON_DEPENDENCY_FIELDS.has(baseName(manifest.path)))
|
|
127
|
+
return manifest.dependencies.has(name);
|
|
128
|
+
const pattern = new RegExp(`(^|[^a-z0-9_.])${escapeRegExp(name.toLowerCase())}`, "m");
|
|
129
|
+
return pattern.test(manifest.text.toLowerCase());
|
|
130
|
+
}
|
|
131
|
+
function escapeRegExp(text) {
|
|
132
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
133
|
+
}
|