project-librarian 0.3.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.
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) {
@@ -170,35 +172,111 @@ This block is managed by \`--refresh-index\`. Move useful rows into a hand-writt
170
172
  | --- | --- | --- | --- |
171
173
  ${rows}<!-- PROJECT-WIKI-AUTO-INDEX:END -->`;
172
174
  }
175
+ function termOccurrences(text, terms) {
176
+ const lowered = text.toLowerCase();
177
+ return terms.reduce((sum, term) => sum + (lowered.split(term).length - 1), 0);
178
+ }
179
+ function blockKindBoost(block) {
180
+ if (block.kind === "heading")
181
+ return 4;
182
+ if (block.kind === "table_row")
183
+ return 3;
184
+ if (block.kind === "list_item")
185
+ return 2;
186
+ return 1;
187
+ }
188
+ function scoreQueryBlock(block, terms) {
189
+ const occurrences = termOccurrences(`${block.headingPath.join(" ")}\n${block.text}`, terms);
190
+ return occurrences > 0 ? occurrences + blockKindBoost(block) : 0;
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
+ }
173
213
  // Answer-shaped query output (2026-06-12 method-transfer decision): first line is
174
- // the answer, each result carries the page's TL;DR first bullet so the agent can
175
- // pick a page without opening it, and the whole body sits under the shared hard
176
- // cap with an explicit truncation notice.
214
+ // the answer, each result carries the page's TL;DR first bullet and the strongest
215
+ // matching block so the agent can pick a page without opening it, and the whole
216
+ // body sits under the shared hard cap with an explicit truncation notice.
177
217
  function runQueryMode() {
178
218
  if (!args_1.queryTerm.trim()) {
179
219
  console.error("missing query: use --query \"search terms\"");
180
220
  process.exit(1);
181
221
  }
182
222
  const terms = args_1.queryTerm.toLowerCase().split(/\s+/).filter(Boolean);
183
- const matches = (0, wiki_files_1.wikiMarkdownFiles)().map((file) => {
184
- const text = (0, workspace_1.read)(file);
223
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
224
+ const pages = corpus.pages;
225
+ const graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus);
226
+ const routerDepths = (0, wiki_graph_1.wikiRouterDepths)(graph);
227
+ const matches = pages.map(({ file, text }) => {
185
228
  const body = (0, workspace_1.stripMetadataHeader)(text);
186
229
  const title = (0, wiki_files_1.wikiTitleForFile)(file, text);
187
230
  const meta = (0, wiki_files_1.metadataSummary)(file, text);
188
- const weighted = `${file}\n${title}\n${meta.scope}\n${(0, workspace_1.metadataValue)(text, "tags")}\n${body}`.toLowerCase();
189
- const score = terms.reduce((sum, term) => sum + (weighted.split(term).length - 1) + (file.toLowerCase().includes(term) ? 3 : 0) + (title.toLowerCase().includes(term) ? 5 : 0), 0);
190
- return { file, title, score, tldr: (0, wiki_files_1.firstTldrBullet)(text), ...meta };
231
+ const metadataScore = termOccurrences(`${file}\n${title}\n${meta.scope}\n${(0, workspace_1.metadataValue)(text, "tags")}`, terms)
232
+ + terms.reduce((sum, term) => sum + (file.toLowerCase().includes(term) ? 3 : 0) + (title.toLowerCase().includes(term) ? 5 : 0), 0);
233
+ const blocks = (0, wiki_files_1.extractMarkdownBlocks)(body)
234
+ .map((block) => ({ block, score: scoreQueryBlock(block, terms) }))
235
+ .filter((item) => item.score > 0)
236
+ .sort((left, right) => right.score - left.score || left.block.line - right.block.line || left.block.id.localeCompare(right.block.id));
237
+ const topBlock = blocks[0]?.block;
238
+ const blockScore = blocks.slice(0, 5).reduce((sum, item) => sum + item.score, 0);
239
+ const score = querySurfaceScore(file, meta, metadataScore + blockScore, terms);
240
+ return {
241
+ blockKind: topBlock?.kind ?? "",
242
+ blockLine: topBlock?.line ?? 0,
243
+ blockSnippet: topBlock ? (0, wiki_files_1.markdownBlockSnippet)(topBlock) : "",
244
+ file,
245
+ graphEvidence: (0, wiki_graph_1.wikiQueryGraphEvidence)(graph, file, routerDepths),
246
+ title,
247
+ score,
248
+ tldr: (0, wiki_files_1.firstTldrBullet)(text),
249
+ ...meta,
250
+ };
191
251
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
192
- const results = matches.slice(0, 10);
193
- const best = results[0];
194
- const lines = [best
195
- ? `Project wiki query "${args_1.queryTerm}": best match ${best.file} — ${best.title} (${matches.length} matching page${matches.length === 1 ? "" : "s"}, top ${results.length} shown).`
196
- : `Project wiki query "${args_1.queryTerm}": no matches.`];
197
- for (const item of results) {
198
- lines.push(`${item.score.toString().padStart(3)} ${item.file} [${item.scope}|${item.status}|${item.budget}] ${item.title}`);
252
+ const resultBlocks = matches.slice(0, 10).map((item) => {
253
+ const lines = [`${item.score.toString().padStart(3)} ${item.file} [${item.scope}|${item.status}|${item.budget}] ${item.title}`];
199
254
  if (item.tldr)
200
255
  lines.push(` tldr: ${item.tldr}`);
256
+ if (item.blockSnippet)
257
+ lines.push(` match: ${item.blockKind}@L${item.blockLine}: ${item.blockSnippet}`);
258
+ if (item.graphEvidence)
259
+ lines.push(` graph: ${item.graphEvidence}`);
260
+ return lines;
261
+ });
262
+ const selectedBlocks = [];
263
+ const answerBudget = wiki_graph_1.wikiAnswerCharCap - wiki_graph_1.wikiAnswerTruncationNotice.length - 1;
264
+ const headlineFor = (shown) => matches[0]
265
+ ? `Project wiki query "${args_1.queryTerm}": best match ${matches[0].file} — ${matches[0].title} (${matches.length} matching page${matches.length === 1 ? "" : "s"}, top ${shown} shown).`
266
+ : `Project wiki query "${args_1.queryTerm}": no matches.`;
267
+ for (const block of resultBlocks) {
268
+ const candidateBlocks = [...selectedBlocks, block];
269
+ const candidate = [headlineFor(candidateBlocks.length), ...candidateBlocks.flat()].join("\n");
270
+ if (candidate.length > answerBudget && selectedBlocks.length > 0)
271
+ break;
272
+ selectedBlocks.push(block);
201
273
  }
274
+ const best = matches[0];
275
+ const lines = [best
276
+ ? headlineFor(selectedBlocks.length)
277
+ : `Project wiki query "${args_1.queryTerm}": no matches.`];
278
+ for (const block of selectedBlocks)
279
+ lines.push(...block);
202
280
  console.log((0, wiki_graph_1.finalizeWikiAnswer)(lines.join("\n")));
203
281
  }
204
282
  // Wiki impact mode: backlink/decision_ref/routing evidence for a page so wiki
@@ -208,8 +286,8 @@ function runWikiImpactMode() {
208
286
  console.error("missing wiki impact target: use --wiki-impact \"page-or-term\"");
209
287
  process.exit(1);
210
288
  }
211
- const pages = (0, wiki_files_1.wikiMarkdownFiles)().map((file) => ({ file, text: (0, workspace_1.read)(file) }));
212
- 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)));
213
291
  }
