project-librarian 0.4.0 → 0.4.1

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.
@@ -43,6 +43,7 @@ Options:
43
43
  --review-migration Sync unit coverage and compatible inbox statuses into migration review files.
44
44
  --no-git-config Install hook files without changing git core.hooksPath.
45
45
  --code-index Build the disposable .project-wiki code evidence index.
46
+ --code-index-health Inspect code evidence cache compatibility and print rebuild guidance without writing.
46
47
  --acknowledge-small-repo With --code-index, proceed below the small-repo scale gate after its cost warning.
47
48
  --incremental With --code-index, require an existing compatible index and update only changes.
48
49
  --code-index-full With --code-index, force a full rebuild even when incremental update is possible.
@@ -68,6 +69,20 @@ Commands:
68
69
  function exitAfterStdoutDrain(code) {
69
70
  process.stdout.write("", () => process.exit(code));
70
71
  }
72
+ function activeCodeEvidenceCliModes() {
73
+ const modes = [
74
+ { active: args_1.codeQueryMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeQueryMode() },
75
+ { active: args_1.codeReportMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeReportMode() },
76
+ { active: args_1.codeStatusMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeStatusMode() },
77
+ { active: args_1.codeFilesMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeFilesMode() },
78
+ { active: args_1.codeImpactMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeImpactMode() },
79
+ { active: args_1.codeContextPackMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeContextPackMode() },
80
+ { active: args_1.codeSearchSymbolMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeSearchSymbolMode() },
81
+ { active: args_1.codeIndexHealthMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeIndexHealthMode() },
82
+ { active: args_1.codeIndexMode, drainStdout: false, run: (codeIndexModule) => codeIndexModule.runCodeIndexMode() },
83
+ ];
84
+ return modes.filter((mode) => mode.active);
85
+ }
71
86
  if (args_1.helpMode) {
72
87
  printUsage();
73
88
  process.exit(0);
@@ -147,50 +162,20 @@ else {
147
162
  runInitCommand();
148
163
  }
149
164
  function runInitCommand() {
150
- const activeCodeModes = [args_1.codeQueryMode, args_1.codeReportMode, args_1.codeStatusMode, args_1.codeFilesMode, args_1.codeImpactMode, args_1.codeContextPackMode, args_1.codeSearchSymbolMode, args_1.codeIndexMode].filter(Boolean).length;
151
- if (activeCodeModes > 1) {
152
- console.error("Use one code evidence mode at a time: --code-index, --code-query, --code-report, --code-status, --code-files, --code-impact, --code-context-pack, or --code-search-symbol.");
165
+ const activeCodeModes = activeCodeEvidenceCliModes();
166
+ if (activeCodeModes.length > 1) {
167
+ console.error("Use one code evidence mode at a time: --code-index, --code-index-health, --code-query, --code-report, --code-status, --code-files, --code-impact, --code-context-pack, or --code-search-symbol.");
153
168
  process.exit(1);
154
169
  }
155
- if (args_1.codeQueryMode) {
156
- codeIndex().runCodeQueryMode();
157
- exitAfterStdoutDrain(0);
158
- return;
159
- }
160
- if (args_1.codeReportMode) {
161
- codeIndex().runCodeReportMode();
162
- exitAfterStdoutDrain(0);
163
- return;
164
- }
165
- if (args_1.codeStatusMode) {
166
- codeIndex().runCodeStatusMode();
167
- exitAfterStdoutDrain(0);
168
- return;
169
- }
170
- if (args_1.codeFilesMode) {
171
- codeIndex().runCodeFilesMode();
172
- exitAfterStdoutDrain(0);
173
- return;
174
- }
175
- if (args_1.codeImpactMode) {
176
- codeIndex().runCodeImpactMode();
177
- exitAfterStdoutDrain(0);
178
- return;
179
- }
180
- if (args_1.codeContextPackMode) {
181
- codeIndex().runCodeContextPackMode();
182
- exitAfterStdoutDrain(0);
183
- return;
184
- }
185
- if (args_1.codeSearchSymbolMode) {
186
- codeIndex().runCodeSearchSymbolMode();
187
- exitAfterStdoutDrain(0);
170
+ const activeCodeMode = activeCodeModes[0];
171
+ if (activeCodeMode) {
172
+ activeCodeMode.run(codeIndex());
173
+ if (activeCodeMode.drainStdout)
174
+ exitAfterStdoutDrain(0);
175
+ else
176
+ process.exit(0);
188
177
  return;
189
178
  }
190
- if (args_1.codeIndexMode) {
191
- codeIndex().runCodeIndexMode();
192
- process.exit(0);
193
- }
194
179
  if (args_1.wikiImpactMode) {
195
180
  (0, modes_1.runWikiImpactMode)();
196
181
  exitAfterStdoutDrain(0);
@@ -258,6 +243,10 @@ function runInitCommand() {
258
243
  (0, modes_1.runLintMode)();
259
244
  process.exit(0);
260
245
  }
246
+ if (args_1.refreshIndexMode && !args_1.migrateMode && !args_1.glossaryMode && !args_1.captureInboxMode) {
247
+ runRefreshIndexOnlyMode();
248
+ process.exit(0);
249
+ }
261
250
  const migrationState = args_1.migrateMode ? (0, migration_1.prepareMigrationMode)() : null;
262
251
  const results = [];
263
252
  if (migrationState)
@@ -366,3 +355,12 @@ function runInitCommand() {
366
355
  console.log(`${String(status).padEnd(7)} ${relativePath}`);
367
356
  }
368
357
  }
358
+ function runRefreshIndexOnlyMode() {
359
+ const results = [];
360
+ results.push(["wiki/index.md", (0, workspace_1.writeStarter)("wiki/index.md", templates_1.index)]);
361
+ results.push(["wiki/index.md auto-discovered pages", (0, workspace_1.upsertMarkedSection)("wiki/index.md", "<!-- PROJECT-WIKI-AUTO-INDEX:START -->", "<!-- PROJECT-WIKI-AUTO-INDEX:END -->", (0, modes_1.buildRefreshIndexBlock)())]);
362
+ console.log("Project Librarian refresh-index complete.");
363
+ for (const [relativePath, status] of results) {
364
+ console.log(`${String(status).padEnd(7)} ${relativePath}`);
365
+ }
366
+ }
@@ -58,6 +58,7 @@ const fs = __importStar(require("node:fs"));
58
58
  const path = __importStar(require("node:path"));
59
59
  const code_index_1 = require("./code-index");
60
60
  const code_index_file_policy_1 = require("./code-index-file-policy");
61
+ const schema_1 = require("./code-index/schema");
61
62
  const workspace_1 = require("./workspace");
62
63
  // Pinned MCP protocol version. This is the spec revision this server is written
63
64
  // against; one constant so the supported version is auditable in a single place.
@@ -366,8 +367,8 @@ function ownershipAnswer(filePath) {
366
367
  lines.push(`Workspace owner: ${info.owner} (source: ${info.owner_source})${info.codeowners ? `; codeowners ${info.codeowners}` : ""}.`);
367
368
  return lines.join("\n");
368
369
  }
369
- function impactAnswer(database, term) {
370
- const impact = (0, code_index_1.codeImpact)(database, term);
370
+ function impactAnswer(database, term, context) {
371
+ const impact = (0, code_index_1.codeImpact)(database, term, { staleness: context.staleness });
371
372
  const matches = (impact.matches ?? {});
372
373
  const edges = (impact.edges ?? {});
373
374
  const owners = asRows(impact.impacted_owners);
@@ -483,7 +484,7 @@ const TOOLS = [
483
484
  properties: { term: { type: "string", description: "Path, symbol, route, module, or concept to build a compact code context pack for." } },
484
485
  required: ["term"],
485
486
  },
486
- run: (database, args) => (0, code_index_1.codeContextPack)(database, requireStringArg(args, "term")),
487
+ run: (database, args, context) => (0, code_index_1.codeContextPack)(database, requireStringArg(args, "term"), { staleness: context.staleness }),
487
488
  },
488
489
  {
489
490
  name: "code_impact",
@@ -493,7 +494,7 @@ const TOOLS = [
493
494
  properties: { term: { type: "string", description: "File path, symbol name, route, or module to trace." } },
494
495
  required: ["term"],
495
496
  },
496
- run: (database, args) => impactAnswer(database, requireStringArg(args, "term")),
497
+ run: (database, args, context) => impactAnswer(database, requireStringArg(args, "term"), context),
497
498
  },
498
499
  {
499
500
  name: "code_ownership",
@@ -537,6 +538,59 @@ const TOOLS = [
537
538
  },
538
539
  ];
539
540
  const TOOLS_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
541
+ let cachedStaleness = null;
542
+ function statFingerprint(relativePath) {
543
+ const absolutePath = path.join(workspace_1.root, relativePath);
544
+ try {
545
+ const stat = fs.statSync(absolutePath);
546
+ if (!stat.isFile())
547
+ return `${relativePath}:not-file`;
548
+ return [
549
+ relativePath,
550
+ stat.size,
551
+ stat.mtimeMs,
552
+ stat.ctimeMs,
553
+ ].join(":");
554
+ }
555
+ catch {
556
+ return `${relativePath}:missing`;
557
+ }
558
+ }
559
+ function codeEvidenceFreshnessCacheKey(opened) {
560
+ try {
561
+ const database = opened.database;
562
+ const scopes = (0, schema_1.indexedScopes)(database);
563
+ const parserMode = (0, schema_1.indexedParserMode)(database);
564
+ const schemaVersion = (0, schema_1.readMetaValue)(database, "schema_version");
565
+ const updatedAt = (0, schema_1.readMetaValue)(database, "updated_at");
566
+ const currentFiles = (0, code_index_file_policy_1.discoverCodeFiles)(scopes.length > 0 ? scopes : ["."]);
567
+ const currentFingerprint = currentFiles.map(statFingerprint).join("\n");
568
+ const indexedFingerprint = database.prepare("SELECT path, hash, mtime_ms, size FROM files ORDER BY path")
569
+ .all()
570
+ .map((row) => `${String(row.path)}:${String(row.hash)}:${Number(row.mtime_ms)}:${Number(row.size)}`)
571
+ .join("\n");
572
+ return [
573
+ opened.relativePath,
574
+ schemaVersion,
575
+ updatedAt,
576
+ parserMode,
577
+ scopes.join("\0"),
578
+ currentFingerprint,
579
+ indexedFingerprint,
580
+ ].join("\n---\n");
581
+ }
582
+ catch {
583
+ return null;
584
+ }
585
+ }
586
+ function codeIndexStalenessForToolCall(opened) {
587
+ const cacheKey = codeEvidenceFreshnessCacheKey(opened);
588
+ if (cacheKey && cachedStaleness?.cacheKey === cacheKey)
589
+ return cachedStaleness.staleness;
590
+ const staleness = (0, code_index_1.codeIndexStaleness)(opened.database);
591
+ cachedStaleness = cacheKey ? { cacheKey, staleness } : null;
592
+ return staleness;
593
+ }
540
594
  // ---------------------------------------------------------------------------
541
595
  // tools/call dispatch
542
596
  // ---------------------------------------------------------------------------
@@ -559,10 +613,10 @@ function callTool(name, rawArgs) {
559
613
  throw error;
560
614
  }
561
615
  try {
562
- const staleness = (0, code_index_1.codeIndexStaleness)(opened.database);
616
+ const staleness = codeIndexStalenessForToolCall(opened);
563
617
  const body = name === "code_status"
564
618
  ? statusAnswer(opened.database, opened.relativePath, staleness)
565
- : TOOLS_BY_NAME.get(name).run(opened.database, args);
619
+ : TOOLS_BY_NAME.get(name).run(opened.database, args, { staleness });
566
620
  return toolResultContent(finalizeAnswer(body, staleness));
567
621
  }
568
622
  catch (error) {
package/dist/migration.js CHANGED
@@ -37,6 +37,7 @@ exports.generatedMigrationInboxFiles = void 0;
37
37
  exports.classifyMarkdown = classifyMarkdown;
38
38
  exports.formOnlyMigrationDocumentReason = formOnlyMigrationDocumentReason;
39
39
  exports.extractMigrationUnits = extractMigrationUnits;
40
+ exports.loadMigrationUnitContext = loadMigrationUnitContext;
40
41
  exports.collectMigrationCoverageDiagnostics = collectMigrationCoverageDiagnostics;
41
42
  exports.collectMigrationUnitMapDiagnostics = collectMigrationUnitMapDiagnostics;
42
43
  exports.collectMigrationSplitPlanDiagnostics = collectMigrationSplitPlanDiagnostics;
@@ -321,6 +322,9 @@ function expectedMigrationUnits() {
321
322
  .flatMap((legacyRoot) => (0, wiki_files_1.walkMarkdownFiles)((0, workspace_1.abs)(legacyRoot), [], (0, workspace_1.abs)(legacyRoot)))
322
323
  .flatMap((file) => extractMigrationUnits(file.basePath, (0, workspace_1.read)(file.path)));
323
324
  }
325
+ function loadMigrationUnitContext() {
326
+ return { units: expectedMigrationUnits() };
327
+ }
324
328
  function isMigrationConfidence(value) {
325
329
  return ["high", "medium", "low"].includes(value);
326
330
  }
@@ -353,8 +357,8 @@ function isReviewedCoverageRetarget(cells) {
353
357
  return note.includes("reviewed low-confidence content; retargeted")
354
358
  || reason.includes("reviewed source context; taxonomy target assigned");
355
359
  }
356
- function collectMigrationCoverageDiagnostics() {
357
- const units = expectedMigrationUnits();
360
+ function collectMigrationCoverageDiagnostics(context = loadMigrationUnitContext()) {
361
+ const units = context.units;
358
362
  if (units.length === 0)
359
363
  return [];
360
364
  if (!(0, workspace_1.exists)("wiki/migration/coverage.md")) {
@@ -417,8 +421,8 @@ function collectMigrationCoverageDiagnostics() {
417
421
  }
418
422
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
419
423
  }
420
- function collectMigrationUnitMapDiagnostics() {
421
- const units = expectedMigrationUnits();
424
+ function collectMigrationUnitMapDiagnostics(context = loadMigrationUnitContext()) {
425
+ const units = context.units;
422
426
  if (units.length === 0)
423
427
  return [];
424
428
  const file = "wiki/migration/unit-map.md";
@@ -485,8 +489,8 @@ function collectMigrationUnitMapDiagnostics() {
485
489
  }
486
490
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
487
491
  }
488
- function collectMigrationSplitPlanDiagnostics() {
489
- const units = expectedMigrationUnits();
492
+ function collectMigrationSplitPlanDiagnostics(context = loadMigrationUnitContext()) {
493
+ const units = context.units;
490
494
  if (units.length === 0)
491
495
  return [];
492
496
  const file = "wiki/migration/split-plan.md";
package/dist/modes.js CHANGED
@@ -63,6 +63,8 @@ const templates_1 = require("./templates");
63
63
  const migration_1 = require("./migration");
64
64
  const wiki_files_1 = require("./wiki-files");
65
65
  const wiki_graph_1 = require("./wiki-graph");
66
+ const wiki_corpus_1 = require("./wiki-corpus");
67
+ const wiki_diagnostics_1 = require("./wiki-diagnostics");
66
68
  const scopedAutoIndexThreshold = 40;
67
69
  const scopedAutoIndexMarker = "<!-- PROJECT-WIKI-SCOPED-AUTO-INDEX -->";
68
70
  function isScopedAutoIndex(file) {
@@ -187,6 +189,27 @@ function scoreQueryBlock(block, terms) {
187
189
  const occurrences = termOccurrences(`${block.headingPath.join(" ")}\n${block.text}`, terms);
188
190
  return occurrences > 0 ? occurrences + blockKindBoost(block) : 0;
189
191
  }
192
+ function hasMigrationQueryIntent(terms) {
193
+ return terms.some((term) => /^(migrat|legacy|coverage|unit|ledger|review)/.test(term));
194
+ }
195
+ function isMigrationSurface(file, meta) {
196
+ return file.startsWith("wiki/migration/")
197
+ || /(?:^|-)migration(?:-|$)/.test(file)
198
+ || /migration|legacy/.test(meta.scope);
199
+ }
200
+ function querySurfaceScore(file, meta, rawScore, terms) {
201
+ if (rawScore <= 0)
202
+ return 0;
203
+ if (isMigrationSurface(file, meta) && !hasMigrationQueryIntent(terms)) {
204
+ return Math.max(1, Math.floor(rawScore * 0.25) - 20);
205
+ }
206
+ let score = rawScore;
207
+ if (file.startsWith("wiki/canonical/") && meta.status === "active")
208
+ score += 12;
209
+ else if (meta.status === "active")
210
+ score += 2;
211
+ return score;
212
+ }
190
213
  // Answer-shaped query output (2026-06-12 method-transfer decision): first line is
191
214
  // the answer, each result carries the page's TL;DR first bullet and the strongest
192
215
  // matching block so the agent can pick a page without opening it, and the whole
@@ -197,8 +220,9 @@ function runQueryMode() {
197
220
  process.exit(1);
198
221
  }
199
222
  const terms = args_1.queryTerm.toLowerCase().split(/\s+/).filter(Boolean);
200
- const pages = (0, wiki_files_1.wikiMarkdownFiles)().map((file) => ({ file, text: (0, workspace_1.read)(file) }));
201
- const graph = (0, wiki_graph_1.buildWikiGraph)(pages);
223
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
224
+ const pages = corpus.pages;
225
+ const graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus);
202
226
  const routerDepths = (0, wiki_graph_1.wikiRouterDepths)(graph);
203
227
  const matches = pages.map(({ file, text }) => {
204
228
  const body = (0, workspace_1.stripMetadataHeader)(text);
@@ -212,7 +236,7 @@ function runQueryMode() {
212
236
  .sort((left, right) => right.score - left.score || left.block.line - right.block.line || left.block.id.localeCompare(right.block.id));
213
237
  const topBlock = blocks[0]?.block;
214
238
  const blockScore = blocks.slice(0, 5).reduce((sum, item) => sum + item.score, 0);
215
- const score = metadataScore + blockScore;
239
+ const score = querySurfaceScore(file, meta, metadataScore + blockScore, terms);
216
240
  return {
217
241
  blockKind: topBlock?.kind ?? "",
218
242
  blockLine: topBlock?.line ?? 0,
@@ -262,8 +286,8 @@ function runWikiImpactMode() {
262
286
  console.error("missing wiki impact target: use --wiki-impact \"page-or-term\"");
263
287
  process.exit(1);
264
288
  }
265
- const pages = (0, wiki_files_1.wikiMarkdownFiles)().map((file) => ({ file, text: (0, workspace_1.read)(file) }));
266
- console.log((0, wiki_graph_1.wikiImpactAnswer)(pages, args_1.wikiImpactTarget.trim()));
289
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
290
+ console.log((0, wiki_graph_1.wikiImpactAnswer)(corpus.pages, args_1.wikiImpactTarget.trim(), (0, wiki_corpus_1.wikiCorpusGraph)(corpus)));
267
291
  }
268
292
  function projectCandidatesContent() {
269
293
  return `${(0, templates_1.metadata)("inbox", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "candidates are adopted, rejected, or stale")}
@@ -533,11 +557,11 @@ function printDiagnostics(title, diagnostics, checked) {
533
557
  console.log(`passed: ${checked} wiki markdown files checked, ${warnings} warnings`);
534
558
  return true;
535
559
  }
536
- function collectLinkDiagnostics() {
560
+ function collectLinkDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
537
561
  const diagnostics = [];
538
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
539
- const fileSet = new Set(files);
540
- const graph = (0, wiki_graph_1.buildWikiGraph)(files.map((file) => ({ file, text: (0, workspace_1.read)(file) })));
562
+ const files = corpus.files;
563
+ const fileSet = corpus.fileSet;
564
+ const graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus);
541
565
  for (const link of graph.links) {
542
566
  if (!fileSet.has(link.normalizedTarget)) {
543
567
  diagnostics.push({
@@ -643,10 +667,10 @@ function shouldGuardAgainstLegacyReference(file) {
643
667
  return false;
644
668
  return !file.endsWith("/migration-inbox.md");
645
669
  }
646
- function migrationLegacyReferenceDiagnostics(files) {
670
+ function migrationLegacyReferenceDiagnostics(files, corpus) {
647
671
  return files
648
672
  .filter(shouldGuardAgainstLegacyReference)
649
- .filter((file) => /\bwiki_legacy(?:_|\b|\/)/.test((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(file))))
673
+ .filter((file) => /\bwiki_legacy(?:_|\b|\/)/.test((0, workspace_1.stripMetadataHeader)((0, wiki_corpus_1.wikiCorpusText)(corpus, file))))
650
674
  .map((file) => ({
651
675
  code: "migration-legacy-reference",
652
676
  severity: "error",
@@ -654,12 +678,12 @@ function migrationLegacyReferenceDiagnostics(files) {
654
678
  message: "new project truth must not link to or cite wiki_legacy*; migrate the meaning or keep unresolved material in migration inboxes",
655
679
  }));
656
680
  }
657
- function collectQualityDiagnostics() {
681
+ function collectQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
658
682
  const diagnostics = [];
659
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
683
+ const files = corpus.files;
660
684
  const titles = new Map();
661
685
  for (const file of files) {
662
- const text = (0, workspace_1.read)(file);
686
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
663
687
  const body = (0, workspace_1.stripMetadataHeader)(text);
664
688
  const title = (0, wiki_files_1.wikiTitleForFile)(file, text).toLowerCase();
665
689
  titles.set(title, [...(titles.get(title) ?? []), file]);
@@ -671,8 +695,9 @@ function collectQualityDiagnostics() {
671
695
  if (tldrExpected && !/##\s+TL;DR/.test(body)) {
672
696
  diagnostics.push({ code: "missing-tldr", severity: "warn", file, message: "add a compact TL;DR near the top" });
673
697
  }
674
- if (status === "active" && updated && updated < workspace_1.today && /project-canonical|project-decisions|source-summary|wiki-meta/.test(scope)) {
675
- diagnostics.push({ code: "stale-review", severity: "warn", file, message: `updated before today: ${updated}` });
698
+ const reviewAge = updated ? (0, wiki_diagnostics_1.staleReviewAge)(updated, workspace_1.today) : null;
699
+ if (status === "active" && reviewAge !== null && /project-canonical|project-decisions|source-summary|wiki-meta/.test(scope)) {
700
+ diagnostics.push({ code: "stale-review", severity: "warn", file, message: `updated ${reviewAge} days ago: ${updated}` });
676
701
  }
677
702
  if (status === "active" && !/inbox|migration-inbox/.test(scope) && /proposed|undecided|TODO|TBD|미정/i.test(body)) {
678
703
  diagnostics.push({ code: "unresolved-signal", severity: "warn", file, message: "contains pending/proposed/undecided language" });
@@ -700,9 +725,8 @@ function collectQualityDiagnostics() {
700
725
  }
701
726
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
702
727
  }
703
- function collectMigrationQualityDiagnostics() {
704
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
705
- return migrationLegacyReferenceDiagnostics(files).sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
728
+ function collectMigrationQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
729
+ return migrationLegacyReferenceDiagnostics(corpus.files, corpus).sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
706
730
  }
707
731
  function collectMigrationLintDiagnostics() {
708
732
  if (legacyWikiRoots().length === 0)
@@ -728,23 +752,27 @@ function collectMigrationLintDiagnostics() {
728
752
  file,
729
753
  message: "migration review files are missing; run --migrate or keep migration diagnostics out of normal doctor",
730
754
  }));
731
- diagnostics.push(...(0, migration_1.collectMigrationCoverageDiagnostics)());
732
- diagnostics.push(...(0, migration_1.collectMigrationUnitMapDiagnostics)());
733
- diagnostics.push(...(0, migration_1.collectMigrationSplitPlanDiagnostics)());
755
+ const migrationContext = (0, migration_1.loadMigrationUnitContext)();
756
+ diagnostics.push(...(0, migration_1.collectMigrationCoverageDiagnostics)(migrationContext));
757
+ diagnostics.push(...(0, migration_1.collectMigrationUnitMapDiagnostics)(migrationContext));
758
+ diagnostics.push(...(0, migration_1.collectMigrationSplitPlanDiagnostics)(migrationContext));
734
759
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
735
760
  }
736
761
  function runLinkCheckMode() {
737
- const ok = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
762
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
763
+ const ok = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(corpus), corpus.files.length);
738
764
  if (!ok)
739
765
  process.exit(1);
740
766
  }
741
767
  function runQualityCheckMode() {
742
- const ok = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
768
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
769
+ const ok = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(corpus), corpus.files.length);
743
770
  if (!ok)
744
771
  process.exit(1);
745
772
  }
746
773
  function runMigrationQualityCheckMode() {
747
- const ok = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
774
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
775
+ const ok = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(corpus), corpus.files.length);
748
776
  if (!ok)
749
777
  process.exit(1);
750
778
  }
@@ -754,8 +782,9 @@ function runMigrationLintMode() {
754
782
  process.exit(1);
755
783
  }
756
784
  function runMigrationDoctorMode() {
757
- const lintOk = printDiagnostics("Project wiki migration lint", collectMigrationLintDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
758
- const qualityOk = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
785
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
786
+ const lintOk = printDiagnostics("Project wiki migration lint", collectMigrationLintDiagnostics(), corpus.files.length);
787
+ const qualityOk = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(corpus), corpus.files.length);
759
788
  if (!lintOk || !qualityOk)
760
789
  process.exit(1);
761
790
  }
@@ -794,11 +823,12 @@ function extractSectionBody(markdown, headingText) {
794
823
  const nextHeading = /^##\s/m.exec(afterHeading);
795
824
  return nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading;
796
825
  }
797
- function collectRouterTruthDiagnostics() {
826
+ function collectRouterTruthDiagnostics(corpus) {
798
827
  const logPath = "wiki/decisions/log.md";
799
- if (!(0, workspace_1.exists)(logPath))
828
+ const hasLog = corpus ? corpus.fileSet.has(logPath) : (0, workspace_1.exists)(logPath);
829
+ if (!hasLog)
800
830
  return [];
801
- const logHasDatedEntry = /\b\d{4}-\d{2}-\d{2}\b/.test((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(logPath)));
831
+ const logHasDatedEntry = /\b\d{4}-\d{2}-\d{2}\b/.test((0, workspace_1.stripMetadataHeader)((0, wiki_corpus_1.wikiCorpusText)(corpus, logPath)));
802
832
  if (!logHasDatedEntry)
803
833
  return [];
804
834
  const diagnostics = [];
@@ -810,9 +840,10 @@ function collectRouterTruthDiagnostics() {
810
840
  ["wiki/decisions/recent.md", "Decisions", "Decisions"],
811
841
  ];
812
842
  for (const [file, heading, surface] of routers) {
813
- if (!(0, workspace_1.exists)(file))
843
+ const hasFile = corpus ? corpus.fileSet.has(file) : (0, workspace_1.exists)(file);
844
+ if (!hasFile)
814
845
  continue;
815
- const section = extractSectionBody((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(file)), heading);
846
+ const section = extractSectionBody((0, workspace_1.stripMetadataHeader)((0, wiki_corpus_1.wikiCorpusText)(corpus, file)), heading);
816
847
  if (section === "")
817
848
  continue; // section absent — skip rather than false-positive
818
849
  if (ROUTER_TRUTH_NONE_YET_REGEX.test(section)) {
@@ -837,11 +868,11 @@ function runDoctorMode(fix) {
837
868
  console.log("skipped wiki/index.md auto-discovered pages: missing wiki/index.md");
838
869
  }
839
870
  }
840
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
841
- const linkOk = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(), files.length);
842
- const qualityOk = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(), files.length);
843
- const routerTruthOk = printDiagnostics("Project wiki router-truth check", collectRouterTruthDiagnostics(), files.length);
844
- runLintMode();
871
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
872
+ const linkOk = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(corpus), corpus.files.length);
873
+ const qualityOk = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(corpus), corpus.files.length);
874
+ const routerTruthOk = printDiagnostics("Project wiki router-truth check", collectRouterTruthDiagnostics(corpus), corpus.files.length);
875
+ runLintMode(corpus);
845
876
  if (!linkOk || !qualityOk || !routerTruthOk)
846
877
  process.exit(1);
847
878
  }
@@ -872,7 +903,7 @@ function activeLintAgentSurfaces() {
872
903
  }
873
904
  return active;
874
905
  }
875
- function runLintMode() {
906
+ function runLintMode(corpus) {
876
907
  const errors = [];
877
908
  const warnings = [];
878
909
  const activeAgents = activeLintAgentSurfaces();
@@ -884,10 +915,10 @@ function runLintMode() {
884
915
  if (!(0, workspace_1.exists)(file))
885
916
  errors.push(`missing required file: ${file}`);
886
917
  }
887
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
918
+ const files = corpus?.files ?? (0, wiki_files_1.wikiMarkdownFiles)();
888
919
  const requiredMetadataKeys = ["status", "updated", "scope", "read_budget", "decision_ref", "review_trigger"];
889
920
  for (const file of files) {
890
- const text = (0, workspace_1.read)(file);
921
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
891
922
  if (!(0, workspace_1.hasMetadataHeader)(text)) {
892
923
  errors.push(`missing metadata header: ${file}`);
893
924
  continue;
@@ -897,8 +928,8 @@ function runLintMode() {
897
928
  errors.push(`missing metadata key ${key}: ${file}`);
898
929
  }
899
930
  }
900
- const startupLength = (0, workspace_1.exists)("wiki/startup.md") ? (0, workspace_1.read)("wiki/startup.md").length : 0;
901
- const indexLength = (0, workspace_1.exists)("wiki/index.md") ? (0, workspace_1.read)("wiki/index.md").length : 0;
931
+ const startupLength = (0, workspace_1.exists)("wiki/startup.md") ? (0, wiki_corpus_1.wikiCorpusText)(corpus, "wiki/startup.md").length : 0;
932
+ const indexLength = (0, workspace_1.exists)("wiki/index.md") ? (0, wiki_corpus_1.wikiCorpusText)(corpus, "wiki/index.md").length : 0;
902
933
  if (startupLength > 3500)
903
934
  warnings.push(`startup exceeds hook budget: ${startupLength}/3500 chars`);
904
935
  if (indexLength > 4500)
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commonIgnoredDirectories = void 0;
4
+ exports.ignoredDirectorySet = ignoredDirectorySet;
5
+ exports.pathContainsIgnoredDirectory = pathContainsIgnoredDirectory;
6
+ const workspace_1 = require("./workspace");
7
+ exports.commonIgnoredDirectories = [
8
+ ".git",
9
+ ".codex",
10
+ ".claude",
11
+ ".cursor",
12
+ ".gemini",
13
+ "node_modules",
14
+ ".next",
15
+ "dist",
16
+ "build",
17
+ "coverage",
18
+ "vendor",
19
+ "tmp",
20
+ "temp",
21
+ ];
22
+ function ignoredDirectorySet(extraDirectories = []) {
23
+ return new Set([...exports.commonIgnoredDirectories, ...extraDirectories]);
24
+ }
25
+ function pathContainsIgnoredDirectory(relativePath, ignoredDirectories) {
26
+ return (0, workspace_1.normalizePath)(relativePath)
27
+ .split("/")
28
+ .filter(Boolean)
29
+ .some((part) => ignoredDirectories.has(part));
30
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadWikiCorpus = loadWikiCorpus;
4
+ exports.wikiCorpusGraph = wikiCorpusGraph;
5
+ exports.wikiCorpusText = wikiCorpusText;
6
+ const wiki_graph_1 = require("./wiki-graph");
7
+ const workspace_1 = require("./workspace");
8
+ const wiki_files_1 = require("./wiki-files");
9
+ function loadWikiCorpus() {
10
+ const files = (0, wiki_files_1.wikiMarkdownFiles)();
11
+ const pages = files.map((file) => ({ file, text: (0, workspace_1.read)(file) }));
12
+ return {
13
+ files,
14
+ fileSet: new Set(files),
15
+ pages,
16
+ textByFile: new Map(pages.map((page) => [page.file, page.text])),
17
+ };
18
+ }
19
+ function wikiCorpusGraph(corpus) {
20
+ corpus.graph ??= (0, wiki_graph_1.buildWikiGraph)(corpus.pages);
21
+ return corpus.graph;
22
+ }
23
+ function wikiCorpusText(corpus, file) {
24
+ return corpus?.textByFile.get(file) ?? (0, workspace_1.read)(file);
25
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.staleReviewAgeDays = void 0;
4
+ exports.staleReviewAge = staleReviewAge;
5
+ exports.staleReviewAgeDays = 30;
6
+ function dateOnlyMillis(value) {
7
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
8
+ return null;
9
+ const millis = Date.parse(`${value}T00:00:00Z`);
10
+ return Number.isNaN(millis) ? null : millis;
11
+ }
12
+ function staleReviewAge(updated, currentDate) {
13
+ const updatedMillis = dateOnlyMillis(updated);
14
+ const currentMillis = dateOnlyMillis(currentDate);
15
+ if (updatedMillis === null || currentMillis === null)
16
+ return null;
17
+ const ageDays = Math.floor((currentMillis - updatedMillis) / 86_400_000);
18
+ return ageDays > exports.staleReviewAgeDays ? ageDays : null;
19
+ }
@@ -54,6 +54,7 @@ exports.firstTldrBullet = firstTldrBullet;
54
54
  exports.canonicalBodyForLint = canonicalBodyForLint;
55
55
  const fs = __importStar(require("node:fs"));
56
56
  const path = __importStar(require("node:path"));
57
+ const path_ignore_policy_1 = require("./path-ignore-policy");
57
58
  const workspace_1 = require("./workspace");
58
59
  exports.standardWikiFiles = new Set([
59
60
  "AGENTS.md",
@@ -99,7 +100,7 @@ exports.standardWikiFiles = new Set([
99
100
  "tools/project-librarian/agents/openai.yaml",
100
101
  "tools/project-librarian/dist/init-project-wiki.js",
101
102
  ]);
102
- exports.ignoredDirs = new Set([".git", ".codex", ".claude", ".cursor", ".gemini", "node_modules", ".next", "dist", "build", "coverage", "vendor", "tmp", "temp"]);
103
+ exports.ignoredDirs = (0, path_ignore_policy_1.ignoredDirectorySet)();
103
104
  function walkMarkdownFiles(dir = workspace_1.root, acc = [], baseDir = workspace_1.root) {
104
105
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
105
106
  const fullPath = path.join(dir, entry.name);
@@ -124,8 +124,7 @@ function wikiQueryGraphEvidence(graph, file, depths = wikiRouterDepths(graph), l
124
124
  parts.push(`decision_ref-by ${incomingRefs.length}: ${sampled(incomingRefs, listCap)}`);
125
125
  return parts.join("; ");
126
126
  }
127
- function wikiImpactAnswer(pages, term) {
128
- const graph = buildWikiGraph(pages);
127
+ function wikiImpactAnswer(pages, term, graph = buildWikiGraph(pages)) {
129
128
  const depths = wikiRouterDepths(graph);
130
129
  const textByFile = new Map(pages.map((page) => [page.file, page.text]));
131
130
  const lowered = term.toLowerCase();