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/README.ko.md +49 -9
- package/README.md +44 -4
- package/dist/agent-surfaces.js +67 -0
- package/dist/args.js +94 -89
- 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 +309 -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 +167 -1850
- package/dist/init-project-wiki.js +105 -66
- package/dist/install-skill.js +3 -3
- package/dist/mcp-server.js +60 -6
- package/dist/migration.js +10 -6
- package/dist/modes.js +121 -63
- package/dist/path-ignore-policy.js +30 -0
- package/dist/wiki-corpus.js +25 -0
- package/dist/wiki-diagnostics.js +19 -0
- package/dist/wiki-files.js +2 -1
- package/dist/wiki-graph.js +1 -2
- package/package.json +15 -3
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
|
-
|
|
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: ${
|
|
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()).
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
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
|
|
201
|
-
const
|
|
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
|
|
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 =
|
|
539
|
-
const fileSet =
|
|
540
|
-
const graph = (0,
|
|
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,
|
|
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 =
|
|
716
|
+
const files = corpus.files;
|
|
660
717
|
const titles = new Map();
|
|
661
718
|
for (const file of files) {
|
|
662
|
-
const text = (0,
|
|
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
|
-
|
|
675
|
-
|
|
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
|
-
|
|
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
|
-
|
|
732
|
-
diagnostics.push(...(0, migration_1.
|
|
733
|
-
diagnostics.push(...(0, migration_1.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
758
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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) =>
|
|
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,
|
|
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,
|
|
901
|
-
const indexLength = (0, workspace_1.exists)("wiki/index.md") ? (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
|
+
}
|
package/dist/wiki-files.js
CHANGED
|
@@ -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 =
|
|
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);
|
package/dist/wiki-graph.js
CHANGED
|
@@ -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.
|
|
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
|
}
|