project-librarian 0.5.7 → 0.5.8

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 CHANGED
@@ -104,15 +104,30 @@ Project Librarian은 작은 첫 읽기와 더 깊은 정본으로 가는 경로
104
104
 
105
105
  여기 수치는 모든 상황에 대한 약속이 아니라 관리자 릴리스 근거입니다. 모든 값은 실제 Codex JSONL 사용량과 로컬 실행 시간이며, Project Librarian이 없는 `organic` 대조군과 격리된 환경에서 비교했습니다. 위키 라우팅 트랙과 코드 그래프 트랙은 분리해 측정하며, 한 트랙의 우위가 다른 트랙의 주장을 뒷받침하지 않습니다.
106
106
 
107
- 위키 트랙, 대조군 대비 비용 가중 토큰:
108
-
109
- | 규모 | decision_lookup | aggregation | multi_session |
110
- | --- | --- | --- | --- |
111
- | 소형 | 14.4% 적음 | 81.0% 많음 | 22.0% 적음 |
112
- | 중형 | 52.0% 적음 | 19.0% 적음 | 54.1% 적음 |
113
- | 대형 | 71.1% 적음 | 29.0% 적음 | 71.8% 적음 |
114
-
115
- 최신 합성 위키 라우팅 트랙 릴리스 후보는 2026-06-19에 `gpt-5.5`로 측정했으며, 42 시나리오를 3회 측정하고 1회 예열했습니다. 전체 주장 게이트는 통과했습니다. 이 주장은 합성 위키 라우팅 트랙과 표기된 작업 유형에 한정되며, 코드 그래프 동작, 실제 저장소, 모든 에이전트 표면, 모든 질문 형태에 대한 주장이 아닙니다.
107
+ 최신 clean 합성 위키 라우팅 트랙 릴리스 근거는 2026-06-29에 `gpt-5.5`, `perf/small-repo-code-evidence-safeguards` 브랜치의 `ae79390` 커밋에서 측정했습니다. 42개 시나리오, 21개 with/without 쌍을 각 3회 측정하고 1회 예열했으며, 전체 주장 게이트는 통과했고 주장 ledger는 이 보고서를 release-claimable로 분류했습니다. repair 실행은 보존된 raw JSONL에서 claimable 측정 실행 125개를 재사용하고 남은 실패 슬롯 1개를 다시 측정했습니다. 이 주장은 합성 위키 라우팅 트랙과 표기된 작업 유형에 한정되며, 코드 그래프 동작, 실제 저장소, 모든 에이전트 표면, 모든 질문 형태에 대한 주장이 아닙니다.
108
+
109
+ 위키 트랙 집계, 대조군 대비 변화:
110
+
111
+ | 규모 | 비용 가중 토큰 | 전체 토큰 | 실행 시간 | 명령 수 | 도구 출력 |
112
+ | --- | ---: | ---: | ---: | ---: | ---: |
113
+ | 전체 | 51.39% 적음 | 48.67% 적음 | 19.83% 적음 | 18.40% 적음 | 85.88% 적음 |
114
+ | 소형 | 21.59% 적음 | 9.31% 적음 | 13.91% 적음 | 12.64% 적음 | 43.89% 적음 |
115
+ | 중형 | 45.95% 적음 | 37.42% 적음 | 11.88% 적음 | 4.30% 적음 | 69.12% 적음 |
116
+ | 대형 | 66.97% 적음 | 69.87% 적음 | 31.90% 적음 | 35.19% 적음 | 95.58% 적음 |
117
+
118
+ 위키 트랙 작업 유형별 비용 가중 토큰 변화:
119
+
120
+ | 작업 유형 | 변화 |
121
+ | --- | ---: |
122
+ | onboarding | 65.95% 적음 |
123
+ | decision_lookup | 48.46% 적음 |
124
+ | code_impact | 55.61% 적음 |
125
+ | release_policy | 58.22% 적음 |
126
+ | change_location | 29.59% 적음 |
127
+ | multi_session | 52.58% 적음 |
128
+ | aggregation | 42.53% 적음 |
129
+
130
+ 실행 시간과 명령 수의 한계도 있습니다. 이번 보고서에서 `code_impact`, `change_location`, `aggregation`은 토큰과 출력 바이트가 모두 개선됐지만, 실행 시간이나 명령 수에서는 일부 역전이 남아 있습니다.
116
131
 
117
132
  코드 그래프 트랙, 대조군 대비 비용 가중 토큰:
118
133
 
package/README.md CHANGED
@@ -104,15 +104,30 @@ See [Usage](docs/usage.md) for install scopes, runner paths, generated files, mi
104
104
 
105
105
  These numbers are maintainer release evidence, not a blanket promise. Every value is real Codex JSONL usage and local wall-clock time, measured hermetically against an `organic` control with no Project Librarian. The wiki-routing track and the code-graph track are measured separately; a win on one never backs a claim about the other.
106
106
 
