project-librarian 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/modes.js CHANGED
@@ -57,13 +57,17 @@ const fs = __importStar(require("node:fs"));
57
57
  const childProcess = __importStar(require("node:child_process"));
58
58
  const os = __importStar(require("node:os"));
59
59
  const path = __importStar(require("node:path"));
60
+ const agent_surfaces_1 = require("./agent-surfaces");
60
61
  const args_1 = require("./args");
61
62
  const workspace_1 = require("./workspace");
62
63
  const templates_1 = require("./templates");
63
64
  const migration_1 = require("./migration");
64
65
  const wiki_files_1 = require("./wiki-files");
65
66
  const wiki_graph_1 = require("./wiki-graph");
67
+ const wiki_corpus_1 = require("./wiki-corpus");
68
+ const wiki_diagnostics_1 = require("./wiki-diagnostics");
66
69
  const scopedAutoIndexThreshold = 40;
70
+ const scopedAutoIndexCharLimit = 7600;
67
71
  const scopedAutoIndexMarker = "<!-- PROJECT-WIKI-SCOPED-AUTO-INDEX -->";
68
72
  function isScopedAutoIndex(file) {
69
73
  return /^wiki\/indexes\/auto-[a-z0-9-]+\.md$/.test(file);
@@ -87,16 +91,18 @@ function routeAreaForWikiFile(file) {
87
91
  return slugPart(directory);
88
92
  return "misc";
89
93
  }
90
- function scopedIndexPath(area) {
91
- return `wiki/indexes/auto-${slugPart(area)}.md`;
94
+ function scopedIndexPath(area, partIndex = 0, partCount = 1) {
95
+ const slug = slugPart(area);
96
+ return partCount <= 1 ? `wiki/indexes/auto-${slug}.md` : `wiki/indexes/auto-${slug}-${partIndex + 1}.md`;
92
97
  }
93
- function scopedIndexContent(area, files) {
98
+ function scopedIndexContent(area, files, partIndex = 0, partCount = 1) {
99
+ const title = partCount <= 1 ? area : `${area} (${partIndex + 1}/${partCount})`;
94
100
  const rows = files.map((file) => {
95
101
  const meta = (0, wiki_files_1.metadataSummary)(file, (0, workspace_1.read)(file));
96
102
  return `| ${(0, wiki_files_1.wikiLinkForFile)(file)} | ${meta.scope} | ${meta.status} | ${meta.budget} |`;
97
103
  }).join("\n");
98
104
  return `${(0, templates_1.metadata)("wiki-router", "medium", "wiki/meta/wiki-ops-v1-decisions.md", "auto-discovered scoped routes change")}${scopedAutoIndexMarker}
99
- # Auto Index: ${area}
105
+ # Auto Index: ${title}
100
106
 
101
107
  ## TL;DR
102
108
 
@@ -108,6 +114,23 @@ function scopedIndexContent(area, files) {
108
114
  ${rows}
109
115
  `;
110
116
  }
117
+ function splitScopedIndexFiles(area, files) {
118
+ const parts = [];
119
+ let current = [];
120
+ for (const file of files) {
121
+ const candidate = [...current, file];
122
+ if (current.length > 0 && scopedIndexContent(area, candidate).length > scopedAutoIndexCharLimit) {
123
+ parts.push(current);
124
+ current = [file];
125
+ }
126
+ else {
127
+ current = candidate;
128
+ }
129
+ }
130
+ if (current.length > 0)
131
+ parts.push(current);
132
+ return parts;
133
+ }
111
134
  function removeStaleScopedAutoIndexes(keepPaths) {
112
135
  if (!(0, workspace_1.exists)("wiki/indexes"))
113
136
  return;
@@ -124,16 +147,23 @@ function syncScopedAutoIndexes(files) {
124
147
  const area = routeAreaForWikiFile(file);
125
148
  groups.set(area, [...(groups.get(area) ?? []), file]);
126
149
  }
127
- const summaries = Array.from(groups.entries()).map(([area, areaFiles]) => ({
128
- area,
129
- count: areaFiles.length,
130
- file: scopedIndexPath(area),
131
- files: areaFiles.sort(),
132
- })).sort((left, right) => right.count - left.count || left.area.localeCompare(right.area));
150
+ const summaries = Array.from(groups.entries()).flatMap(([area, areaFiles]) => {
151
+ const sortedFiles = areaFiles.sort();
152
+ const parts = splitScopedIndexFiles(area, sortedFiles);
153
+ return parts.map((files, partIndex) => ({
154
+ area: parts.length <= 1 ? area : `${area} ${partIndex + 1}`,
155
+ baseArea: area,
156
+ count: files.length,
157
+ file: scopedIndexPath(area, partIndex, parts.length),
158
+ files,
159
+ partIndex,
160
+ partCount: parts.length,
161
+ }));
162
+ }).sort((left, right) => right.count - left.count || left.area.localeCompare(right.area));
133
163
  const keepPaths = new Set(summaries.map((summary) => summary.file));
134
164
  removeStaleScopedAutoIndexes(keepPaths);
135
165
  for (const summary of summaries)
136
- (0, workspace_1.write)(summary.file, scopedIndexContent(summary.area, summary.files));
166
+ (0, workspace_1.write)(summary.file, scopedIndexContent(summary.baseArea, summary.files, summary.partIndex, summary.partCount));
137
167
  return summaries.map(({ area, count, file }) => ({ area, count, file }));
138
168
  }
139
169
  function buildRefreshIndexBlock() {
@@ -147,8 +177,6 @@ function buildRefreshIndexBlock() {
147
177
  return `<!-- PROJECT-WIKI-AUTO-INDEX:START -->
148
178
  ## Auto-Discovered Pages
149
179
 
150
- This block is managed by \`--refresh-index\`. Large route sets are split into scoped generated routers to keep \`wiki/index.md\` within startup-hook budget.
151
-
152
180
  | Scoped Router | Area | Pages |
153
181
  | --- | --- | ---: |
154
182
  ${rows}
@@ -187,6 +215,34 @@ function scoreQueryBlock(block, terms) {
187
215
  const occurrences = termOccurrences(`${block.headingPath.join(" ")}\n${block.text}`, terms);
188
216
  return occurrences > 0 ? occurrences + blockKindBoost(block) : 0;
189
217
  }
218
+ function hasMigrationQueryIntent(terms) {
219
+ return terms.some((term) => /^(migrat|legacy|coverage|unit|ledger|review)/.test(term));
220
+ }
221
+ function hasDeepReferenceQueryIntent(terms) {
222
+ return hasMigrationQueryIntent(terms)
223
+ || terms.some((term) => /^(archive|audit|decision|detail|history|ledger|raw|record|source|trace|verification)/.test(term));
224
+ }
225
+ function isMigrationSurface(file, meta) {
226
+ return file.startsWith("wiki/migration/")
227
+ || /(?:^|-)migration(?:-|$)/.test(file)
228
+ || /migration|legacy/.test(meta.scope);
229
+ }
230
+ function querySurfaceScore(file, meta, rawScore, terms) {
231
+ if (rawScore <= 0)
232
+ return 0;
233
+ if (isMigrationSurface(file, meta) && !hasMigrationQueryIntent(terms)) {
234
+ return Math.max(1, Math.floor(rawScore * 0.25) - 20);
235
+ }
236
+ let score = rawScore;
237
+ if (meta.budget === "on-demand" && !hasDeepReferenceQueryIntent(terms)) {
238
+ score = Math.max(1, Math.min(Math.floor(score * 0.5), 24));
239
+ }
240
+ if (file.startsWith("wiki/canonical/") && meta.status === "active")
241
+ score += 12;
242
+ else if (meta.status === "active")
243
+ score += 2;
244
+ return score;
245
+ }
190
246
  // Answer-shaped query output (2026-06-12 method-transfer decision): first line is
191
247
  // the answer, each result carries the page's TL;DR first bullet and the strongest
192
248
  // matching block so the agent can pick a page without opening it, and the whole
@@ -197,8 +253,9 @@ function runQueryMode() {
197
253
  process.exit(1);
198
254
  }
199
255
  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);
256
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
257
+ const pages = corpus.pages;
258
+ const graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus);
202
259
  const routerDepths = (0, wiki_graph_1.wikiRouterDepths)(graph);
203
260
  const matches = pages.map(({ file, text }) => {
204
261
  const body = (0, workspace_1.stripMetadataHeader)(text);
@@ -212,7 +269,7 @@ function runQueryMode() {
212
269
  .sort((left, right) => right.score - left.score || left.block.line - right.block.line || left.block.id.localeCompare(right.block.id));
213
270
  const topBlock = blocks[0]?.block;
214
271
  const blockScore = blocks.slice(0, 5).reduce((sum, item) => sum + item.score, 0);
215
- const score = metadataScore + blockScore;
272
+ const score = querySurfaceScore(file, meta, metadataScore + blockScore, terms);
216
273
  return {
217
274
  blockKind: topBlock?.kind ?? "",
218
275
  blockLine: topBlock?.line ?? 0,
@@ -262,8 +319,8 @@ function runWikiImpactMode() {
262
319
  console.error("missing wiki impact target: use --wiki-impact \"page-or-term\"");
263
320
  process.exit(1);
264
321
  }
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()));
322
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
323
+ console.log((0, wiki_graph_1.wikiImpactAnswer)(corpus.pages, args_1.wikiImpactTarget.trim(), (0, wiki_corpus_1.wikiCorpusGraph)(corpus)));
267
324
  }
268
325
  function projectCandidatesContent() {
269
326
  return `${(0, templates_1.metadata)("inbox", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "candidates are adopted, rejected, or stale")}
@@ -533,11 +590,11 @@ function printDiagnostics(title, diagnostics, checked) {
533
590
  console.log(`passed: ${checked} wiki markdown files checked, ${warnings} warnings`);
534
591
  return true;
535
592
  }
536
- function collectLinkDiagnostics() {
593
+ function collectLinkDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
537
594
  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) })));
595
+ const files = corpus.files;
596
+ const fileSet = corpus.fileSet;
597
+ const graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus);
541
598
  for (const link of graph.links) {
542
599
  if (!fileSet.has(link.normalizedTarget)) {
543
600
  diagnostics.push({
@@ -643,10 +700,10 @@ function shouldGuardAgainstLegacyReference(file) {
643
700
  return false;
644
701
  return !file.endsWith("/migration-inbox.md");
645
702
  }
646
- function migrationLegacyReferenceDiagnostics(files) {
703
+ function migrationLegacyReferenceDiagnostics(files, corpus) {
647
704
  return files
648
705
  .filter(shouldGuardAgainstLegacyReference)
649
- .filter((file) => /\bwiki_legacy(?:_|\b|\/)/.test((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(file))))
706
+ .filter((file) => /\bwiki_legacy(?:_|\b|\/)/.test((0, workspace_1.stripMetadataHeader)((0, wiki_corpus_1.wikiCorpusText)(corpus, file))))
650
707
  .map((file) => ({
651
708
  code: "migration-legacy-reference",
652
709
  severity: "error",
@@ -654,12 +711,12 @@ function migrationLegacyReferenceDiagnostics(files) {
654
711
  message: "new project truth must not link to or cite wiki_legacy*; migrate the meaning or keep unresolved material in migration inboxes",
655
712
  }));
656
713
  }
657
- function collectQualityDiagnostics() {
714
+ function collectQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
658
715
  const diagnostics = [];
659
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
716
+ const files = corpus.files;
660
717
  const titles = new Map();
661
718
  for (const file of files) {
662
- const text = (0, workspace_1.read)(file);
719
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
663
720
  const body = (0, workspace_1.stripMetadataHeader)(text);
664
721
  const title = (0, wiki_files_1.wikiTitleForFile)(file, text).toLowerCase();
665
722
  titles.set(title, [...(titles.get(title) ?? []), file]);
@@ -671,8 +728,9 @@ function collectQualityDiagnostics() {
671
728
  if (tldrExpected && !/##\s+TL;DR/.test(body)) {
672
729
  diagnostics.push({ code: "missing-tldr", severity: "warn", file, message: "add a compact TL;DR near the top" });
673
730
  }
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}` });
731
+ const reviewAge = updated ? (0, wiki_diagnostics_1.staleReviewAge)(updated, workspace_1.today) : null;
732
+ if (status === "active" && reviewAge !== null && /project-canonical|project-decisions|source-summary|wiki-meta/.test(scope)) {
733
+ diagnostics.push({ code: "stale-review", severity: "warn", file, message: `updated ${reviewAge} days ago: ${updated}` });
676
734
  }
677
735
  if (status === "active" && !/inbox|migration-inbox/.test(scope) && /proposed|undecided|TODO|TBD|미정/i.test(body)) {
678
736
  diagnostics.push({ code: "unresolved-signal", severity: "warn", file, message: "contains pending/proposed/undecided language" });
@@ -700,9 +758,8 @@ function collectQualityDiagnostics() {
700
758
  }
701
759
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
702
760
  }
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));
761
+ function collectMigrationQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
762
+ return migrationLegacyReferenceDiagnostics(corpus.files, corpus).sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
706
763
  }
707
764
  function collectMigrationLintDiagnostics() {
708
765
  if (legacyWikiRoots().length === 0)
@@ -728,23 +785,27 @@ function collectMigrationLintDiagnostics() {
728
785
  file,
729
786
  message: "migration review files are missing; run --migrate or keep migration diagnostics out of normal doctor",
730
787
  }));
731
- diagnostics.push(...(0, migration_1.collectMigrationCoverageDiagnostics)());
732
- diagnostics.push(...(0, migration_1.collectMigrationUnitMapDiagnostics)());
733
- diagnostics.push(...(0, migration_1.collectMigrationSplitPlanDiagnostics)());
788
+ const migrationContext = (0, migration_1.loadMigrationUnitContext)();
789
+ diagnostics.push(...(0, migration_1.collectMigrationCoverageDiagnostics)(migrationContext));
790
+ diagnostics.push(...(0, migration_1.collectMigrationUnitMapDiagnostics)(migrationContext));
791
+ diagnostics.push(...(0, migration_1.collectMigrationSplitPlanDiagnostics)(migrationContext));
734
792
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
735
793
  }
736
794
  function runLinkCheckMode() {
737
- const ok = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
795
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
796
+ const ok = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(corpus), corpus.files.length);
738
797
  if (!ok)
739
798
  process.exit(1);
740
799
  }
741
800
  function runQualityCheckMode() {
742
- const ok = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
801
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
802
+ const ok = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(corpus), corpus.files.length);
743
803
  if (!ok)
744
804
  process.exit(1);
745
805
  }
746
806
  function runMigrationQualityCheckMode() {
747
- const ok = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
807
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
808
+ const ok = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(corpus), corpus.files.length);
748
809
  if (!ok)
749
810
  process.exit(1);
750
811
  }
@@ -754,8 +815,9 @@ function runMigrationLintMode() {
754
815
  process.exit(1);
755
816
  }
756
817
  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);
818
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
819
+ const lintOk = printDiagnostics("Project wiki migration lint", collectMigrationLintDiagnostics(), corpus.files.length);
820
+ const qualityOk = printDiagnostics("Project wiki migration quality-check", collectMigrationQualityDiagnostics(corpus), corpus.files.length);
759
821
  if (!lintOk || !qualityOk)
760
822
  process.exit(1);
761
823
  }
@@ -794,11 +856,12 @@ function extractSectionBody(markdown, headingText) {
794
856
  const nextHeading = /^##\s/m.exec(afterHeading);
795
857
  return nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading;
796
858
  }
797
- function collectRouterTruthDiagnostics() {
859
+ function collectRouterTruthDiagnostics(corpus) {
798
860
  const logPath = "wiki/decisions/log.md";
799
- if (!(0, workspace_1.exists)(logPath))
861
+ const hasLog = corpus ? corpus.fileSet.has(logPath) : (0, workspace_1.exists)(logPath);
862
+ if (!hasLog)
800
863
  return [];
801
- const logHasDatedEntry = /\b\d{4}-\d{2}-\d{2}\b/.test((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(logPath)));
864
+ const logHasDatedEntry = /\b\d{4}-\d{2}-\d{2}\b/.test((0, workspace_1.stripMetadataHeader)((0, wiki_corpus_1.wikiCorpusText)(corpus, logPath)));
802
865
  if (!logHasDatedEntry)
803
866
  return [];
804
867
  const diagnostics = [];
@@ -810,9 +873,10 @@ function collectRouterTruthDiagnostics() {
810
873
  ["wiki/decisions/recent.md", "Decisions", "Decisions"],
811
874
  ];
812
875
  for (const [file, heading, surface] of routers) {
813
- if (!(0, workspace_1.exists)(file))
876
+ const hasFile = corpus ? corpus.fileSet.has(file) : (0, workspace_1.exists)(file);
877
+ if (!hasFile)
814
878
  continue;
815
- const section = extractSectionBody((0, workspace_1.stripMetadataHeader)((0, workspace_1.read)(file)), heading);
879
+ const section = extractSectionBody((0, workspace_1.stripMetadataHeader)((0, wiki_corpus_1.wikiCorpusText)(corpus, file)), heading);
816
880
  if (section === "")
817
881
  continue; // section absent — skip rather than false-positive
818
882
  if (ROUTER_TRUTH_NONE_YET_REGEX.test(section)) {
@@ -837,11 +901,11 @@ function runDoctorMode(fix) {
837
901
  console.log("skipped wiki/index.md auto-discovered pages: missing wiki/index.md");
838
902
  }
839
903
  }
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();
904
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
905
+ const linkOk = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(corpus), corpus.files.length);
906
+ const qualityOk = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(corpus), corpus.files.length);
907
+ const routerTruthOk = printDiagnostics("Project wiki router-truth check", collectRouterTruthDiagnostics(corpus), corpus.files.length);
908
+ runLintMode(corpus);
845
909
  if (!linkOk || !qualityOk || !routerTruthOk)
846
910
  process.exit(1);
847
911
  }
@@ -858,36 +922,30 @@ const commonLintRequiredFiles = [
858
922
  ".githooks/prepare-commit-msg",
859
923
  ".githooks/wiki-commit-trailers.js",
860
924
  ];
861
- const agentLintRequiredFiles = {
862
- codex: [".codex/hooks/wiki-session-start.js", ".codex/hooks.json"],
863
- claude: ["CLAUDE.md", ".claude/hooks/wiki-session-start.js", ".claude/settings.json"],
864
- cursor: [".cursor/rules/project-librarian.mdc", ".cursor/hooks/wiki-session-start.js", ".cursor/hooks.json"],
865
- gemini: ["GEMINI.md", ".gemini/hooks/wiki-session-start.js", ".gemini/settings.json"],
866
- };
867
925
  function activeLintAgentSurfaces() {
868
926
  const active = new Set();
869
- for (const [agent, files] of Object.entries(agentLintRequiredFiles)) {
927
+ for (const [agent, files] of Object.entries(agent_surfaces_1.agentSurfaceRequiredFiles)) {
870
928
  if (files.some((file) => (0, workspace_1.exists)(file)))
871
929
  active.add(agent);
872
930
  }
873
931
  return active;
874
932
  }
875
- function runLintMode() {
933
+ function runLintMode(corpus) {
876
934
  const errors = [];
877
935
  const warnings = [];
878
936
  const activeAgents = activeLintAgentSurfaces();
879
937
  const requiredFiles = [
880
938
  ...commonLintRequiredFiles,
881
- ...Array.from(activeAgents).flatMap((agent) => agentLintRequiredFiles[agent]),
939
+ ...Array.from(activeAgents).flatMap((agent) => agent_surfaces_1.agentSurfaceRequiredFiles[agent]),
882
940
  ];
883
941
  for (const file of requiredFiles) {
884
942
  if (!(0, workspace_1.exists)(file))
885
943
  errors.push(`missing required file: ${file}`);
886
944
  }
887
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
945
+ const files = corpus?.files ?? (0, wiki_files_1.wikiMarkdownFiles)();
888
946
  const requiredMetadataKeys = ["status", "updated", "scope", "read_budget", "decision_ref", "review_trigger"];
889
947
  for (const file of files) {
890
- const text = (0, workspace_1.read)(file);
948
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
891
949
  if (!(0, workspace_1.hasMetadataHeader)(text)) {
892
950
  errors.push(`missing metadata header: ${file}`);
893
951
  continue;
@@ -897,8 +955,8 @@ function runLintMode() {
897
955
  errors.push(`missing metadata key ${key}: ${file}`);
898
956
  }
899
957
  }
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;
958
+ const startupLength = (0, workspace_1.exists)("wiki/startup.md") ? (0, wiki_corpus_1.wikiCorpusText)(corpus, "wiki/startup.md").length : 0;
959
+ const indexLength = (0, workspace_1.exists)("wiki/index.md") ? (0, wiki_corpus_1.wikiCorpusText)(corpus, "wiki/index.md").length : 0;
902
960
  if (startupLength > 3500)
903
961
  warnings.push(`startup exceeds hook budget: ${startupLength}/3500 chars`);
904
962
  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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-librarian",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Create and maintain compact project context for humans and LLM coding agents.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -41,18 +41,31 @@
41
41
  "benchmark:ci": "npm run benchmark:ci-smoke",
42
42
  "benchmark:ci-smoke": "npm run benchmark:llm:parse-smoke && npm run benchmark:llm:dry-run",
43
43
  "benchmark:llm": "npm run build && node benchmarks/codex-llm-metrics.js",
44
+ "benchmark:llm:delta-analysis": "node benchmarks/tools/analyze-llm-deltas.js",
44
45
  "benchmark:llm:dry-run": "npm run build && node benchmarks/codex-llm-metrics.js --dry-run",
45
46
  "benchmark:llm:parse-smoke": "node tests/validators/codex-llm-benchmark-smoke.js",
47
+ "benchmark:llm:prune-raw": "node benchmarks/tools/prune-llm-raw.js",
48
+ "benchmark:llm:raw-audit": "node benchmarks/tools/audit-llm-raw.js",
46
49
  "benchmark:injection-sentinel": "node benchmarks/tools/injection-sentinel.js",
50
+ "benchmark:agent-surface-smoke": "node benchmarks/tools/agent-surface-smoke.js",
51
+ "benchmark:claim-ledger": "node benchmarks/tools/benchmark-claim-ledger.js benchmarks/llm/samples/codex-measured-report.json benchmarks/reports/llm/payload-preview.json",
47
52
  "benchmark:real-corpus:demo": "npm run build && node benchmarks/tools/real-corpus-offline-demo.js",
48
53
  "benchmark:release:preview": "npm run benchmark:llm -- --payload-preview benchmarks/reports/llm/payload-preview.json --sanitized-pack --full-matrix --runs 3 --warmup-runs 1 --min-runs-for-claim 3 --require-clean --require-claimable --model gpt-5.5",
54
+ "benchmark:release:diagnose": "npm run benchmark:llm -- --sanitized-pack --runs 1 --warmup-runs 0 --model gpt-5.5 --out benchmarks/reports/llm/diagnostic-current.json --markdown benchmarks/reports/llm/diagnostic-current.md",
49
55
  "benchmark:release": "npm run benchmark:llm -- --sanitized-pack --full-matrix --runs 3 --warmup-runs 1 --min-runs-for-claim 3 --require-clean --require-claimable --model gpt-5.5 --out benchmarks/reports/llm/current.json --markdown benchmarks/reports/llm/current.md",
50
56
  "build": "tsc && chmod +x dist/init-project-wiki.js",
57
+ "perf:code-efficiency": "node benchmarks/tools/code-performance-efficiency.js --full",
58
+ "release:check": "node benchmarks/tools/release-readiness.js",
51
59
  "typecheck": "tsc --noEmit",
60
+ "typecheck:ts7": "npx --yes -p typescript@rc tsc --noEmit --project tsconfig.json",
52
61
  "unit": "node --test tests/unit/*.test.js",
62
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-include=dist/**/*.js --test-coverage-include=benchmarks/**/*.js --test-coverage-lines=65 --test-coverage-branches=65 --test-coverage-functions=65 tests/unit/*.test.js",
53
63
  "test": "npm run build && npm run typecheck && npm run unit && bash tests/smoke.sh",
54
64
  "prepack": "npm run build"
55
65
  },
66
+ "dependencies": {
67
+ "typescript": "^6.0.3"
68
+ },
56
69
  "optionalDependencies": {
57
70
  "@sengac/tree-sitter": "^0.25.15",
58
71
  "@sengac/tree-sitter-c": "^0.25.15",
@@ -69,7 +82,6 @@
69
82
  "@sengac/tree-sitter-typescript": "^0.25.15"
70
83
  },
71
84
  "devDependencies": {
72
- "@types/node": "^25.9.2",
73
- "typescript": "^6.0.3"
85
+ "@types/node": "^25.9.2"
74
86
  }
75
87
  }