214
292
  function projectCandidatesContent() {
215
293
  return `${(0, templates_1.metadata)("inbox", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "candidates are adopted, rejected, or stale")}
@@ -479,11 +557,11 @@ function printDiagnostics(title, diagnostics, checked) {
479
557
  console.log(`passed: ${checked} wiki markdown files checked, ${warnings} warnings`);
480
558
  return true;
481
559
  }
482
- function collectLinkDiagnostics() {
560
+ function collectLinkDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
483
561
  const diagnostics = [];
484
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
485
- const fileSet = new Set(files);
486
- 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);
487
565
  for (const link of graph.links) {
488
566
  if (!fileSet.has(link.normalizedTarget)) {
489
567
  diagnostics.push({
@@ -589,10 +667,10 @@ function shouldGuardAgainstLegacyReference(file) {
589
667
  return false;
590
668
  return !file.endsWith("/migration-inbox.md");
591
669
  }
592
- function migrationLegacyReferenceDiagnostics(files) {
670
+ function migrationLegacyReferenceDiagnostics(files, corpus) {
593
671
  return files
594
672
  .filter(shouldGuardAgainstLegacyReference)
595
- .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))))
596
674
  .map((file) => ({
597
675
  code: "migration-legacy-reference",
598
676
  severity: "error",
@@ -600,12 +678,12 @@ function migrationLegacyReferenceDiagnostics(files) {
600
678
  message: "new project truth must not link to or cite wiki_legacy*; migrate the meaning or keep unresolved material in migration inboxes",
601
679
  }));
602
680
  }
603
- function collectQualityDiagnostics() {
681
+ function collectQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
604
682
  const diagnostics = [];
605
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
683
+ const files = corpus.files;
606
684
  const titles = new Map();
607
685
  for (const file of files) {
608
- const text = (0, workspace_1.read)(file);
686
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
609
687
  const body = (0, workspace_1.stripMetadataHeader)(text);
610
688
  const title = (0, wiki_files_1.wikiTitleForFile)(file, text).toLowerCase();
611
689
  titles.set(title, [...(titles.get(title) ?? []), file]);
@@ -617,8 +695,9 @@ function collectQualityDiagnostics() {
617
695
  if (tldrExpected && !/##\s+TL;DR/.test(body)) {
618
696
  diagnostics.push({ code: "missing-tldr", severity: "warn", file, message: "add a compact TL;DR near the top" });
619
697
  }
620
- if (status === "active" && updated && updated < workspace_1.today && /project-canonical|project-decisions|source-summary|wiki-meta/.test(scope)) {
621
- 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}` });
622
701
  }
