project-librarian 0.2.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/dist/args.js CHANGED
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.issueDraftTitle = exports.issueBodyFile = exports.captureCategory = exports.captureContent = exports.captureTitle = exports.codeIndexScopes = exports.codeParser = exports.codeIndexOutput = exports.codeSearchSymbol = exports.codeReportSection = exports.codeQuerySql = exports.codeImpactTarget = exports.queryTerm = exports.codeSearchSymbolMode = exports.codeQueryMode = exports.codeImpactMode = exports.codeParserMode = exports.codeFilesMode = exports.codeStatusMode = exports.codeReportMode = exports.codeIndexFullMode = exports.codeIndexIncrementalMode = exports.codeIndexMode = exports.noGitConfigMode = exports.reviewMigrationMode = exports.pruneCheckMode = exports.captureInboxMode = exports.refreshIndexMode = exports.issueDraftMode = exports.issueCreateMode = exports.glossaryMode = exports.fixMode = exports.doctorMode = exports.qualityCheckMode = exports.linkCheckMode = exports.lintMode = exports.migrateMode = exports.missingValueOptions = exports.unexpectedValueOptions = exports.unknownOptions = exports.args = exports.commandArgs = exports.command = exports.unknownCommand = exports.helpMode = exports.parsedArgs = exports.rawArgs = void 0;
3
+ exports.captureContent = exports.captureTitle = exports.codeIndexScopes = exports.codeParser = exports.codeIndexOutput = exports.codeSearchSymbol = exports.codeReportSection = exports.codeQuerySql = exports.codeImpactTarget = exports.wikiImpactTarget = exports.wikiImpactMode = exports.queryTerm = exports.codeSearchSymbolMode = exports.codeQueryMode = exports.codeImpactMode = exports.codeParserMode = exports.codeFilesMode = exports.codeStatusMode = exports.codeReportMode = exports.codeIndexFullMode = exports.codeIndexIncrementalMode = exports.codeIndexMode = exports.acknowledgeSmallRepoMode = exports.noGitConfigMode = exports.reviewMigrationMode = exports.pruneCheckMode = exports.captureInboxMode = exports.refreshIndexMode = exports.issueDraftMode = exports.issueCreateMode = exports.glossaryMode = exports.fixMode = exports.doctorMode = exports.qualityCheckMode = exports.linkCheckMode = exports.migrationQualityCheckMode = exports.migrationLintMode = exports.migrationDoctorMode = exports.lintMode = exports.migrateMode = exports.missingValueOptions = exports.unexpectedValueOptions = exports.unknownOptions = exports.args = exports.commandArgs = exports.command = exports.unknownCommand = exports.helpMode = exports.parsedArgs = exports.rawArgs = void 0;
4
+ exports.issueDraftTitle = exports.issueBodyFile = exports.captureCategory = void 0;
4
5
  exports.parseArgs = parseArgs;
5
6
  exports.argValue = argValue;
6
7
  exports.argValues = argValues;
7
8
  exports.rawArgs = process.argv.slice(2);
