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/README.ko.md +42 -11
- package/README.md +39 -9
- package/SKILL.md +1 -0
- package/dist/args.js +20 -3
- package/dist/code-index/evidence.js +79 -0
- package/dist/code-index/extractors/config.js +58 -0
- package/dist/code-index/extractors/light-languages.js +52 -0
- package/dist/code-index/extractors/registry.js +137 -0
- package/dist/code-index/extractors/shared.js +36 -0
- package/dist/code-index/extractors/tree-sitter.js +319 -0
- package/dist/code-index/extractors/types.js +2 -0
- package/dist/code-index/extractors/typescript.js +211 -0
- package/dist/code-index/incremental.js +16 -0
- package/dist/code-index/index-health.js +164 -0
- package/dist/code-index/modes.js +303 -0
- package/dist/code-index/ownership.js +183 -0
- package/dist/code-index/reports.js +322 -0
- package/dist/code-index/schema.js +196 -0
- package/dist/code-index/search.js +153 -0
- package/dist/code-index-file-policy.js +21 -27
- package/dist/code-index.js +224 -1657
- package/dist/init-project-wiki.js +65 -37
- package/dist/mcp-server.js +325 -11
- package/dist/migration.js +10 -6
- package/dist/modes.js +139 -54
- package/dist/path-ignore-policy.js +30 -0
- package/dist/retrieval-eval.js +68 -0
- package/dist/wiki-concepts.js +49 -0
- package/dist/wiki-corpus.js +25 -0
- package/dist/wiki-diagnostics.js +19 -0
- package/dist/wiki-files.js +107 -1
- package/dist/wiki-graph.js +26 -2
- package/dist/wiki-visualizer.js +558 -0
- package/package.json +11 -4
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
|
|
175
|
-
// pick a page without opening it, and the whole
|
|
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
|
|
184
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
193
|
-
|
|
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
|
|
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 =
|
|
485
|
-
const fileSet =
|
|
486
|
-
const graph = (0,
|
|
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,
|
|
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 =
|
|
683
|
+
const files = corpus.files;
|
|
606
684
|
const titles = new Map();
|
|
607
685
|
for (const file of files) {
|
|
608
|
-
const text = (0,
|
|
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
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
diagnostics.push(...(0, migration_1.
|
|
679
|
-
diagnostics.push(...(0, migration_1.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
704
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
847
|
-
const indexLength = (0, workspace_1.exists)("wiki/index.md") ? (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
|
+
}
|