@gitset-dev/cli 2.3.2 → 2.4.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/index.js +19 -0
- package/lib/generate-local.js +1 -0
- package/lib/knowledge/budget.js +85 -0
- package/lib/knowledge/ci.js +99 -0
- package/lib/knowledge/discover.js +212 -0
- package/lib/knowledge/index.js +89 -0
- package/lib/knowledge/map.js +239 -0
- package/lib/knowledge/secrets.js +99 -0
- package/lib/knowledge/state.js +81 -0
- package/lib/knowledge/validate.js +180 -0
- package/lib/knowledge/write.js +266 -0
- package/lib/prompts/defaults.js +76 -2
- package/package.json +1 -1
- package/src/commands/knowledge.js +582 -0
- package/src/commands/release.js +9 -0
package/index.js
CHANGED
|
@@ -27,6 +27,7 @@ ${theme.bold('AI commands')} (use your own provider key):
|
|
|
27
27
|
release Generate release notes from the commit range
|
|
28
28
|
gitignore Generate a .gitignore for the detected stack
|
|
29
29
|
repo about AI repo description + topics, applied via gh
|
|
30
|
+
knowledge Build an agent-ready knowledge base from your source code
|
|
30
31
|
|
|
31
32
|
${theme.bold('Local tools')} (no AI):
|
|
32
33
|
repo license Generate a LICENSE file (offline)
|
|
@@ -121,6 +122,22 @@ Manage & apply a label pack (local pack + gh).
|
|
|
121
122
|
without asking (--sync is an alias)`,
|
|
122
123
|
dependabot: `Usage: gitset dependabot [--resolve] [--dry-run] [--save-dev]
|
|
123
124
|
Resolve Dependabot alerts for the current repo (local + gh).`,
|
|
125
|
+
knowledge: `Usage: gitset knowledge <init|scan|generate|update|automate> [flags]
|
|
126
|
+
Build and maintain docs/gitset-knowledge/ — an agent-ready knowledge base
|
|
127
|
+
derived from your source code, manifests and CI config (never from existing
|
|
128
|
+
prose docs, so it can't inherit their drift). Secrets are redacted locally
|
|
129
|
+
before anything reaches your AI provider.
|
|
130
|
+
init scaffold .gitset-knowledge.json (optional scan config)
|
|
131
|
+
scan structural pass: zero AI calls, prints plan + cost estimate
|
|
132
|
+
generate full run (always shows the estimate and asks first)
|
|
133
|
+
update incremental: re-summarizes only changed modules
|
|
134
|
+
automate write a GitHub Actions workflow that runs update in CI
|
|
135
|
+
and opens a PR when docs change (always asks first)
|
|
136
|
+
--since <ref> update selector via git diff instead of content hashes
|
|
137
|
+
--mode <m> automate trigger: push | releases | weekly
|
|
138
|
+
--provider <p> override the default provider for this run
|
|
139
|
+
--model <m> override the model
|
|
140
|
+
--yes skip confirmations (CI)`,
|
|
124
141
|
status: `Usage: gitset status
|
|
125
142
|
Show git + configured-provider status.`,
|
|
126
143
|
template: `Usage: gitset template <list|show|edit|path>
|
|
@@ -213,6 +230,8 @@ async function main() {
|
|
|
213
230
|
code = await require('./src/commands/license').runLicenseCommand(rest); break;
|
|
214
231
|
case 'labelspack':
|
|
215
232
|
code = await require('./src/commands/labelspack').runLabelspackCommand(rest); break;
|
|
233
|
+
case 'knowledge':
|
|
234
|
+
code = await require('./src/commands/knowledge').runKnowledgeCommand(rest); break;
|
|
216
235
|
case 'dependabot':
|
|
217
236
|
code = (await require('./src/commands/dependabot-resolver')(null, rest)) || 0; break;
|
|
218
237
|
case 'feedback':
|
package/lib/generate-local.js
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
maxFileChars: 6000,
|
|
5
|
+
signatureChars: 3000,
|
|
6
|
+
maxBatchChars: 24000,
|
|
7
|
+
writeInputChars: 40000,
|
|
8
|
+
summarizeMaxTokens: 2048,
|
|
9
|
+
writeMaxTokens: 6144,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DECLARATION_RE = /^[ \t]*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|def|interface|type|enum|struct|impl|fn|func|module\.exports|exports\.)[^\n]*$/gm;
|
|
13
|
+
|
|
14
|
+
function estimateTokens(chars) {
|
|
15
|
+
return Math.ceil(chars / 4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractSignatures(content, limit) {
|
|
19
|
+
const head = content.slice(0, Math.floor(limit / 3));
|
|
20
|
+
const declarations = [];
|
|
21
|
+
DECLARATION_RE.lastIndex = 0;
|
|
22
|
+
let m;
|
|
23
|
+
while ((m = DECLARATION_RE.exec(content)) !== null) {
|
|
24
|
+
const line = m[0].trim();
|
|
25
|
+
if (line.length > 3) declarations.push(line.slice(0, 200));
|
|
26
|
+
if (declarations.length >= 80) break;
|
|
27
|
+
}
|
|
28
|
+
const sigBlock = declarations.join('\n');
|
|
29
|
+
const combined = `${head}\n\n[declarations extracted from the rest of the file]\n${sigBlock}`;
|
|
30
|
+
return combined.length > limit ? `${combined.slice(0, limit)}\n…(truncated)` : combined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function budgetFileContent(content, opts = {}) {
|
|
34
|
+
const { maxFileChars, signatureChars } = { ...DEFAULTS, ...opts };
|
|
35
|
+
if (content.length <= maxFileChars) {
|
|
36
|
+
return { content, mode: 'full' };
|
|
37
|
+
}
|
|
38
|
+
return { content: extractSignatures(content, signatureChars), mode: 'signatures' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function batchModules(preparedModules, opts = {}) {
|
|
42
|
+
const { maxBatchChars } = { ...DEFAULTS, ...opts };
|
|
43
|
+
const batches = [];
|
|
44
|
+
for (const mod of preparedModules) {
|
|
45
|
+
let current = { module: mod.name, part: 1, files: [], chars: 0 };
|
|
46
|
+
for (const file of mod.files) {
|
|
47
|
+
const fileChars = file.content ? file.content.length : 0;
|
|
48
|
+
if (current.files.length > 0 && current.chars + fileChars > maxBatchChars) {
|
|
49
|
+
batches.push(current);
|
|
50
|
+
current = { module: mod.name, part: current.part + 1, files: [], chars: 0 };
|
|
51
|
+
}
|
|
52
|
+
current.files.push(file);
|
|
53
|
+
current.chars += fileChars;
|
|
54
|
+
}
|
|
55
|
+
if (current.files.length > 0) batches.push(current);
|
|
56
|
+
}
|
|
57
|
+
return batches;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function estimateRun({ batches, docCount, systemOverheadChars = 1500 }, opts = {}) {
|
|
61
|
+
const { summarizeMaxTokens, writeMaxTokens, writeInputChars } = { ...DEFAULTS, ...opts };
|
|
62
|
+
const summarizeCalls = batches.length;
|
|
63
|
+
const summarizeInputTokens = batches.reduce(
|
|
64
|
+
(sum, b) => sum + estimateTokens(b.chars + systemOverheadChars),
|
|
65
|
+
0,
|
|
66
|
+
);
|
|
67
|
+
const writeCalls = docCount;
|
|
68
|
+
const writeInputTokens = writeCalls * estimateTokens(writeInputChars + systemOverheadChars);
|
|
69
|
+
return {
|
|
70
|
+
totalCalls: summarizeCalls + writeCalls,
|
|
71
|
+
summarizeCalls,
|
|
72
|
+
writeCalls,
|
|
73
|
+
estInputTokens: summarizeInputTokens + writeInputTokens,
|
|
74
|
+
estMaxOutputTokens: summarizeCalls * summarizeMaxTokens + writeCalls * writeMaxTokens,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
DEFAULTS,
|
|
80
|
+
budgetFileContent,
|
|
81
|
+
extractSignatures,
|
|
82
|
+
batchModules,
|
|
83
|
+
estimateRun,
|
|
84
|
+
estimateTokens,
|
|
85
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WORKFLOW_PATH = '.github/workflows/gitset-knowledge.yml';
|
|
4
|
+
|
|
5
|
+
const MODES = {
|
|
6
|
+
push: 'on every push to the default branch (paths-filtered; unchanged code costs nothing)',
|
|
7
|
+
releases: 'on every published release',
|
|
8
|
+
weekly: 'weekly (Mondays 06:00 UTC)',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function buildTrigger(mode, defaultBranch) {
|
|
12
|
+
if (mode === 'push') {
|
|
13
|
+
return [
|
|
14
|
+
'on:',
|
|
15
|
+
' workflow_dispatch:',
|
|
16
|
+
' push:',
|
|
17
|
+
` branches: [${defaultBranch}]`,
|
|
18
|
+
' paths-ignore:',
|
|
19
|
+
" - 'docs/**'",
|
|
20
|
+
" - '**.md'",
|
|
21
|
+
" - '.gitignore'",
|
|
22
|
+
" - 'LICENSE'",
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
if (mode === 'releases') {
|
|
26
|
+
return [
|
|
27
|
+
'on:',
|
|
28
|
+
' workflow_dispatch:',
|
|
29
|
+
' release:',
|
|
30
|
+
' types: [published]',
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (mode === 'weekly') {
|
|
34
|
+
return [
|
|
35
|
+
'on:',
|
|
36
|
+
' workflow_dispatch:',
|
|
37
|
+
' schedule:',
|
|
38
|
+
" - cron: '0 6 * * 1'",
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unknown automation mode "${mode}". One of: ${Object.keys(MODES).join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildKnowledgeWorkflow({ mode, provider, envKey, model, defaultBranch = 'main' }) {
|
|
45
|
+
if (!provider || !envKey) throw new Error('provider and envKey are required');
|
|
46
|
+
const updateCmd = `gitset knowledge update --yes --provider ${provider}${model ? ` --model ${model}` : ''}`;
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
'name: Gitset Knowledge Mapper',
|
|
50
|
+
`# Incrementally refreshes docs/gitset-knowledge/ with your own AI key.`,
|
|
51
|
+
`# Requires ONE repository secret: ${envKey} (Settings > Secrets > Actions).`,
|
|
52
|
+
'# The key is stored encrypted by GitHub and is only sent to your AI',
|
|
53
|
+
'# provider — never to Gitset. Runs whose mapped source files are',
|
|
54
|
+
'# unchanged exit without any AI call (content-hash diff), so harmless',
|
|
55
|
+
'# triggers cost nothing beyond a few CI seconds.',
|
|
56
|
+
...buildTrigger(mode, defaultBranch),
|
|
57
|
+
'permissions:',
|
|
58
|
+
' contents: write',
|
|
59
|
+
' pull-requests: write',
|
|
60
|
+
'concurrency:',
|
|
61
|
+
' group: gitset-knowledge',
|
|
62
|
+
' cancel-in-progress: false',
|
|
63
|
+
'jobs:',
|
|
64
|
+
' update:',
|
|
65
|
+
' runs-on: ubuntu-latest',
|
|
66
|
+
' steps:',
|
|
67
|
+
' - uses: actions/checkout@v4',
|
|
68
|
+
' - uses: actions/setup-node@v4',
|
|
69
|
+
' with:',
|
|
70
|
+
" node-version: '20'",
|
|
71
|
+
' - run: npm install -g @gitset-dev/cli',
|
|
72
|
+
' - name: Update knowledge base',
|
|
73
|
+
' env:',
|
|
74
|
+
` ${envKey}: \${{ secrets.${envKey} }}`,
|
|
75
|
+
` run: ${updateCmd}`,
|
|
76
|
+
' - name: Open a PR when the knowledge base changed',
|
|
77
|
+
' env:',
|
|
78
|
+
' GH_TOKEN: ${{ github.token }}',
|
|
79
|
+
' run: |',
|
|
80
|
+
' if git diff --quiet -- docs/gitset-knowledge AGENTS.md; then',
|
|
81
|
+
' echo "Knowledge base already up to date."',
|
|
82
|
+
' exit 0',
|
|
83
|
+
' fi',
|
|
84
|
+
' git config user.name "github-actions[bot]"',
|
|
85
|
+
' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
|
|
86
|
+
' git switch -c gitset/knowledge-update',
|
|
87
|
+
' git add docs/gitset-knowledge AGENTS.md',
|
|
88
|
+
' git commit -m "docs: refresh knowledge base"',
|
|
89
|
+
' git push -f origin gitset/knowledge-update',
|
|
90
|
+
' gh pr create --title "docs: refresh knowledge base" --body "Automated incremental update by [Gitset](https://gitset.dev) Knowledge Mapper. Only changed modules were re-analyzed; review like any other docs change." || true',
|
|
91
|
+
'',
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
WORKFLOW_PATH,
|
|
97
|
+
MODES,
|
|
98
|
+
buildKnowledgeWorkflow,
|
|
99
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const SOURCE_EXTS = new Set([
|
|
8
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts',
|
|
9
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.h',
|
|
10
|
+
'.cpp', '.hpp', '.cc', '.cs', '.php', '.scala', '.ex', '.exs',
|
|
11
|
+
'.sh', '.bash', '.zsh', '.astro', '.vue', '.svelte',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const MANIFEST_NAMES = new Set([
|
|
15
|
+
'package.json', 'pyproject.toml', 'cargo.toml', 'go.mod', 'gemfile',
|
|
16
|
+
'requirements.txt', 'setup.py', 'setup.cfg', 'composer.json',
|
|
17
|
+
'pom.xml', 'build.gradle', 'pnpm-workspace.yaml', 'deno.json',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const CONFIG_PATTERNS = [
|
|
21
|
+
/^\.?[\w-]+rc(\.\w+)?$/i,
|
|
22
|
+
/\.config\.(js|cjs|mjs|ts|json)$/i,
|
|
23
|
+
/^tsconfig(\..+)?\.json$/i,
|
|
24
|
+
/^dockerfile$/i,
|
|
25
|
+
/^docker-compose(\..+)?\.ya?ml$/i,
|
|
26
|
+
/^makefile$/i,
|
|
27
|
+
/^cloudbuild\.ya?ml$/i,
|
|
28
|
+
/^\.gitattributes$/i,
|
|
29
|
+
/^\.editorconfig$/i,
|
|
30
|
+
/^\.npmrc$/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const DOC_EXTS = new Set(['.md', '.mdx', '.rst', '.txt', '.adoc']);
|
|
34
|
+
const DOC_NAMES = new Set(['license', 'notice', 'authors', 'codeowners', 'changelog']);
|
|
35
|
+
|
|
36
|
+
const LOCKFILE_NAMES = new Set([
|
|
37
|
+
'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'cargo.lock',
|
|
38
|
+
'poetry.lock', 'gemfile.lock', 'composer.lock', 'bun.lockb',
|
|
39
|
+
'deno.lock', 'uv.lock', 'pipfile.lock', 'go.sum',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const ASSET_EXTS = new Set([
|
|
43
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.avif',
|
|
44
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot', '.mp4', '.webm', '.mp3',
|
|
45
|
+
'.wav', '.pdf', '.zip', '.gz', '.tar', '.br', '.wasm', '.map',
|
|
46
|
+
'.lock', '.lockb', '.min.js', '.min.css',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const TEST_PATTERNS = [
|
|
50
|
+
/(^|\/)__tests__\//,
|
|
51
|
+
/(^|\/)tests?\//,
|
|
52
|
+
/\.(test|spec)\.[jt]sx?$/,
|
|
53
|
+
/_test\.(go|py|rb)$/,
|
|
54
|
+
/(^|\/)conftest\.py$/,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const SENSITIVE_FILE_PATTERNS = [
|
|
58
|
+
/(^|\/)\.env(\.|$)/i,
|
|
59
|
+
/\.pem$/i,
|
|
60
|
+
/\.key$/i,
|
|
61
|
+
/(^|\/)id_(rsa|ed25519|ecdsa|dsa)(\.|$)/i,
|
|
62
|
+
/\.p12$/i,
|
|
63
|
+
/\.pfx$/i,
|
|
64
|
+
/\.keystore$/i,
|
|
65
|
+
/(^|\/)credentials?(\.|$)/i,
|
|
66
|
+
/(^|\/)secrets?\.(json|ya?ml|toml|txt)$/i,
|
|
67
|
+
/\.tfstate(\.|$)/i,
|
|
68
|
+
/(^|\/)\.netrc$/i,
|
|
69
|
+
/(^|\/)\.npmrc$/i,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const ALWAYS_IGNORED_DIRS = new Set([
|
|
73
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'coverage', '.next',
|
|
74
|
+
'.astro', '.vercel', '.turbo', '.cache', 'vendor', '__pycache__',
|
|
75
|
+
'.venv', 'venv', 'target', '.idea', '.vscode', '.DS_Store',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const OUTPUT_DIR = 'docs/gitset-knowledge';
|
|
79
|
+
const MAX_CONTENT_BYTES = 200 * 1024;
|
|
80
|
+
|
|
81
|
+
function gitListFiles(rootDir) {
|
|
82
|
+
try {
|
|
83
|
+
const out = execFileSync(
|
|
84
|
+
'git',
|
|
85
|
+
['ls-files', '-z', '--cached', '--others', '--exclude-standard'],
|
|
86
|
+
{ cwd: rootDir, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] },
|
|
87
|
+
);
|
|
88
|
+
return out.split('\0').filter(Boolean);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function walkFiles(rootDir) {
|
|
95
|
+
const results = [];
|
|
96
|
+
const stack = [''];
|
|
97
|
+
while (stack.length) {
|
|
98
|
+
const rel = stack.pop();
|
|
99
|
+
const abs = path.join(rootDir, rel);
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.isSymbolicLink()) continue;
|
|
108
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
if (!ALWAYS_IGNORED_DIRS.has(entry.name)) stack.push(childRel);
|
|
111
|
+
} else if (entry.isFile()) {
|
|
112
|
+
results.push(childRel);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isSensitivePath(relPath) {
|
|
120
|
+
const p = relPath.toLowerCase();
|
|
121
|
+
return SENSITIVE_FILE_PATTERNS.some((re) => re.test(p));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function classify(relPath) {
|
|
125
|
+
const base = path.basename(relPath).toLowerCase();
|
|
126
|
+
const ext = path.extname(base);
|
|
127
|
+
const stem = base.replace(/\.[^.]*$/, '');
|
|
128
|
+
|
|
129
|
+
if (isSensitivePath(relPath)) return 'sensitive';
|
|
130
|
+
if (LOCKFILE_NAMES.has(base)) return 'asset';
|
|
131
|
+
if (MANIFEST_NAMES.has(base)) return 'manifest';
|
|
132
|
+
if (base.endsWith('.gemspec')) return 'manifest';
|
|
133
|
+
if (TEST_PATTERNS.some((re) => re.test(relPath))) return 'test';
|
|
134
|
+
if (/^\.github\/workflows\/.+\.ya?ml$/.test(relPath)) return 'config';
|
|
135
|
+
if (CONFIG_PATTERNS.some((re) => re.test(base))) return 'config';
|
|
136
|
+
if (DOC_EXTS.has(ext) || ((DOC_NAMES.has(stem) || DOC_NAMES.has(base)) && !SOURCE_EXTS.has(ext))) return 'doc';
|
|
137
|
+
if (relPath.startsWith('docs/')) return 'doc';
|
|
138
|
+
if (ASSET_EXTS.has(ext) || base.endsWith('.min.js') || base.endsWith('.min.css')) return 'asset';
|
|
139
|
+
if (SOURCE_EXTS.has(ext)) return 'source';
|
|
140
|
+
if (ext === '.json' || ext === '.yaml' || ext === '.yml' || ext === '.toml') return 'config';
|
|
141
|
+
return 'other';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function looksBinary(buffer) {
|
|
145
|
+
const slice = buffer.subarray(0, Math.min(buffer.length, 8192));
|
|
146
|
+
return slice.includes(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function discover(rootDir, options = {}) {
|
|
150
|
+
const { include = [], exclude = [] } = options;
|
|
151
|
+
let paths = gitListFiles(rootDir);
|
|
152
|
+
const viaGit = paths !== null;
|
|
153
|
+
if (!paths) paths = walkFiles(rootDir);
|
|
154
|
+
|
|
155
|
+
const excludeRes = exclude.map(globToRegExp);
|
|
156
|
+
const includeRes = include.map(globToRegExp);
|
|
157
|
+
|
|
158
|
+
const files = [];
|
|
159
|
+
for (const relPath of paths.sort()) {
|
|
160
|
+
const norm = relPath.split(path.sep).join('/');
|
|
161
|
+
if (norm.startsWith(`${OUTPUT_DIR}/`)) continue;
|
|
162
|
+
if (norm.split('/').some((seg) => ALWAYS_IGNORED_DIRS.has(seg))) continue;
|
|
163
|
+
if (excludeRes.some((re) => re.test(norm))) continue;
|
|
164
|
+
if (includeRes.length && !includeRes.some((re) => re.test(norm))) continue;
|
|
165
|
+
|
|
166
|
+
let size = 0;
|
|
167
|
+
try {
|
|
168
|
+
size = fs.statSync(path.join(rootDir, norm)).size;
|
|
169
|
+
} catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
files.push({ path: norm, kind: classify(norm), size });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { files, viaGit };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readContent(rootDir, file) {
|
|
180
|
+
if (file.kind === 'sensitive' || file.kind === 'asset') return null;
|
|
181
|
+
if (file.size > MAX_CONTENT_BYTES) return null;
|
|
182
|
+
let buffer;
|
|
183
|
+
try {
|
|
184
|
+
buffer = fs.readFileSync(path.join(rootDir, file.path));
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (looksBinary(buffer)) return null;
|
|
189
|
+
return buffer.toString('utf8');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function globToRegExp(glob) {
|
|
193
|
+
const pattern = String(glob)
|
|
194
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
195
|
+
.replace(/\*\*\/|\*\*|\*|\?/g, (tok) => {
|
|
196
|
+
if (tok === '**/') return '(?:.*/)?';
|
|
197
|
+
if (tok === '**') return '.*';
|
|
198
|
+
if (tok === '*') return '[^/]*';
|
|
199
|
+
return '[^/]';
|
|
200
|
+
});
|
|
201
|
+
return new RegExp(`^${pattern}$`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
discover,
|
|
206
|
+
classify,
|
|
207
|
+
readContent,
|
|
208
|
+
isSensitivePath,
|
|
209
|
+
globToRegExp,
|
|
210
|
+
OUTPUT_DIR,
|
|
211
|
+
MAX_CONTENT_BYTES,
|
|
212
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// VENDORED from gitset-core-v2/lib/knowledge — do not edit here. Run `pnpm sync:ai`.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const discoverLib = require('./discover');
|
|
5
|
+
const secretsLib = require('./secrets');
|
|
6
|
+
const mapLib = require('./map');
|
|
7
|
+
const budgetLib = require('./budget');
|
|
8
|
+
const stateLib = require('./state');
|
|
9
|
+
const writeLib = require('./write');
|
|
10
|
+
const validateLib = require('./validate');
|
|
11
|
+
const ciLib = require('./ci');
|
|
12
|
+
|
|
13
|
+
const LLM_ELIGIBLE_KINDS = new Set(['source', 'manifest', 'config']);
|
|
14
|
+
|
|
15
|
+
function prepareRun(rootDir, options = {}) {
|
|
16
|
+
const { include = [], exclude = [], budgets = {} } = options;
|
|
17
|
+
|
|
18
|
+
const { files, viaGit } = discoverLib.discover(rootDir, { include, exclude });
|
|
19
|
+
|
|
20
|
+
const contentCache = new Map();
|
|
21
|
+
const readCached = (file) => {
|
|
22
|
+
if (contentCache.has(file.path)) return contentCache.get(file.path);
|
|
23
|
+
const content = discoverLib.readContent(rootDir, file);
|
|
24
|
+
contentCache.set(file.path, content);
|
|
25
|
+
return content;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const map = mapLib.buildMap(files, readCached);
|
|
29
|
+
|
|
30
|
+
const redactionReport = { totalFindings: 0, droppedFiles: [], findingsByRule: {} };
|
|
31
|
+
const preparedByModule = new Map();
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (!LLM_ELIGIBLE_KINDS.has(file.kind)) continue;
|
|
35
|
+
const raw = readCached(file);
|
|
36
|
+
if (raw === null) continue;
|
|
37
|
+
|
|
38
|
+
file.hash = stateLib.hashContent(raw);
|
|
39
|
+
|
|
40
|
+
const sanitized = secretsLib.sanitizeForPrompt(raw);
|
|
41
|
+
for (const rule of sanitized.findings) {
|
|
42
|
+
redactionReport.totalFindings += 1;
|
|
43
|
+
redactionReport.findingsByRule[rule] = (redactionReport.findingsByRule[rule] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
if (sanitized.dropped) {
|
|
46
|
+
redactionReport.droppedFiles.push(file.path);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const budgeted = budgetLib.budgetFileContent(sanitized.content, budgets);
|
|
51
|
+
const moduleKey = mapLib.moduleKeyFor(file.path);
|
|
52
|
+
if (!preparedByModule.has(moduleKey)) preparedByModule.set(moduleKey, []);
|
|
53
|
+
preparedByModule.get(moduleKey).push({
|
|
54
|
+
path: file.path,
|
|
55
|
+
kind: file.kind,
|
|
56
|
+
mode: budgeted.mode,
|
|
57
|
+
centrality: map.centrality.get(file.path) || 0,
|
|
58
|
+
content: budgeted.content,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const preparedModules = [...preparedByModule.entries()]
|
|
63
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
64
|
+
.map(([name, prepared]) => ({
|
|
65
|
+
name,
|
|
66
|
+
files: prepared.sort((a, b) => b.centrality - a.centrality || a.path.localeCompare(b.path)),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const batches = budgetLib.batchModules(preparedModules, budgets);
|
|
70
|
+
const estimate = budgetLib.estimateRun(
|
|
71
|
+
{ batches, docCount: writeLib.DOC_SPECS.length },
|
|
72
|
+
budgets,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { files, map, preparedModules, batches, estimate, redactionReport, viaGit };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
prepareRun,
|
|
80
|
+
LLM_ELIGIBLE_KINDS,
|
|
81
|
+
...discoverLib,
|
|
82
|
+
...secretsLib,
|
|
83
|
+
...mapLib,
|
|
84
|
+
...budgetLib,
|
|
85
|
+
...stateLib,
|
|
86
|
+
...writeLib,
|
|
87
|
+
...validateLib,
|
|
88
|
+
...ciLib,
|
|
89
|
+
};
|