8
- const knownCommands = new Set(["init", "install-skill"]);
9
+ const knownCommands = new Set(["init", "install-skill", "mcp"]);
9
10
  const flagsWithoutValues = new Set([
11
+ "--acknowledge-small-repo",
10
12
  "--adopt-existing",
11
13
  "--capture-inbox",
12
14
  "--code-evidence-files",
@@ -32,6 +34,9 @@ const flagsWithoutValues = new Set([
32
34
  "--link-check",
33
35
  "--lint",
34
36
  "--migrate",
37
+ "--migration-doctor",
38
+ "--migration-lint",
39
+ "--migration-quality-check",
35
40
  "--no-git-config",
36
41
  "--prune-check",
37
42
  "--quality-check",
@@ -62,6 +67,7 @@ const flagsWithValues = new Set([
62
67
  "--query",
63
68
  "--scope",
64
69
  "--title",
70
+ "--wiki-impact",
65
71
  ]);
66
72
  const knownFlags = new Set([...flagsWithoutValues, ...flagsWithValues, "--help", "-h"]);
67
73
  function flagName(arg) {
@@ -127,6 +133,7 @@ function parseArgs(argv) {
127
133
  const codeQuerySql = argValue("--code-query") || argValue("--code-evidence-query");
128
134
  const codeSearchSymbol = argValue("--code-search-symbol") || argValue("--code-evidence-symbol");
129
135
  return {
136
+ acknowledgeSmallRepoMode: args.has("--acknowledge-small-repo"),
130
137
  args,
131
138
  captureCategory: argValue("--category") || "project-candidate",
132
139
  captureContent: argValue("--content"),
@@ -161,6 +168,9 @@ function parseArgs(argv) {
161
168
  issueDraftTitle: argValue("--issue-title"),
162
169
  linkCheckMode: args.has("--link-check"),
163
170
  lintMode: args.has("--lint"),
171
+ migrationDoctorMode: args.has("--migration-doctor"),
172
+ migrationLintMode: args.has("--migration-lint"),
173
+ migrationQualityCheckMode: args.has("--migration-quality-check"),
164
174
  migrateMode: args.has("--migrate") || args.has("--adopt-existing"),
165
175
  missingValueOptions: Array.from(flagsWithValues).filter((flag) => hasFlag(flag) && !flagHasValue(commandArgs, flag)),
166
176
  noGitConfigMode: args.has("--no-git-config"),
@@ -179,6 +189,8 @@ function parseArgs(argv) {
179
189
  .filter((arg) => arg.startsWith("-"))
180
190
  .map(flagName)
181
191
  .filter((arg) => !knownFlags.has(arg)))),
192
+ wikiImpactMode: hasFlag("--wiki-impact"),
193
+ wikiImpactTarget: argValue("--wiki-impact"),
182
194
  };
183
195
  }
184
196
  exports.parsedArgs = parseArgs(exports.rawArgs);
@@ -192,6 +204,9 @@ exports.unexpectedValueOptions = exports.parsedArgs.unexpectedValueOptions;
192
204
  exports.missingValueOptions = exports.parsedArgs.missingValueOptions;
193
205
  exports.migrateMode = exports.parsedArgs.migrateMode;
194
206
  exports.lintMode = exports.parsedArgs.lintMode;
207
+ exports.migrationDoctorMode = exports.parsedArgs.migrationDoctorMode;
208
+ exports.migrationLintMode = exports.parsedArgs.migrationLintMode;
209
+ exports.migrationQualityCheckMode = exports.parsedArgs.migrationQualityCheckMode;
195
210
  exports.linkCheckMode = exports.parsedArgs.linkCheckMode;
196
211
  exports.qualityCheckMode = exports.parsedArgs.qualityCheckMode;
197
212
  exports.doctorMode = exports.parsedArgs.doctorMode;
@@ -204,6 +219,7 @@ exports.captureInboxMode = exports.parsedArgs.captureInboxMode;
204
219
  exports.pruneCheckMode = exports.parsedArgs.pruneCheckMode;
205
220
  exports.reviewMigrationMode = exports.parsedArgs.reviewMigrationMode;
206
221
  exports.noGitConfigMode = exports.parsedArgs.noGitConfigMode;
222
+ exports.acknowledgeSmallRepoMode = exports.parsedArgs.acknowledgeSmallRepoMode;
207
223
  exports.codeIndexMode = exports.parsedArgs.codeIndexMode;
208
224
  exports.codeIndexIncrementalMode = exports.parsedArgs.codeIndexIncrementalMode;
209
225
  exports.codeIndexFullMode = exports.parsedArgs.codeIndexFullMode;
@@ -221,6 +237,8 @@ function argValues(name) {
221
237
  return argValuesFrom(exports.commandArgs, name);
222
238
  }
223
239
  exports.queryTerm = exports.parsedArgs.queryTerm;
240
+ exports.wikiImpactMode = exports.parsedArgs.wikiImpactMode;
241
+ exports.wikiImpactTarget = exports.parsedArgs.wikiImpactTarget;
224
242
  exports.codeImpactTarget = exports.parsedArgs.codeImpactTarget;
225
243
  exports.codeQuerySql = exports.parsedArgs.codeQuerySql;
226
244
  exports.codeReportSection = exports.parsedArgs.codeReportSection;
@@ -33,18 +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
- ".project-wiki",
55
+ ".cursor",
56
+ ".gemini",
57
+ exports.codeEvidenceDirectory,
48
58
  "node_modules",
49
59
  ".next",
50
60
  "dist",
@@ -118,3 +128,98 @@ function isIgnoredCodePath(relativePath) {
118
128
  .filter(Boolean)
119
129
  .some((part) => exports.ignoredDirectories.has(part));
120
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
+ }
@@ -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 = discoverCodeFiles(scopes).map((filePath) => readCodeFile(filePath, parserMode));
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
@@ -33,12 +33,21 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.gitWikiCommitTrailersScript = exports.gitPrepareCommitMsgHook = exports.hookScript = void 0;
36
+ exports.gitWikiCommitTrailersScript = exports.gitPrepareCommitMsgHook = exports.cursorHookScript = exports.hookScript = void 0;
37
37
  exports.upsertGitHooksPath = upsertGitHooksPath;
38
38
  exports.upsertHookConfig = upsertHookConfig;
39
39
  exports.upsertClaudeHookConfig = upsertClaudeHookConfig;
40
+ exports.upsertGeminiHookConfig = upsertGeminiHookConfig;
41
+ exports.upsertCursorHookConfig = upsertCursorHookConfig;
42
+ exports.codeEvidenceIndexExists = codeEvidenceIndexExists;
43
+ exports.mcpRegistrationGate = mcpRegistrationGate;
44
+ exports.upsertClaudeMcpConfig = upsertClaudeMcpConfig;
45
+ exports.upsertCursorMcpConfig = upsertCursorMcpConfig;
46
+ exports.upsertGeminiMcpConfig = upsertGeminiMcpConfig;
40
47
  const childProcess = __importStar(require("node:child_process"));
48
+ const fs = __importStar(require("node:fs"));
41
49
  const args_1 = require("./args");
50
+ const code_index_file_policy_1 = require("./code-index-file-policy");
42
51
  const workspace_1 = require("./workspace");
43
52
  function upsertGitHooksPath() {
44
53
  if (args_1.noGitConfigMode)
@@ -109,7 +118,102 @@ function upsertHookConfig() {
109
118
  function upsertClaudeHookConfig() {
110
119
  return upsertSessionStartHookConfig(".claude/settings.json", "node .claude/hooks/wiki-session-start.js", ["startup", "resume", "clear", "compact"]);
111
120
  }
112
- exports.hookScript = `#!/usr/bin/env node
121
+ function upsertGeminiHookConfig() {
122
+ return upsertSessionStartHookConfig(".gemini/settings.json", 'node "$GEMINI_PROJECT_DIR/.gemini/hooks/wiki-session-start.js"', ["startup", "resume", "clear"]);
123
+ }
124
+ function isCursorHookCommand(value) {
125
+ return Boolean(value) && typeof value === "object" && typeof value.command === "string";
126
+ }
127
+ function upsertCursorHookConfig() {
128
+ const relativePath = ".cursor/hooks.json";
129
+ const command = "node .cursor/hooks/wiki-session-start.js";
130
+ const config = (0, workspace_1.parseJson)(relativePath, { version: 1, hooks: {} });
131
+ if (typeof config.version !== "number")
132
+ config.version = 1;
133
+ if (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
134
+ config.hooks = {};
135
+ }
136
+ const existing = Array.isArray(config.hooks.sessionStart) ? config.hooks.sessionStart.filter(isCursorHookCommand) : [];
137
+ config.hooks.sessionStart = [
138
+ ...existing.filter((hook) => hook.command !== command),
139
+ { command },
140
+ ];
141
+ const next = `${JSON.stringify(config, null, 2)}\n`;
142
+ const previous = (0, workspace_1.exists)(relativePath) ? (0, workspace_1.read)(relativePath) : "";
143
+ (0, workspace_1.write)(relativePath, next);
144
+ return previous === next ? "exists" : previous ? "updated" : "created";
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
+ }
215
+ function buildStartupHookScript(output) {
216
+ return `#!/usr/bin/env node
113
217
 
114
218
  const fs = require("fs");
115
219
  const path = require("path");
@@ -127,7 +231,7 @@ function readHookInput() {
127
231
  }
128
232
 
129
233
  const hookInput = readHookInput();
130
- const cwd = process.env.CODEX_WORKSPACE_DIR || hookInput.cwd || process.cwd();
234
+ const cwd = process.env.GEMINI_PROJECT_DIR || process.env.CODEX_WORKSPACE_DIR || process.env.CLAUDE_PROJECT_DIR || hookInput.cwd || process.cwd();
131
235
 
132
236
  function readIfExists(relativePath, maxChars) {
133
237
  const filePath = path.join(cwd, relativePath);
@@ -155,6 +259,8 @@ const sections = files
155
259
 
156
260
  const additionalContext = [
157
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.",
158
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.",
159
265
  "Project canonical content language is selected from user/project context; do not assume a fixed default language.",
160
266
  "When project planning content is added, changed, or removed, update ./wiki in the same turn.",
@@ -164,10 +270,15 @@ const additionalContext = [
164
270
  ].join("\\n");
165
271
 
166
272
  process.stdout.write(JSON.stringify({
167
- continue: true,
168
- hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
273
+ ${output}
169
274
  }));
170
275
  `;
276
+ }
277
+ exports.hookScript = buildStartupHookScript(` continue: true,
278
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
279
+ `);
280
+ exports.cursorHookScript = buildStartupHookScript(` additional_context: additionalContext,
281
+ `);
171
282
  exports.gitPrepareCommitMsgHook = `#!/bin/sh
172
283
  MSG_FILE="$1"
173
284
  SOURCE="$2"
@@ -253,7 +364,10 @@ function wikiScope(files) {
253
364
  else if (file === "wiki/index.md") add("index");
254
365
  else if (file.startsWith(".codex/hooks/") || file === ".codex/hooks.json") add("codex-hooks");
255
366
  else if (file.startsWith(".claude/hooks/") || file === ".claude/settings.json") add("claude-hooks");
256
- else if (file === "AGENTS.md" || file === "CLAUDE.md") add("agents");
367
+ else if (file.startsWith(".cursor/hooks/") || file === ".cursor/hooks.json") add("cursor-hooks");
368
+ else if (file.startsWith(".cursor/rules/")) add("cursor-rules");
369
+ else if (file.startsWith(".gemini/hooks/") || file === ".gemini/settings.json") add("gemini-hooks");
370
+ else if (file === "AGENTS.md" || file === "CLAUDE.md" || file === "GEMINI.md") add("agents");
257
371
  else if (file.startsWith(".githooks/")) add("git-hooks");
258
372
  else if (file.startsWith("tools/project-librarian/")) add("skill");
259
373
  }
@@ -266,15 +380,31 @@ function validationTrailers() {
266
380
  "tools/project-librarian/dist/init-project-wiki.js",
267
381
  path.join(home, ".codex/skills/project-librarian/dist/init-project-wiki.js"),
268
382
  path.join(home, ".claude/skills/project-librarian/dist/init-project-wiki.js"),
383
+ path.join(home, ".cursor/skills/project-librarian/dist/init-project-wiki.js"),
384
+ path.join(home, ".gemini/skills/project-librarian/dist/init-project-wiki.js"),
269
385
  ].find((candidate) => fs.existsSync(candidate));
270
386
  const lintOk = Boolean(lintScript) && commandOk("node", [lintScript, "--lint"]);
271
387
  const codexSessionHookOk = fs.existsSync(".codex/hooks/wiki-session-start.js") && commandOk("node", [".codex/hooks/wiki-session-start.js"]);
272
388
  const claudeSessionHookOk = fs.existsSync(".claude/hooks/wiki-session-start.js") && commandOk("node", [".claude/hooks/wiki-session-start.js"]);
273
- if (lintOk && codexSessionHookOk && claudeSessionHookOk) return { tested: "project wiki lint; Codex and Claude wiki session-start hooks", notTested: "none" };
389
+ const cursorSessionHookOk = fs.existsSync(".cursor/hooks/wiki-session-start.js") && commandOk("node", [".cursor/hooks/wiki-session-start.js"]);
390
+ const cursorHookConfigOk = fs.existsSync(".cursor/hooks.json") && existingFile(".cursor/hooks.json").includes("node .cursor/hooks/wiki-session-start.js");
391
+ const geminiSessionHookOk = fs.existsSync(".gemini/hooks/wiki-session-start.js") && commandOk("node", [".gemini/hooks/wiki-session-start.js"]);
392
+ const geminiHookConfigOk = fs.existsSync(".gemini/settings.json") && existingFile(".gemini/settings.json").includes('node "$GEMINI_PROJECT_DIR/.gemini/hooks/wiki-session-start.js"');
393
+ const geminiInstructionsOk = fs.existsSync("GEMINI.md") && existingFile("GEMINI.md").includes("@AGENTS.md");
394
+ const cursorRuleOk = fs.existsSync(".cursor/rules/project-librarian.mdc") && existingFile(".cursor/rules/project-librarian.mdc").includes("@AGENTS.md");
395
+ if (lintOk && codexSessionHookOk && claudeSessionHookOk && cursorSessionHookOk && cursorHookConfigOk && geminiSessionHookOk && geminiHookConfigOk && geminiInstructionsOk && cursorRuleOk) {
396
+ return { tested: "project wiki lint; Codex, Claude, Cursor, and Gemini wiki session-start hooks; Cursor and Gemini instruction files", notTested: "none" };
397
+ }
274
398
  const gaps = [];
275
399
  if (!lintOk) gaps.push("project wiki lint");
276
400
  if (!codexSessionHookOk) gaps.push("Codex wiki session-start hook");
277
401
  if (!claudeSessionHookOk) gaps.push("Claude wiki session-start hook");
402
+ if (!cursorSessionHookOk) gaps.push("Cursor wiki session-start hook");
403
+ if (!cursorHookConfigOk) gaps.push("Cursor hook config");
404
+ if (!geminiSessionHookOk) gaps.push("Gemini wiki SessionStart hook");
405
+ if (!geminiHookConfigOk) gaps.push("Gemini hook config");
406
+ if (!cursorRuleOk) gaps.push("Cursor project rule");
407
+ if (!geminiInstructionsOk) gaps.push("Gemini instructions");
278
408
  return { tested: "prepare-commit-msg generated wiki trailers", notTested: gaps.join("; ") || "unknown" };
279
409
  }
280
410
 
@@ -287,10 +417,16 @@ const wikiFiles = staged.filter((file) => {
287
417
  return file.startsWith("wiki/")
288
418
  || file === "AGENTS.md"
289
419
  || file === "CLAUDE.md"
420
+ || file === "GEMINI.md"
290
421
  || file === ".codex/hooks.json"
291
422
  || file.startsWith(".codex/hooks/")
292
423
  || file === ".claude/settings.json"
293
424
  || file.startsWith(".claude/hooks/")
425
+ || file.startsWith(".cursor/rules/")
426
+ || file === ".cursor/hooks.json"
427
+ || file.startsWith(".cursor/hooks/")
428
+ || file === ".gemini/settings.json"
429
+ || file.startsWith(".gemini/hooks/")
294
430
  || file.startsWith(".githooks/")
295
431
  || file.startsWith("tools/project-librarian/");
296
432
  });