project-librarian 0.5.4 → 0.5.6

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.
Files changed (46) hide show
  1. package/CONTRIBUTING.md +36 -0
  2. package/README.ko.md +57 -360
  3. package/README.md +56 -359
  4. package/dist/args.js +6 -1
  5. package/dist/code-index/extractors/light-languages.js +285 -0
  6. package/dist/code-index/extractors/registry.js +12 -0
  7. package/dist/code-index/extractors/shared.js +18 -1
  8. package/dist/code-index/extractors/typescript.js +30 -16
  9. package/dist/code-index/modes.js +136 -32
  10. package/dist/code-index/native-helper-matrix.js +99 -0
  11. package/dist/code-index/native-helper.js +292 -0
  12. package/dist/code-index/ownership.js +8 -6
  13. package/dist/code-index/schema.js +72 -13
  14. package/dist/code-index/search.js +1 -1
  15. package/dist/code-index-db.js +20 -12
  16. package/dist/code-index-file-policy.js +17 -11
  17. package/dist/code-index.js +365 -13
  18. package/dist/hooks.js +5 -5
  19. package/dist/init-project-wiki.js +7 -1
  20. package/dist/install-skill.js +99 -6
  21. package/dist/mcp-server.js +4 -4
  22. package/dist/migration.js +27 -2
  23. package/dist/native/darwin-arm64/project-librarian-indexer +0 -0
  24. package/dist/native/darwin-x64/project-librarian-indexer +0 -0
  25. package/dist/native/linux-arm64/project-librarian-indexer +0 -0
  26. package/dist/native/linux-arm64-musl/project-librarian-indexer +0 -0
  27. package/dist/native/linux-x64/project-librarian-indexer +0 -0
  28. package/dist/native/linux-x64-musl/project-librarian-indexer +0 -0
  29. package/dist/native/project-librarian-indexer-manifest.json +70 -0
  30. package/dist/native/win32-arm64/project-librarian-indexer.exe +0 -0
  31. package/dist/native/win32-x64/project-librarian-indexer.exe +0 -0
  32. package/dist/templates.js +4 -3
  33. package/dist/workspace.js +137 -10
  34. package/docs/README.md +11 -0
  35. package/docs/benchmarks.md +64 -0
  36. package/docs/cli-reference.md +60 -0
  37. package/docs/code-evidence.md +87 -0
  38. package/docs/ko/README.md +13 -0
  39. package/docs/ko/benchmarks.md +64 -0
  40. package/docs/ko/cli-reference.md +60 -0
  41. package/docs/ko/code-evidence.md +87 -0
  42. package/docs/ko/maintainer.md +76 -0
  43. package/docs/ko/usage.md +167 -0
  44. package/docs/maintainer.md +76 -0
  45. package/docs/usage.md +175 -0
  46. package/package.json +13 -2
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CodeEvidenceIndexUnavailableError = exports.codeContextPackTruncationNotice = exports.codeContextPackCharCap = exports.codeIndexSnapshot = exports.workspaceSummary = exports.workspaceDependencyGraph = exports.searchSymbols = exports.ownershipInfo = exports.ownershipContext = exports.matchedCodeownerRules = exports.evidenceCoverage = exports.codeownerRules = void 0;
36
+ exports.CodeEvidenceIndexUnavailableError = exports.nativeCodeIndexAutoFileThreshold = exports.codeContextPackTruncationNotice = exports.codeContextPackCharCap = exports.codeIndexSnapshot = exports.workspaceSummary = exports.workspaceDependencyGraph = exports.searchSymbols = exports.ownershipInfo = exports.ownershipContext = exports.matchedCodeownerRules = exports.evidenceCoverage = exports.codeownerRules = void 0;
37
37
  exports.codeIndexStaleness = codeIndexStaleness;
38
38
  exports.codeIndexHealth = codeIndexHealth;
39
39
  exports.codeImpact = codeImpact;
@@ -61,6 +61,7 @@ const registry_1 = require("./code-index/extractors/registry");
61
61
  const shared_1 = require("./code-index/extractors/shared");
62
62
  const index_health_1 = require("./code-index/index-health");
63
63
  const modes_1 = require("./code-index/modes");
64
+ const native_helper_1 = require("./code-index/native-helper");
64
65
  const ownership_1 = require("./code-index/ownership");
65
66
  Object.defineProperty(exports, "codeownerRules", { enumerable: true, get: function () { return ownership_1.codeownerRules; } });
66
67
  Object.defineProperty(exports, "matchedCodeownerRules", { enumerable: true, get: function () { return ownership_1.matchedCodeownerRules; } });
