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