@agentskit/doc-bridge 0.1.0-alpha.2 → 0.1.0-alpha.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.3
4
+
5
+ Dogfood round-2 fixes (search ranking, full-text body, peers, federation soft-fail).
6
+
7
+ ### Fixes
8
+ - **Search ranking:** exact id / basename boost; ownership preferred for routing questions; path dedupe
9
+ - **Full-text search:** knowledge entries store `body` excerpt; descriptions prefer frontmatter `purpose` and complete sentences
10
+ - **ask:** next command prefers ownership match over knowledge-only
11
+ - **Text UX:** multi-line search/ask matches (`[type] id`, path, summary)
12
+ - **Federation:** missing/404 remote `llms.txt` soft-skipped (no hard fail)
13
+ - **Peers:** optional peer ranges widened (`@agentskit/core` `>=1.0`, adapters `>=0.12`) so Layer 0 install is not blocked
14
+ - **humanDoc:** more aliases (`packages/id`, `reference/packages/id`, path suffixes)
15
+
3
16
  ## 0.1.0-alpha.2
4
17
 
5
18
  Dogfood-driven polish after ecosystem install on agentskit, agentskit-os, playbook, and registry.
package/README.md CHANGED
@@ -121,7 +121,7 @@ Contract: [`docs/spec/config-v1.md`](docs/spec/config-v1.md) · CLI: [`docs/spec
121
121
 
122
122
  ## Status
123
123
 
124
- **v0.1.0-alpha.2** — Dogfood polish (pnpm-aware checks, corpus ownership inference, playbook gate preset, git prepare build). Layer 1 RAG/chat via optional peers.
124
+ **v0.1.0-alpha.3** — Dogfood polish (pnpm-aware checks, corpus ownership inference, playbook gate preset, git prepare build). Layer 1 RAG/chat via optional peers.
125
125
 
126
126
  ```bash
127
127
  pnpm install && pnpm build && pnpm test
@@ -469,7 +469,7 @@ var firstHeading = (markdown) => {
469
469
  }
470
470
  return void 0;
471
471
  };
472
- var firstParagraph = (markdown) => {
472
+ var firstParagraph = (markdown, maxLen = 400) => {
473
473
  const { body } = parseFrontmatter(markdown);
474
474
  const lines = body.split("\n");
475
475
  const buf = [];
@@ -481,11 +481,27 @@ var firstParagraph = (markdown) => {
481
481
  }
482
482
  if (t.startsWith("#")) continue;
483
483
  if (t.startsWith("---")) continue;
484
+ if (t.startsWith("```")) break;
485
+ if (t.startsWith("|") || t.startsWith("- [") || t.startsWith("* [")) {
486
+ if (buf.length) break;
487
+ continue;
488
+ }
484
489
  buf.push(t);
485
- if (buf.join(" ").length > 40) break;
486
- }
487
- const text = buf.join(" ").trim();
488
- return text || void 0;
490
+ if (buf.join(" ").length >= maxLen) break;
491
+ }
492
+ let text = buf.join(" ").replace(/\s+/g, " ").trim();
493
+ if (!text) return void 0;
494
+ if (text.length <= maxLen) return text;
495
+ const sliced = text.slice(0, maxLen);
496
+ const sentenceEnd = Math.max(sliced.lastIndexOf(". "), sliced.lastIndexOf("! "), sliced.lastIndexOf("? "));
497
+ if (sentenceEnd > maxLen * 0.4) return sliced.slice(0, sentenceEnd + 1).trim();
498
+ const wordEnd = sliced.lastIndexOf(" ");
499
+ return (wordEnd > 0 ? sliced.slice(0, wordEnd) : sliced).trim();
500
+ };
501
+ var extractSearchBody = (markdown, maxLen = 6e3) => {
502
+ const { body } = parseFrontmatter(markdown);
503
+ const text = body.replace(/```[\s\S]*?```/g, " ").replace(/!\[[^\]]*\]\([^)]+\)/g, " ").replace(/\[[^\]]*\]\([^)]+\)/g, " ").replace(/^#+\s+/gm, "").replace(/[|>*_`#]/g, " ").replace(/\s+/g, " ").trim();
504
+ return text.length > maxLen ? text.slice(0, maxLen) : text;
489
505
  };
