project-librarian 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,8 @@ exports.ignoredDirs = exports.standardWikiFiles = void 0;
37
37
  exports.walkMarkdownFiles = walkMarkdownFiles;
38
38
  exports.firstHeading = firstHeading;
39
39
  exports.compactSummary = compactSummary;
40
+ exports.markdownBlockSnippet = markdownBlockSnippet;
41
+ exports.extractMarkdownBlocks = extractMarkdownBlocks;
40
42
  exports.splitMarkdownRow = splitMarkdownRow;
41
43
  exports.parseMarkdownTableRows = parseMarkdownTableRows;
42
44
  exports.wikiMarkdownFiles = wikiMarkdownFiles;
@@ -52,6 +54,7 @@ exports.firstTldrBullet = firstTldrBullet;
52
54
  exports.canonicalBodyForLint = canonicalBodyForLint;
53
55
  const fs = __importStar(require("node:fs"));
54
56
  const path = __importStar(require("node:path"));
57
+ const path_ignore_policy_1 = require("./path-ignore-policy");
55
58
  const workspace_1 = require("./workspace");
56
59
  exports.standardWikiFiles = new Set([
57
60
  "AGENTS.md",
@@ -97,7 +100,7 @@ exports.standardWikiFiles = new Set([
97
100
  "tools/project-librarian/agents/openai.yaml",
98
101
  "tools/project-librarian/dist/init-project-wiki.js",
99
102
  ]);
100
- exports.ignoredDirs = new Set([".git", ".codex", ".claude", ".cursor", ".gemini", "node_modules", ".next", "dist", "build", "coverage", "vendor", "tmp", "temp"]);
103
+ exports.ignoredDirs = (0, path_ignore_policy_1.ignoredDirectorySet)();
101
104
  function walkMarkdownFiles(dir = workspace_1.root, acc = [], baseDir = workspace_1.root) {
102
105
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
103
106
  const fullPath = path.join(dir, entry.name);
@@ -134,6 +137,109 @@ function compactSummary(text) {
134
137
  .trim()
135
138
  .slice(0, 180);
136
139
  }
140
+ function slugForBlockId(value) {
141
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48) || "block";
142
+ }
143
+ function isMarkdownHeading(line) {
144
+ return line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
145
+ }
146
+ function isMarkdownTableSeparator(cells) {
147
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s/g, "")));
148
+ }
149
+ function normalizeBlockText(text) {
150
+ return text.replace(/\s+/g, " ").trim();
151
+ }
152
+ function blockId(kind, line, text) {
153
+ return `${kind}:${line}:${slugForBlockId(text)}`;
154
+ }
155
+ function markdownBlockSnippet(block, maxLength = 180) {
156
+ const prefix = block.headingPath.length > 0 && block.kind !== "heading" ? `${block.headingPath.join(" > ")}: ` : "";
157
+ return `${prefix}${normalizeBlockText(block.text)}`.slice(0, maxLength);
158
+ }
159
+ function extractMarkdownBlocks(text) {
160
+ const body = (0, workspace_1.stripMetadataHeader)(text);
161
+ const lines = body.split(/\r?\n/);
162
+ const blocks = [];
163
+ const headingPath = [];
164
+ let paragraph = null;
165
+ let fence = null;
166
+ function addBlock(kind, line, blockText) {
167
+ const normalized = normalizeBlockText(blockText);
168
+ if (!normalized)
169
+ return;
170
+ blocks.push({
171
+ headingPath: [...headingPath],
172
+ id: blockId(kind, line, normalized),
173
+ kind,
174
+ line,
175
+ text: normalized,
176
+ });
177
+ }
178
+ function flushParagraph() {
179
+ if (!paragraph)
180
+ return;
181
+ addBlock("paragraph", paragraph.line, paragraph.lines.join(" "));
182
+ paragraph = null;
183
+ }
184
+ lines.forEach((line, index) => {
185
+ const lineNumber = index + 1;
186
+ const trimmed = line.trim();
187
+ const fenceMatch = trimmed.match(/^(```+|~~~+)\s*(.*)$/);
188
+ if (fence) {
189
+ const closingFence = fenceMatch?.[1] ?? "";
190
+ if (closingFence.startsWith(fence.fence.slice(0, 3)) && closingFence.length >= fence.fence.length) {
191
+ const sample = fence.lines.map((item) => item.trim()).filter(Boolean).slice(0, 3).join(" ");
192
+ addBlock("code_fence", fence.line, `code fence${fence.lang ? ` ${fence.lang}` : ""}: ${sample}`);
193
+ fence = null;
194
+ }
195
+ else {
196
+ fence.lines.push(line);
197
+ }
198
+ return;
199
+ }
200
+ if (fenceMatch) {
201
+ flushParagraph();
202
+ fence = { fence: fenceMatch[1] ?? "```", lang: (fenceMatch[2] ?? "").trim(), line: lineNumber, lines: [] };
203
+ return;
204
+ }
205
+ if (!trimmed) {
206
+ flushParagraph();
207
+ return;
208
+ }
209
+ const heading = isMarkdownHeading(line);
210
+ if (heading?.[1] && heading[2]) {
211
+ flushParagraph();
212
+ const level = heading[1].length;
213
+ const title = heading[2].trim();
214
+ headingPath.splice(level - 1);
215
+ headingPath[level - 1] = title;
216
+ addBlock("heading", lineNumber, title);
217
+ return;
218
+ }
219
+ if (/^\s{0,3}([-*+]|\d+\.)\s+\S/.test(line)) {
220
+ flushParagraph();
221
+ addBlock("list_item", lineNumber, trimmed);
222
+ return;
223
+ }
224
+ if (/^\|.+\|$/.test(trimmed)) {
225
+ flushParagraph();
226
+ const cells = splitMarkdownRow(line);
227
+ if (!isMarkdownTableSeparator(cells))
228
+ addBlock("table_row", lineNumber, cells.join(" | "));
229
+ return;
230
+ }
231
+ if (!paragraph)
232
+ paragraph = { line: lineNumber, lines: [] };
233
+ paragraph.lines.push(trimmed);
234
+ });
235
+ const unclosedFence = fence;
236
+ if (unclosedFence) {
237
+ const sample = unclosedFence.lines.map((item) => item.trim()).filter(Boolean).slice(0, 3).join(" ");
238
+ addBlock("code_fence", unclosedFence.line, `code fence${unclosedFence.lang ? ` ${unclosedFence.lang}` : ""}: ${sample}`);
239
+ }
240
+ flushParagraph();
241
+ return blocks;
242
+ }
137
243
  function splitMarkdownRow(line) {
138
244
  const trimmed = line.trim();
139
245
  const row = trimmed.replace(/^\|/, "").replace(/\|$/, "");
@@ -4,6 +4,7 @@ exports.wikiAnswerTruncationNotice = exports.wikiAnswerCharCap = exports.wikiRou
4
4
  exports.finalizeWikiAnswer = finalizeWikiAnswer;
5
5
  exports.buildWikiGraph = buildWikiGraph;
6
6
  exports.wikiRouterDepths = wikiRouterDepths;
7
+ exports.wikiQueryGraphEvidence = wikiQueryGraphEvidence;
7
8
  exports.wikiImpactAnswer = wikiImpactAnswer;
8
9
  const wiki_files_1 = require("./wiki-files");
9
10
  const workspace_1 = require("./workspace");
@@ -99,8 +100,31 @@ function uniqueSorted(values) {
99
100
  function plural(count, noun) {
100
101
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
101
102
  }
102
- function wikiImpactAnswer(pages, term) {
103
- const graph = buildWikiGraph(pages);
103
+ function wikiQueryGraphEvidence(graph, file, depths = wikiRouterDepths(graph), listCap = 3) {
104
+ const outgoing = uniqueSorted((graph.outgoingLinks.get(file) ?? [])
105
+ .map((link) => link.normalizedTarget)
106
+ .filter((target) => target !== file && graph.files.has(target)));
107
+ const incoming = uniqueSorted((graph.incomingLinks.get(file) ?? [])
108
+ .map((link) => link.file)
109
+ .filter((source) => source !== file && graph.files.has(source)));
110
+ const incomingRefs = uniqueSorted((graph.incomingDecisionRefs.get(file) ?? [])
111
+ .filter((source) => source !== file && graph.files.has(source)));
112
+ const outgoingRef = graph.outgoingDecisionRef.get(file);
113
+ const depth = depths.get(file);
114
+ const parts = [
115
+ depth === undefined ? `router unreachable from ${exports.wikiRouterRoot}` : `router depth ${depth}`,
116
+ ];
117
+ if (outgoing.length > 0)
118
+ parts.push(`links-out ${outgoing.length}: ${sampled(outgoing, listCap)}`);
119
+ if (outgoingRef && outgoingRef !== file && graph.files.has(outgoingRef))
120
+ parts.push(`decision_ref-> ${outgoingRef}`);
121
+ if (incoming.length > 0)
122
+ parts.push(`linked-by ${incoming.length}: ${sampled(incoming, listCap)}`);
123
+ if (incomingRefs.length > 0)
124
+ parts.push(`decision_ref-by ${incomingRefs.length}: ${sampled(incomingRefs, listCap)}`);
125
+ return parts.join("; ");
126
+ }
127
+ function wikiImpactAnswer(pages, term, graph = buildWikiGraph(pages)) {
104
128
  const depths = wikiRouterDepths(graph);
105
129
  const textByFile = new Map(pages.map((page) => [page.file, page.text]));
106
130
  const lowered = term.toLowerCase();