project-librarian 0.5.8 → 0.6.0

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 CHANGED
@@ -77,9 +77,9 @@ Project Librarian은 작은 첫 읽기와 더 깊은 정본으로 가는 경로
77
77
  - **첫 읽기를 작게.** 시작 훅은 `wiki/startup.md`와 `wiki/index.md`만 주입하고, 에이전트는 필요할 때 더 깊은 페이지로 이동합니다.
78
78
  - **한 번 설정, 네 에이전트.** Codex, Claude Code, Cursor, Gemini CLI가 같은 저장소 로컬 기억 계약을 공유합니다.
79
79
  - **구조적인 위키 작성.** 새 프로젝트 내용은 작성하거나 취합하기 전에 분류하므로 PRD, 정책, UX, 데이터, API, QA, 릴리즈, 운영 메모가 하나의 잡다한 페이지로 합쳐지지 않습니다.
80
- - **검토 가능한 위키 그래프.** `--wiki-visualize`는 시작 컨텍스트를 늘리지 않고 `.project-wiki/` 아래에 정적 HTML 그래프를 작성합니다.
81
80
  - **측정된 주장.** 벤치마크에서 이긴 경우와 손해를 본 경우를 주장 경계와 함께 공개합니다.
82
81
  - **로컬 세션 핸드오프.** `--handoff-save`는 `.project-wiki/session/` 아래에 생성형 재개 메모를 저장하되 실행 기억을 프로젝트 정본으로 바꾸지 않습니다.
82
+ - **답변형 위키 토폴로지.** `--wiki-impact`, `--wiki-neighborhood`, topology diagnostics가 graph visualizer 없이 backlinks, decision 근거, router depth, 가까운 읽기 순서를 노출합니다.
83
83
  - **선택적 코드 근거.** SQLite 인덱스와 답변 형태 MCP 도구가 큰 저장소의 비싼 탐색 질문에 답하며, MCP SDK 의존성은 추가하지 않습니다.
84
84
  - **다시 실행해도 안전.** 부트스트랩은 멱등하고 기존 내용 보존을 우선합니다.
85
85
 
@@ -94,6 +94,7 @@ Project Librarian은 작은 첫 읽기와 더 깊은 정본으로 가는 경로
94
94
  | 기존 문서/위키 마이그레이션 | "Project Librarian으로 기존 docs/wiki 내용을 마이그레이션해줘." |
95
95
  | 진단 실행 | "Project Librarian 진단을 실행해줘." |
96
96
  | 프로젝트 기억 검색 | "Project Librarian 위키에서 authentication decisions를 찾아줘." |
97
+ | 가까운 위키 맥락 찾기 | "canonical/project-brief의 Project Librarian wiki neighborhood를 보여줘." |
97
98
  | 코드 근거 구축 | "Project Librarian 코드 근거를 `src`에 대해 구축해줘." |
98
99
  | 코드 영향 확인 | "`healthHandler`에 대한 Project Librarian 영향 근거를 보여줘." |
99
100
  | 세션 핸드오프 저장 | "현재 작업에 대한 Project Librarian 세션 핸드오프를 저장해줘." |
@@ -115,7 +116,7 @@ Project Librarian은 작은 첫 읽기와 더 깊은 정본으로 가는 경로
115
116
  | 중형 | 45.95% 적음 | 37.42% 적음 | 11.88% 적음 | 4.30% 적음 | 69.12% 적음 |
116
117
  | 대형 | 66.97% 적음 | 69.87% 적음 | 31.90% 적음 | 35.19% 적음 | 95.58% 적음 |
117
118
 
118
- 위키 트랙 작업 유형별 비용 가중 토큰 변화:
119
+ 위키 트랙 작업 유형별 비용 가중 토큰 변화, 전체 규모 합산:
119
120
 
120
121
  | 작업 유형 | 변화 |
121
122
  | --- | ---: |
@@ -127,6 +128,8 @@ Project Librarian은 작은 첫 읽기와 더 깊은 정본으로 가는 경로
127
128
  | multi_session | 52.58% 적음 |
128
129
  | aggregation | 42.53% 적음 |
129
130
 