490
506
  var slugFromPath = (relPath) => {
491
507
  const base = relPath.replace(/\.mdx?$/, "");
@@ -543,7 +559,9 @@ var scanAgentCorpus = (root, config) => {
543
559
  const { data: frontmatter } = parseFrontmatter(raw);
544
560
  const id = frontmatterString(frontmatter, "id") ?? frontmatterString(frontmatter, "package") ?? slugFromPath(relToCorpus);
545
561
  const title = firstHeading(raw) ?? id;
546
- const description = firstParagraph(raw);
562
+ const purpose = frontmatterString(frontmatter, "purpose");
563
+ const description = purpose ?? firstParagraph(raw, 400);
564
+ const body = extractSearchBody(raw);
547
565
  return {
548
566
  id,
549
567
  type: "agent-doc",
@@ -552,7 +570,8 @@ var scanAgentCorpus = (root, config) => {
552
570
  absPath: abs,
553
571
  relPath,
554
572
  frontmatter,
555
- ...description ? { description } : {}
573
+ ...description ? { description } : {},
574
+ ...body ? { body } : {}
556
575
  };
557
576
  });
558
577
  };
@@ -685,11 +704,19 @@ var resolveHumanDoc = (packageId, override, fmHuman, humanDocs = {}) => {
685
704
  packageId,
686
705
  packageId.replace(/^@[^/]+\//, ""),
687
706
  packageId.replace(/^os-/, ""),
688
- packageId.replace(/-pattern$/, "")
707
+ packageId.replace(/-pattern$/, ""),
708
+ `packages/${packageId}`,
709
+ `reference/packages/${packageId}`,
710
+ `packages/${packageId}/index`
689
711
  ];
690
712
  for (const alias of aliases) {
691
713
  if (humanDocs[alias]) return humanDocs[alias];
692
714
  }
715
+ for (const [key, url] of Object.entries(humanDocs)) {
716
+ if (key === packageId || key.endsWith(`/${packageId}`) || key.endsWith(`/${packageId}/index`) || key.endsWith(`-${packageId}`)) {
717
+ return url;
718
+ }
719
+ }
693
720
  return void 0;
694
721
  };
695
722
  var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root = process.cwd()) => {
@@ -775,7 +802,7 @@ var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root
775
802
  };
776
803
 
777
804
  // src/version.ts
778
- var PACKAGE_VERSION = "0.1.0-alpha.2";
805
+ var PACKAGE_VERSION = "0.1.0-alpha.3";
779
806
 
780
807
  // src/index-builder/capabilities.ts
