project-librarian 0.2.1 → 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.ko.md +245 -93
- package/README.md +174 -76
- package/SKILL.md +15 -10
- package/dist/args.js +11 -2
- package/dist/code-index-file-policy.js +105 -2
- package/dist/code-index.js +77 -59
- package/dist/hooks.js +78 -0
- package/dist/init-project-wiki.js +234 -166
- package/dist/install-skill.js +2 -9
- package/dist/mcp-server.js +449 -0
- package/dist/migration.js +875 -60
- package/dist/modes.js +205 -59
- package/dist/taxonomy.js +193 -0
- package/dist/templates.js +214 -36
- package/dist/wiki-files.js +15 -27
- package/dist/wiki-graph.js +141 -0
- package/package.json +3 -3
- package/README.ja.md +0 -215
- package/README.zh.md +0 -215
|
@@ -33,20 +33,28 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.maxIndexedBytes = exports.ignoredDirectories = void 0;
|
|
36
|
+
exports.SMALL_REPO_FILE_THRESHOLD = exports.maxIndexedBytes = exports.ignoredDirectories = exports.codeEvidenceDirectory = void 0;
|
|
37
37
|
exports.fileLanguage = fileLanguage;
|
|
38
38
|
exports.isJavaScriptLike = isJavaScriptLike;
|
|
39
39
|
exports.shouldIndexFile = shouldIndexFile;
|
|
40
40
|
exports.isIgnoredCodePath = isIgnoredCodePath;
|
|
41
|
+
exports.discoverCodeFiles = discoverCodeFiles;
|
|
42
|
+
exports.smallRepoCodeIndexGate = smallRepoCodeIndexGate;
|
|
43
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
41
45
|
const path = __importStar(require("node:path"));
|
|
42
46
|
const workspace_1 = require("./workspace");
|
|
47
|
+
// Single source of truth for the disposable code-evidence directory name. Both
|
|
48
|
+
// the heavy code-index module and the light bootstrap path (hooks.ts) key off
|
|
49
|
+
// this; keeping it here avoids loading typescript/node:sqlite during bootstrap.
|
|
50
|
+
exports.codeEvidenceDirectory = ".project-wiki";
|
|
43
51
|
exports.ignoredDirectories = new Set([
|
|
44
52
|
".git",
|
|
45
53
|
".codex",
|
|
46
54
|
".claude",
|
|
47
55
|
".cursor",
|
|
48
56
|
".gemini",
|
|
49
|
-
|
|
57
|
+
exports.codeEvidenceDirectory,
|
|
50
58
|
"node_modules",
|
|
51
59
|
".next",
|
|
52
60
|
"dist",
|
|
@@ -120,3 +128,98 @@ function isIgnoredCodePath(relativePath) {
|
|
|
120
128
|
.filter(Boolean)
|
|
121
129
|
.some((part) => exports.ignoredDirectories.has(part));
|
|
122
130
|
}
|
|
131
|
+
// Mirrors normalizeProjectRelative in code-index.ts for git ls-files output, but
|
|
132
|
+
// throws instead of exiting so the light bootstrap path can use discovery too.
|
|
133
|
+
// Git emits repo-relative paths, so the escape branch is unreachable for real
|
|
134
|
+
// output; it stays as a loud guard, not a recovery path.
|
|
135
|
+
function normalizeGitIndexedFile(input) {
|
|
136
|
+
const raw = input.trim() || ".";
|
|
137
|
+
const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(workspace_1.root, raw);
|
|
138
|
+
const rootResolved = path.resolve(workspace_1.root);
|
|
139
|
+
if (resolved !== rootResolved && !resolved.startsWith(`${rootResolved}${path.sep}`)) {
|
|
140
|
+
throw new Error(`git-indexed file must stay inside the project root: ${input}`);
|
|
141
|
+
}
|
|
142
|
+
return (0, workspace_1.normalizePath)(path.relative(rootResolved, resolved)) || ".";
|
|
143
|
+
}
|
|
144
|
+
function walkCodeFiles(relativePath, files = []) {
|
|
145
|
+
if (isIgnoredCodePath(relativePath))
|
|
146
|
+
return files.sort();
|
|
147
|
+
const target = (0, workspace_1.abs)(relativePath);
|
|
148
|
+
if (!fs.existsSync(target))
|
|
149
|
+
return files;
|
|
150
|
+
const stat = fs.statSync(target);
|
|
151
|
+
if (stat.isFile()) {
|
|
152
|
+
if (stat.size <= exports.maxIndexedBytes && shouldIndexFile(relativePath))
|
|
153
|
+
files.push(relativePath);
|
|
154
|
+
return files.sort();
|
|
155
|
+
}
|
|
156
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
|
|
157
|
+
const child = (0, workspace_1.normalizePath)(path.join(relativePath, entry.name));
|
|
158
|
+
if (entry.isDirectory()) {
|
|
159
|
+
if (!exports.ignoredDirectories.has(entry.name))
|
|
160
|
+
walkCodeFiles(child, files);
|
|
161
|
+
}
|
|
162
|
+
else if (entry.isFile() && shouldIndexFile(child)) {
|
|
163
|
+
const childStat = fs.statSync((0, workspace_1.abs)(child));
|
|
164
|
+
if (childStat.size <= exports.maxIndexedBytes)
|
|
165
|
+
files.push(child);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return files.sort();
|
|
169
|
+
}
|
|
170
|
+
function gitTrackedAndUnignoredFiles(scopes) {
|
|
171
|
+
try {
|
|
172
|
+
const output = childProcess.execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...scopes], {
|
|
173
|
+
cwd: workspace_1.root,
|
|
174
|
+
encoding: "buffer",
|
|
175
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
176
|
+
});
|
|
177
|
+
return output.toString("utf8").split("\0").filter(Boolean).map((file) => normalizeGitIndexedFile(file));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function discoverCodeFiles(scopes) {
|
|
184
|
+
const gitFiles = gitTrackedAndUnignoredFiles(scopes);
|
|
185
|
+
const candidates = gitFiles ?? scopes.flatMap((scope) => walkCodeFiles(scope));
|
|
186
|
+
return Array.from(new Set(candidates))
|
|
187
|
+
.filter((file) => !isIgnoredCodePath(file))
|
|
188
|
+
.filter((file) => fs.existsSync((0, workspace_1.abs)(file)))
|
|
189
|
+
.filter((file) => fs.statSync((0, workspace_1.abs)(file)).isFile())
|
|
190
|
+
.filter((file) => shouldIndexFile(file))
|
|
191
|
+
.filter((file) => fs.statSync((0, workspace_1.abs)(file)).size <= exports.maxIndexedBytes)
|
|
192
|
+
.sort();
|
|
193
|
+
}
|
|
194
|
+
// --- Scale-aware code-evidence gate ------------------------------------------
|
|
195
|
+
//
|
|
196
|
+
// Threshold in indexable files (the discoverCodeFiles count) below which the
|
|
197
|
+
// code-evidence surfaces warn (--code-index) or skip by default (bootstrap MCP
|
|
198
|
+
// auto-registration). Evidence: stageR1 real-corpus run, 2026-06-12
|
|
199
|
+
// (benchmarks/reports/llm/stageR1-real.md) — the ~1.2k-file repo LOST every
|
|
200
|
+
// measured question (impact_trace +116.9%, workspace_graph +106.5% cost-weighted
|
|
201
|
+
// tokens vs direct reads) while the ~11.8k-file repo still lost the cheap
|
|
202
|
+
// ownership lookup (+99.0%) and won only the expensive traversal questions
|
|
203
|
+
// (impact_trace -27.7%, workspace_graph -2.6%); stage2d synthetic scales
|
|
204
|
+
// (benchmarks/reports/llm/stage2d-codegraph.md) lost across the board. 5000 sits
|
|
205
|
+
// between the two real measured points: an n=2 extrapolation (1.2k all-loss /
|
|
206
|
+
// 11.8k partial win), explicitly subject to revision as more scales are measured.
|
|
207
|
+
exports.SMALL_REPO_FILE_THRESHOLD = 5000;
|
|
208
|
+
// Decision for the --code-index scale gate: below the threshold the build halts
|
|
209
|
+
// with an evidence-citing warning unless the caller explicitly acknowledged the
|
|
210
|
+
// measured cost. Consent is honored, never refused (2026-06-12 decision: defaults
|
|
211
|
+
// follow the evidence, explicit requests get warning + consent).
|
|
212
|
+
function smallRepoCodeIndexGate(indexableFileCount, acknowledged) {
|
|
213
|
+
if (acknowledged || indexableFileCount >= exports.SMALL_REPO_FILE_THRESHOLD)
|
|
214
|
+
return { proceed: true, warning: "" };
|
|
215
|
+
return {
|
|
216
|
+
proceed: false,
|
|
217
|
+
warning: [
|
|
218
|
+
`--code-index halted: ${indexableFileCount} indexable files is below the ${exports.SMALL_REPO_FILE_THRESHOLD}-file scale threshold.`,
|
|
219
|
+
"Measured evidence (real-corpus stageR1): on a ~1.2k-file repo the code-evidence track cost MORE on every measured question (impact_trace +116.9%, workspace_graph +106.5% cost-weighted tokens vs direct reads), and even on a ~11.8k-file repo ownership-style cheap lookups lost (+99.0%); only expensive traversal questions won there (impact_trace -27.7%).",
|
|
220
|
+
"Reports: benchmarks/reports/llm/stageR1-real.md and benchmarks/reports/llm/stage2d-codegraph.md in the project-librarian repo (threshold is an n=2 extrapolation, subject to revision).",
|
|
221
|
+
"Not measured, so not disproven: human-facing report value (--code-report) and answer accuracy/grounding value. This gate reflects measured LLM token cost only.",
|
|
222
|
+
"To build the index anyway, re-run with --acknowledge-small-repo.",
|
|
223
|
+
].join("\n"),
|
|
224
|
+
};
|
|
225
|
+
}
|
package/dist/code-index.js
CHANGED
|
@@ -33,6 +33,19 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CodeEvidenceIndexUnavailableError = void 0;
|
|
37
|
+
exports.codeIndexStaleness = codeIndexStaleness;
|
|
38
|
+
exports.codeownerRules = codeownerRules;
|
|
39
|
+
exports.matchedCodeownerRules = matchedCodeownerRules;
|
|
40
|
+
exports.ownershipContext = ownershipContext;
|
|
41
|
+
exports.ownershipInfo = ownershipInfo;
|
|
42
|
+
exports.evidenceCoverage = evidenceCoverage;
|
|
43
|
+
exports.workspaceSummary = workspaceSummary;
|
|
44
|
+
exports.workspaceDependencyGraph = workspaceDependencyGraph;
|
|
45
|
+
exports.codeReportMetadata = codeReportMetadata;
|
|
46
|
+
exports.codeImpact = codeImpact;
|
|
47
|
+
exports.searchSymbols = searchSymbols;
|
|
48
|
+
exports.openCodeEvidenceDatabaseForServing = openCodeEvidenceDatabaseForServing;
|
|
36
49
|
exports.runCodeIndexMode = runCodeIndexMode;
|
|
37
50
|
exports.runCodeQueryMode = runCodeQueryMode;
|
|
38
51
|
exports.runCodeReportMode = runCodeReportMode;
|
|
@@ -43,7 +56,6 @@ exports.runCodeSearchSymbolMode = runCodeSearchSymbolMode;
|
|
|
43
56
|
exports.isCodeEvidenceMode = isCodeEvidenceMode;
|
|
44
57
|
exports.isCodeEvidenceModeFor = isCodeEvidenceModeFor;
|
|
45
58
|
const crypto = __importStar(require("node:crypto"));
|
|
46
|
-
const childProcess = __importStar(require("node:child_process"));
|
|
47
59
|
const fs = __importStar(require("node:fs"));
|
|
48
60
|
const path = __importStar(require("node:path"));
|
|
49
61
|
const ts = __importStar(require("typescript"));
|
|
@@ -52,7 +64,6 @@ const code_index_db_1 = require("./code-index-db");
|
|
|
52
64
|
const code_index_file_policy_1 = require("./code-index-file-policy");
|
|
53
65
|
const code_index_sql_1 = require("./code-index-sql");
|
|
54
66
|
const workspace_1 = require("./workspace");
|
|
55
|
-
const codeEvidenceDirectory = ".project-wiki";
|
|
56
67
|
const codeIndexSchemaVersion = "3";
|
|
57
68
|
const httpMethods = new Set(["all", "delete", "get", "patch", "post", "put"]);
|
|
58
69
|
const treeSitterGrammarPackages = {
|
|
@@ -117,11 +128,11 @@ function normalizeProjectRelative(input, label) {
|
|
|
117
128
|
return (0, workspace_1.normalizePath)(path.relative(rootResolved, resolved)) || ".";
|
|
118
129
|
}
|
|
119
130
|
function codeEvidenceDatabasePath() {
|
|
120
|
-
const raw = args_1.codeIndexOutput.trim() || `${codeEvidenceDirectory}/code-evidence.sqlite`;
|
|
131
|
+
const raw = args_1.codeIndexOutput.trim() || `${code_index_file_policy_1.codeEvidenceDirectory}/code-evidence.sqlite`;
|
|
121
132
|
const absolutePath = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(workspace_1.root, raw);
|
|
122
|
-
const evidenceRoot = path.resolve(workspace_1.root, codeEvidenceDirectory);
|
|
133
|
+
const evidenceRoot = path.resolve(workspace_1.root, code_index_file_policy_1.codeEvidenceDirectory);
|
|
123
134
|
if (absolutePath === evidenceRoot || !absolutePath.startsWith(`${evidenceRoot}${path.sep}`)) {
|
|
124
|
-
fail(`--code-index-out must stay inside ${codeEvidenceDirectory}/`);
|
|
135
|
+
fail(`--code-index-out must stay inside ${code_index_file_policy_1.codeEvidenceDirectory}/`);
|
|
125
136
|
}
|
|
126
137
|
return {
|
|
127
138
|
absolutePath,
|
|
@@ -182,56 +193,6 @@ function extractionProfile(relativePath, parserMode) {
|
|
|
182
193
|
return "config";
|
|
183
194
|
return "inventory-only";
|
|
184
195
|
}
|
|
185
|
-
function walkCodeFiles(relativePath, files = []) {
|
|
186
|
-
if ((0, code_index_file_policy_1.isIgnoredCodePath)(relativePath))
|
|
187
|
-
return files.sort();
|
|
188
|
-
const target = (0, workspace_1.abs)(relativePath);
|
|
189
|
-
if (!fs.existsSync(target))
|
|
190
|
-
return files;
|
|
191
|
-
const stat = fs.statSync(target);
|
|
192
|
-
if (stat.isFile()) {
|
|
193
|
-
if (stat.size <= code_index_file_policy_1.maxIndexedBytes && (0, code_index_file_policy_1.shouldIndexFile)(relativePath))
|
|
194
|
-
files.push(relativePath);
|
|
195
|
-
return files.sort();
|
|
196
|
-
}
|
|
197
|
-
for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
|
|
198
|
-
const child = (0, workspace_1.normalizePath)(path.join(relativePath, entry.name));
|
|
199
|
-
if (entry.isDirectory()) {
|
|
200
|
-
if (!code_index_file_policy_1.ignoredDirectories.has(entry.name))
|
|
201
|
-
walkCodeFiles(child, files);
|
|
202
|
-
}
|
|
203
|
-
else if (entry.isFile() && (0, code_index_file_policy_1.shouldIndexFile)(child)) {
|
|
204
|
-
const childStat = fs.statSync((0, workspace_1.abs)(child));
|
|
205
|
-
if (childStat.size <= code_index_file_policy_1.maxIndexedBytes)
|
|
206
|
-
files.push(child);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return files.sort();
|
|
210
|
-
}
|
|
211
|
-
function gitTrackedAndUnignoredFiles(scopes) {
|
|
212
|
-
try {
|
|
213
|
-
const output = childProcess.execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...scopes], {
|
|
214
|
-
cwd: workspace_1.root,
|
|
215
|
-
encoding: "buffer",
|
|
216
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
217
|
-
});
|
|
218
|
-
return output.toString("utf8").split("\0").filter(Boolean).map((file) => normalizeProjectRelative(file, "git-indexed file"));
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
function discoverCodeFiles(scopes) {
|
|
225
|
-
const gitFiles = gitTrackedAndUnignoredFiles(scopes);
|
|
226
|
-
const candidates = gitFiles ?? scopes.flatMap((scope) => walkCodeFiles(scope));
|
|
227
|
-
return Array.from(new Set(candidates))
|
|
228
|
-
.filter((file) => !(0, code_index_file_policy_1.isIgnoredCodePath)(file))
|
|
229
|
-
.filter((file) => fs.existsSync((0, workspace_1.abs)(file)))
|
|
230
|
-
.filter((file) => fs.statSync((0, workspace_1.abs)(file)).isFile())
|
|
231
|
-
.filter((file) => (0, code_index_file_policy_1.shouldIndexFile)(file))
|
|
232
|
-
.filter((file) => fs.statSync((0, workspace_1.abs)(file)).size <= code_index_file_policy_1.maxIndexedBytes)
|
|
233
|
-
.sort();
|
|
234
|
-
}
|
|
235
196
|
function readCodeFile(relativePath, parserMode = "default") {
|
|
236
197
|
const text = fs.readFileSync((0, workspace_1.abs)(relativePath), "utf8");
|
|
237
198
|
return {
|
|
@@ -1163,7 +1124,7 @@ function removeDatabaseFiles(databasePath) {
|
|
|
1163
1124
|
function codeIndexStaleness(database) {
|
|
1164
1125
|
const scopes = indexedScopes(database);
|
|
1165
1126
|
const parserMode = indexedParserMode(database);
|
|
1166
|
-
const current = new Map(discoverCodeFiles(scopes.length > 0 ? scopes : ["."]).map((file) => {
|
|
1127
|
+
const current = new Map((0, code_index_file_policy_1.discoverCodeFiles)(scopes.length > 0 ? scopes : ["."]).map((file) => {
|
|
1167
1128
|
const codeFile = readCodeFile(file, parserMode);
|
|
1168
1129
|
return [codeFile.path, codeFile.hash];
|
|
1169
1130
|
}));
|
|
@@ -1311,6 +1272,13 @@ function matchingCodeowners(filePath, rules) {
|
|
|
1311
1272
|
const matches = rules.filter((rule) => codeownerPatternMatches(rule.pattern, filePath));
|
|
1312
1273
|
return matches[matches.length - 1]?.owners ?? [];
|
|
1313
1274
|
}
|
|
1275
|
+
// Return every CODEOWNERS rule that matches a path, in file order. The last entry
|
|
1276
|
+
// is the effective owner under last-match-wins; earlier entries are overridden.
|
|
1277
|
+
// Reuses the same matcher as matchingCodeowners so precedence answers stay
|
|
1278
|
+
// consistent with --code-report / --code-impact.
|
|
1279
|
+
function matchedCodeownerRules(filePath, rules) {
|
|
1280
|
+
return rules.filter((rule) => codeownerPatternMatches(rule.pattern, filePath));
|
|
1281
|
+
}
|
|
1314
1282
|
function ownershipContext() {
|
|
1315
1283
|
return {
|
|
1316
1284
|
codeownerRules: codeownerRules(),
|
|
@@ -1670,16 +1638,66 @@ function codeImpact(database, target) {
|
|
|
1670
1638
|
})).sort((left, right) => right.files - left.files || left.owner.localeCompare(right.owner)),
|
|
1671
1639
|
};
|
|
1672
1640
|
}
|
|
1641
|
+
// Symbol search reusing the exact SELECT behind --code-search-symbol so the MCP
|
|
1642
|
+
// code_search tool returns identical rows without duplicating the query string.
|
|
1643
|
+
function searchSymbols(database, term) {
|
|
1644
|
+
const like = `%${term}%`;
|
|
1645
|
+
return database.prepare("SELECT name, kind, file_path, line, signature FROM symbols WHERE name LIKE ? OR signature LIKE ? ORDER BY file_path, line LIMIT 50").all(like, like);
|
|
1646
|
+
}
|
|
1647
|
+
// Error thrown when the code-evidence index is missing or schema-incompatible.
|
|
1648
|
+
// The MCP server catches this to return an isError tool result (tools/list still
|
|
1649
|
+
// works); CLI modes keep their own process.exit path via requireExistingIndex.
|
|
1650
|
+
class CodeEvidenceIndexUnavailableError extends Error {
|
|
1651
|
+
constructor(message) {
|
|
1652
|
+
super(message);
|
|
1653
|
+
this.name = "CodeEvidenceIndexUnavailableError";
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
exports.CodeEvidenceIndexUnavailableError = CodeEvidenceIndexUnavailableError;
|
|
1657
|
+
// Open the existing .project-wiki code-evidence index READ-ONLY for serving:
|
|
1658
|
+
// validates existence and schema version, then pins PRAGMA query_only = ON. Uses
|
|
1659
|
+
// the same path resolution and schema constant as the indexer so the server and
|
|
1660
|
+
// the writer share one contract. Throws CodeEvidenceIndexUnavailableError (never
|
|
1661
|
+
// exits) so the MCP server can answer with guidance to run --code-index.
|
|
1662
|
+
function openCodeEvidenceDatabaseForServing() {
|
|
1663
|
+
const databasePath = codeEvidenceDatabasePath();
|
|
1664
|
+
if (!fs.existsSync(databasePath.absolutePath)) {
|
|
1665
|
+
throw new CodeEvidenceIndexUnavailableError(`missing code evidence index: ${databasePath.relativePath}; run \`project-librarian --code-index\` first`);
|
|
1666
|
+
}
|
|
1667
|
+
const database = openDatabase(databasePath.absolutePath);
|
|
1668
|
+
let schemaVersion = "";
|
|
1669
|
+
try {
|
|
1670
|
+
schemaVersion = readMetaValue(database, "schema_version");
|
|
1671
|
+
}
|
|
1672
|
+
catch (error) {
|
|
1673
|
+
database.close();
|
|
1674
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1675
|
+
throw new CodeEvidenceIndexUnavailableError(`code evidence index at ${databasePath.relativePath} is not readable; rebuild with \`project-librarian --code-index\`. Error: ${message}`);
|
|
1676
|
+
}
|
|
1677
|
+
if (schemaVersion !== codeIndexSchemaVersion) {
|
|
1678
|
+
database.close();
|
|
1679
|
+
throw new CodeEvidenceIndexUnavailableError(`code evidence index schema version ${schemaVersion || "(missing)"} is incompatible with ${codeIndexSchemaVersion}; rebuild with \`project-librarian --code-index\``);
|
|
1680
|
+
}
|
|
1681
|
+
database.exec("PRAGMA query_only = ON");
|
|
1682
|
+
return { database, relativePath: databasePath.relativePath };
|
|
1683
|
+
}
|
|
1673
1684
|
function prepareOutputPath() {
|
|
1674
1685
|
const databasePath = codeEvidenceDatabasePath();
|
|
1675
1686
|
(0, workspace_1.mkdirp)(path.dirname(databasePath.relativePath));
|
|
1676
|
-
(0, workspace_1.mkdirp)(codeEvidenceDirectory);
|
|
1677
|
-
fs.writeFileSync((0, workspace_1.abs)(`${codeEvidenceDirectory}/.gitignore`), "*\n!.gitignore\n");
|
|
1687
|
+
(0, workspace_1.mkdirp)(code_index_file_policy_1.codeEvidenceDirectory);
|
|
1688
|
+
fs.writeFileSync((0, workspace_1.abs)(`${code_index_file_policy_1.codeEvidenceDirectory}/.gitignore`), "*\n!.gitignore\n");
|
|
1678
1689
|
}
|
|
1679
1690
|
function runCodeIndexMode() {
|
|
1680
1691
|
const databasePath = codeEvidenceDatabasePath();
|
|
1681
1692
|
const scopes = codeScopes();
|
|
1682
1693
|
const parserMode = selectedCodeParserMode();
|
|
1694
|
+
// Scale gate before ANY write or database work: below the measured threshold
|
|
1695
|
+
// the build halts with the evidence-citing warning unless --acknowledge-small-repo
|
|
1696
|
+
// was passed (2026-06-12 scale-aware guidance decision).
|
|
1697
|
+
const discoveredFiles = (0, code_index_file_policy_1.discoverCodeFiles)(scopes);
|
|
1698
|
+
const scaleGate = (0, code_index_file_policy_1.smallRepoCodeIndexGate)(discoveredFiles.length, args_1.acknowledgeSmallRepoMode);
|
|
1699
|
+
if (!scaleGate.proceed)
|
|
1700
|
+
fail(scaleGate.warning);
|
|
1683
1701
|
const existingIndex = fs.existsSync(databasePath.absolutePath);
|
|
1684
1702
|
if (args_1.codeIndexIncrementalMode && !existingIndex) {
|
|
1685
1703
|
fail(`--incremental requires an existing compatible code evidence index: ${databasePath.relativePath}`);
|
|
@@ -1706,7 +1724,7 @@ function runCodeIndexMode() {
|
|
|
1706
1724
|
if (!incremental)
|
|
1707
1725
|
setupDatabase(database);
|
|
1708
1726
|
const statements = createIndexStatements(database);
|
|
1709
|
-
const currentFiles =
|
|
1727
|
+
const currentFiles = discoveredFiles.map((filePath) => readCodeFile(filePath, parserMode));
|
|
1710
1728
|
const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
|
|
1711
1729
|
const indexed = incremental ? new Map(database.prepare("SELECT path, hash FROM files").all().map((row) => [String(row.path), String(row.hash)])) : new Map();
|
|
1712
1730
|
const deletedPaths = incremental ? Array.from(indexed.keys()).filter((filePath) => !currentByPath.has(filePath)) : [];
|
package/dist/hooks.js
CHANGED
|
@@ -39,8 +39,15 @@ exports.upsertHookConfig = upsertHookConfig;
|
|
|
39
39
|
exports.upsertClaudeHookConfig = upsertClaudeHookConfig;
|
|
40
40
|
exports.upsertGeminiHookConfig = upsertGeminiHookConfig;
|
|
41
41
|
exports.upsertCursorHookConfig = upsertCursorHookConfig;
|
|
42
|
+
exports.codeEvidenceIndexExists = codeEvidenceIndexExists;
|
|
43
|
+
exports.mcpRegistrationGate = mcpRegistrationGate;
|
|
44
|
+
exports.upsertClaudeMcpConfig = upsertClaudeMcpConfig;
|
|
45
|
+
exports.upsertCursorMcpConfig = upsertCursorMcpConfig;
|
|
46
|
+
exports.upsertGeminiMcpConfig = upsertGeminiMcpConfig;
|
|
42
47
|
const childProcess = __importStar(require("node:child_process"));
|
|
48
|
+
const fs = __importStar(require("node:fs"));
|
|
43
49
|
const args_1 = require("./args");
|
|
50
|
+
const code_index_file_policy_1 = require("./code-index-file-policy");
|
|
44
51
|
const workspace_1 = require("./workspace");
|
|
45
52
|
function upsertGitHooksPath() {
|
|
46
53
|
if (args_1.noGitConfigMode)
|
|
@@ -136,6 +143,75 @@ function upsertCursorHookConfig() {
|
|
|
136
143
|
(0, workspace_1.write)(relativePath, next);
|
|
137
144
|
return previous === next ? "exists" : previous ? "updated" : "created";
|
|
138
145
|
}
|
|
146
|
+
const mcpServerName = "project-librarian";
|
|
147
|
+
// Candidate local-runner paths, project root relative, in the recorded
|
|
148
|
+
// local-runner-first order (mirrors validationTrailers()). The first existing
|
|
149
|
+
// runner wins; absent any local install we register the published binary.
|
|
150
|
+
const localRunnerCandidates = [
|
|
151
|
+
"tools/project-librarian/dist/init-project-wiki.js",
|
|
152
|
+
".codex/skills/project-librarian/dist/init-project-wiki.js",
|
|
153
|
+
".claude/skills/project-librarian/dist/init-project-wiki.js",
|
|
154
|
+
".cursor/skills/project-librarian/dist/init-project-wiki.js",
|
|
155
|
+
".gemini/skills/project-librarian/dist/init-project-wiki.js",
|
|
156
|
+
];
|
|
157
|
+
// Deterministic command policy for the registered MCP server: if the repo
|
|
158
|
+
// contains a local runner, register `node <runner> mcp`; otherwise register the
|
|
159
|
+
// installed binary `project-librarian mcp`. This mirrors the local-runner-first
|
|
160
|
+
// skill policy (run the installed local copy with node, not npx) so registration
|
|
161
|
+
// does not depend on network/registry access. The runner path is stored project
|
|
162
|
+
// relative so the registration stays portable across clones.
|
|
163
|
+
function mcpServerEntry() {
|
|
164
|
+
const runner = localRunnerCandidates.find((candidate) => fs.existsSync((0, workspace_1.abs)(candidate)));
|
|
165
|
+
if (runner)
|
|
166
|
+
return { command: "node", args: [runner, "mcp"] };
|
|
167
|
+
return { command: mcpServerName, args: ["mcp"] };
|
|
168
|
+
}
|
|
169
|
+
// Preservation-first, idempotent merge of the project-librarian MCP server into a
|
|
170
|
+
// JSON config file's `mcpServers` map. Unknown keys and other servers are never
|
|
171
|
+
// clobbered; only `mcpServers["project-librarian"]` is set. A second run with an
|
|
172
|
+
// unchanged entry returns "exists". Used for Claude `.mcp.json`, Cursor
|
|
173
|
+
// `.cursor/mcp.json`, and (via the same map) Gemini `.gemini/settings.json`.
|
|
174
|
+
//
|
|
175
|
+
// Codex boundary: `codex mcp` only manages USER-level config (~/.codex/config.toml
|
|
176
|
+
// via `codex mcp add`); there is no documented project-level MCP config file under
|
|
177
|
+
// `.codex/`. Per the no-user-level-writes rule we do not register Codex here; the
|
|
178
|
+
// README documents running `codex mcp add project-librarian -- node <runner> mcp`.
|
|
179
|
+
function upsertMcpServersFile(relativePath) {
|
|
180
|
+
const config = (0, workspace_1.parseJson)(relativePath, {});
|
|
181
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object" || Array.isArray(config.mcpServers)) {
|
|
182
|
+
config.mcpServers = {};
|
|
183
|
+
}
|
|
184
|
+
config.mcpServers[mcpServerName] = mcpServerEntry();
|
|
185
|
+
const next = `${JSON.stringify(config, null, 2)}\n`;
|
|
186
|
+
const previous = (0, workspace_1.exists)(relativePath) ? (0, workspace_1.read)(relativePath) : "";
|
|
187
|
+
if (previous === next)
|
|
188
|
+
return "exists";
|
|
189
|
+
(0, workspace_1.write)(relativePath, next);
|
|
190
|
+
return previous ? "updated" : "created";
|
|
191
|
+
}
|
|
192
|
+
function codeEvidenceIndexExists() {
|
|
193
|
+
return (0, workspace_1.walkFilesUnder)(code_index_file_policy_1.codeEvidenceDirectory, (file) => file.endsWith(".sqlite")).length > 0;
|
|
194
|
+
}
|
|
195
|
+
function mcpRegistrationGate() {
|
|
196
|
+
if (codeEvidenceIndexExists())
|
|
197
|
+
return { register: true };
|
|
198
|
+
const indexableFileCount = (0, code_index_file_policy_1.discoverCodeFiles)(["."]).length;
|
|
199
|
+
if (indexableFileCount >= code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD)
|
|
200
|
+
return { register: true };
|
|
201
|
+
return {
|
|
202
|
+
register: false,
|
|
203
|
+
reason: `skipped-small-repo ${indexableFileCount} indexable files < ${code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD} (code-evidence tools measured costlier than direct reads at this scale: stageR1; opt in via --code-index --acknowledge-small-repo, then re-run bootstrap)`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function upsertClaudeMcpConfig() {
|
|
207
|
+
return upsertMcpServersFile(".mcp.json");
|
|
208
|
+
}
|
|
209
|
+
function upsertCursorMcpConfig() {
|
|
210
|
+
return upsertMcpServersFile(".cursor/mcp.json");
|
|
211
|
+
}
|
|
212
|
+
function upsertGeminiMcpConfig() {
|
|
213
|
+
return upsertMcpServersFile(".gemini/settings.json");
|
|
214
|
+
}
|
|
139
215
|
function buildStartupHookScript(output) {
|
|
140
216
|
return `#!/usr/bin/env node
|
|
141
217
|
|
|
@@ -183,6 +259,8 @@ const sections = files
|
|
|
183
259
|
|
|
184
260
|
const additionalContext = [
|
|
185
261
|
"[Project wiki startup review]",
|
|
262
|
+
"Injected context: wiki/startup.md and wiki/index.md are ALREADY included below this line.",
|
|
263
|
+
"Do not re-read these two files this session; route any further reads through the index.",
|
|
186
264
|
"Use ./wiki as the project-planning source of truth only. Start with compact routing context; read detailed project canonical, decision, or meta files on demand.",
|
|
187
265
|
"Project canonical content language is selected from user/project context; do not assume a fixed default language.",
|
|
188
266
|
"When project planning content is added, changed, or removed, update ./wiki in the same turn.",
|