107
- Wiki track, cost-weighted tokens vs control:
108
-
109
- | Scale | decision_lookup | aggregation | multi_session |
110
- | --- | --- | --- | --- |
111
- | Small | 14.4% less | 81.0% more | 22.0% less |
112
- | Medium | 52.0% less | 19.0% less | 54.1% less |
113
- | Large | 71.1% less | 29.0% less | 71.8% less |
114
-
115
- Latest synthetic wiki-routing track release candidate: 2026-06-19, `gpt-5.5`, 42 scenarios, 3 measured runs plus 1 warmup each. The overall claim gate **passed**. This is bounded to the synthetic wiki-routing track and listed task families; it is not a claim about code-graph behavior, real repositories, every agent surface, or every question shape.
107
+ Latest clean synthetic wiki-routing track release evidence: 2026-06-29, `gpt-5.5`, branch `perf/small-repo-code-evidence-safeguards` at `ae79390`, 42 scenarios, 21 with/without pairs, 3 measured runs plus 1 warmup each. The overall claim gate **passed**, and the claim ledger classified the report as release-claimable. A repair run reused 125 claimable measured runs from retained raw JSONL and remeasured the remaining failed slot. This is bounded to the synthetic wiki-routing track and listed task families; it is not a claim about code-graph behavior, real repositories, every agent surface, or every question shape.
108
+
109
+ Wiki track aggregate deltas vs control:
110
+
111
+ | Scale | Cost-weighted tokens | Total tokens | Wall time | Commands | Tool output |
112
+ | --- | ---: | ---: | ---: | ---: | ---: |
113
+ | All | 51.39% less | 48.67% less | 19.83% less | 18.40% less | 85.88% less |
114
+ | Small | 21.59% less | 9.31% less | 13.91% less | 12.64% less | 43.89% less |
115
+ | Medium | 45.95% less | 37.42% less | 11.88% less | 4.30% less | 69.12% less |
116
+ | Large | 66.97% less | 69.87% less | 31.90% less | 35.19% less | 95.58% less |
117
+
118
+ Wiki track task-family cost-weighted token deltas vs control:
119
+
120
+ | Task family | Delta |
121
+ | --- | ---: |
122
+ | onboarding | 65.95% less |
123
+ | decision_lookup | 48.46% less |
124
+ | code_impact | 55.61% less |
125
+ | release_policy | 58.22% less |
126
+ | change_location | 29.59% less |
127
+ | multi_session | 52.58% less |
128
+ | aggregation | 42.53% less |
129
+
130
+ Timing and command-count caveat: `code_impact`, `change_location`, and `aggregation` still had wall-time or command-count regressions in this report, even though token and output-byte metrics improved for every task family.
116
131
 
117
132
  Code-graph track, cost-weighted tokens vs control:
118
133
 
@@ -131,6 +131,36 @@ function emitCodeIndexPhaseTimings(timings) {
131
131
  return;
132
132
  console.error(`code_index_phase_timings ${JSON.stringify(timings)}`);
133
133
  }
