@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 +13 -0
- package/README.md +1 -1
- package/dist/cli/program.js +137 -47
- package/dist/cli/program.js.map +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.js +124 -36
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD-ROUND2.md +142 -0
- package/package.json +8 -8
- package/src/cli/program.ts +28 -14
- package/src/federation/llms.ts +20 -11
- package/src/index-builder/build-handoffs.ts +16 -1
- package/src/index-builder/scan-corpus.ts +5 -1
- package/src/lib/markdown.ts +31 -4
- package/src/query/search.ts +85 -17
- package/src/schemas/doc-bridge-index.ts +3 -1
- package/src/schemas/json-schemas.ts +2 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Dogfood round 2 — published `@agentskit/doc-bridge@0.1.0-alpha.2`
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-09
|
|
4
|
+
**Install:** npm registry tag `alpha` → `0.1.0-alpha.2`
|
|
5
|
+
**Consumers:** agentskit · agentskit-os · agents-playbook · agentskit-registry
|
|
6
|
+
|
|
7
|
+
## Scoreboard
|
|
8
|
+
|
|
9
|
+
| Repo | Install | Version | Knowledge | Handoffs | Gate | Sample handoff quality |
|
|
10
|
+
|------|---------|---------|-----------|----------|------|------------------------|
|
|
11
|
+
| **agentskit** | `pnpm add -Dw @0.1.0-alpha.2` ✅ | 0.1.0-alpha.2 | 25 | 24 | ✅ freshness + 23 humanDoc | Excellent: pnpm filter checks + humanDoc `/docs/reference/packages/core` |
|
|
12
|
+
| **agentskit-os** | pnpm store conflict; used **`npx`** ✅ | 0.1.0-alpha.2 | 170 | 92 | ✅ freshness + 9 humanDoc | Good act path; weak human bridge (9/92) |
|
|
13
|
+
| **agents-playbook** | `pnpm add -D` ✅ | 0.1.0-alpha.2 | 127 | 81 | ✅ freshness + okf-type | Pattern ownership + `check:okf-type` ✅ |
|
|
14
|
+
| **agentskit-registry** | needs `--legacy-peer-deps` ⚠️ | 0.1.0-alpha.2 | 36 | 36 | ✅ freshness | Full agent ownership + humanDoc ✅ |
|
|
15
|
+
|
|
16
|
+
### npm dist-tags (observed)
|
|
17
|
+
|
|
18
|
+
| Tag | Version |
|
|
19
|
+
|-----|---------|
|
|
20
|
+
| `latest` | `0.1.0-alpha.1` |
|
|
21
|
+
| `alpha` | `0.1.0-alpha.2` |
|
|
22
|
+
|
|
23
|
+
**Recommendation:** keep prereleases only on `alpha` until stable; document `npm i -D @agentskit/doc-bridge@alpha`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## What works well (validated)
|
|
28
|
+
|
|
29
|
+
1. **Published binary works** — `ak-docs 0.1.0-alpha.2` from registry / npx.
|
|
30
|
+
2. **Monorepo handoffs** — `editRoots` + `pnpm --filter @agentskit/<pkg> test|lint` on agentskit and AKOS.
|
|
31
|
+
3. **Playbook preset** — gates green without crushing large OKF corpus.
|
|
32
|
+
4. **Registry ownership** — all 36 agents resolve; checks use `npm run validate`.
|
|
33
|
+
5. **Fumadocs humanDoc** on agentskit points at **reference** docs, not nested for-agents.
|
|
34
|
+
6. **Pattern checks** — playbook patterns emit `pnpm run check:okf-type`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Issues found (prioritized)
|
|
39
|
+
|
|
40
|
+
### P0 — correctness / ranking (agents will take wrong action)
|
|
41
|
+
|
|
42
|
+
| ID | Issue | Evidence | Suggested fix |
|
|
43
|
+
|----|--------|----------|----------------|
|
|
44
|
+
| **R2-1** | **Search/ask ranking ignores exact package id** | `search core --agent` bestMatch = **angular** (score 8) tied with **core** (score 8) because summary contains `@agentskit/core` | Boost exact `id` / path segment matches; prefer ownership id === term before body mentions |
|
|
45
|
+
| **R2-2** | **`ask` suggests wrong handoff** | “where do I change the **core** package?” → best match **angular**, next `query ownership angular` | Same ranking fix; optional: prefer ownership over knowledge for “package/module” language |
|
|
46
|
+
| **R2-3** | **Purpose/notes truncated mid-sentence** | core notes: `"Stable TypeScript contracts (Adapter, Tool, Memory, Retriever, Skill,"` | Raise firstParagraph budget; strip trailing incomplete commas; prefer frontmatter `purpose` full line |
|
|
47
|
+
|
|
48
|
+
### P1 — install / peer DX
|
|
49
|
+
|
|
50
|
+
| ID | Issue | Evidence | Suggested fix |
|
|
51
|
+
|----|--------|----------|----------------|
|
|
52
|
+
| **R2-4** | **Optional peers still break npm install** | registry: `ERESOLVE` with optional peers vs `@agentskit/adapters@0.12.x` | Peer ranges more permissive (`>=0.12` / `>=1.0`); document `--legacy-peer-deps`; ensure optional peers never hard-fail Layer 0 |
|
|
53
|
+
| **R2-5** | **Peer `@agentskit/core@^1.10.0` vs monorepo 1.9.0** | pnpm warning on agentskit install | Align peer to `^1.9.0 \|\| ^1.10.0` or `>=1.9.0 <2` for dogfood until monorepo bumps |
|
|
54
|
+
| **R2-6** | **`latest` still points at alpha.1** | `npm view` latest=alpha.1, alpha=alpha.2 | Publish hygiene: don’t put prereleases on `latest`, or retag after publish |
|
|
55
|
+
| **R2-7** | **AKOS cannot `pnpm add` (store v10 vs v11)** | `ERR_PNPM_UNEXPECTED_STORE` | Docs: use npx / align store; not a doc-bridge bug but dogfood friction |
|
|
56
|
+
|
|
57
|
+
### P2 — search / retrieval quality
|
|
58
|
+
|
|
59
|
+
| ID | Issue | Evidence | Suggested fix |
|
|
60
|
+
|----|--------|----------|----------------|
|
|
61
|
+
| **R2-8** | **Search miss on common English** | agentskit `search documentation` → **0 matches** | Index more body text / titles; lower threshold; BM25 over full file not only description |
|
|
62
|
+
| **R2-9** | **Playbook `search handoff` → 0** | term may only appear deep in body | Same as R2-8; chunk full markdown into search corpus |
|
|
63
|
+
| **R2-10** | **Federation hard-fail noise** | `retrieve` → `Failed to fetch https://registry.agentskit.io/llms.txt: 404` | Soft-fail federation sources; warn once; don’t fail command |
|
|
64
|
+
| **R2-11** | **Text search UX** | tab-separated dense lines | Columnar/TTY formatting (`type id path summary`) |
|
|
65
|
+
|
|
66
|
+
### P3 — bridge coverage & product polish
|
|
67
|
+
|
|
68
|
+
| ID | Issue | Evidence | Suggested fix |
|
|
69
|
+
|----|--------|----------|----------------|
|
|
70
|
+
| **R2-12** | **AKOS humanDoc still sparse** | 9/92 handoffs with humanDoc | Map for-agents frontmatter `humanDoc`; product Fumadocs catalog; optional plain-markdown `docs/errors` etc. |
|
|
71
|
+
| **R2-13** | **Duplicate knowledge+ownership rows in search** | same path appears as ownership + knowledge | Deduplicate by path; show ownership preferred |
|
|
72
|
+
| **R2-14** | **Default checks for patterns without okf script** | N/A playbook ok | Fine; document convention |
|
|
73
|
+
| **R2-15** | **MCP not re-smoked this round** | — | Keep in CI smoke; optional round 3 |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Sample outputs (good)
|
|
78
|
+
|
|
79
|
+
### agentskit — `query package core --agent`
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"startHere": "apps/docs-next/content/docs/for-agents/core.mdx",
|
|
84
|
+
"editRoots": ["packages/core"],
|
|
85
|
+
"checks": [
|
|
86
|
+
"pnpm --filter @agentskit/core test",
|
|
87
|
+
"pnpm --filter @agentskit/core lint"
|
|
88
|
+
],
|
|
89
|
+
"humanDoc": "/docs/reference/packages/core"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### playbook — pattern handoff
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"startHere": "content/docs/pillars/ai-collaboration/open-knowledge-format-pattern.md",
|
|
98
|
+
"checks": ["pnpm run check:okf-type"],
|
|
99
|
+
"humanDoc": "/docs/pillars/ai-collaboration/open-knowledge-format-pattern"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### registry — `docs-chat`
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"startHere": "registry/docs-chat/README.md",
|
|
108
|
+
"editRoots": ["registry/docs-chat"],
|
|
109
|
+
"checks": ["npm run validate", "npm test"],
|
|
110
|
+
"humanDoc": "/agents/docs-chat"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Recommended next alpha (0.1.0-alpha.3) scope
|
|
117
|
+
|
|
118
|
+
1. **Ranking:** exact id / basename boost; package-intent heuristics for `ask`
|
|
119
|
+
2. **Search corpus:** full-text over body (not only description); dedupe paths
|
|
120
|
+
3. **Descriptions:** full purpose from frontmatter or first complete paragraph
|
|
121
|
+
4. **Peers:** widen optional peer ranges; Layer 0 install never requires AgentsKit packages
|
|
122
|
+
5. **Federation:** soft-fail missing `llms.txt` sources
|
|
123
|
+
6. **Publish:** keep `latest` free of broken prereleases; doc install as `@alpha`
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Consumer follow-ups (not doc-bridge code)
|
|
128
|
+
|
|
129
|
+
| Repo | Action |
|
|
130
|
+
|------|--------|
|
|
131
|
+
| agentskit | Bump `@agentskit/core` to 1.10+ or live with peer warning |
|
|
132
|
+
| agentskit-os | Align pnpm store / commit lock with published dep; add CI `docs:bridge:gate` |
|
|
133
|
+
| agents-playbook | Optional CI workflow for gate |
|
|
134
|
+
| agentskit-registry | Publish `public/llms.txt` at site root (404 today); use `--legacy-peer-deps` until peer ranges loosen |
|
|
135
|
+
| all | After alpha.3: switch dep to `@agentskit/doc-bridge@alpha` only |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Conclusion
|
|
140
|
+
|
|
141
|
+
**Round 2 proves Layer 0 is useful on real monorepos and OKF/registry corpora with the published package.**
|
|
142
|
+
The biggest remaining gap for “agents do the right thing” is **discovery ranking (R2-1/R2-2)** and **search body coverage (R2-8/R2-9)** — not indexing or handoff structure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentskit/doc-bridge",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "Human↔agent documentation bridge — deterministic handoffs, doc-site links, memory→docs, optional AgentsKit RAG/chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"changeset": "changeset",
|
|
47
47
|
"version-packages": "changeset version",
|
|
48
48
|
"release": "pnpm build && pnpm test && changeset publish",
|
|
49
|
-
"prepublishOnly": "
|
|
49
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
50
50
|
"prepare": "node scripts/prepare.mjs",
|
|
51
|
-
"prepack": "
|
|
51
|
+
"prepack": "pnpm build"
|
|
52
52
|
},
|
|
53
53
|
"keywords": [
|
|
54
54
|
"agentskit",
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"zod": "^3.24.2"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@agentskit/adapters": "
|
|
79
|
-
"@agentskit/core": "
|
|
80
|
-
"@agentskit/ink": "
|
|
81
|
-
"@agentskit/memory": "
|
|
82
|
-
"@agentskit/rag": "
|
|
78
|
+
"@agentskit/adapters": ">=0.12.0 <1",
|
|
79
|
+
"@agentskit/core": ">=1.0.0 <2",
|
|
80
|
+
"@agentskit/ink": ">=0.8.0 <1",
|
|
81
|
+
"@agentskit/memory": ">=0.9.0 <1",
|
|
82
|
+
"@agentskit/rag": ">=0.3.0 <1",
|
|
83
83
|
"react": ">=18.0.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependenciesMeta": {
|
package/src/cli/program.ts
CHANGED
|
@@ -160,9 +160,11 @@ const writeTextQuery = (payload: unknown): void => {
|
|
|
160
160
|
writeLines([
|
|
161
161
|
`Search: ${String(data.term ?? '')}`,
|
|
162
162
|
`Matches: ${String(data.count ?? matches.length)}`,
|
|
163
|
-
...matches.
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
...(matches.length
|
|
164
|
+
? matches.map((match) =>
|
|
165
|
+
formatSearchMatch(match as { type: string; id: string; path: string; summary?: string }),
|
|
166
|
+
)
|
|
167
|
+
: [' (none)']),
|
|
166
168
|
])
|
|
167
169
|
return
|
|
168
170
|
}
|
|
@@ -170,6 +172,18 @@ const writeTextQuery = (payload: unknown): void => {
|
|
|
170
172
|
writeLines([textValue(result.data)])
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
const formatSearchMatch = (match: {
|
|
176
|
+
readonly type: string
|
|
177
|
+
readonly id: string
|
|
178
|
+
readonly path: string
|
|
179
|
+
readonly summary?: string
|
|
180
|
+
readonly score?: number
|
|
181
|
+
}): string => {
|
|
182
|
+
const summary = match.summary ? match.summary.replace(/\s+/g, ' ').slice(0, 100) : ''
|
|
183
|
+
const score = typeof match.score === 'number' ? ` score=${match.score}` : ''
|
|
184
|
+
return ` [${match.type}] ${match.id}${score}\n ${match.path}${summary ? `\n ${summary}` : ''}`
|
|
185
|
+
}
|
|
186
|
+
|
|
173
187
|
const writeTextSearch = (
|
|
174
188
|
term: string,
|
|
175
189
|
matches: readonly {
|
|
@@ -177,14 +191,13 @@ const writeTextSearch = (
|
|
|
177
191
|
readonly id: string
|
|
178
192
|
readonly path: string
|
|
179
193
|
readonly summary?: string
|
|
194
|
+
readonly score?: number
|
|
180
195
|
}[],
|
|
181
196
|
): void => {
|
|
182
197
|
writeLines([
|
|
183
198
|
`Search: ${term}`,
|
|
184
199
|
`Matches: ${matches.length}`,
|
|
185
|
-
...matches.map((
|
|
186
|
-
[match.type, match.id, match.path, match.summary].filter(Boolean).join('\t'),
|
|
187
|
-
),
|
|
200
|
+
...(matches.length ? matches.map(formatSearchMatch) : [' (none)']),
|
|
188
201
|
])
|
|
189
202
|
}
|
|
190
203
|
|
|
@@ -193,11 +206,14 @@ const writeAsk = (
|
|
|
193
206
|
matches: ReturnType<typeof searchIndex>,
|
|
194
207
|
index: DocBridgeIndexV1,
|
|
195
208
|
): void => {
|
|
196
|
-
|
|
197
|
-
const owner =
|
|
209
|
+
// Prefer ownership match for routing questions
|
|
210
|
+
const owner =
|
|
211
|
+
matches.find((match) => match.type === 'ownership') ??
|
|
212
|
+
matches.find((match) => Boolean(index.lookup?.ownership?.[match.id]))
|
|
213
|
+
const best = owner ?? matches[0]
|
|
198
214
|
const bestQuery =
|
|
199
|
-
|
|
200
|
-
? `ak-docs query ownership ${
|
|
215
|
+
best && (best.type === 'ownership' || index.lookup?.ownership?.[best.id])
|
|
216
|
+
? `ak-docs query ownership ${best.id} --agent`
|
|
201
217
|
: 'ak-docs list knowledge --text'
|
|
202
218
|
writeLines([
|
|
203
219
|
`Question: ${question}`,
|
|
@@ -206,10 +222,8 @@ const writeAsk = (
|
|
|
206
222
|
'Matches:',
|
|
207
223
|
...(
|
|
208
224
|
matches.length
|
|
209
|
-
? matches.slice(0, 5).map(
|
|
210
|
-
|
|
211
|
-
)
|
|
212
|
-
: ['No local matches. Try: ak-docs search <term>']
|
|
225
|
+
? matches.slice(0, 5).map(formatSearchMatch)
|
|
226
|
+
: [' No local matches. Try: ak-docs search <term>']
|
|
213
227
|
),
|
|
214
228
|
'',
|
|
215
229
|
'Next commands:',
|
package/src/federation/llms.ts
CHANGED
|
@@ -35,11 +35,15 @@ const sourceText = async (
|
|
|
35
35
|
root: string,
|
|
36
36
|
source: string,
|
|
37
37
|
fetchText: FetchText,
|
|
38
|
-
): Promise<string> => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
): Promise<string | null> => {
|
|
39
|
+
try {
|
|
40
|
+
if (/^https?:\/\//.test(source)) return await fetchText(source)
|
|
41
|
+
const path = resolve(root, source)
|
|
42
|
+
if (!existsSync(path)) return null
|
|
43
|
+
return readFileSync(path, 'utf8')
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const sameOrigin = (base: string, target: string): boolean => {
|
|
@@ -97,9 +101,14 @@ export const loadFederatedChunks = async (
|
|
|
97
101
|
): Promise<DocBridgeRetrievedChunk[]> => {
|
|
98
102
|
const fetchText = options.fetchText ?? defaultFetchText
|
|
99
103
|
const chunks: DocBridgeRetrievedChunk[] = []
|
|
104
|
+
const warnings: string[] = []
|
|
100
105
|
for (const source of config.federation?.sources ?? []) {
|
|
101
106
|
if (source.includeInRetriever === false || !source.llmsTxt) continue
|
|
102
107
|
const llms = await sourceText(root, source.llmsTxt, fetchText)
|
|
108
|
+
if (!llms) {
|
|
109
|
+
warnings.push(`federation source skipped (unavailable): ${source.id} → ${source.llmsTxt}`)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
103
112
|
chunks.push(...chunksFromMarkdown(source.id, llms, source.llmsTxt))
|
|
104
113
|
const links = parseLlmsTxtLinks(llms)
|
|
105
114
|
for (const link of links) {
|
|
@@ -108,14 +117,14 @@ export const loadFederatedChunks = async (
|
|
|
108
117
|
: link.url
|
|
109
118
|
if (!/\.(md|txt)(?:$|\?)/.test(url)) continue
|
|
110
119
|
if (!sameOrigin(source.llmsTxt, url)) continue
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
chunks.push(...chunksFromMarkdown(source.id, raw, url))
|
|
114
|
-
} catch {
|
|
115
|
-
// ponytail: federation is best-effort; surface-level health belongs in a future diagnostic command.
|
|
116
|
-
}
|
|
120
|
+
const raw = await sourceText(root, url, fetchText)
|
|
121
|
+
if (raw) chunks.push(...chunksFromMarkdown(source.id, raw, url))
|
|
117
122
|
}
|
|
118
123
|
}
|
|
124
|
+
// Soft-fail: never throw for missing remote sources; one-line warn for agents/humans.
|
|
125
|
+
if (warnings.length && process.stderr.isTTY) {
|
|
126
|
+
for (const w of warnings) process.stderr.write(`${w}\n`)
|
|
127
|
+
}
|
|
119
128
|
return chunks
|
|
120
129
|
}
|
|
121
130
|
|
|
@@ -103,16 +103,31 @@ const resolveHumanDoc = (
|
|
|
103
103
|
if (override) return override
|
|
104
104
|
if (fmHuman) return fmHuman
|
|
105
105
|
if (humanDocs[packageId]) return humanDocs[packageId]
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
const aliases = [
|
|
108
108
|
packageId,
|
|
109
109
|
packageId.replace(/^@[^/]+\//, ''),
|
|
110
110
|
packageId.replace(/^os-/, ''),
|
|
111
111
|
packageId.replace(/-pattern$/, ''),
|
|
112
|
+
`packages/${packageId}`,
|
|
113
|
+
`reference/packages/${packageId}`,
|
|
114
|
+
`packages/${packageId}/index`,
|
|
112
115
|
]
|
|
113
116
|
for (const alias of aliases) {
|
|
114
117
|
if (humanDocs[alias]) return humanDocs[alias]
|
|
115
118
|
}
|
|
119
|
+
|
|
120
|
+
// Fuzzy: id is a path segment or suffix of a human doc id
|
|
121
|
+
for (const [key, url] of Object.entries(humanDocs)) {
|
|
122
|
+
if (
|
|
123
|
+
key === packageId ||
|
|
124
|
+
key.endsWith(`/${packageId}`) ||
|
|
125
|
+
key.endsWith(`/${packageId}/index`) ||
|
|
126
|
+
key.endsWith(`-${packageId}`)
|
|
127
|
+
) {
|
|
128
|
+
return url
|
|
129
|
+
}
|
|
130
|
+
}
|
|
116
131
|
return undefined
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
5
|
import {
|
|
6
|
+
extractSearchBody,
|
|
6
7
|
firstHeading,
|
|
7
8
|
firstParagraph,
|
|
8
9
|
frontmatterString,
|
|
@@ -47,7 +48,9 @@ export const scanAgentCorpus = (root: string, config: DocBridgeConfigV1): Corpus
|
|
|
47
48
|
frontmatterString(frontmatter, 'package') ??
|
|
48
49
|
slugFromPath(relToCorpus)
|
|
49
50
|
const title = firstHeading(raw) ?? id
|
|
50
|
-
const
|
|
51
|
+
const purpose = frontmatterString(frontmatter, 'purpose')
|
|
52
|
+
const description = purpose ?? firstParagraph(raw, 400)
|
|
53
|
+
const body = extractSearchBody(raw)
|
|
51
54
|
return {
|
|
52
55
|
id,
|
|
53
56
|
type: 'agent-doc',
|
|
@@ -57,6 +60,7 @@ export const scanAgentCorpus = (root: string, config: DocBridgeConfigV1): Corpus
|
|
|
57
60
|
relPath,
|
|
58
61
|
frontmatter,
|
|
59
62
|
...(description ? { description } : {}),
|
|
63
|
+
...(body ? { body } : {}),
|
|
60
64
|
}
|
|
61
65
|
})
|
|
62
66
|
}
|
package/src/lib/markdown.ts
CHANGED
|
@@ -64,7 +64,8 @@ export const firstHeading = (markdown: string): string | undefined => {
|
|
|
64
64
|
return undefined
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/** Complete first prose block — prefer full sentences, cap length without mid-word cuts. */
|
|
68
|
+
export const firstParagraph = (markdown: string, maxLen = 400): string | undefined => {
|
|
68
69
|
const { body } = parseFrontmatter(markdown)
|
|
69
70
|
const lines = body.split('\n')
|
|
70
71
|
const buf: string[] = []
|
|
@@ -76,11 +77,37 @@ export const firstParagraph = (markdown: string): string | undefined => {
|
|
|
76
77
|
}
|
|
77
78
|
if (t.startsWith('#')) continue
|
|
78
79
|
if (t.startsWith('---')) continue
|
|
80
|
+
if (t.startsWith('```')) break
|
|
81
|
+
if (t.startsWith('|') || t.startsWith('- [') || t.startsWith('* [')) {
|
|
82
|
+
if (buf.length) break
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
79
85
|
buf.push(t)
|
|
80
|
-
if (buf.join(' ').length
|
|
86
|
+
if (buf.join(' ').length >= maxLen) break
|
|
81
87
|
}
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
let text = buf.join(' ').replace(/\s+/g, ' ').trim()
|
|
89
|
+
if (!text) return undefined
|
|
90
|
+
if (text.length <= maxLen) return text
|
|
91
|
+
// Prefer ending on sentence boundary
|
|
92
|
+
const sliced = text.slice(0, maxLen)
|
|
93
|
+
const sentenceEnd = Math.max(sliced.lastIndexOf('. '), sliced.lastIndexOf('! '), sliced.lastIndexOf('? '))
|
|
94
|
+
if (sentenceEnd > maxLen * 0.4) return sliced.slice(0, sentenceEnd + 1).trim()
|
|
95
|
+
const wordEnd = sliced.lastIndexOf(' ')
|
|
96
|
+
return (wordEnd > 0 ? sliced.slice(0, wordEnd) : sliced).trim()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Flatten markdown body for search (strip fences/links noise lightly). */
|
|
100
|
+
export const extractSearchBody = (markdown: string, maxLen = 6_000): string => {
|
|
101
|
+
const { body } = parseFrontmatter(markdown)
|
|
102
|
+
const text = body
|
|
103
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
104
|
+
.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
|
|
105
|
+
.replace(/\[[^\]]*\]\([^)]+\)/g, ' ')
|
|
106
|
+
.replace(/^#+\s+/gm, '')
|
|
107
|
+
.replace(/[|>*_`#]/g, ' ')
|
|
108
|
+
.replace(/\s+/g, ' ')
|
|
109
|
+
.trim()
|
|
110
|
+
return text.length > maxLen ? text.slice(0, maxLen) : text
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
export const slugFromPath = (relPath: string): string => {
|
package/src/query/search.ts
CHANGED
|
@@ -11,23 +11,80 @@ export type SearchMatch = {
|
|
|
11
11
|
const tokenize = (value: string): string[] =>
|
|
12
12
|
value
|
|
13
13
|
.toLowerCase()
|
|
14
|
-
.split(/[^a-z0-9]+/)
|
|
14
|
+
.split(/[^a-z0-9@/_-]+/)
|
|
15
15
|
.filter((t) => t.length >= 2)
|
|
16
16
|
|
|
17
|
+
const PACKAGE_INTENT =
|
|
18
|
+
/\b(package|module|pkg|edit|change|where|owns?|ownership|handoff|start)\b/i
|
|
19
|
+
|
|
20
|
+
const scoreHay = (tokens: readonly string[], hay: string, weight = 1): number => {
|
|
21
|
+
let score = 0
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
if (!hay.includes(token)) continue
|
|
24
|
+
score += token.length * weight
|
|
25
|
+
// whole-word-ish bonus
|
|
26
|
+
if (new RegExp(`(?:^|[^a-z0-9])${token}(?:[^a-z0-9]|$)`).test(hay)) {
|
|
27
|
+
score += token.length
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return score
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Exact / near-exact identity boost so "core" ranks the core package over mentions of @agentskit/core. */
|
|
34
|
+
const identityBoost = (id: string, path: string, tokens: readonly string[], term: string): number => {
|
|
35
|
+
const idLower = id.toLowerCase()
|
|
36
|
+
const termLower = term.toLowerCase().trim()
|
|
37
|
+
const base = path.split('/').pop()?.replace(/\.mdx?$/i, '').toLowerCase() ?? ''
|
|
38
|
+
let boost = 0
|
|
39
|
+
|
|
40
|
+
if (idLower === termLower || base === termLower) boost += 200
|
|
41
|
+
if (tokens.length === 1 && (idLower === tokens[0] || base === tokens[0])) boost += 200
|
|
42
|
+
|
|
43
|
+
for (const token of tokens) {
|
|
44
|
+
if (idLower === token) boost += 120
|
|
45
|
+
else if (idLower.startsWith(`${token}-`) || idLower.endsWith(`-${token}`)) boost += 40
|
|
46
|
+
else if (idLower.includes(token) && idLower.length <= token.length + 4) boost += 30
|
|
47
|
+
if (base === token) boost += 100
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Prefer short ids when term is the id (package name)
|
|
51
|
+
if (tokens.includes(idLower)) boost += Math.max(0, 40 - idLower.length)
|
|
52
|
+
|
|
53
|
+
return boost
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const preferOwnership = (term: string): boolean =>
|
|
57
|
+
PACKAGE_INTENT.test(term) || /^(where|how).*(edit|change|package|module)/i.test(term)
|
|
58
|
+
|
|
17
59
|
export const searchIndex = (index: DocBridgeIndexV1, term: string, limit = 20): SearchMatch[] => {
|
|
18
60
|
const tokens = tokenize(term)
|
|
19
61
|
if (!tokens.length) return []
|
|
20
62
|
|
|
21
|
-
const
|
|
63
|
+
const wantOwnership = preferOwnership(term)
|
|
64
|
+
const byPath = new Map<string, SearchMatch>()
|
|
22
65
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
66
|
+
const consider = (match: SearchMatch) => {
|
|
67
|
+
const key = match.path
|
|
68
|
+
const existing = byPath.get(key)
|
|
69
|
+
if (!existing) {
|
|
70
|
+
byPath.set(key, match)
|
|
71
|
+
return
|
|
28
72
|
}
|
|
73
|
+
// Prefer ownership over knowledge for same path; else higher score
|
|
74
|
+
const prefer =
|
|
75
|
+
match.score > existing.score ||
|
|
76
|
+
(match.score === existing.score && match.type === 'ownership' && existing.type !== 'ownership') ||
|
|
77
|
+
(wantOwnership && match.type === 'ownership' && existing.type !== 'ownership' && match.score >= existing.score - 20)
|
|
78
|
+
if (prefer) byPath.set(key, match)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const entry of index.knowledge) {
|
|
82
|
+
const body = (entry as { body?: string }).body ?? ''
|
|
83
|
+
const hay = `${entry.id} ${entry.title} ${entry.description ?? ''} ${entry.path} ${body}`.toLowerCase()
|
|
84
|
+
let score = scoreHay(tokens, hay, 1)
|
|
85
|
+
score += identityBoost(entry.id, entry.path, tokens, term)
|
|
29
86
|
if (score > 0) {
|
|
30
|
-
|
|
87
|
+
consider({
|
|
31
88
|
type: 'knowledge',
|
|
32
89
|
id: entry.id,
|
|
33
90
|
path: entry.path,
|
|
@@ -38,21 +95,32 @@ export const searchIndex = (index: DocBridgeIndexV1, term: string, limit = 20):
|
|
|
38
95
|
}
|
|
39
96
|
|
|
40
97
|
for (const [id, owner] of Object.entries(index.lookup?.ownership ?? {})) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
98
|
+
const path = owner.agentDoc ?? owner.path
|
|
99
|
+
const hay = `${id} ${owner.path} ${owner.purpose ?? ''} ${owner.group ?? ''} ${owner.agentDoc ?? ''} ${owner.humanDoc ?? ''}`.toLowerCase()
|
|
100
|
+
let score = scoreHay(tokens, hay, 2)
|
|
101
|
+
score += identityBoost(id, path, tokens, term)
|
|
102
|
+
// Ownership is primary for routing questions
|
|
103
|
+
if (wantOwnership) score += 25
|
|
104
|
+
score += 15 // slight base preference for actionable ownership targets
|
|
46
105
|
if (score > 0) {
|
|
47
|
-
|
|
106
|
+
consider({
|
|
48
107
|
type: 'ownership',
|
|
49
108
|
id,
|
|
50
|
-
path
|
|
109
|
+
path,
|
|
51
110
|
...(owner.purpose ? { summary: owner.purpose } : {}),
|
|
52
111
|
score,
|
|
53
112
|
})
|
|
54
113
|
}
|
|
55
114
|
}
|
|
56
115
|
|
|
57
|
-
return
|
|
58
|
-
|
|
116
|
+
return [...byPath.values()].sort((a, b) => {
|
|
117
|
+
if (b.score !== a.score) return b.score - a.score
|
|
118
|
+
// Tie-break: exact id match, then ownership, then shorter id
|
|
119
|
+
const aExact = tokens.includes(a.id.toLowerCase()) ? 1 : 0
|
|
120
|
+
const bExact = tokens.includes(b.id.toLowerCase()) ? 1 : 0
|
|
121
|
+
if (bExact !== aExact) return bExact - aExact
|
|
122
|
+
if (a.type === 'ownership' && b.type !== 'ownership') return -1
|
|
123
|
+
if (b.type === 'ownership' && a.type !== 'ownership') return 1
|
|
124
|
+
return a.id.localeCompare(b.id)
|
|
125
|
+
}).slice(0, limit)
|
|
126
|
+
}
|
|
@@ -21,7 +21,9 @@ export const KnowledgeEntrySchema = z
|
|
|
21
21
|
type: z.string().min(1).max(128),
|
|
22
22
|
title: z.string().min(1).max(256),
|
|
23
23
|
path: z.string().min(1).max(512),
|
|
24
|
-
description: z.string().max(
|
|
24
|
+
description: z.string().max(2_048).optional(),
|
|
25
|
+
/** Flattened body excerpt for full-text search (not for display). */
|
|
26
|
+
body: z.string().max(8_000).optional(),
|
|
25
27
|
links: z.array(z.string().min(1).max(512)).max(64).optional(),
|
|
26
28
|
tags: z.array(z.string().min(1).max(64)).max(32).optional(),
|
|
27
29
|
})
|
|
@@ -121,7 +121,8 @@ export const DocBridgeIndexV1JsonSchema = {
|
|
|
121
121
|
type: { type: 'string', minLength: 1, maxLength: 128 },
|
|
122
122
|
title: { type: 'string', minLength: 1, maxLength: 256 },
|
|
123
123
|
path: { type: 'string', minLength: 1, maxLength: 512 },
|
|
124
|
-
description: { type: 'string', maxLength:
|
|
124
|
+
description: { type: 'string', maxLength: 2048 },
|
|
125
|
+
body: { type: 'string', maxLength: 8000 },
|
|
125
126
|
links: stringArray(64),
|
|
126
127
|
tags: stringArray(32),
|
|
127
128
|
},
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '0.1.0-alpha.
|
|
1
|
+
export const PACKAGE_VERSION = '0.1.0-alpha.3'
|