@@ -77,6 +78,8 @@ Object.defineProperty(exports, "searchSymbols", { enumerable: true, get: functio
77
78
  const workspace_1 = require("./workspace");
78
79
  exports.codeContextPackCharCap = 4000;
79
80
  exports.codeContextPackTruncationNotice = "[truncated - refine the query]";
81
+ exports.nativeCodeIndexAutoFileThreshold = 1;
82
+ const codeFileFingerprintPaths = new Map();
80
83
  function fail(message) {
81
84
  console.error(message);
82
85
  process.exit(1);
@@ -110,30 +113,82 @@ function selectedCodeParserMode() {
110
113
  return "tree-sitter";
111
114
  fail(`invalid --code-parser: ${args_1.codeParser}; expected one of: default, tree-sitter`);
112
115
  }
116
+ function selectedCodeIndexEngine() {
117
+ const requested = args_1.codeIndexEngine.trim().toLowerCase();
118
+ if (!requested || requested === "auto")
119
+ return "auto";
120
+ if (requested === "typescript")
121
+ return "typescript";
122
+ if (requested === "native-rust")
123
+ return "native-rust";
124
+ fail(`invalid --code-index-engine: ${args_1.codeIndexEngine}; expected one of: auto, typescript, native-rust`);
125
+ }
126
+ function codeIndexEngineSelectionContext(discoveredFiles, parserMode) {
127
+ let nativeEligibleFileCount = 0;
128
+ for (const filePath of discoveredFiles) {
129
+ const language = (0, code_index_file_policy_1.fileLanguage)(filePath) || "config";
130
+ if (nativeAutoEligibleProfile((0, registry_1.extractionProfile)(filePath, language, parserMode)))
131
+ nativeEligibleFileCount += 1;
132
+ }
133
+ return {
134
+ discoveredFileCount: discoveredFiles.length,
135
+ nativeEligibleFileCount,
136
+ nativeIneligibleFileCount: discoveredFiles.length - nativeEligibleFileCount,
137
+ };
138
+ }
139
+ function shouldUseNativeCodeIndexAuto(context) {
140
+ return context.nativeEligibleFileCount >= exports.nativeCodeIndexAutoFileThreshold
141
+ && (0, native_helper_1.nativeCodeIndexHelperAvailable)();
142
+ }
143
+ function nativeCodeIndexAvailable() {
144
+ return (0, native_helper_1.nativeCodeIndexHelperAvailable)();
145
+ }
113
146
  function normalizedMtimeMs(stat) {
114
147
  return Number(stat.mtimeMs.toFixed(3));
115
148
  }
116
149
  function readCodeFileFingerprint(relativePath) {
117
- const stat = fs.statSync((0, workspace_1.abs)(relativePath));
150
+ const discovered = (0, code_index_file_policy_1.cachedDiscoveredCodeFileStat)(relativePath);
151
+ const { absolutePath, stat } = discovered ?? (0, workspace_1.requireContainedProjectFile)(relativePath, "code-index file");
152
+ codeFileFingerprintPaths.set(relativePath, absolutePath);
118
153
  return {
119
154
  mtimeMs: normalizedMtimeMs(stat),
120
155
  path: relativePath,
121
156
  size: stat.size,
122
157
  };
123
158
  }
124
- function readCodeFile(relativePath, parserMode = "default") {
125
- const text = fs.readFileSync((0, workspace_1.abs)(relativePath), "utf8");
126
- const fingerprint = readCodeFileFingerprint(relativePath);
159
+ function lineCount(text) {
160
+ if (text.length === 0)
161
+ return 0;
162
+ let lines = 1;
163
+ for (let index = 0; index < text.length; index += 1) {
164
+ if (text.charCodeAt(index) === 10)
165
+ lines += 1;
166
+ }
167
+ return lines;
168
+ }
169
+ function readCodeFile(relativePath, parserMode = "default", fingerprint) {
170
+ let effectiveFingerprint = fingerprint;
171
+ let absolutePath = effectiveFingerprint ? codeFileFingerprintPaths.get(relativePath) : undefined;
172
+ if (!absolutePath || !effectiveFingerprint) {
173
+ const contained = (0, workspace_1.requireContainedProjectFile)(relativePath, "code-index file");
174
+ absolutePath = contained.absolutePath;
175
+ effectiveFingerprint ??= {
176
+ mtimeMs: normalizedMtimeMs(contained.stat),
177
+ path: relativePath,
178
+ size: contained.stat.size,
179
+ };
180
+ }
181
+ const text = fs.readFileSync(absolutePath, "utf8");
127
182
  const language = (0, code_index_file_policy_1.fileLanguage)(relativePath) || "config";
128
183
  return {
129
- bytes: fingerprint.size,
184
+ bytes: effectiveFingerprint.size,
130
185
  hash: crypto.createHash("sha256").update(text).digest("hex"),
131
186
  language,
132
- lines: text.length === 0 ? 0 : text.split(/\r?\n/).length,
133
- mtimeMs: fingerprint.mtimeMs,
187
+ lines: lineCount(text),
188
+ mtimeMs: effectiveFingerprint.mtimeMs,
134
189
  path: relativePath,
135
190
  profile: (0, registry_1.extractionProfile)(relativePath, language, parserMode),
136
- size: fingerprint.size,
191
+ size: effectiveFingerprint.size,
137
192
  text,
138
193
  };
139
194
  }
@@ -142,8 +197,9 @@ function extractionBackendForProfile(profile) {
142
197
  return extractionBackendRegistry.backendForProfile(profile);
143
198
  }
144
199
  function indexCodeFile(file, statements) {
145
- statements.insertFile.run(file.path, file.language, file.profile, file.language === "config" ? "config" : "source", file.bytes, file.lines, file.hash, file.mtimeMs, file.size);
146
- statements.insertFileFts.run(file.path, file.language, file.profile, file.text);
200
+ const ftsRowid = (0, schema_1.fileFtsRowid)(file.path);
201
+ statements.insertFile.run(file.path, ftsRowid, file.language, file.profile, file.language === "config" ? "config" : "source", file.bytes, file.lines, file.hash, file.mtimeMs, file.size);
202
+ statements.insertFileFts.run(ftsRowid, file.path, file.language, file.profile, file.text);
147
203
  extractionBackendForProfile(file.profile).index(file, statements);
148
204
  }
149
205
  function printRows(rows) {
@@ -172,6 +228,295 @@ function removeDatabaseFiles(databasePath) {
172
228
  fs.unlinkSync(filePath);
173
229
  }
174
230
  }
231
+ function moveDatabaseFiles(sourcePath, targetPath) {
232
+ const backupPath = `${targetPath}.backup-${process.pid}-${Date.now()}`;
233
+ removeDatabaseFiles(backupPath);
234
+ for (const suffix of ["", "-wal", "-shm"]) {
235
+ const target = `${targetPath}${suffix}`;
236
+ if (fs.existsSync(target))
237
+ fs.renameSync(target, `${backupPath}${suffix}`);
238
+ }
239
+ try {
240
+ for (const suffix of ["", "-wal", "-shm"]) {
241
+ const source = `${sourcePath}${suffix}`;
242
+ if (fs.existsSync(source))
243
+ fs.renameSync(source, `${targetPath}${suffix}`);
244
+ }
245
+ removeDatabaseFiles(backupPath);
246
+ }
247
+ catch (error) {
248
+ removeDatabaseFiles(targetPath);
249
+ for (const suffix of ["", "-wal", "-shm"]) {
250
+ const backup = `${backupPath}${suffix}`;
251
+ if (fs.existsSync(backup))
252
+ fs.renameSync(backup, `${targetPath}${suffix}`);
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+ function removeTemporaryDatabaseFiles(databasePath) {
258
+ for (const suffix of ["", "-wal", "-shm"]) {
259
+ const filePath = `${databasePath}${suffix}`;
260
+ if (fs.existsSync(filePath))
261
+ fs.unlinkSync(filePath);
262
+ }
263
+ }
264
+ function nativeCodeIndexFileFor(filePath, parserMode) {
265
+ const fingerprint = readCodeFileFingerprint(filePath);
266
+ const language = (0, code_index_file_policy_1.fileLanguage)(filePath) || "config";
267
+ return {
268
+ ...fingerprint,
269
+ language,
270
+ profile: (0, registry_1.extractionProfile)(filePath, language, parserMode),
271
+ };
272
+ }
273
+ function nativeCodeIndexFileFromCodeFile(file) {
274
+ return {
275
+ language: file.language,
276
+ mtimeMs: file.mtimeMs,
277
+ path: file.path,
278
+ profile: file.profile,
279
+ size: file.size,
280
+ };
281
+ }
282
+ function nativeCodeIndexFileFromFingerprint(file, parserMode) {
283
+ const language = (0, code_index_file_policy_1.fileLanguage)(file.path) || "config";
284
+ return {
285
+ language,
286
+ mtimeMs: file.mtimeMs,
287
+ path: file.path,
288
+ profile: (0, registry_1.extractionProfile)(file.path, language, parserMode),
289
+ size: file.size,
290
+ };
291
+ }
292
+ function nativeEligibleProfile(profile) {
293
+ return nativeAutoEligibleProfile(profile) || profile === "config" || profile === "inventory-only";
294
+ }
295
+ function nativeCodeIndexIncrementalEligible(files, parserMode) {
296
+ return files.every((file) => nativeEligibleProfile(nativeCodeIndexFileFromFingerprint(file, parserMode).profile));
297
+ }
298
+ function nativeAutoEligibleProfile(profile) {
299
+ return [
300
+ "typescript-ast",
301
+ "python-light",
302
+ "go-light",
303
+ "c-light",
304
+ "cpp-light",
305
+ "csharp-light",
306
+ "java-light",
307
+ "kotlin-light",
308
+ "php-light",
309
+ "rust-light",
310
+ "swift-light",
311
+ ].includes(profile);
312
+ }
313
+ function nativeCodeIndexOutputMode() {
314
+ const requested = (process.env.PROJECT_LIBRARIAN_NATIVE_INDEXER_STRATEGY ?? "sqlite-direct").trim();
315
+ if (requested === "row-stream" || requested === "sqlite-bridge" || requested === "sqlite-direct")
316
+ return requested;
317
+ fail(`invalid PROJECT_LIBRARIAN_NATIVE_INDEXER_STRATEGY: ${requested}; expected row-stream, sqlite-bridge, or sqlite-direct`);
318
+ }
319
+ function appendTypeScriptPartitionToNativeDatabase(databasePath, files, parserMode) {
320
+ if (files.length === 0)
321
+ return 0;
322
+ const database = openDatabase(databasePath);
323
+ try {
324
+ const statements = (0, schema_1.createIndexStatements)(database);
325
+ database.exec("BEGIN");
326
+ for (const file of files)
327
+ indexCodeFile(readCodeFile(file.path, parserMode, file), statements);
328
+ database.exec("COMMIT");
329
+ return files.length;
330
+ }
331
+ catch (error) {
332
+ try {
333
+ database.exec("ROLLBACK");
334
+ }
335
+ catch {
336
+ // Ignore rollback failures after helper-created database errors.
337
+ }
338
+ throw error;
339
+ }
340
+ finally {
341
+ database.close();
342
+ }
343
+ }
344
+ function writeNativeRowsToDatabase(databasePath, rows, scopes, parserMode) {
345
+ removeDatabaseFiles(databasePath);
346
+ const database = openDatabase(databasePath);
347
+ try {
348
+ (0, schema_1.setupDatabase)(database, { secondaryIndexes: false });
349
+ const statements = (0, schema_1.createIndexStatements)(database);
350
+ database.exec("BEGIN");
351
+ statements.insertMeta.run("created_at", new Date().toISOString());
352
+ (0, schema_1.writeIndexMetadata)(scopes, parserMode, statements);
353
+ for (const file of rows.files) {
354
+ const ftsRowid = (0, schema_1.fileFtsRowid)(file.path);
355
+ statements.insertFile.run(file.path, ftsRowid, file.language, file.profile, file.kind, file.bytes, file.lines, file.hash, file.mtime_ms, file.size);
356
+ statements.insertFileFts.run(ftsRowid, file.path, file.language, file.profile, file.content);
357
+ }
358
+ for (const symbol of rows.symbols) {
359
+ statements.insertSymbol.run(symbol.name, symbol.kind, symbol.file_path, symbol.line, symbol.signature);
360
+ statements.insertSymbolFts.run(symbol.name, symbol.kind, symbol.file_path, symbol.signature);
361
+ }
362
+ for (const imported of rows.imports) {
363
+ statements.insertImport.run(imported.from_file, imported.to_ref, imported.imported, imported.line, imported.raw);
364
+ }
365
+ for (const route of rows.routes) {
366
+ statements.insertRoute.run(route.method, route.route, route.file_path, route.line, route.handler);
367
+ }
368
+ for (const config of rows.configs) {
369
+ statements.insertConfig.run(config.key, config.value, config.file_path, config.line);
370
+ }
371
+ for (const edge of rows.edges) {
372
+ statements.insertEdge.run(edge.kind, edge.source_kind, edge.source, edge.target_kind, edge.target, edge.file_path, edge.line, edge.evidence);
373
+ }
374
+ (0, schema_1.createSecondaryIndexes)(database);
375
+ database.exec("COMMIT");
376
+ }
377
+ catch (error) {
378
+ try {
379
+ database.exec("ROLLBACK");
380
+ }
381
+ catch {
382
+ // Ignore rollback failures after row-stream setup errors.
383
+ }
384
+ throw error;
385
+ }
386
+ finally {
387
+ database.close();
388
+ }
389
+ }
390
+ function runNativeCodeIndexMode(request) {
391
+ let helperPath = "";
392
+ try {
393
+ helperPath = (0, native_helper_1.requireNativeCodeIndexHelperPath)();
394
+ }
395
+ catch (error) {
396
+ fail(error instanceof Error ? error.message : String(error));
397
+ }
398
+ prepareOutputPath();
399
+ const tempDatabasePath = `${request.databasePath.absolutePath}.native-${process.pid}-${Date.now()}.tmp`;
400
+ removeDatabaseFiles(tempDatabasePath);
401
+ const manifestFiles = [];
402
+ const nativeFiles = [];
403
+ const typescriptFiles = [];
404
+ for (const filePath of request.discoveredFiles) {
405
+ const file = nativeCodeIndexFileFor(filePath, request.parserMode);
406
+ manifestFiles.push(file);
407
+ if (nativeEligibleProfile(file.profile))
408
+ nativeFiles.push(file);
409
+ else
410
+ typescriptFiles.push(file);
411
+ }
412
+ const typescriptProfiles = [...new Set(typescriptFiles.map((file) => file.profile))].sort();
413
+ const outputMode = nativeCodeIndexOutputMode();
414
+ const rowsPath = `${tempDatabasePath}.rows.json`;
415
+ const job = (0, native_helper_1.buildNativeCodeIndexJob)({
416
+ database_path: tempDatabasePath,
417
+ files: nativeFiles,
418
+ output_mode: outputMode,
419
+ parser_mode: request.parserMode,
420
+ schema_version: schema_1.codeIndexSchemaVersion,
421
+ scopes: request.scopes,
422
+ ...(outputMode === "row-stream" ? { rows_path: rowsPath } : {}),
423
+ });
424
+ let summary;
425
+ let typescriptIndexedFiles = 0;
426
+ try {
427
+ if (outputMode === "row-stream") {
428
+ const result = (0, native_helper_1.runNativeCodeIndexRowsHelper)(job, { helperPath });
429
+ summary = result.summary;
430
+ writeNativeRowsToDatabase(tempDatabasePath, result.rows, request.scopes, request.parserMode);
431
+ }
432
+ else {
433
+ summary = (0, native_helper_1.runNativeCodeIndexHelper)(job, { helperPath });
434
+ }
435
+ if (!fs.existsSync(tempDatabasePath)) {
436
+ fail(`native code index helper did not create database: ${tempDatabasePath}`);
437
+ }
438
+ typescriptIndexedFiles = appendTypeScriptPartitionToNativeDatabase(tempDatabasePath, typescriptFiles, request.parserMode);
439
+ moveDatabaseFiles(tempDatabasePath, request.databasePath.absolutePath);
440
+ }
441
+ catch (error) {
442
+ removeTemporaryDatabaseFiles(tempDatabasePath);
443
+ fail(error instanceof Error ? error.message : String(error));
444
+ }
445
+ finally {
446
+ if (fs.existsSync(rowsPath))
447
+ fs.unlinkSync(rowsPath);
448
+ }
449
+ console.log("Project wiki code evidence index complete.");
450
+ console.log(`database: ${request.databasePath.relativePath}`);
451
+ console.log("mode: full");
452
+ console.log(`parser_mode: ${request.parserMode}`);
453
+ console.log(`engine: ${typescriptIndexedFiles > 0 ? "mixed-native-rust" : "native-rust"}`);
454
+ if (request.requestedEngine === "auto")
455
+ console.log("engine_selection: auto");
456
+ console.log(`native_strategy: ${outputMode}`);
457
+ console.log(`scopes: ${request.scopes.join(", ")}`);
458
+ console.log(`files: ${manifestFiles.length}`);
459
+ console.log(`native_files: ${nativeFiles.length}`);
460
+ console.log(`typescript_files: ${typescriptIndexedFiles}`);
461
+ if (typescriptProfiles.length > 0)
462
+ console.log(`typescript_profiles: ${typescriptProfiles.join(", ")}`);
463
+ console.log(`reindexed_files: ${manifestFiles.length}`);
464
+ console.log(`deleted_files: ${summary.deleted_files ?? 0}`);
465
+ console.log(`unchanged_files: ${summary.unchanged_files ?? 0}`);
466
+ }
467
+ function runNativeCodeIndexIncrementalMode(request) {
468
+ let helperPath = "";
469
+ try {
470
+ helperPath = (0, native_helper_1.requireNativeCodeIndexHelperPath)();
471
+ }
472
+ catch (error) {
473
+ fail(error instanceof Error ? error.message : String(error));
474
+ }
475
+ const nativeFiles = request.reindexedFiles.map((file) => nativeCodeIndexFileFromFingerprint(file, request.parserMode));
476
+ const ineligibleProfiles = [...new Set(nativeFiles
477
+ .filter((file) => !nativeEligibleProfile(file.profile))
478
+ .map((file) => file.profile))].sort();
479
+ if (ineligibleProfiles.length > 0) {
480
+ fail(`native incremental writer does not support parser profiles: ${ineligibleProfiles.join(", ")}`);
481
+ }
482
+ prepareOutputPath();
483
+ const outputMode = "sqlite-direct";
484
+ const job = (0, native_helper_1.buildNativeCodeIndexJob)({
485
+ database_path: request.databasePath.absolutePath,
486
+ deleted_paths: request.deletedPaths,
487
+ files: nativeFiles,
488
+ mode: "incremental",
489
+ output_mode: outputMode,
490
+ parser_mode: request.parserMode,
491
+ schema_version: schema_1.codeIndexSchemaVersion,
492
+ scopes: request.scopes,
493
+ });
494
+ let summary;
495
+ try {
496
+ summary = (0, native_helper_1.runNativeCodeIndexHelper)(job, { helperPath });
497
+ if (!fs.existsSync(request.databasePath.absolutePath)) {
498
+ fail(`native incremental writer did not preserve database: ${request.databasePath.absolutePath}`);
499
+ }
500
+ }
501
+ catch (error) {
502
+ fail(error instanceof Error ? error.message : String(error));
503
+ }
504
+ console.log("Project wiki code evidence index complete.");
505
+ console.log(`database: ${request.databasePath.relativePath}`);
506
+ console.log("mode: incremental");
507
+ console.log(`parser_mode: ${request.parserMode}`);
508
+ console.log("engine: native-rust");
509
+ if (request.requestedEngine === "auto")
510
+ console.log("engine_selection: auto");
511
+ console.log(`native_strategy: ${outputMode}`);
512
+ console.log(`scopes: ${request.scopes.join(", ")}`);
513
+ console.log(`files: ${request.discoveredFiles.length}`);
514
+ console.log(`native_files: ${nativeFiles.length}`);
515
+ console.log("typescript_files: 0");
516
+ console.log(`reindexed_files: ${summary.reindexed_files ?? nativeFiles.length}`);
517
+ console.log(`deleted_files: ${summary.deleted_files ?? request.deletedPaths.length}`);
518
+ console.log(`unchanged_files: ${request.unchangedFiles}`);
519
+ }
175
520
  function codeIndexStaleness(database) {
176
521
  const scopes = (0, schema_1.indexedScopes)(database);
177
522
  const parserMode = (0, schema_1.indexedParserMode)(database);
@@ -193,7 +538,7 @@ function codeIndexStaleness(database) {
193
538
  }
194
539
  if (existing.mtimeMs === file.mtimeMs && existing.size === file.size)
195
540
  continue;
196
- if (readCodeFile(file.path, parserMode).hash !== existing.hash)
541
+ if (readCodeFile(file.path, parserMode, file).hash !== existing.hash)
197
542
  changed += 1;
198
543
  }
199
544
  const deleted = indexedRows.filter((row) => !currentPaths.has(String(row.path))).length;
@@ -383,7 +728,7 @@ function prepareOutputPath() {
383
728
  const databasePath = codeEvidenceDatabasePath();
384
729
  (0, workspace_1.mkdirp)(path.dirname(databasePath.relativePath));
385
730
  (0, workspace_1.mkdirp)(code_index_file_policy_1.codeEvidenceDirectory);
386
- fs.writeFileSync((0, workspace_1.abs)(`${code_index_file_policy_1.codeEvidenceDirectory}/.gitignore`), "*\n!.gitignore\n");
731
+ (0, workspace_1.write)(`${code_index_file_policy_1.codeEvidenceDirectory}/.gitignore`, "*\n!.gitignore\n");
387
732
  }
388
733
  function codeIndexModeRuntime() {
389
734
  return {
@@ -396,13 +741,20 @@ function codeIndexModeRuntime() {
396
741
  codeScopes,
397
742
  fail,
398
743
  indexCodeFile,
744
+ nativeCodeIndexAvailable,
745
+ nativeCodeIndexIncrementalEligible,
399
746
  openDatabase,
400
747
  prepareOutputPath,
401
748
  readCodeFileFingerprint,
402
749
  readCodeFile,
403
750
  removeDatabaseFiles,
404
751
  requireExistingIndex,
752
+ runNativeCodeIndexIncrementalMode,
753
+ runNativeCodeIndexMode,
754
+ selectedCodeIndexEngine,
405
755
  selectedCodeParserMode,
756
+ codeIndexEngineSelectionContext,
757
+ shouldUseNativeCodeIndexAuto,
406
758
  warnIfCodeIndexStale,
407
759
  };
408
760
  }
package/dist/hooks.js CHANGED
@@ -372,15 +372,15 @@ exports.gitPrepareCommitMsgHook = `#!/bin/sh
372
372
  MSG_FILE="$1"
373
373
  SOURCE="$2"
374
374
 
375
+ # Security boundary: prepare-commit-msg runs before the commit is created, so it
376
+ # must not execute scripts from the mutable worktree. Trailer generation remains
377
+ # available as an explicit maintainer action, but this hook is intentionally
378
+ # passive.
375
379
  case "$SOURCE" in
376
- merge|squash|commit)
380
+ merge|squash|commit|*)
377
381
  exit 0
378
382
  ;;
379
383
  esac
380
-
381
- if command -v node >/dev/null 2>&1 && [ -f ".githooks/wiki-commit-trailers.js" ]; then
382
- node .githooks/wiki-commit-trailers.js "$MSG_FILE"
383
- fi
384
384
  `;
385
385
  exports.gitWikiCommitTrailersScript = `#!/usr/bin/env node
386
386
 
@@ -17,7 +17,7 @@ function codeIndex() {
17
17
  function printUsage() {
18
18
  console.log(`Usage:
19
19
  project-librarian [init|update] [options]
20
- project-librarian install [--scope user|project] [--agents codex|claude|cursor|gemini|all]
20
+ project-librarian install [--scope user|project] [--agents codex|claude|cursor|gemini|all] [--dry-run]
21
21
  project-librarian mcp
22
22
 
23
23
  Options:
@@ -34,6 +34,7 @@ Options:
34
34
  --issue-draft Print a problem/side-effect GitHub issue body draft.
35
35
  --issue-body-file <path> With --issue-create, use an existing Markdown body file.
36
36
  --issue-title <title> Override the generated issue draft title.
37
+ --dry-run With install, preview copied skill files without writing them.
37
38
  --query <terms> Search wiki paths, metadata, titles, and bodies (answer-shaped, capped output).
38
39
  --wiki-impact <page-or-term> Show wiki backlinks, decision_ref citations, and router depth for matching pages.
39
40
  --wiki-visualize Write a static wiki graph visualizer to .project-wiki/wiki-graph.html.
@@ -61,6 +62,7 @@ Options:
61
62
  --acknowledge-small-repo With --code-index, proceed below the small-repo scale gate after its cost warning.
62
63
  --incremental With --code-index, require an existing compatible index and update only changes.
63
64
  --code-index-full With --code-index, force a full rebuild even when incremental update is possible.
65
+ --code-index-engine <engine> With --code-index, override default auto engine: typescript or native-rust.
64
66
  --code-parser <mode> With --code-index, use parser mode default or tree-sitter.
65
67
  --code-query <sql> Run conservative read-only SQL over the code evidence index.
66
68
  --code-status, --code-files Inspect the code evidence index.
@@ -164,6 +166,10 @@ if (args_1.codeIndexFullMode && !args_1.codeIndexMode) {
164
166
  console.error("--code-index-full is only supported with --code-index.");
165
167
  process.exit(1);
166
168
  }
169
+ if (args_1.codeIndexEngineMode && !args_1.codeIndexMode) {
170
+ console.error("--code-index-engine is only supported with --code-index.");
171
+ process.exit(1);
172
+ }
167
173
  if (args_1.codeParserMode && !args_1.codeIndexMode) {
168
174
  console.error("--code-parser is only supported with --code-index.");
169
175
  process.exit(1);
@@ -112,26 +112,119 @@ function installTarget(agent, scope) {
112
112
  const base = scope === "user" ? userAgentRoot(agent) : path.join(process.cwd(), projectAgentRoot(agent));
113
113
  return path.join(base, "skills", skillName);
114
114
  }
115
+ function assertNoTargetSymlink(targetRoot, target, includeLeaf) {
116
+ const rootResolved = path.resolve(targetRoot);
117
+ const targetResolved = path.resolve(target);
118
+ if (targetResolved !== rootResolved && !targetResolved.startsWith(`${rootResolved}${path.sep}`)) {
119
+ fail(`skill install target escaped target root: ${target}`);
120
+ }
121
+ if (fs.existsSync(rootResolved) && fs.lstatSync(rootResolved).isSymbolicLink()) {
122
+ fail("skill install refuses to follow destination symlink: .");
123
+ }
124
+ const relative = path.relative(rootResolved, targetResolved);
125
+ const parts = relative ? relative.split(path.sep).filter(Boolean) : [];
126
+ const checkedParts = includeLeaf ? parts : parts.slice(0, -1);
127
+ let current = rootResolved;
128
+ for (const part of checkedParts) {
129
+ current = path.join(current, part);
130
+ if (!fs.existsSync(current))
131
+ continue;
132
+ const stat = fs.lstatSync(current);
133
+ if (stat.isSymbolicLink()) {
134
+ fail(`skill install refuses to follow destination symlink: ${path.relative(rootResolved, current) || "."}`);
135
+ }
136
+ if (current !== targetResolved && !stat.isDirectory()) {
137
+ fail(`skill install target has a non-directory path component: ${path.relative(rootResolved, current)}`);
138
+ }
139
+ }
140
+ }
141
+ function isInsidePath(base, target) {
142
+ const baseResolved = path.resolve(base);
143
+ const targetResolved = path.resolve(target);
144
+ return targetResolved === baseResolved || targetResolved.startsWith(`${baseResolved}${path.sep}`);
145
+ }
146
+ function mkdirFromBaseNoSymlink(base, target) {
147
+ const baseResolved = path.resolve(base);
148
+ const targetResolved = path.resolve(target);
149
+ if (!isInsidePath(baseResolved, targetResolved)) {
150
+ fail(`skill install target escaped checked base: ${target}`);
151
+ }
152
+ let current = baseResolved;
153
+ const parts = path.relative(baseResolved, targetResolved).split(path.sep).filter(Boolean);
154
+ for (const part of parts) {
155
+ current = path.join(current, part);
156
+ if (fs.existsSync(current)) {
157
+ const stat = fs.lstatSync(current);
158
+ if (stat.isSymbolicLink()) {
159
+ fail(`skill install refuses to follow destination symlink: ${path.relative(baseResolved, current)}`);
160
+ }
161
+ if (!stat.isDirectory()) {
162
+ fail(`skill install target has a non-directory path component: ${path.relative(baseResolved, current)}`);
163
+ }
164
+ continue;
165
+ }
166
+ fs.mkdirSync(current);
167
+ }
168
+ }
169
+ function mkdirpNoTargetSymlink(targetRoot, target) {
170
+ const rootResolved = path.resolve(targetRoot);
171
+ const targetResolved = path.resolve(target);
172
+ const cwdResolved = path.resolve(process.cwd());
173
+ if (isInsidePath(cwdResolved, rootResolved)) {
174
+ mkdirFromBaseNoSymlink(cwdResolved, rootResolved);
175
+ }
176
+ else {
177
+ fs.mkdirSync(rootResolved, { recursive: true });
178
+ }
179
+ const relative = path.relative(rootResolved, targetResolved);
180
+ let current = rootResolved;
181
+ if (!relative) {
182
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
183
+ fail("skill install refuses to follow destination symlink: .");
184
+ }
185
+ if (!fs.existsSync(current))
186
+ fs.mkdirSync(current);
187
+ return;
188
+ }
189
+ for (const part of relative.split(path.sep).filter(Boolean)) {
190
+ current = path.join(current, part);
191
+ if (fs.existsSync(current)) {
192
+ const stat = fs.lstatSync(current);
193
+ if (stat.isSymbolicLink()) {
194
+ fail(`skill install refuses to follow destination symlink: ${path.relative(rootResolved, current)}`);
195
+ }
196
+ if (!stat.isDirectory()) {
197
+ fail(`skill install target has a non-directory path component: ${path.relative(rootResolved, current)}`);
198
+ }
199
+ continue;
200
+ }
201
+ fs.mkdirSync(current);
202
+ }
203
+ }
115
204
  function sameFile(source, target) {
116
- if (!fs.existsSync(target) || !fs.statSync(target).isFile())
205
+ if (!fs.existsSync(target))
206
+ return false;
207
+ const stat = fs.lstatSync(target);
208
+ if (stat.isSymbolicLink() || !stat.isFile())
117
209
  return false;
118
210
  return fs.readFileSync(source).equals(fs.readFileSync(target));
119
211
  }
120
- function copyPath(source, target, dryRun) {
212
+ function copyPath(source, target, targetRoot, dryRun) {
121
213
  if (!fs.existsSync(source))
122
214
  fail(`missing package file: ${source}`);
123
215
  const existed = fs.existsSync(target);
124
216
  if (dryRun)
125
217
  return "dry-run";
126
218
  const sourceStat = fs.statSync(source);
219
+ assertNoTargetSymlink(targetRoot, target, true);
127
220
  if (sourceStat.isDirectory()) {
128
- fs.mkdirSync(target, { recursive: true });
221
+ mkdirpNoTargetSymlink(targetRoot, target);
129
222
  for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
130
- copyPath(path.join(source, entry.name), path.join(target, entry.name), false);
223
+ copyPath(path.join(source, entry.name), path.join(target, entry.name), targetRoot, false);
131
224
  }
132
225
  return existed ? "updated" : "created";
133
226
  }
134
- fs.mkdirSync(path.dirname(target), { recursive: true });
227
+ mkdirpNoTargetSymlink(targetRoot, path.dirname(target));
135
228
  if (sameFile(source, target))
136
229
  return "exists";
137
230
  fs.copyFileSync(source, target);
@@ -146,7 +239,7 @@ function copyPackageFiles(targetRoot, dryRun, labelRoot = targetRoot) {
146
239
  return packageFiles.map((relativePath) => {
147
240
  const source = path.join(root, relativePath);
148
241
  const target = path.join(targetRoot, relativePath);
149
- return [path.join(labelRoot, relativePath), copyPath(source, target, dryRun)];
242
+ return [path.join(labelRoot, relativePath), copyPath(source, target, targetRoot, dryRun)];
150
243
  });
151
244
  }
152
245
  function syncProjectSkillInstall(agent) {
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  // Hand-rolled MCP stdio server for the code-evidence index.
3
3
  //
4
- // Why hand-rolled: the product holds a zero-runtime-dependency posture (only
5
- // node:sqlite plus optional tree-sitter grammars). Adopting @modelcontextprotocol/sdk
6
- // would add a hard runtime dependency, so we implement the small slice of MCP we
7
- // need directly: JSON-RPC 2.0 over newline-delimited JSON on stdio.
4
+ // Why hand-rolled: the package keeps the MCP surface dependency-light. Code
5
+ // evidence already depends on node:sqlite, the package's TypeScript dependency,
6
+ // and optional tree-sitter grammars; adopting @modelcontextprotocol/sdk would add
7
+ // another hard runtime dependency for a small stdio slice.
8
8
  //
9
9
  // Transport: MCP stdio transport carries one JSON-RPC message per line on stdin
10
10
  // and stdout (newline-delimited JSON, "ndjson"); stderr is free for logging. We