134
+ function finishCodeEvidencePhaseTimings(started, timings) {
135
+ timings.total_ms = Number(elapsedMs(started).toFixed(3));
136
+ emitCodeIndexPhaseTimings(timings);
137
+ }
138
+ function runWithCodeEvidenceDatabase(runtime, operation, options = {}) {
139
+ const totalStarted = process.hrtime.bigint();
140
+ const phaseTimings = {};
141
+ let database;
142
+ try {
143
+ measurePhase(phaseTimings, "require_existing_index_ms", () => runtime.requireExistingIndex());
144
+ options.beforeOpen?.();
145
+ database = measurePhase(phaseTimings, "open_database_ms", () => runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath));
146
+ operation(database, phaseTimings);
147
+ }
148
+ finally {
149
+ if (database) {
150
+ const openedDatabase = database;
151
+ measurePhase(phaseTimings, "close_database_ms", () => openedDatabase.close());
152
+ }
153
+ finishCodeEvidencePhaseTimings(totalStarted, phaseTimings);
154
+ }
155
+ }
156
+ function requireCompatibleDatabaseTimed(database, runtime, timings) {
157
+ measurePhase(timings, "compatibility_ms", () => requireCompatibleDatabase(database, runtime));
158
+ }
159
+ function checkedCodeIndexStaleness(database, runtime, timings) {
160
+ const staleness = measurePhase(timings, "staleness_ms", () => runtime.codeIndexStaleness(database));
161
+ runtime.warnIfCodeIndexStale(database, staleness);
162
+ return staleness;
163
+ }
134
164
  function configureBulkWriteConnection(database) {
135
165
  database.exec(`
136
166
  PRAGMA synchronous = OFF;
@@ -199,13 +229,11 @@ function runCodeIndexMode(runtime) {
199
229
  if (!args_1.codeIndexIncrementalMode) {
200
230
  try {
201
231
  measurePhase(phaseTimings, "native_helper_ms", () => runtime.runNativeCodeIndexMode({ databasePath, discoveredFiles, parserMode, requestedEngine, scopes }));
202
- phaseTimings.total_ms = Number(elapsedMs(totalStarted).toFixed(3));
203
- emitCodeIndexPhaseTimings(phaseTimings);
232
+ finishCodeEvidencePhaseTimings(totalStarted, phaseTimings);
204
233
  return;
205
234
  }
206
235
  catch (error) {
207
- phaseTimings.total_ms = Number(elapsedMs(totalStarted).toFixed(3));
208
- emitCodeIndexPhaseTimings(phaseTimings);
236
+ finishCodeEvidencePhaseTimings(totalStarted, phaseTimings);
209
237
  throw error;
210
238
  }
211
239
  }
@@ -278,8 +306,7 @@ function runCodeIndexMode(runtime) {
278
306
  scopes,
279
307
  unchangedFiles,
280
308
  }));
281
- phaseTimings.total_ms = Number(elapsedMs(totalStarted).toFixed(3));
282
- emitCodeIndexPhaseTimings(phaseTimings);
309
+ finishCodeEvidencePhaseTimings(totalStarted, phaseTimings);
283
310
  return;
284
311
  }
285
312
  }
@@ -303,7 +330,6 @@ function runCodeIndexMode(runtime) {
303
330
  (0, schema_1.createSecondaryIndexes)(activeDatabase);
304
331
  activeDatabase.exec("COMMIT");
305
332
  });
306
- phaseTimings.total_ms = Number(elapsedMs(totalStarted).toFixed(3));
307
333
  console.log("Project wiki code evidence index complete.");
308
334
  console.log(`database: ${databasePath.relativePath}`);
309
335
  console.log(`mode: ${incremental ? "incremental" : "full"}`);
@@ -316,7 +342,7 @@ function runCodeIndexMode(runtime) {
316
342
  console.log(`reindexed_files: ${reindexedFiles.length}`);
317
343
  console.log(`deleted_files: ${deletedPaths.length}`);
318
344
  console.log(`unchanged_files: ${unchangedFiles}`);
319
- emitCodeIndexPhaseTimings(phaseTimings);
345
+ finishCodeEvidencePhaseTimings(totalStarted, phaseTimings);
320
346
  }
321
347
  catch (error) {
322
348
  try {
@@ -336,123 +362,93 @@ function runCodeQueryMode(runtime) {
336
362
  console.error("missing SQL: use --code-query \"select ...\"");
337
363
  process.exit(1);
338
364
  }
339
- runtime.requireExistingIndex();
340
- if (!(0, code_index_sql_1.isReadOnlySql)(args_1.codeQuerySql)) {
341
- console.error("code queries must be read-only SQL starting with SELECT or WITH");
342
- process.exit(1);
343
- }
344
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
345
- try {
365
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
346
366
  database.exec("PRAGMA query_only = ON");
347
- requireCompatibleDatabase(database, runtime);
348
- runtime.warnIfCodeIndexStale(database);
349
- printRows(database.prepare(args_1.codeQuerySql).all());
350
- }
351
- finally {
352
- database.close();
353
- }
367
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
368
+ checkedCodeIndexStaleness(database, runtime, phaseTimings);
369
+ const rows = measurePhase(phaseTimings, "query_ms", () => database.prepare(args_1.codeQuerySql).all());
370
+ measurePhase(phaseTimings, "render_ms", () => printRows(rows));
371
+ }, {
372
+ beforeOpen() {
373
+ if (!(0, code_index_sql_1.isReadOnlySql)(args_1.codeQuerySql)) {
374
+ console.error("code queries must be read-only SQL starting with SELECT or WITH");
375
+ process.exit(1);
376
+ }
377
+ },
378
+ });
354
379
  }
355
380
  function runCodeReportMode(runtime) {
356
- runtime.requireExistingIndex();
357
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
358
- try {
359
- requireCompatibleDatabase(database, runtime);
360
- const staleness = runtime.codeIndexStaleness(database);
361
- runtime.warnIfCodeIndexStale(database, staleness);
362
- const report = runtime.codeReportForRequestedSection(database, args_1.codeReportSection, { staleness });
381
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
382
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
383
+ const staleness = checkedCodeIndexStaleness(database, runtime, phaseTimings);
384
+ const report = measurePhase(phaseTimings, "query_ms", () => runtime.codeReportForRequestedSection(database, args_1.codeReportSection, { staleness }));
363
385
  if (!report)
364
386
  runtime.fail((0, reports_1.invalidCodeReportSectionMessage)(args_1.codeReportSection));
365
- printJson(report);
366
- }
367
- finally {
368
- database.close();
369
- }
387
+ measurePhase(phaseTimings, "render_ms", () => printJson(report));
388
+ });
370
389
  }
371
390
  function runCodeStatusMode(runtime) {
372
- runtime.requireExistingIndex();
373
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
374
- try {
375
- requireCompatibleDatabase(database, runtime);
376
- const rows = database.prepare(`
391
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
392
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
393
+ const rows = measurePhase(phaseTimings, "query_ms", () => database.prepare(`
377
394
  SELECT 'files' AS metric, count(*) AS value FROM files
378
395
  UNION ALL SELECT 'symbols', count(*) FROM symbols
379
396
  UNION ALL SELECT 'imports', count(*) FROM imports
380
397
  UNION ALL SELECT 'routes', count(*) FROM routes
381
398
  UNION ALL SELECT 'edges', count(*) FROM edges
382
399
  UNION ALL SELECT 'configs', count(*) FROM configs
383
- `).all();
384
- const staleness = runtime.codeIndexStaleness(database);
400
+ `).all());
401
+ const staleness = measurePhase(phaseTimings, "staleness_ms", () => runtime.codeIndexStaleness(database));
385
402
  rows.push({ metric: "stale_files", value: staleness.added + staleness.changed + staleness.deleted }, { metric: "stale_changed_files", value: staleness.changed }, { metric: "stale_added_files", value: staleness.added }, { metric: "stale_deleted_files", value: staleness.deleted });
386
- printRows(rows);
387
- }
388
- finally {
389
- database.close();
390
- }
403
+ measurePhase(phaseTimings, "render_ms", () => printRows(rows));
404
+ });
391
405
  }
392
406
  function runCodeIndexHealthMode(runtime) {
393
407
  printJson(runtime.codeIndexHealth());
394
408
  }
395
409
  function runCodeFilesMode(runtime) {
396
- runtime.requireExistingIndex();
397
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
398
- try {
399
- requireCompatibleDatabase(database, runtime);
400
- runtime.warnIfCodeIndexStale(database);
401
- printRows(database.prepare("SELECT path, language, profile, kind, lines, bytes FROM files ORDER BY path").all());
402
- }
403
- finally {
404
- database.close();
405
- }
410
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
411
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
412
+ checkedCodeIndexStaleness(database, runtime, phaseTimings);
413
+ const rows = measurePhase(phaseTimings, "query_ms", () => database.prepare("SELECT path, language, profile, kind, lines, bytes FROM files ORDER BY path").all());
414
+ measurePhase(phaseTimings, "render_ms", () => printRows(rows));
415
+ });
406
416
  }
407
417
  function runCodeImpactMode(runtime) {
408
418
  if (!args_1.codeImpactTarget.trim()) {
409
419
  console.error("missing impact target: use --code-impact \"path-or-symbol-or-module\"");
410
420
  process.exit(1);
411
421
  }
412
- runtime.requireExistingIndex();
413
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
414
- try {
415
- requireCompatibleDatabase(database, runtime);
416
- const staleness = runtime.codeIndexStaleness(database);
417
- runtime.warnIfCodeIndexStale(database, staleness);
418
- printJson(runtime.codeImpact(database, args_1.codeImpactTarget.trim(), { staleness }));
419
- }
420
- finally {
421
- database.close();
422
- }
422
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
423
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
424
+ const staleness = checkedCodeIndexStaleness(database, runtime, phaseTimings);
425
+ const impact = measurePhase(phaseTimings, "query_ms", () => runtime.codeImpact(database, args_1.codeImpactTarget.trim(), { staleness }));
426
+ measurePhase(phaseTimings, "render_ms", () => printJson(impact));
427
+ });
423
428
  }
424
429
  function runCodeContextPackMode(runtime) {
425
430
  if (!args_1.codeContextPackTarget.trim()) {
426
431
  console.error("missing context pack query: use --code-context-pack \"path-or-symbol-or-route\"");
427
432
  process.exit(1);
428
433
  }
429
- runtime.requireExistingIndex();
430
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
431
- try {
432
- requireCompatibleDatabase(database, runtime);
433
- const staleness = runtime.codeIndexStaleness(database);
434
- runtime.warnIfCodeIndexStale(database, staleness);
435
- console.log(runtime.codeContextPack(database, args_1.codeContextPackTarget.trim(), { staleness }));
436
- }
437
- finally {
438
- database.close();
439
- }
434
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
435
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
436
+ const staleness = checkedCodeIndexStaleness(database, runtime, phaseTimings);
437
+ const pack = measurePhase(phaseTimings, "query_ms", () => runtime.codeContextPack(database, args_1.codeContextPackTarget.trim(), { staleness }));
438
+ measurePhase(phaseTimings, "render_ms", () => console.log(pack));
439
+ });
440
440
  }
441
441
  function runCodeSearchSymbolMode(runtime) {
442
442
  if (!args_1.codeSearchSymbol.trim()) {
443
443
  console.error("missing symbol search term: use --code-search-symbol \"term\"");
444
444
  process.exit(1);
445
445
  }
446
- runtime.requireExistingIndex();
447
- const database = runtime.openDatabase(runtime.codeEvidenceDatabasePath().absolutePath);
448
- try {
449
- requireCompatibleDatabase(database, runtime);
450
- runtime.warnIfCodeIndexStale(database);
451
- printRows((0, search_1.searchSymbols)(database, args_1.codeSearchSymbol.trim()));
452
- }
453
- finally {
454
- database.close();
455
- }
446
+ runWithCodeEvidenceDatabase(runtime, (database, phaseTimings) => {
447
+ requireCompatibleDatabaseTimed(database, runtime, phaseTimings);
448
+ checkedCodeIndexStaleness(database, runtime, phaseTimings);
449
+ const rows = measurePhase(phaseTimings, "query_ms", () => (0, search_1.searchSymbols)(database, args_1.codeSearchSymbol.trim()));
450
+ measurePhase(phaseTimings, "render_ms", () => printRows(rows));
451
+ });
456
452
  }
457
453
  function isCodeEvidenceModeFor(flags) {
458
454
  return Boolean(flags.codeContextPackTarget)
@@ -65,15 +65,126 @@ function readJsonObject(relativePath) {
65
65
  function workspacePatternsFromRootPackage() {
66
66
  const rootPackage = readJsonObject("package.json");
67
67
  const workspaces = rootPackage?.workspaces;
68
- if (Array.isArray(workspaces))
69
- return workspaces.filter((value) => typeof value === "string");
68
+ if (Array.isArray(workspaces)) {
69
+ return workspaces
70
+ .filter((value) => typeof value === "string")
71
+ .map((pattern) => ({ pattern, source: "package.json workspaces" }));
72
+ }
70
73
  if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) {
71
74
  const packages = workspaces.packages;
72
- if (Array.isArray(packages))
73
- return packages.filter((value) => typeof value === "string");
75
+ if (Array.isArray(packages)) {
76
+ return packages
77
+ .filter((value) => typeof value === "string")
78
+ .map((pattern) => ({ pattern, source: "package.json workspaces" }));
79
+ }
74
80
  }
75
81
  return [];
76
82
  }
83
+ function trimYamlComment(value) {
84
+ let quote = "";
85
+ for (let index = 0; index < value.length; index += 1) {
86
+ const character = value[index] ?? "";
87
+ if (quote) {
88
+ if (character === quote)
89
+ quote = "";
90
+ continue;
91
+ }
92
+ if (character === "\"" || character === "'") {
93
+ quote = character;
94
+ continue;
95
+ }
96
+ if (character === "#" && (index === 0 || /\s/.test(value[index - 1] ?? "")))
97
+ return value.slice(0, index).trimEnd();
98
+ }
99
+ return value.trimEnd();
100
+ }
101
+ function parseYamlStringScalar(value) {
102
+ const trimmed = trimYamlComment(value).trim();
103
+ if (!trimmed)
104
+ return null;
105
+ if (trimmed.startsWith("\"")) {
106
+ const match = trimmed.match(/^"((?:\\.|[^"\\])*)"/);
107
+ if (!match)
108
+ return null;
109
+ return match[1]?.replace(/\\"/g, "\"") ?? "";
110
+ }
111
+ if (trimmed.startsWith("'")) {
112
+ const match = trimmed.match(/^'((?:''|[^'])*)'/);
113
+ if (!match)
114
+ return null;
115
+ return match[1]?.replace(/''/g, "'") ?? "";
116
+ }
117
+ return trimmed;
118
+ }
119
+ function splitYamlFlowArray(value) {
120
+ const trimmed = trimYamlComment(value).trim();
121
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
122
+ return null;
123
+ const entries = [];
124
+ let quote = "";
125
+ let current = "";
126
+ for (const character of trimmed.slice(1, -1)) {
127
+ if (quote) {
128
+ current += character;
129
+ if (character === quote)
130
+ quote = "";
131
+ continue;
132
+ }
133
+ if (character === "\"" || character === "'") {
134
+ quote = character;
135
+ current += character;
136
+ continue;
137
+ }
138
+ if (character === ",") {
139
+ entries.push(current);
140
+ current = "";
141
+ continue;
142
+ }
143
+ current += character;
144
+ }
145
+ entries.push(current);
146
+ return entries;
147
+ }
148
+ function workspacePatternsFromPnpmWorkspace() {
149
+ const filePath = "pnpm-workspace.yaml";
150
+ if (!(0, workspace_1.containedProjectFileStat)(filePath))
151
+ return [];
152
+ const lines = (0, workspace_1.read)(filePath).split(/\r?\n/);
153
+ const patterns = [];
154
+ let packagesIndent = null;
155
+ for (const line of lines) {
156
+ if (!line.trim() || line.trimStart().startsWith("#"))
157
+ continue;
158
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
159
+ const packagesMatch = line.match(/^(\s*)packages\s*:\s*(.*)$/);
160
+ if (packagesMatch) {
161
+ packagesIndent = packagesMatch[1]?.length ?? 0;
162
+ const inlineArray = splitYamlFlowArray(packagesMatch[2] ?? "");
163
+ if (inlineArray) {
164
+ for (const entry of inlineArray) {
165
+ const pattern = parseYamlStringScalar(entry);
166
+ if (pattern && !pattern.startsWith("!"))
167
+ patterns.push({ pattern, source: "pnpm-workspace.yaml packages" });
168
+ }
169
+ }
170
+ continue;
171
+ }
172
+ if (packagesIndent === null)
173
+ continue;
174
+ if (indent <= packagesIndent)
175
+ break;
176
+ const item = line.slice(indent).match(/^-\s+(.+)$/);
177
+ if (!item)
178
+ continue;
179
+ const pattern = parseYamlStringScalar(item[1] ?? "");
180
+ if (pattern && !pattern.startsWith("!"))
181
+ patterns.push({ pattern, source: "pnpm-workspace.yaml packages" });
182
+ }
183
+ return patterns;
184
+ }
185
+ function workspacePatterns() {
186
+ return [...workspacePatternsFromRootPackage(), ...workspacePatternsFromPnpmWorkspace()];
187
+ }
77
188
  function workspacePatternCandidates(pattern) {
78
189
  const normalized = (0, workspace_1.normalizePath)(pattern).replace(/^\/+/, "").replace(/\/+$/, "");
79
190
  if (!normalized || normalized.includes(".."))
@@ -94,8 +205,8 @@ function workspacePatternCandidates(pattern) {
94
205
  }
95
206
  function workspacePackages() {
96
207
  const packages = new Map();
97
- for (const pattern of workspacePatternsFromRootPackage()) {
98
- for (const candidate of workspacePatternCandidates(pattern)) {
208
+ for (const workspacePattern of workspacePatterns()) {
209
+ for (const candidate of workspacePatternCandidates(workspacePattern.pattern)) {
99
210
  const packageJsonPath = (0, workspace_1.normalizePath)(path.join(candidate, "package.json"));
100
211
  if (!(0, workspace_1.containedProjectFileStat)(packageJsonPath))
101
212
  continue;
@@ -104,8 +215,8 @@ function workspacePackages() {
104
215
  packages.set(candidate, {
105
216
  name: packageName,
106
217
  root: candidate,
107
- source: "package.json workspaces",
108
- workspace_pattern: pattern,
218
+ source: workspacePattern.source,
219
+ workspace_pattern: workspacePattern.pattern,
109
220
  });
110
221
  }
111
222
  }
@@ -47,7 +47,7 @@ exports.incrementalCompatibility = incrementalCompatibility;
47
47
  exports.codeIndexSnapshot = codeIndexSnapshot;
48
48
  const crypto = __importStar(require("node:crypto"));
49
49
  const workspace_1 = require("../workspace");
50
- exports.codeIndexSchemaVersion = "5";
50
+ exports.codeIndexSchemaVersion = "6";
51
51
  function stableFtsRowid(parts) {
52
52
  const hash = crypto.createHash("sha256");
53
53
  for (const part of parts) {
@@ -158,7 +158,7 @@ function createIndexStatements(database) {
158
158
  insertMeta: database.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)"),
159
159
  insertRoute: database.prepare("INSERT INTO routes (method, route, file_path, line, handler) VALUES (?, ?, ?, ?, ?)"),
160
160
  insertSymbol: database.prepare("INSERT INTO symbols (name, kind, file_path, line, signature) VALUES (?, ?, ?, ?, ?)"),
161
- insertSymbolFts: database.prepare("INSERT INTO symbols_fts (name, kind, file_path, signature) VALUES (?, ?, ?, ?)"),
161
+ insertSymbolFts: database.prepare("INSERT INTO symbols_fts (rowid, name, kind, file_path, signature) VALUES (last_insert_rowid(), ?, ?, ?, ?)"),
162
162
  };
163
163
  }
164
164
  function removeIndexedFile(filePath, statements) {
@@ -130,11 +130,7 @@ function searchSymbols(database, term, limit = 50) {
130
130
  const ftsRows = database.prepare(`
131
131
  SELECT symbols.name, symbols.kind, symbols.file_path, symbols.line, symbols.signature
132
132
  FROM symbols_fts
133
- JOIN symbols
134
- ON symbols.name = symbols_fts.name
135
- AND symbols.kind = symbols_fts.kind
136
- AND symbols.file_path = symbols_fts.file_path
137
- AND symbols.signature = symbols_fts.signature
133
+ JOIN symbols ON symbols.id = symbols_fts.rowid
138
134
  WHERE symbols_fts MATCH ?
139
135
  ORDER BY bm25(symbols_fts, 8.0, 1.0, 4.0, 2.0), symbols.file_path, symbols.line
140
136
  LIMIT ?
@@ -648,16 +648,22 @@ function codeContextScaleLine(fileCount) {
648
648
  ? `scale small (${fileCount} indexed files < ${code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD}); direct reads are usually cheaper for simple lookups`
649
649
  : `scale large (${fileCount} indexed files >= ${code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD}); indexed traversal is useful for impact-style context`;
650
650
  }
651
- function structuralSignature(value) {
652
- const signature = (0, shared_1.oneLine)(String(value ?? ""));
653
- const bodyStart = signature.indexOf("{");
654
- return bodyStart >= 0 ? signature.slice(0, bodyStart).trimEnd() : signature;
655
- }
656
- function codeContextPack(database, query, options = {}) {
657
- const normalized = query.trim();
658
- if (!normalized)
659
- return 'Code context pack: missing query; use --code-context-pack "path-or-symbol-or-route".';
660
- const evidence = (0, evidence_1.collectCodeEvidence)(database, normalized, {
651
+ function codeContextCollectorOptions(fileCount) {
652
+ if (fileCount < code_index_file_policy_1.SMALL_REPO_FILE_THRESHOLD) {
653
+ return {
654
+ edgeLimit: 8,
655
+ fileLimit: 8,
656
+ includeEdgeEvidenceMatches: false,
657
+ includeOwnerCodeowners: false,
658
+ includeRouteEdges: false,
659
+ importLimit: 12,
660
+ ownerSampleLimit: 3,
661
+ routeEdgeLimit: 0,
662
+ routeLimit: 8,
663
+ symbolLimit: 12,
664
+ };
665
+ }
666
+ return {
661
667
  edgeLimit: 30,
662
668
  fileLimit: 12,
663
669
  includeEdgeEvidenceMatches: true,
@@ -668,14 +674,26 @@ function codeContextPack(database, query, options = {}) {
668
674
  routeEdgeLimit: 0,
669
675
  routeLimit: 20,
670
676
  symbolLimit: 20,
671
- });
672
- const staleness = options.staleness ?? codeIndexStaleness(database);
677
+ };
678
+ }
679
+ function structuralSignature(value) {
680
+ const signature = (0, shared_1.oneLine)(String(value ?? ""));
681
+ const bodyStart = signature.indexOf("{");
682
+ return bodyStart >= 0 ? signature.slice(0, bodyStart).trimEnd() : signature;
683
+ }
684
+ function codeContextPack(database, query, options = {}) {
685
+ const normalized = query.trim();
686
+ if (!normalized)
687
+ return 'Code context pack: missing query; use --code-context-pack "path-or-symbol-or-route".';
673
688
  const coverage = (0, reports_1.evidenceCoverage)(database);
689
+ const indexedFileCount = Number(coverage.files ?? 0);
690
+ const evidence = (0, evidence_1.collectCodeEvidence)(database, normalized, codeContextCollectorOptions(indexedFileCount));
691
+ const staleness = options.staleness ?? codeIndexStaleness(database);
674
692
  const staleLabel = staleness.stale
675
693
  ? `STALE ${staleness.changed} changed, ${staleness.added} added, ${staleness.deleted} deleted`
676
694
  : "fresh";
677
695
  const lines = [
678
- `Code context pack "${normalized}": ${evidence.files.length} file matches, ${evidence.symbols.length} symbols, ${evidence.routes.length} routes, ${evidence.imports.length} imports, ${evidence.incomingEdges.length} incoming / ${evidence.outgoingEdges.length} outgoing edges; index ${staleLabel}; ${codeContextScaleLine(Number(coverage.files ?? 0))}.`,
696
+ `Code context pack "${normalized}": ${evidence.files.length} file matches, ${evidence.symbols.length} symbols, ${evidence.routes.length} routes, ${evidence.imports.length} imports, ${evidence.incomingEdges.length} incoming / ${evidence.outgoingEdges.length} outgoing edges; index ${staleLabel}; ${codeContextScaleLine(indexedFileCount)}.`,
679
697
  "Evidence is structural only: paths, lines, signatures, routes, imports, edges, and owners; no source snippets are included.",
680
698
  ];
681
699
  pushBudgetedSection(lines, "Files:", evidence.files, 8, (row) => ` file-match ${String(row.path)} (${String(row.language)}, ${String(row.profile)}, ${Number(row.lines ?? 0)} lines)`);
@@ -5,7 +5,7 @@
5
5
  "architecture": "arm64",
6
6
  "format": "mach-o",
7
7
  "path": "dist/native/darwin-arm64/project-librarian-indexer",
8
- "sha256": "f5c69130f6c0441044141c761f1ccdc7e6ee4d06e327ce0f50d43a7aed76cc95",
8
+ "sha256": "848c29daf166e55672add5febf22fcd2ba261619a37969d9bc87cff06815787c",
9
9
  "size": 2697520,
10
10
  "triple": "darwin-arm64"
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "architecture": "x64",
14
14
  "format": "mach-o",
15
15
  "path": "dist/native/darwin-x64/project-librarian-indexer",
16
- "sha256": "25041398ba2de3ed749c14103d358ed4a7934a05459eb63c6cf5f6ae58c7fb03",
16
+ "sha256": "b419d82b7560defe1d43fc4d99d96b10222f1ead5bf6d7a6acabbae425ee7545",
17
17
  "size": 2770592,
18
18
  "triple": "darwin-x64"
19
19
  },
@@ -21,7 +21,7 @@
21
21
  "architecture": "arm64",
22
22
  "format": "elf",
23
23
  "path": "dist/native/linux-arm64/project-librarian-indexer",
24
- "sha256": "01bf4437fd62446958438ac58e9ca8bef811bdc6d70a63fc1a0ca1b627521e67",
24
+ "sha256": "f3cc884acca4f5863fb6a6545a4a9f9a37a67872bfe780f4b14f0c4a8a667f16",
25
25
  "size": 3065520,
26
26
  "triple": "linux-arm64"
27
27
  },
@@ -29,31 +29,31 @@
29
29
  "architecture": "arm64",
30
30
  "format": "elf",
31
31
  "path": "dist/native/linux-arm64-musl/project-librarian-indexer",
32
- "sha256": "b7287995bea678a6f55bd03761e5b0985d483e875d7594395b81bdb1e90a8dd9",
33
- "size": 3100840,
32
+ "sha256": "80f7440c4d115d05805b63b8118a172ddeb5a374b7154593e5b1d0b804a938ae",
33
+ "size": 3100816,
34
34
  "triple": "linux-arm64-musl"
35
35
  },
36
36
  {
37
37
  "architecture": "x64",
38
38
  "format": "elf",
39
39
  "path": "dist/native/linux-x64/project-librarian-indexer",
40
- "sha256": "5d93bb7576afbdde0acbde7983a874f2666f0d59227524c1eb42cc75b457c5ed",
41
- "size": 3110696,
40
+ "sha256": "3d93408db344567d7de6f477b58fdaf6aba19c943336fb7ab9720e3e9fa68c31",
41
+ "size": 3110832,
42
42
  "triple": "linux-x64"
43
43
  },
44
44
  {
45
45
  "architecture": "x64",
46
46
  "format": "elf",
47
47
  "path": "dist/native/linux-x64-musl/project-librarian-indexer",
48
- "sha256": "518aea0fc53ed7731751db4bb436a0be98f8583659d51aeff769bedb7a7cb8ab",
49
- "size": 3247384,
48
+ "sha256": "0055ec9fefa385d6d637fa1e457308c38f892e0fbdb389db3fcbdf546f0daca8",
49
+ "size": 3247400,
50
50
  "triple": "linux-x64-musl"
51
51
  },
52
52
  {
53
53
  "architecture": "arm64",
54
54
  "format": "pe",
55
55
  "path": "dist/native/win32-arm64/project-librarian-indexer.exe",
56
- "sha256": "e8e5200660a8852d1b7554daf0d16361e29e5599baa5cd406eb3f2266abd98f6",
56
+ "sha256": "8c357be6b1e0c3c13337abccdc0edefc628267b272c8aca30be50c679bffb4af",
57
57
  "size": 2098688,
58
58
  "triple": "win32-arm64"
59
59
  },
@@ -61,7 +61,7 @@
61
61
  "architecture": "x64",
62
62
  "format": "pe",
63
63
  "path": "dist/native/win32-x64/project-librarian-indexer.exe",
64
- "sha256": "dc076bd61271f23ab87b428c7cbfa3af45911c00f7bf7986a2ed9d517e1cfb6f",
64
+ "sha256": "f4f9f6424fef446ece0684760afd8a404135acc007794c3e0f3effccdc8b4801",
65
65
  "size": 2375680,
66
66
  "triple": "win32-x64"
67
67
  }
package/dist/templates.js CHANGED
@@ -199,7 +199,7 @@ exports.startup = `${(0, exports.metadata)("startup-router", "short", "wiki/meta
199
199
  - This project is in an initial planning state unless the canonical wiki says otherwise.
200
200
  - Project truth lives in \`wiki/canonical/\`, future work in \`wiki/roadmaps/\` and \`wiki/plans/\`, project decisions in \`wiki/decisions/\`, and sources in \`wiki/sources/\`.
201
201
  - Wiki operating rules and wiki operating decisions live in \`wiki/meta/\`.
202
- - At session start, read only this file and \`wiki/index.md\` first; read detailed files on demand.
202
+ - At session start, read only this file and \`wiki/index.md\` first; use the index as a route table, open matching detail files directly, and avoid broad repo/wiki search unless no route matches.
203
203
  - Project canonical content language is not fixed by this bootstrap. The LLM should choose the language that best matches the user and project context.
204
204
  - Completed roadmaps/plans are removed after truth/rationale/evidence capture.
205
205
  - Update the wiki in the same turn when project-planning content changes.
@@ -232,7 +232,7 @@ exports.startup = `${(0, exports.metadata)("startup-router", "short", "wiki/meta
232
232
  ## Token Discipline
233
233
 
234
234
  - Codex, Claude Code, Cursor, and Gemini CLI session-start hooks inject only this file and \`wiki/index.md\`.
235
- - Detailed files are selected by the "read when" rules in \`wiki/index.md\`.
235
+ - Detailed files are selected by \`wiki/index.md\`; use broad wiki search only when no route matches or evidence conflicts.
236
236
  - Long decision history is not injected wholesale; read only relevant Decision Packs or ADRs.
237
237
  `;
238
238
  exports.index = `${(0, exports.metadata)("wiki-router", "short", "wiki/meta/wiki-ops-v1-decisions.md", "wiki page added, moved, removed, or routing changes")}
@@ -240,7 +240,7 @@ exports.index = `${(0, exports.metadata)("wiki-router", "short", "wiki/meta/wiki
240
240
 
241
241
  ## How To Use This Index
242
242
 
243
- This file is a router, not a file to expand into every answer. Read only the files that are relevant to the current question.
243
+ This is a route table, not a page inventory. Open the matching route first; use broad wiki search or file listing only when no route matches or evidence conflicts.
244
244
 
245
245
  ## Language Policy
246
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-librarian",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Create and maintain compact project context for humans and LLM coding agents.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",