623
702
  if (status === "active" && !/inbox|migration-inbox/.test(scope) && /proposed|undecided|TODO|TBD|미정/i.test(body)) {
624
703
  diagnostics.push({ code: "unresolved-signal", severity: "warn", file, message: "contains pending/proposed/undecided language" });
@@ -646,9 +725,8 @@ function collectQualityDiagnostics() {
646
725
  }
647
726
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
648
727
  }
649
- function collectMigrationQualityDiagnostics() {
650
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
651
- 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));
652
730
  }
653
731
  function collectMigrationLintDiagnostics() {
654
732
  if (legacyWikiRoots().length === 0)
@@ -674,23 +752,27 @@ function collectMigrationLintDiagnostics() {
674
752
  file,
675
753
  message: "migration review files are missing; run --migrate or keep migration diagnostics out of normal doctor",
676
754
  }));
677
- diagnostics.push(...(0, migration_1.collectMigrationCoverageDiagnostics)());
678
- diagnostics.push(...(0, migration_1.collectMigrationUnitMapDiagnostics)());
679
- 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));
680
759
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
681
760
  }
682
761
  function runLinkCheckMode() {
683
- 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);
684
764
  if (!ok)
685
765
  process.exit(1);
686
766
  }
687
767
  function runQualityCheckMode() {
688
- 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);
689
770
  if (!ok)
690
771
  process.exit(1);
691
772
  }
692
773
  function runMigrationQualityCheckMode() {
693
- 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);
694
776
  if (!ok)
695
777
  process.exit(1);
696
778
  }
@@ -700,8 +782,9 @@ function runMigrationLintMode() {
700
782
  process.exit(1);
701
783
  }
702
784
  function runMigrationDoctorMode() {
703
- const lintOk = printDiagnostics("Project wiki migration lint", collectMigrationLintDiagnostics(), (0, wiki_files_1.wikiMarkdownFiles)().length);
704
- 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);
705
788
  if (!lintOk || !qualityOk)
706
789
  process.exit(1);
707
790
  }
@@ -740,11 +823,12 @@ function extractSectionBody(markdown, headingText) {
740
823
  const nextHeading = /^##\s/m.exec(afterHeading);
741
824
  return nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading;
742
825
  }
743
- function collectRouterTruthDiagnostics() {
826
+ function collectRouterTruthDiagnostics(corpus) {
744
827
  const logPath = "wiki/decisions/log.md";
745
- if (!(0, workspace_1.exists)(logPath))
828
+ const hasLog = corpus ? corpus.fileSet.has(logPath) : (0, workspace_1.exists)(logPath);
829
+ if (!hasLog)
746
830
  return [];
747
- 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)));
748
832
  if (!logHasDatedEntry)
749
833
  return [];
750
834
  const diagnostics = [];
@@ -756,9 +840,10 @@ function collectRouterTruthDiagnostics() {
756
840
  ["wiki/decisions/recent.md", "Decisions", "Decisions"],
757
841
  ];
758
842
  for (const [file, heading, surface] of routers) {
759
- if (!(0, workspace_1.exists)(file))
843
+ const hasFile = corpus ? corpus.fileSet.has(file) : (0, workspace_1.exists)(file);
844
+ if (!hasFile)
760
845
  continue;
761
- 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);
762
847
  if (section === "")