131
+ 규모별 값은 서로 다릅니다. 소형/중형/대형 작업 유형별 실제 행렬과 아직 역전이 남은 셀은 [벤치마크 근거](docs/ko/benchmarks.md#위키-트랙)에 있습니다.
132
+
130
133
  실행 시간과 명령 수의 한계도 있습니다. 이번 보고서에서 `code_impact`, `change_location`, `aggregation`은 토큰과 출력 바이트가 모두 개선됐지만, 실행 시간이나 명령 수에서는 일부 역전이 남아 있습니다.
131
134
 
132
135
  코드 그래프 트랙, 대조군 대비 비용 가중 토큰:
package/README.md CHANGED
@@ -77,9 +77,9 @@ The core idea is not "write more docs." It is "keep the first agent read small,
77
77
  - **Small first read.** Startup hooks inject only `wiki/startup.md` and `wiki/index.md`; agents route to deeper pages on demand.
78
78
  - **One setup, four agents.** Codex, Claude Code, Cursor, and Gemini CLI share the same repo-local memory contract.
79
79
  - **Structured wiki writing.** New project content is classified before it is written or consolidated, so PRDs, policies, UX, data, APIs, QA, release, and operations notes do not collapse into one catch-all page.
80
- - **Inspectable wiki graph.** `--wiki-visualize` writes a static HTML graph under `.project-wiki/` without adding to startup context.
81
80
  - **Measured claims.** Benchmark wins and losses are published together, with claim boundaries attached.
82
81
  - **Local session handoff.** `--handoff-save` stores generated resume notes under `.project-wiki/session/` without turning execution memory into canonical project truth.
82
+ - **Answer-shaped wiki topology.** `--wiki-impact`, `--wiki-neighborhood`, and topology diagnostics expose backlinks, decision evidence, router depth, and nearby read order without a graph visualizer.
83
83
  - **Optional code evidence.** A SQLite index plus answer-shaped MCP tools answer expensive traversal questions on large repositories without adding an MCP SDK dependency.
84
84
  - **Safe to re-run.** Bootstrap is idempotent and preservation-first.
85
85
 
@@ -94,6 +94,7 @@ Ask your agent for the outcome you want; the installed skill maps the request to
94
94
  | Migrate existing docs/wiki content | "Use Project Librarian to migrate the existing docs/wiki content." |
95
95
  | Run diagnostics | "Run Project Librarian diagnostics." |
96
96
  | Search project memory | "Search the Project Librarian wiki for authentication decisions." |
97
+ | Find nearby wiki context | "Show Project Librarian wiki neighborhood for canonical/project-brief." |
97
98
  | Build code evidence | "Build Project Librarian code evidence for `src`." |
98
99
  | Inspect code impact | "Show Project Librarian impact evidence for `healthHandler`." |
99
100
  | Save a handoff | "Save a Project Librarian session handoff for the current work." |
@@ -115,7 +116,7 @@ Wiki track aggregate deltas vs control:
115
116
  | Medium | 45.95% less | 37.42% less | 11.88% less | 4.30% less | 69.12% less |
116
117
  | Large | 66.97% less | 69.87% less | 31.90% less | 35.19% less | 95.58% less |
117
118
 
118
- Wiki track task-family cost-weighted token deltas vs control:
119
+ Wiki track task-family cost-weighted token deltas vs control, all scales combined:
119
120
 
120
121
  | Task family | Delta |
121
122
  | --- | ---: |
@@ -127,6 +128,8 @@ Wiki track task-family cost-weighted token deltas vs control:
127
128
  | multi_session | 52.58% less |
128
129
  | aggregation | 42.53% less |
129
130
 
131
+ Scale-specific values vary; see [Benchmark Evidence](docs/benchmarks.md#wiki-track) for the small/medium/large task-family matrix and the cells that still regress.
132
+
130
133
  Timing and command-count caveat: `code_impact`, `change_location`, and `aggregation` still had wall-time or command-count regressions in this report, even though token and output-byte metrics improved for every task family.
131
134
 
132
135
  Code-graph track, cost-weighted tokens vs control:
package/dist/args.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codeIndexMode = exports.acknowledgeSmallRepoMode = exports.noGitConfigMode = exports.reviewMigrationMode = exports.pruneCheckStrictMode = exports.pruneCheckMode = exports.captureInboxMode = exports.refreshIndexMode = exports.issueDraftMode = exports.issueCreateMode = exports.handoffVerification = exports.handoffStatusMode = exports.handoffState = exports.handoffShowMode = exports.handoffSaveMode = exports.handoffPromoteInboxMode = exports.handoffInjectionStatusMode = exports.handoffInjectionEnableMode = exports.handoffInjectionDisableMode = exports.handoffOpenQuestions = exports.handoffNextActions = exports.handoffLastSuccessCommand = exports.handoffLastFailureCommand = exports.handoffInputMode = exports.handoffGoal = exports.handoffDecisions = exports.handoffClearMode = exports.handoffBlocked = exports.glossaryMode = exports.fixMode = exports.doctorMode = exports.qualityCheckMode = exports.linkCheckMode = exports.migrationQualityCheckMode = exports.migrationLintMode = exports.migrationDoctorMode = exports.lintMode = exports.migrateMode = exports.invalidAgentTargets = exports.missingValueOptions = exports.unexpectedValueOptions = exports.unknownOptions = exports.args = exports.commandArgs = exports.command = exports.unknownCommand = exports.helpMode = exports.agentTargets = exports.parsedArgs = exports.rawArgs = void 0;
4
- exports.issueDraftTitle = exports.issueBodyFile = exports.captureCategory = exports.captureContent = exports.captureTitle = exports.codeIndexScopes = exports.codeParser = exports.codeIndexOutput = exports.codeSearchSymbol = exports.codeReportSection = exports.codeQuerySql = exports.codeImpactTarget = exports.codeContextPackTarget = exports.wikiVisualizeOutput = exports.wikiVisualizeMode = exports.wikiImpactTarget = exports.wikiImpactMode = exports.queryTerm = exports.codeSearchSymbolMode = exports.codeQueryMode = exports.codeImpactMode = exports.codeParserMode = exports.codeContextPackMode = exports.codeFilesMode = exports.codeStatusMode = exports.codeReportMode = exports.codeIndexHealthMode = exports.codeIndexFullMode = exports.codeIndexMigrateMode = exports.codeIndexIncrementalMode = exports.codeIndexEngineMode = exports.codeIndexEngine = void 0;
4
+ exports.issueDraftTitle = exports.issueBodyFile = exports.captureCategory = exports.captureContent = exports.captureTitle = exports.codeIndexScopes = exports.codeParser = exports.codeIndexOutput = exports.codeSearchSymbol = exports.codeReportSection = exports.codeQuerySql = exports.codeImpactTarget = exports.codeContextPackTarget = exports.wikiNeighborhoodTarget = exports.wikiNeighborhoodMode = exports.wikiImpactTarget = exports.wikiImpactMode = exports.queryTerm = exports.codeSearchSymbolMode = exports.codeQueryMode = exports.codeImpactMode = exports.codeParserMode = exports.codeContextPackMode = exports.codeFilesMode = exports.codeStatusMode = exports.codeReportMode = exports.codeIndexHealthMode = exports.codeIndexFullMode = exports.codeIndexMigrateMode = exports.codeIndexIncrementalMode = exports.codeIndexEngineMode = exports.codeIndexEngine = void 0;
5
5
  exports.parseArgs = parseArgs;
6
6
  exports.argValue = argValue;
7
7
  exports.argValues = argValues;
@@ -72,10 +72,8 @@ const flagDefinitions = [
72
72
  { name: "--review-migration", value: "none", aliases: ["--semantic-migrate"] },
73
73
  { name: "--scope", value: "value" },
74
74
  { name: "--title", value: "value" },
75
- { name: "--wiki-graph-html", value: "none" },
76
75
  { name: "--wiki-impact", value: "value" },
77
- { name: "--wiki-visualize", value: "none" },
78
- { name: "--wiki-visualize-out", value: "value" },
76
+ { name: "--wiki-neighborhood", value: "value" },
79
77
  ];
80
78
  function definitionNames(definition) {
81
79
  return [definition.name, ...(definition.aliases ?? [])];
@@ -156,6 +154,7 @@ function parseArgs(argv) {
156
154
  const codeContextPackTarget = argValueFromAny("--code-context-pack");
157
155
  const codeQuerySql = argValueFromAny("--code-query");
158
156
  const codeSearchSymbol = argValueFromAny("--code-search-symbol");
157
+ const wikiNeighborhoodTarget = argValue("--wiki-neighborhood");
159
158
  const parsedAgentTargets = (0, agent_surfaces_1.parseAgentSurfaceValues)(argValues("--agents"));
160
159
  const handoffInputMode = [
161
160
  "--blocked",
@@ -254,8 +253,8 @@ function parseArgs(argv) {
254
253
  .filter((arg) => !knownFlags.has(arg)))),
255
254
  wikiImpactMode: hasFlag("--wiki-impact"),
256
255
  wikiImpactTarget: argValue("--wiki-impact"),
257
- wikiVisualizeMode: hasFlag("--wiki-visualize") || hasFlag("--wiki-graph-html"),
258
- wikiVisualizeOutput: argValue("--wiki-visualize-out"),
256
+ wikiNeighborhoodMode: hasFlag("--wiki-neighborhood"),
257
+ wikiNeighborhoodTarget,
259
258
  };
260
259
  }
261
260
  exports.parsedArgs = parseArgs(exports.rawArgs);
@@ -330,8 +329,8 @@ function argValues(name) {
330
329
  exports.queryTerm = exports.parsedArgs.queryTerm;
331
330
  exports.wikiImpactMode = exports.parsedArgs.wikiImpactMode;
332
331
  exports.wikiImpactTarget = exports.parsedArgs.wikiImpactTarget;
333
- exports.wikiVisualizeMode = exports.parsedArgs.wikiVisualizeMode;
334
- exports.wikiVisualizeOutput = exports.parsedArgs.wikiVisualizeOutput;
332
+ exports.wikiNeighborhoodMode = exports.parsedArgs.wikiNeighborhoodMode;
333
+ exports.wikiNeighborhoodTarget = exports.parsedArgs.wikiNeighborhoodTarget;
335
334
  exports.codeContextPackTarget = exports.parsedArgs.codeContextPackTarget;
336
335
  exports.codeImpactTarget = exports.parsedArgs.codeImpactTarget;
337
336
  exports.codeQuerySql = exports.parsedArgs.codeQuerySql;
@@ -9,7 +9,6 @@ const modes_1 = require("./modes");
9
9
  const migration_1 = require("./migration");
10
10
  const session_handoff_1 = require("./session-handoff");
11
11
  const templates_1 = require("./templates");
12
- const wiki_visualizer_1 = require("./wiki-visualizer");
13
12
  const workspace_1 = require("./workspace");
14
13
  function codeIndex() {
15
14
  return require("./code-index");
@@ -37,8 +36,8 @@ Options:
37
36
  --dry-run With install, preview copied skill files without writing them.
38
37
  --query <terms> Search wiki paths, metadata, titles, and bodies (answer-shaped, capped output).
39
38
  --wiki-impact <page-or-term> Show wiki backlinks, decision_ref citations, and router depth for matching pages.
40
- --wiki-visualize Write a static wiki graph visualizer to .project-wiki/wiki-graph.html.
41
- --wiki-visualize-out <path> With --wiki-visualize, write under a custom .project-wiki/ path.
39
+ --wiki-neighborhood <page-or-term>
40
+ Show a bounded read order for nearby wiki pages.
42
41
  --refresh-index Update the managed auto-discovered wiki index block.
43
42
  --capture-inbox Append a candidate note with --title, --content, and optional --category.
44
43
  --handoff-save Save local generated session handoff state under .project-wiki/session/.
@@ -147,10 +146,6 @@ if (args_1.codeReportSection && !args_1.codeReportMode) {
147
146
  console.error("--code-report-section is only supported with --code-report.");
148
147
  process.exit(1);
149
148
  }
150
- if (args_1.wikiVisualizeOutput && !args_1.wikiVisualizeMode) {
151
- console.error("--wiki-visualize-out is only supported with --wiki-visualize.");
152
- process.exit(1);
153
- }
154
149
  if (args_1.pruneCheckStrictMode && !args_1.pruneCheckMode) {
155
150
  console.error("--prune-check-strict is only supported with --prune-check.");
156
151
  process.exit(1);
@@ -266,16 +261,9 @@ function runInitCommand() {
266
261
  exitAfterStdoutDrain(0);
267
262
  return;
268
263
  }
269
- if (args_1.wikiVisualizeMode) {
270
- try {
271
- const output = (0, wiki_visualizer_1.writeWikiVisualizer)(args_1.wikiVisualizeOutput);
272
- console.log(`Project wiki visualizer written: ${output}`);
273
- exitAfterStdoutDrain(0);
274
- }
275
- catch (error) {
276
- console.error(error instanceof Error ? error.message : String(error));
277
- process.exit(1);
278
- }
264
+ if (args_1.wikiNeighborhoodMode) {
265
+ (0, modes_1.runWikiNeighborhoodMode)();
266
+ exitAfterStdoutDrain(0);
279
267
  return;
280
268
  }
281
269
  if (args_1.queryTerm) {
package/dist/modes.js CHANGED
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.buildRefreshIndexBlock = buildRefreshIndexBlock;
37
37
  exports.runQueryMode = runQueryMode;
38
38
  exports.runWikiImpactMode = runWikiImpactMode;
39
+ exports.runWikiNeighborhoodMode = runWikiNeighborhoodMode;
39
40
  exports.projectCandidatesContent = projectCandidatesContent;
40
41
  exports.appendProjectCandidate = appendProjectCandidate;
41
42
  exports.appendCaptureInbox = appendCaptureInbox;
@@ -325,6 +326,14 @@ function runWikiImpactMode() {
325
326
  const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
326
327
  console.log((0, wiki_graph_1.wikiImpactAnswer)(corpus.pages, args_1.wikiImpactTarget.trim(), (0, wiki_corpus_1.wikiCorpusGraph)(corpus)));
327
328
  }
329
+ function runWikiNeighborhoodMode() {
330
+ if (!args_1.wikiNeighborhoodTarget.trim()) {
331
+ console.error("missing wiki neighborhood target: use --wiki-neighborhood \"page-or-term\"");
332
+ process.exit(1);
333
+ }
334
+ const corpus = (0, wiki_corpus_1.loadWikiCorpus)();
335
+ console.log((0, wiki_graph_1.wikiNeighborhoodAnswer)(corpus.pages, args_1.wikiNeighborhoodTarget.trim(), (0, wiki_corpus_1.wikiCorpusGraph)(corpus)));
336
+ }
328
337
  function projectCandidatesContent() {
329
338
  return `${(0, templates_1.metadata)("inbox", "on-demand", "wiki/meta/wiki-ops-v1-decisions.md", "candidates are adopted, rejected, or stale")}
330
339
  # Project Candidates Inbox
@@ -699,6 +708,7 @@ function collectLinkDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
699
708
  }
700
709
  }
701
710
  }
711
+ diagnostics.push(...(0, wiki_diagnostics_1.collectTopologyDiagnostics)(corpus));
702
712
  return diagnostics.sort((a, b) => a.severity.localeCompare(b.severity) || a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
703
713
  }
704
714
  function legacyWikiRoots() {
@@ -725,9 +735,10 @@ function migrationLegacyReferenceDiagnostics(files, corpus) {
725
735
  message: "new project truth must not link to or cite wiki_legacy*; migrate the meaning or keep unresolved material in migration inboxes",
726
736
  }));
727
737
  }
728
- function collectQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)()) {
738
+ function collectQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)(), options = {}) {
729
739
  const diagnostics = [];
730
740
  const files = corpus.files;
741
+ const currentDate = options.today ?? workspace_1.today;
731
742
  const titles = new Map();
732
743
  for (const file of files) {
733
744
  const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
@@ -742,7 +753,7 @@ function collectQualityDiagnostics(corpus = (0, wiki_corpus_1.loadWikiCorpus)())
742
753
  if (tldrExpected && !/##\s+TL;DR/.test(body)) {
743
754
  diagnostics.push({ code: "missing-tldr", severity: "warn", file, message: "add a compact TL;DR near the top" });
744
755
  }
745
- const reviewAge = updated ? (0, wiki_diagnostics_1.staleReviewAge)(updated, workspace_1.today) : null;
756
+ const reviewAge = updated ? (0, wiki_diagnostics_1.staleReviewAge)(updated, currentDate) : null;
746
757
  if (status === "active" && reviewAge !== null && /project-canonical|project-decisions|source-summary|wiki-meta/.test(scope)) {
747
758
  diagnostics.push({ code: "stale-review", severity: "warn", file, message: `updated ${reviewAge} days ago: ${updated}` });
748
759
  }
@@ -29,16 +29,16 @@
29
29
  "architecture": "arm64",
30
30
  "format": "elf",
31
31
  "path": "dist/native/linux-arm64-musl/project-librarian-indexer",
32
- "sha256": "80f7440c4d115d05805b63b8118a172ddeb5a374b7154593e5b1d0b804a938ae",
33
- "size": 3100816,
32
+ "sha256": "bcb48020ae87fe3e5ebd6a8d986a91f83e67e5d69c4df9357a7c3a6c78c232b0",
33
+ "size": 3101344,
34
34
  "triple": "linux-arm64-musl"
35
35
  },
36
36
  {
37
37
  "architecture": "x64",
38
38
  "format": "elf",
39
39
  "path": "dist/native/linux-x64/project-librarian-indexer",
40
- "sha256": "3d93408db344567d7de6f477b58fdaf6aba19c943336fb7ab9720e3e9fa68c31",
41
- "size": 3110832,
40
+ "sha256": "22d32884276c23c4597bd611facd5880936b29600c8331bb3d585d2ab370a206",
41
+ "size": 3110688,
42
42
  "triple": "linux-x64"
43
43
  },
44
44
  {
@@ -53,7 +53,7 @@
53
53
  "architecture": "arm64",
54
54
  "format": "pe",
55
55
  "path": "dist/native/win32-arm64/project-librarian-indexer.exe",
56
- "sha256": "8c357be6b1e0c3c13337abccdc0edefc628267b272c8aca30be50c679bffb4af",
56
+ "sha256": "b49ad30a72a9a93ff524cc1643b141c94de4850655ce646d80579edc5b832f71",
57
57
  "size": 2098688,
58
58
  "triple": "win32-arm64"
59
59
  },
@@ -61,7 +61,7 @@
61
61
  "architecture": "x64",
62
62
  "format": "pe",
63
63
  "path": "dist/native/win32-x64/project-librarian-indexer.exe",
64
- "sha256": "f4f9f6424fef446ece0684760afd8a404135acc007794c3e0f3effccdc8b4801",
64
+ "sha256": "36a8416e4975100f89bc57281b311b45158469fb12ea37d33dac2d5855a5e47a",
65
65
  "size": 2375680,
66
66
  "triple": "win32-x64"
67
67
  }
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.staleReviewAgeDays = void 0;
3
+ exports.topologyFanoutThreshold = exports.topologyHubOverloadThreshold = exports.staleReviewAgeDays = void 0;
4
4
  exports.staleReviewAge = staleReviewAge;
5
+ exports.collectTopologyDiagnostics = collectTopologyDiagnostics;
6
+ const wiki_corpus_1 = require("./wiki-corpus");
7
+ const wiki_graph_1 = require("./wiki-graph");
8
+ const workspace_1 = require("./workspace");
5
9
  exports.staleReviewAgeDays = 30;
10
+ exports.topologyHubOverloadThreshold = 60;
11
+ exports.topologyFanoutThreshold = 8;
6
12
  function dateOnlyMillis(value) {
7
13
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
8
14
  return null;
@@ -17,3 +23,107 @@ function staleReviewAge(updated, currentDate) {
17
23
  const ageDays = Math.floor((currentMillis - updatedMillis) / 86_400_000);
18
24
  return ageDays > exports.staleReviewAgeDays ? ageDays : null;
19
25
  }
26
+ function uniqueExisting(values, fileSet) {
27
+ return Array.from(new Set(values.filter((value) => fileSet.has(value)))).sort();
28
+ }
29
+ function isGeneratedScopedRouter(file) {
30
+ return /^wiki\/indexes\/auto-[a-z0-9-]+(?:-\d+)?\.md$/.test(file);
31
+ }
32
+ function isCanonicalTruthPage(file, text) {
33
+ return file.startsWith("wiki/canonical/") && (0, workspace_1.metadataValue)(text, "status") === "active";
34
+ }
35
+ function hasEvidenceClaimSignal(body) {
36
+ return /\b(source-backed|source backed|research-backed|external research|paper-backed|evidence-backed)\b/i.test(body);
37
+ }
38
+ function hasDecisionRefSignal(text) {
39
+ const decisionRef = (0, workspace_1.metadataValue)(text, "decision_ref").trim().toLowerCase();
40
+ return decisionRef !== "" && decisionRef !== "none";
41
+ }
42
+ function hasFocusedAuthoritySignal(text, body) {
43
+ return hasDecisionRefSignal(text) || hasEvidenceClaimSignal(body);
44
+ }
45
+ function hasEvidenceLink(file, corpus, graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus)) {
46
+ const outgoingLinks = graph.outgoingLinks.get(file) ?? [];
47
+ const sourceLink = outgoingLinks.some((link) => link.normalizedTarget.startsWith("wiki/sources/") && corpus.fileSet.has(link.normalizedTarget));
48
+ const decisionRef = graph.outgoingDecisionRef.get(file);
49
+ return sourceLink || Boolean(decisionRef && corpus.fileSet.has(decisionRef));
50
+ }
51
+ function isBroadReviewTrigger(trigger) {
52
+ const normalized = trigger.toLowerCase().trim();
53
+ if (!normalized)
54
+ return false;
55
+ return normalized === "changes"
56
+ || normalized === "project changes"
57
+ || normalized === "routine review"
58
+ || /^any\b.*changes$/.test(normalized)
59
+ || /^general\b.*changes$/.test(normalized);
60
+ }
61
+ function isTopologyHub(file) {
62
+ return file === "wiki/index.md"
63
+ || file.startsWith("wiki/meta/")
64
+ || /^wiki\/indexes\/(?!auto-)[^/]+\.md$/.test(file);
65
+ }
66
+ function collectTopologyDiagnostics(corpus) {
67
+ const diagnostics = [];
68
+ const graph = (0, wiki_corpus_1.wikiCorpusGraph)(corpus);
69
+ for (const file of corpus.files) {
70
+ if (isGeneratedScopedRouter(file))
71
+ continue;
72
+ const outgoing = uniqueExisting((graph.outgoingLinks.get(file) ?? [])
73
+ .map((link) => link.normalizedTarget)
74
+ .filter((target) => target !== file), corpus.fileSet);
75
+ if (isTopologyHub(file) && outgoing.length > exports.topologyHubOverloadThreshold) {
76
+ diagnostics.push({
77
+ code: "hub-overload",
78
+ severity: "warn",
79
+ file,
80
+ message: `${outgoing.length} outgoing wiki links exceed the topology hub threshold ${exports.topologyHubOverloadThreshold}; split or scope the route surface`,
81
+ });
82
+ }
83
+ }
84
+ for (const file of corpus.files) {
85
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
86
+ if (isCanonicalTruthPage(file, text)) {
87
+ const body = (0, workspace_1.stripMetadataHeader)(text);
88
+ const incoming = uniqueExisting((graph.incomingLinks.get(file) ?? [])
89
+ .map((link) => link.file)
90
+ .filter((source) => source !== file), corpus.fileSet);
91
+ if (incoming.length > 0 && incoming.every(isGeneratedScopedRouter) && hasFocusedAuthoritySignal(text, body)) {
92
+ diagnostics.push({
93
+ code: "weak-authority-route",
94
+ severity: "warn",
95
+ file,
96
+ message: "active canonical page with authority signals is routed only by generated auto-index pages; add a focused route when this truth is durable",
97
+ });
98
+ }
99
+ if (hasEvidenceClaimSignal(body) && !hasEvidenceLink(file, corpus, graph)) {
100
+ diagnostics.push({
101
+ code: "missing-evidence-link",
102
+ severity: "warn",
103
+ file,
104
+ message: "canonical page makes a source-backed claim but has no source link or decision_ref evidence link",
105
+ });
106
+ }
107
+ }
108
+ }
109
+ for (const file of corpus.files) {
110
+ if (file === wiki_graph_1.wikiRouterRoot || isGeneratedScopedRouter(file))
111
+ continue;
112
+ const text = (0, wiki_corpus_1.wikiCorpusText)(corpus, file);
113
+ if ((0, workspace_1.metadataValue)(text, "status") !== "active")
114
+ continue;
115
+ const incoming = uniqueExisting((graph.incomingLinks.get(file) ?? [])
116
+ .map((link) => link.file)
117
+ .filter((source) => source !== file), corpus.fileSet);
118
+ const reviewTrigger = (0, workspace_1.metadataValue)(text, "review_trigger");
119
+ if (incoming.length >= exports.topologyFanoutThreshold && isBroadReviewTrigger(reviewTrigger)) {
120
+ diagnostics.push({
121
+ code: "stale-fanout",
122
+ severity: "warn",
123
+ file,
124
+ message: `${incoming.length} incoming links with broad review_trigger "${reviewTrigger}"; tighten review trigger before broad edits`,
125
+ });
126
+ }
127
+ }
128
+ return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code));
129
+ }
@@ -6,6 +6,7 @@ exports.buildWikiGraph = buildWikiGraph;
6
6
  exports.wikiRouterDepths = wikiRouterDepths;
7
7
  exports.wikiQueryGraphEvidence = wikiQueryGraphEvidence;
8
8
  exports.wikiImpactAnswer = wikiImpactAnswer;
9
+ exports.wikiNeighborhoodAnswer = wikiNeighborhoodAnswer;
9
10
  const wiki_files_1 = require("./wiki-files");
10
11
  const workspace_1 = require("./workspace");
11
12
  // Router reachability budget. The benchmark fixture A1 assert guarantees
@@ -163,3 +164,125 @@ function wikiImpactAnswer(pages, term, graph = buildWikiGraph(pages)) {
163
164
  }
164
165
  return finalizeWikiAnswer(lines.join("\n"));
165
166
  }
167
+ const neighborhoodReadCap = 5;
168
+ function pageClassPriority(file, scope) {
169
+ if (file.startsWith("wiki/decisions/") || /decision/.test(scope))
170
+ return 90;
171
+ if (file.startsWith("wiki/sources/") || /source/.test(scope))
172
+ return 80;
173
+ if (file.startsWith("wiki/canonical/") || /canonical/.test(scope))
174
+ return 70;
175
+ if (file.startsWith("wiki/plans/") || /plan/.test(scope))
176
+ return 50;
177
+ if (file.startsWith("wiki/roadmaps/") || /roadmap/.test(scope))
178
+ return 40;
179
+ if (file.startsWith("wiki/meta/") || /wiki-meta/.test(scope))
180
+ return 30;
181
+ return 10;
182
+ }
183
+ function neighborhoodReasonRank(reason) {
184
+ if (reason === "exact page match")
185
+ return 1000;
186
+ if (reason === "title/path match")
187
+ return 900;
188
+ if (reason === "decision_ref target")
189
+ return 800;
190
+ if (reason === "decision_ref citation")
191
+ return 760;
192
+ if (reason === "outgoing link target")
193
+ return 700;
194
+ if (reason === "incoming link source")
195
+ return 650;
196
+ return 0;
197
+ }
198
+ function isGeneratedScopedRouter(file) {
199
+ return /^wiki\/indexes\/auto-[a-z0-9-]+(?:-\d+)?\.md$/.test(file);
200
+ }
201
+ function isRouterOnlySurface(file) {
202
+ return file === exports.wikiRouterRoot || file === "wiki/index.md" || isGeneratedScopedRouter(file);
203
+ }
204
+ function addNeighborhoodCandidate(candidates, file, reason, bestFile, graph) {
205
+ if (!graph.files.has(file) || (file !== bestFile && isRouterOnlySurface(file)))
206
+ return;
207
+ const existing = candidates.get(file);
208
+ if (!existing || neighborhoodReasonRank(reason) > neighborhoodReasonRank(existing.reason)) {
209
+ candidates.set(file, { file, reason });
210
+ }
211
+ }
212
+ function wikiNeighborhoodAnswer(pages, term, graph = buildWikiGraph(pages)) {
213
+ const depths = wikiRouterDepths(graph);
214
+ const textByFile = new Map(pages.map((page) => [page.file, page.text]));
215
+ const lowered = term.toLowerCase();
216
+ const exactTarget = (0, wiki_files_1.normalizeWikiLinkTarget)("wiki/index.md", term.replace(/^\[\[|\]\]$/g, ""));
217
+ const matches = pages
218
+ .map((page) => ({ file: page.file, title: (0, wiki_files_1.wikiTitleForFile)(page.file, page.text), text: page.text }))
219
+ .filter((page) => page.file === exactTarget || page.file.toLowerCase().includes(lowered) || page.title.toLowerCase().includes(lowered))
220
+ .sort((a, b) => {
221
+ const exactDelta = Number(b.file === exactTarget) - Number(a.file === exactTarget);
222
+ if (exactDelta !== 0)
223
+ return exactDelta;
224
+ const titleDelta = Number(b.title.toLowerCase().includes(lowered)) - Number(a.title.toLowerCase().includes(lowered));
225
+ if (titleDelta !== 0)
226
+ return titleDelta;
227
+ const aScope = (0, workspace_1.metadataValue)(a.text, "scope");
228
+ const bScope = (0, workspace_1.metadataValue)(b.text, "scope");
229
+ return pageClassPriority(b.file, bScope) - pageClassPriority(a.file, aScope) || a.file.localeCompare(b.file);
230
+ });
231
+ if (matches.length === 0)
232
+ return `Wiki neighborhood "${term}": no matching wiki pages.`;
233
+ const best = matches[0];
234
+ const candidates = new Map();
235
+ addNeighborhoodCandidate(candidates, best.file, best.file === exactTarget ? "exact page match" : "title/path match", best.file, graph);
236
+ const outgoingRef = graph.outgoingDecisionRef.get(best.file);
237
+ if (outgoingRef)
238
+ addNeighborhoodCandidate(candidates, outgoingRef, "decision_ref target", best.file, graph);
239
+ for (const source of graph.incomingDecisionRefs.get(best.file) ?? [])
240
+ addNeighborhoodCandidate(candidates, source, "decision_ref citation", best.file, graph);
241
+ for (const link of graph.outgoingLinks.get(best.file) ?? [])
242
+ addNeighborhoodCandidate(candidates, link.normalizedTarget, "outgoing link target", best.file, graph);
243
+ for (const link of graph.incomingLinks.get(best.file) ?? [])
244
+ addNeighborhoodCandidate(candidates, link.file, "incoming link source", best.file, graph);
245
+ const sorted = Array.from(candidates.values()).sort((a, b) => {
246
+ const reasonDelta = neighborhoodReasonRank(b.reason) - neighborhoodReasonRank(a.reason);
247
+ if (reasonDelta !== 0)
248
+ return reasonDelta;
249
+ const aText = textByFile.get(a.file) ?? "";
250
+ const bText = textByFile.get(b.file) ?? "";
251
+ const classDelta = pageClassPriority(b.file, (0, workspace_1.metadataValue)(bText, "scope")) - pageClassPriority(a.file, (0, workspace_1.metadataValue)(aText, "scope"));
252
+ if (classDelta !== 0)
253
+ return classDelta;
254
+ const aDepth = depths.get(a.file) ?? Number.POSITIVE_INFINITY;
255
+ const bDepth = depths.get(b.file) ?? Number.POSITIVE_INFINITY;
256
+ return aDepth - bDepth || a.file.localeCompare(b.file);
257
+ }).slice(0, neighborhoodReadCap);
258
+ const lines = [
259
+ `Wiki neighborhood "${term}": best match ${best.file} — ${best.title}; read ${sorted.length} nearby page${sorted.length === 1 ? "" : "s"}.`,
260
+ "",
261
+ "Read order:",
262
+ ];
263
+ sorted.forEach((candidate, index) => {
264
+ const text = textByFile.get(candidate.file) ?? "";
265
+ const depth = depths.get(candidate.file);
266
+ const status = (0, workspace_1.metadataValue)(text, "status") || "unknown";
267
+ const scope = (0, workspace_1.metadataValue)(text, "scope") || "unknown-scope";
268
+ lines.push(`${index + 1}. ${candidate.file} — ${candidate.reason}; ${depth === undefined ? "router unreachable" : `router depth ${depth}`}; ${status} ${scope}`);
269
+ });
270
+ const incoming = uniqueSorted((graph.incomingLinks.get(best.file) ?? [])
271
+ .map((link) => link.file)
272
+ .filter((source) => source !== best.file && graph.files.has(source)));
273
+ const outgoing = uniqueSorted((graph.outgoingLinks.get(best.file) ?? [])
274
+ .map((link) => link.normalizedTarget)
275
+ .filter((target) => target !== best.file && graph.files.has(target)));
276
+ const incomingRefs = uniqueSorted((graph.incomingDecisionRefs.get(best.file) ?? [])
277
+ .filter((source) => source !== best.file && graph.files.has(source)));
278
+ const reviewTrigger = (0, workspace_1.metadataValue)(best.text, "review_trigger");
279
+ lines.push("");
280
+ lines.push("Why:");
281
+ lines.push(`- incoming links: ${sampled(incoming, impactListCap)}`);
282
+ lines.push(`- outgoing links: ${sampled(outgoing, impactListCap)}`);
283
+ lines.push(`- decision_ref: ${outgoingRef && graph.files.has(outgoingRef) ? outgoingRef : "none"}`);
284
+ lines.push(`- decision_ref citations: ${sampled(incomingRefs, impactListCap)}`);
285
+ if (reviewTrigger)
286
+ lines.push(`- review_trigger: ${reviewTrigger}`);
287
+ return finalizeWikiAnswer(lines.join("\n"));
288
+ }
@@ -17,17 +17,33 @@ Generated benchmark reports under `benchmarks/reports/llm/` are ignored by defau
17
17
 
18
18
  ## Wiki Track
19
19
 
20
- Cost-weighted tokens, Project Librarian vs control:
21
-
22
- | Scale | decision_lookup | aggregation | multi_session (2nd session) |
23
- | --- | --- | --- | --- |
24
- | Small | 14.4% less | 81.0% more | 22.0% less |
25
- | Medium | 52.0% less | 19.0% less | 54.1% less |
26
- | Large | 71.1% less | 29.0% less | 71.8% less |
27
-
28
- Latest synthetic wiki-track release candidate: 2026-06-19, `gpt-5.5`, 42 scenarios, 3 measured runs plus 1 warmup each. The overall claim gate **passed**: 42/42 scenarios passed correctness, all 42 scenarios were claimable, and every corpus gate met the 3-run minimum.
29
-
30
- The release claim is bounded to the synthetic wiki-routing track and the listed task families. It is not a claim about code-graph behavior, real repositories, every agent surface, or every question shape. Published boundaries remain visible: small `aggregation` still costs 81.0% more with the wiki, small `release_policy` costs 9.4% more in the full report, and `aggregation` stays slower at every scale even when token cost drops.
20
+ Latest clean synthetic wiki-track release evidence: 2026-06-29, `gpt-5.5`, branch `perf/small-repo-code-evidence-safeguards` at `ae79390`, 42 scenarios, 21 with/without pairs, 3 measured runs plus 1 warmup each. The overall claim gate **passed**, and the claim ledger classified the report as release-claimable. A repair run reused 125 claimable measured runs from retained raw JSONL and remeasured the remaining failed slot.
21
+
22
+ Cost-weighted tokens, Project Librarian vs control, all scales combined:
23
+
24
+ | Task family | Delta |
25
+ | --- | ---: |
26
+ | onboarding | 65.95% less |
27
+ | decision_lookup | 48.46% less |
28
+ | code_impact | 55.61% less |
29
+ | release_policy | 58.22% less |
30
+ | change_location | 29.59% less |
31
+ | multi_session | 52.58% less |
32
+ | aggregation | 42.53% less |
33
+
34
+ Scale-specific cost-weighted token deltas:
35
+
36
+ | Task family | Small | Medium | Large |
37
+ | --- | ---: | ---: | ---: |
38
+ | onboarding | 60.88% less | 65.53% less | 69.17% less |
39
+ | decision_lookup | 19.18% less | 51.29% less | 60.12% less |
40
+ | code_impact | 26.61% less | 54.81% less | 68.43% less |
41
+ | release_policy | 30.23% less | 62.16% less | 67.26% less |
42
+ | change_location | 7.79% less | 2.86% more | 57.27% less |
43
+ | multi_session | 54.29% more | 35.32% less | 82.77% less |
44
+ | aggregation | 7.66% less | 51.32% less | 49.17% less |
45
+
46
+ The release claim is bounded to the synthetic wiki-routing track and the listed task families. It is not a claim about code-graph behavior, real repositories, every agent surface, or every question shape. Published boundaries remain visible: small `multi_session` and medium `change_location` cost more with the wiki in the cost-weighted token metric. `code_impact`, `change_location`, and `aggregation` also still have wall-time or command-count regressions in this report, even though token and output-byte metrics improved at the task-family aggregate level.
31
47
 
32
48
  ## Code-Graph Track
33
49
 
@@ -52,7 +68,11 @@ Repositories under test:
52
68
 
53
69
  Question types:
54
70
 
71
+ - **onboarding** - summarize what the project is, current risks, and where to read next.
55
72
  - **decision_lookup** - find the latest project decision and its date from the wiki.
73
+ - **code_impact** - identify likely impacted files or areas for a benchmark report schema change.
74
+ - **release_policy** - find the checks required before publishing or making benchmark claims.
75
+ - **change_location** - find where an agent should edit to implement the Codex LLM benchmark runner.
56
76
  - **aggregation** - answer a question whose facts are scattered across several pages and must be synthesized.
57
77
  - **multi_session** - a second session on the same project, measuring whether the durable wiki helps the next session, not just the first.
58
78
  - **impact_trace** - trace the full set of direct and indirect importers for a changing module.
@@ -19,7 +19,7 @@ node .codex/skills/project-librarian/dist/init-project-wiki.js install [--scope
19
19
  | `update --agents <list>` | Refresh an existing setup and existing project-scoped skill copies; selected surfaces can be `codex`, `claude`, `cursor`, `gemini`, or `all`. |
20
20
  | `--migrate`, `--adopt-existing` | Preserve an existing wiki as `wiki_legacy*`, create migration inboxes, and generate unit-map/split-plan/coverage review files. |
21
21
  | `--lint` | Validate generated setup without editing files. |
22
- | `--link-check` | Report broken wiki links, duplicate routes, orphan pages, and pages the startup router cannot reach within the depth budget. |
22
+ | `--link-check` | Report broken wiki links, duplicate routes, orphan pages, router reachability, and warning-only topology signals. |
23
23
  | `--quality-check` | Report stale, conflicting, and low-quality wiki document signals. |
24
24
  | `--doctor` | Run lint, link-check, and quality-check together. |
25
25
  | `--doctor --fix` | Safely refresh generated index routing before diagnostics. `--fix` is only a modifier for `--doctor`. |
@@ -28,8 +28,7 @@ node .codex/skills/project-librarian/dist/init-project-wiki.js install [--scope
28
28
  | `--migration-doctor` | Run migration-lint and migration-quality-check together. |
29
29
  | `--query <terms>` | Search wiki paths, metadata, titles, and bodies; answer-first output with per-page TL;DR lines under a hard size cap. |
30
30
  | `--wiki-impact <page-or-term>` | Show wiki backlinks, `decision_ref` citations, outgoing links, and router depth for matching pages. |
31
- | `--wiki-visualize` | Write a self-contained static wiki graph visualizer to `.project-wiki/wiki-graph.html`. |
32
- | `--wiki-visualize-out <path>` | With `--wiki-visualize`, write to a custom repository-relative path under `.project-wiki/`. |
31
+ | `--wiki-neighborhood <page-or-term>` | Show a bounded read order for nearby wiki pages using links, `decision_ref`, metadata, page class, and router depth. |
33
32
  | `--refresh-index` | Update generated auto-discovered wiki routing. |
34
33
  | `--capture-inbox --title <title> --content <content> --category <category>` | Append a candidate note to the wiki inbox; category defaults to `project-candidate`. |
35
34
  | `--handoff-save --goal <goal> --state <state> --next <action>` | Save generated local session handoff state under `.project-wiki/session/`. Repeat `--next`, `--decision`, `--blocked`, `--open-question`, `--verification`, `--last-success-command`, and `--last-failure-command` as needed. |
@@ -59,3 +58,14 @@ node .codex/skills/project-librarian/dist/init-project-wiki.js install [--scope
59
58
  | `--code-context-pack <term>` | Print a budgeted first-pass context pack with structural file, symbol, route, import, edge, and ownership evidence. |
60
59
  | `--code-search-symbol <term>` | Search indexed symbols. |
61
60
  | `--code-query <sql>` | Run conservative read-only SQL over the evidence index. |
61
+
62
+ ### Topology Warnings
63
+
64
+ `--link-check` keeps topology findings warning-only so they can guide cleanup without blocking bootstrap, update, or release flows.
65
+
66
+ | Code | Meaning |
67
+ | --- | --- |
68
+ | `hub-overload` | A hand-maintained router or meta page links to too many wiki pages and should be split or scoped. |
69
+ | `weak-authority-route` | An active canonical page with decision or evidence authority signals is reachable only through generated auto-index routing. |
70
+ | `missing-evidence-link` | An active canonical page makes a source-backed claim without a source link or `decision_ref`. |
71
+ | `stale-fanout` | A heavily linked active page has a broad review trigger that is too weak for topology-sensitive edits. |