781
808
  var renderCapabilitiesJson = (config, index, paths) => {
@@ -1252,19 +1279,58 @@ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
1252
1279
  import { resolve as resolve3 } from "path";
1253
1280
 
1254
1281
  // src/query/search.ts
1255
- var tokenize = (value) => value.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
1282
+ var tokenize = (value) => value.toLowerCase().split(/[^a-z0-9@/_-]+/).filter((t) => t.length >= 2);
1283
+ var PACKAGE_INTENT = /\b(package|module|pkg|edit|change|where|owns?|ownership|handoff|start)\b/i;
1284
+ var scoreHay = (tokens, hay, weight = 1) => {
1285
+ let score = 0;
1286
+ for (const token of tokens) {
1287
+ if (!hay.includes(token)) continue;
1288
+ score += token.length * weight;
1289
+ if (new RegExp(`(?:^|[^a-z0-9])${token}(?:[^a-z0-9]|$)`).test(hay)) {
1290
+ score += token.length;
1291
+ }
1292
+ }
1293
+ return score;
1294
+ };
1295
+ var identityBoost = (id, path, tokens, term) => {
1296
+ const idLower = id.toLowerCase();
1297
+ const termLower = term.toLowerCase().trim();
1298
+ const base = path.split("/").pop()?.replace(/\.mdx?$/i, "").toLowerCase() ?? "";
1299
+ let boost = 0;
1300
+ if (idLower === termLower || base === termLower) boost += 200;
1301
+ if (tokens.length === 1 && (idLower === tokens[0] || base === tokens[0])) boost += 200;
1302
+ for (const token of tokens) {
1303
+ if (idLower === token) boost += 120;
1304
+ else if (idLower.startsWith(`${token}-`) || idLower.endsWith(`-${token}`)) boost += 40;
1305
+ else if (idLower.includes(token) && idLower.length <= token.length + 4) boost += 30;
1306
+ if (base === token) boost += 100;
1307
+ }
1308
+ if (tokens.includes(idLower)) boost += Math.max(0, 40 - idLower.length);
1309
+ return boost;
1310
+ };
1311
+ var preferOwnership = (term) => PACKAGE_INTENT.test(term) || /^(where|how).*(edit|change|package|module)/i.test(term);
1256
1312
  var searchIndex = (index, term, limit = 20) => {
1257
1313
  const tokens = tokenize(term);
1258
1314
  if (!tokens.length) return [];
1259
- const matches = [];
1260
- for (const entry of index.knowledge) {
1261
- const hay = `${entry.id} ${entry.title} ${entry.description ?? ""} ${entry.path}`.toLowerCase();
1262
- let score = 0;
1263
- for (const token of tokens) {
1264
- if (hay.includes(token)) score += token.length;
1315
+ const wantOwnership = preferOwnership(term);
1316
+ const byPath = /* @__PURE__ */ new Map();
1317
+ const consider = (match) => {
1318
+ const key = match.path;
1319
+ const existing = byPath.get(key);
1320
+ if (!existing) {
1321
+ byPath.set(key, match);
1322
+ return;
1265
1323
  }
1324
+ const prefer = match.score > existing.score || match.score === existing.score && match.type === "ownership" && existing.type !== "ownership" || wantOwnership && match.type === "ownership" && existing.type !== "ownership" && match.score >= existing.score - 20;
1325
+ if (prefer) byPath.set(key, match);
1326
+ };
1327
+ for (const entry of index.knowledge) {
1328
+ const body = entry.body ?? "";
1329
+ const hay = `${entry.id} ${entry.title} ${entry.description ?? ""} ${entry.path} ${body}`.toLowerCase();
1330
+ let score = scoreHay(tokens, hay, 1);
1331
+ score += identityBoost(entry.id, entry.path, tokens, term);
1266
1332
  if (score > 0) {
1267
- matches.push({
1333
+ consider({
1268
1334
  type: "knowledge",
1269
1335
  id: entry.id,
1270
1336
  path: entry.path,
@@ -1274,22 +1340,31 @@ var searchIndex = (index, term, limit = 20) => {
1274
1340
  }
1275
1341
  }
1276
1342
  for (const [id, owner] of Object.entries(index.lookup?.ownership ?? {})) {
1277
- const hay = `${id} ${owner.path} ${owner.purpose ?? ""} ${owner.group ?? ""}`.toLowerCase();
1278
- let score = 0;
1279
- for (const token of tokens) {
1280
- if (hay.includes(token)) score += token.length * 2;
1281
- }
1343
+ const path = owner.agentDoc ?? owner.path;
1344
+ const hay = `${id} ${owner.path} ${owner.purpose ?? ""} ${owner.group ?? ""} ${owner.agentDoc ?? ""} ${owner.humanDoc ?? ""}`.toLowerCase();
1345
+ let score = scoreHay(tokens, hay, 2);
1346
+ score += identityBoost(id, path, tokens, term);
1347
+ if (wantOwnership) score += 25;
1348
+ score += 15;
1282
1349
  if (score > 0) {
1283
- matches.push({
1350
+ consider({
1284
1351
  type: "ownership",
1285
1352
  id,
1286
- path: owner.agentDoc ?? owner.path,
1353
+ path,
1287
1354
  ...owner.purpose ? { summary: owner.purpose } : {},
1288
1355
  score
1289
1356
  });
1290
1357
  }
1291
1358
  }
1292
- return matches.sort((a, b) => b.score - a.score).slice(0, limit);
1359
+ return [...byPath.values()].sort((a, b) => {
1360
+ if (b.score !== a.score) return b.score - a.score;
1361
+ const aExact = tokens.includes(a.id.toLowerCase()) ? 1 : 0;
1362
+ const bExact = tokens.includes(b.id.toLowerCase()) ? 1 : 0;
1363
+ if (bExact !== aExact) return bExact - aExact;
1364
+ if (a.type === "ownership" && b.type !== "ownership") return -1;
1365
+ if (b.type === "ownership" && a.type !== "ownership") return 1;
1366
+ return a.id.localeCompare(b.id);
1367
+ }).slice(0, limit);
1293
1368
  };
1294
1369
 
1295
1370
  // src/retriever/doc-bridge-retriever.ts
@@ -1328,10 +1403,14 @@ var defaultFetchText = async (url) => {
1328
1403
  return res.text();
1329
1404
  };
1330
1405
  var sourceText = async (root, source, fetchText) => {
1331
- if (/^https?:\/\//.test(source)) return fetchText(source);
1332
- const path = resolve3(root, source);
1333
- if (!existsSync7(path)) throw new Error(`Federation source not found: ${source}`);
1334
- return readFileSync9(path, "utf8");
1406
+ try {
1407
+ if (/^https?:\/\//.test(source)) return await fetchText(source);
1408
+ const path = resolve3(root, source);
1409
+ if (!existsSync7(path)) return null;
1410
+ return readFileSync9(path, "utf8");
1411
+ } catch {
1412
+ return null;
1413
+ }
1335
1414
  };
1336
1415
  var sameOrigin = (base, target) => {
1337
1416
  if (!/^https?:\/\//.test(base) || !/^https?:\/\//.test(target)) return true;
@@ -1370,22 +1449,28 @@ var chunksFromMarkdown = (property, raw, sourceUrl) => {
1370
1449
  var loadFederatedChunks = async (root, config, options = {}) => {
1371
1450
  const fetchText = options.fetchText ?? defaultFetchText;
1372
1451
  const chunks = [];
1452
+ const warnings = [];
1373
1453
  for (const source of config.federation?.sources ?? []) {
1374
1454
  if (source.includeInRetriever === false || !source.llmsTxt) continue;
1375
1455
  const llms = await sourceText(root, source.llmsTxt, fetchText);
1456
+ if (!llms) {
1457
+ warnings.push(`federation source skipped (unavailable): ${source.id} \u2192 ${source.llmsTxt}`);
1458
+ continue;
1459
+ }
1376
1460
  chunks.push(...chunksFromMarkdown(source.id, llms, source.llmsTxt));
1377
1461
  const links = parseLlmsTxtLinks(llms);
1378
1462
  for (const link of links) {
1379
1463
  const url = !/^https?:\/\//.test(link.url) && source.rawBaseUrl ? `${source.rawBaseUrl.replace(/\/$/, "")}/${link.url.replace(/^\//, "")}` : link.url;
1380
1464
  if (!/\.(md|txt)(?:$|\?)/.test(url)) continue;
1381
1465
  if (!sameOrigin(source.llmsTxt, url)) continue;
1382
- try {
1383
- const raw = await sourceText(root, url, fetchText);
1384
- chunks.push(...chunksFromMarkdown(source.id, raw, url));
1385
- } catch {
1386
- }
1466
+ const raw = await sourceText(root, url, fetchText);
1467
+ if (raw) chunks.push(...chunksFromMarkdown(source.id, raw, url));
1387
1468
  }
1388
1469
  }
1470
+ if (warnings.length && process.stderr.isTTY) {
1471
+ for (const w of warnings) process.stderr.write(`${w}
1472
+ `);
1473
+ }
1389
1474
  return chunks;
1390
1475
  };
1391
1476
  var retrieveHybridChunks = async (root, config, index, query, options = {}) => {
@@ -1485,7 +1570,9 @@ var KnowledgeEntrySchema = z3.object({
1485
1570
  type: z3.string().min(1).max(128),
1486
1571
  title: z3.string().min(1).max(256),
1487
1572
  path: z3.string().min(1).max(512),
1488
- description: z3.string().max(1024).optional(),
1573
+ description: z3.string().max(2048).optional(),
1574
+ /** Flattened body excerpt for full-text search (not for display). */
1575
+ body: z3.string().max(8e3).optional(),
1489
1576
  links: z3.array(z3.string().min(1).max(512)).max(64).optional(),
1490
1577
  tags: z3.array(z3.string().min(1).max(64)).max(32).optional()
1491
1578
  }).strict();
@@ -2614,35 +2701,38 @@ var writeTextQuery = (payload) => {
2614
2701
  writeLines([
2615
2702
  `Search: ${String(data.term ?? "")}`,
2616
2703
  `Matches: ${String(data.count ?? matches.length)}`,
2617
- ...matches.map(
2618
- (match) => [match.type, match.id, match.path, match.summary].filter(Boolean).join(" ")
2619
- )
2704
+ ...matches.length ? matches.map(
2705
+ (match) => formatSearchMatch(match)
2706
+ ) : [" (none)"]
2620
2707
  ]);
2621
2708
  return;
2622
2709
  }
2623
2710
  writeLines([textValue(result.data)]);
2624
2711
  };
2712
+ var formatSearchMatch = (match) => {
2713
+ const summary = match.summary ? match.summary.replace(/\s+/g, " ").slice(0, 100) : "";
2714
+ const score = typeof match.score === "number" ? ` score=${match.score}` : "";
2715
+ return ` [${match.type}] ${match.id}${score}
2716
+ ${match.path}${summary ? `
2717
+ ${summary}` : ""}`;
2718
+ };
2625
2719
  var writeTextSearch = (term, matches) => {
2626
2720
  writeLines([
2627
2721
  `Search: ${term}`,
2628
2722
  `Matches: ${matches.length}`,
2629
- ...matches.map(
2630
- (match) => [match.type, match.id, match.path, match.summary].filter(Boolean).join(" ")
2631
- )
2723
+ ...matches.length ? matches.map(formatSearchMatch) : [" (none)"]
2632
2724
  ]);
2633
2725
  };
2634
2726
  var writeAsk = (question, matches, index) => {
2635
- const best = matches[0];
2636
- const owner = matches.find((match) => match.type === "ownership" || index.lookup?.ownership?.[match.id]);
2637
- const bestQuery = owner ? `ak-docs query ownership ${owner.id} --agent` : "ak-docs list knowledge --text";
2727
+ const owner = matches.find((match) => match.type === "ownership") ?? matches.find((match) => Boolean(index.lookup?.ownership?.[match.id]));
2728
+ const best = owner ?? matches[0];
2729
+ const bestQuery = best && (best.type === "ownership" || index.lookup?.ownership?.[best.id]) ? `ak-docs query ownership ${best.id} --agent` : "ak-docs list knowledge --text";
2638
2730
  writeLines([
2639
2731
  `Question: ${question}`,
2640
2732
  best ? `Best match: ${best.type} ${best.id} (${best.path})` : "Best match: none",
2641
2733
  "",
2642
2734
  "Matches:",
2643
- ...matches.length ? matches.slice(0, 5).map(
2644
- (match) => [match.type, match.id, match.path, match.summary].filter(Boolean).join(" ")
2645
- ) : ["No local matches. Try: ak-docs search <term>"],
2735
+ ...matches.length ? matches.slice(0, 5).map(formatSearchMatch) : [" No local matches. Try: ak-docs search <term>"],
2646
2736
  "",
2647
2737
  "Next commands:",
2648
2738
  ...best ? [