763
848
  continue; // section absent — skip rather than false-positive
764
849
  if (ROUTER_TRUTH_NONE_YET_REGEX.test(section)) {
@@ -783,11 +868,11 @@ function runDoctorMode(fix) {
783
868
  console.log("skipped wiki/index.md auto-discovered pages: missing wiki/index.md");
784
869
  }
785
870
  }
786
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
787
- const linkOk = printDiagnostics("Project wiki link-check", collectLinkDiagnostics(), files.length);
788
- const qualityOk = printDiagnostics("Project wiki quality-check", collectQualityDiagnostics(), files.length);
789
- const routerTruthOk = printDiagnostics("Project wiki router-truth check", collectRouterTruthDiagnostics(), files.length);
790
- 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);
791
876
  if (!linkOk || !qualityOk || !routerTruthOk)
792
877
  process.exit(1);
793
878
  }
@@ -818,7 +903,7 @@ function activeLintAgentSurfaces() {
818
903
  }
819
904
  return active;
820
905
  }
821
- function runLintMode() {
906
+ function runLintMode(corpus) {
822
907
  const errors = [];
823
908
  const warnings = [];
824
909
  const activeAgents = activeLintAgentSurfaces();
@@ -830,10 +915,10 @@ function runLintMode() {
830
915
  if (!(0, workspace_1.exists)(file))
831
916
  errors.push(`missing required file: ${file}`);
832
917
  }
833
- const files = (0, wiki_files_1.wikiMarkdownFiles)();
918
+ const files = corpus?.files ?? (0, wiki_files_1.wikiMarkdownFiles)();
834
919
  const requiredMetadataKeys = ["status", "updated", "scope", "read_budget", "decision_ref", "review_trigger"];
835
920
  for (const file of files) {
836
- const text = (0, workspace_1.read)(file);
921
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
837
922
  if (!(0, workspace_1.hasMetadataHeader)(text)) {
838
923
  errors.push(`missing metadata header: ${file}`);
839
924
  continue;
@@ -843,8 +928,8 @@ function runLintMode() {
843
928
  errors.push(`missing metadata key ${key}: ${file}`);
844
929
  }
845
930
  }
846
- const startupLength = (0, workspace_1.exists)("wiki/startup.md") ? (0, workspace_1.read)("wiki/startup.md").length : 0;
847
- 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;
848
933
  if (startupLength > 3500)
849
934
  warnings.push(`startup exceeds hook budget: ${startupLength}/3500 chars`);
850
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,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeRetrievalMetrics = computeRetrievalMetrics;
4
+ function bytesOf(text) {
5
+ return Buffer.byteLength(text, "utf8");
6
+ }
7
+ function finiteNonNegativeInteger(value, fallback) {
8
+ if (value === undefined || !Number.isFinite(value) || value < 0)
9
+ return fallback;
10
+ return Math.floor(value);
11
+ }
12
+ function itemKeys(item) {
13
+ return new Set([item.id, item.sourceId ?? "", item.blockId ?? ""].filter(Boolean));
14
+ }
15
+ function itemMatches(item, expectedId) {
16
+ return itemKeys(item).has(expectedId);
17
+ }
18
+ function ratio(numerator, denominator, emptyValue) {
19
+ if (denominator === 0)
20
+ return emptyValue;
21
+ return numerator / denominator;
22
+ }
23
+ function uniqueNonEmptyStrings(values) {
24
+ return [...new Set((values ?? []).filter((value) => value.length > 0))];
25
+ }
26
+ function answerCorrect(outputText, terms) {
27
+ const requiredTerms = uniqueNonEmptyStrings(terms);
28
+ if (requiredTerms.length === 0)
29
+ return null;
30
+ const lowered = outputText.toLowerCase();
31
+ return requiredTerms.every((term) => lowered.includes(term.toLowerCase()));
32
+ }
33
+ function computeRetrievalMetrics(results, expectation, options = {}) {
34
+ const topK = finiteNonNegativeInteger(options.topK, results.length);
35
+ const considered = results.slice(0, topK);
36
+ const requiredSourceIds = uniqueNonEmptyStrings(expectation.requiredSourceIds);
37
+ const relevantSourceIds = expectation.relevantSourceIds === undefined
38
+ ? requiredSourceIds
39
+ : uniqueNonEmptyStrings(expectation.relevantSourceIds);
40
+ const requiredHits = requiredSourceIds.filter((sourceId) => considered.some((item) => itemMatches(item, sourceId))).length;
41
+ const relevantHits = relevantSourceIds.length === 0
42
+ ? 0
43
+ : considered.filter((item) => relevantSourceIds.some((sourceId) => itemMatches(item, sourceId))).length;
44
+ const intactBlocks = considered.filter((item) => item.blockIntact !== false).length;
45
+ const maxHop = considered.reduce((currentMax, item) => Math.max(currentMax, finiteNonNegativeInteger(item.hop, 0)), 0);
46
+ const outputText = options.outputText ?? considered.map((item) => item.text ?? "").join("\n");
47
+ const computedScanBytes = considered.reduce((sum, item) => {
48
+ if (item.bytes !== undefined && Number.isFinite(item.bytes) && item.bytes >= 0)
49
+ return sum + Math.floor(item.bytes);
50
+ return sum + bytesOf(item.text ?? "");
51
+ }, 0);
52
+ const scanBytes = options.scanBytes === undefined
53
+ ? computedScanBytes
54
+ : finiteNonNegativeInteger(options.scanBytes, 0);
55
+ return {
56
+ answer_correct: answerCorrect(outputText, expectation.requiredAnswerTerms),
57
+ block_integrity: ratio(intactBlocks, considered.length, 1),
58
+ considered_results: considered.length,
59
+ evidence_precision: ratio(relevantHits, considered.length, 0),
60
+ max_hop_count: maxHop,
61
+ output_bytes: bytesOf(outputText),
62
+ required_source_count: requiredSourceIds.length,
63
+ required_source_hits: requiredHits,
64
+ scan_bytes: scanBytes,
65
+ source_hit_rate: ratio(requiredHits, requiredSourceIds.length, 1),
66
+ top_k: topK,
67
+ };
68
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.conceptIdForFile = conceptIdForFile;
4
+ exports.wikiConceptType = wikiConceptType;
5
+ exports.conceptFromPage = conceptFromPage;
6
+ exports.readWikiConcepts = readWikiConcepts;
7
+ const wiki_files_1 = require("./wiki-files");
8
+ const workspace_1 = require("./workspace");
9
+ function conceptIdForFile(file) {
10
+ return file.replace(/^wiki\//, "").replace(/\.(md|mdx)$/i, "");
11
+ }
12
+ function wikiConceptType(file, scope) {
13
+ if (scope === "startup-router" || file === "wiki/startup.md")
14
+ return "Startup Router";
15
+ if (scope === "wiki-router" || file === "wiki/index.md" || /^wiki\/indexes\//.test(file))
16
+ return "Wiki Router";
17
+ if (scope === "project-canonical" || /^wiki\/canonical\//.test(file))
18
+ return "Project Canonical Concept";
19
+ if (scope === "project-decisions" || /^wiki\/decisions\//.test(file))
20
+ return "Project Decision";
21
+ if (scope === "source-summary" || /^wiki\/sources\//.test(file))
22
+ return "Source Summary";
23
+ if (scope === "wiki-meta" || /^wiki\/meta\//.test(file))
24
+ return "Wiki Operations Concept";
25
+ if (/^migration-/.test(scope) || /^wiki\/migration\//.test(file))
26
+ return "Migration Ledger";
27
+ if (scope === "inbox" || /^wiki\/inbox\//.test(file))
28
+ return "Project Candidate";
29
+ return "Wiki Concept";
30
+ }
31
+ function conceptFromPage(file, text) {
32
+ const scope = (0, workspace_1.metadataValue)(text, "scope") || "-";
33
+ const tldr = (0, wiki_files_1.firstTldrBullet)(text);
34
+ return {
35
+ budget: (0, workspace_1.metadataValue)(text, "read_budget") || "-",
36
+ conceptId: conceptIdForFile(file),
37
+ description: tldr || (0, wiki_files_1.compactSummary)(text),
38
+ file,
39
+ reviewTrigger: (0, workspace_1.metadataValue)(text, "review_trigger"),
40
+ scope,
41
+ status: (0, workspace_1.metadataValue)(text, "status") || "-",
42
+ timestamp: (0, workspace_1.metadataValue)(text, "updated"),
43
+ title: (0, wiki_files_1.wikiTitleForFile)(file, text),
44
+ type: wikiConceptType(file, scope),
45
+ };
46
+ }
47
+ function readWikiConcepts(files = (0, wiki_files_1.wikiMarkdownFiles)()) {
48
+ return files.map((file) => conceptFromPage(file, (0, workspace_1.read)(file)));
49
+ }
@@ -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
+ }