@agentskit/doc-bridge 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +15 -0
- package/README.md +1 -1
- package/dist/cli/program.js +283 -105
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +6 -3
- package/dist/config/index.js.map +1 -1
- package/dist/{index-CD_zmKXf.d.ts → index-CPUJbTbg.d.ts} +19 -10
- package/dist/index.d.ts +5 -4
- package/dist/index.js +278 -100
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD.md +92 -0
- package/package.json +27 -9
- package/scripts/prepare.mjs +44 -0
- package/src/cli/program.ts +933 -0
- package/src/config/defaults.ts +63 -0
- package/src/config/define-config.ts +6 -0
- package/src/config/index.ts +15 -0
- package/src/config/load-config.ts +138 -0
- package/src/config/schema.ts +294 -0
- package/src/federation/llms.ts +145 -0
- package/src/gates/run-gates.ts +274 -0
- package/src/index-builder/build-handoffs.ts +217 -0
- package/src/index-builder/build-index.ts +137 -0
- package/src/index-builder/capabilities.ts +37 -0
- package/src/index-builder/content-hash.ts +19 -0
- package/src/index-builder/human-adapters/core.ts +100 -0
- package/src/index-builder/human-adapters/docusaurus.ts +113 -0
- package/src/index-builder/human-adapters/fumadocs.ts +70 -0
- package/src/index-builder/human-adapters/index.ts +52 -0
- package/src/index-builder/human-adapters/plain-markdown.ts +10 -0
- package/src/index-builder/llms-txt.ts +19 -0
- package/src/index-builder/plugins/human-markdown.ts +7 -0
- package/src/index-builder/plugins/pnpm-monorepo.ts +72 -0
- package/src/index-builder/scan-corpus.ts +181 -0
- package/src/index.ts +120 -0
- package/src/intelligence/adapter.ts +90 -0
- package/src/intelligence/chat.ts +148 -0
- package/src/intelligence/peers.ts +59 -0
- package/src/intelligence/rag.ts +98 -0
- package/src/lib/glob-expand.ts +33 -0
- package/src/lib/markdown.ts +90 -0
- package/src/lib/package-manager.ts +104 -0
- package/src/lib/paths.ts +6 -0
- package/src/lib/walk.ts +45 -0
- package/src/mcp/server.ts +276 -0
- package/src/memory/ingest.ts +51 -0
- package/src/memory/pipeline.ts +119 -0
- package/src/query/load-index.ts +25 -0
- package/src/query/query.ts +142 -0
- package/src/query/search.ts +58 -0
- package/src/retriever/doc-bridge-retriever.ts +62 -0
- package/src/schemas/agent-handoff.ts +82 -0
- package/src/schemas/doc-bridge-index.ts +106 -0
- package/src/schemas/json-schemas.ts +156 -0
- package/src/schemas/memory-candidate.ts +37 -0
- package/src/shims/agentskit-peers.d.ts +80 -0
- package/src/validate.ts +67 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
4
|
+
import { buildDocBridgeIndex } from '../index-builder/build-index.js'
|
|
5
|
+
import { scanAgentCorpus } from '../index-builder/scan-corpus.js'
|
|
6
|
+
import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
|
|
7
|
+
import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
|
|
8
|
+
|
|
9
|
+
export type GateId = 'index-freshness' | 'human-guide-links' | 'okf-type' | 'docs-style'
|
|
10
|
+
|
|
11
|
+
const SUPPORTED_GATES = ['index-freshness', 'human-guide-links', 'okf-type', 'docs-style'] as const
|
|
12
|
+
|
|
13
|
+
export type GateResult = {
|
|
14
|
+
readonly id: GateId
|
|
15
|
+
readonly ok: boolean
|
|
16
|
+
readonly message: string
|
|
17
|
+
readonly expected?: string
|
|
18
|
+
readonly actual?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type GateRunResult = {
|
|
22
|
+
readonly ok: boolean
|
|
23
|
+
readonly results: GateResult[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const runGate = (
|
|
27
|
+
root: string,
|
|
28
|
+
config: DocBridgeConfigV1,
|
|
29
|
+
id: GateId,
|
|
30
|
+
): GateResult => {
|
|
31
|
+
if (id === 'human-guide-links') return runHumanGuideLinksGate(root, config)
|
|
32
|
+
if (id === 'okf-type') return runOkfTypeGate(root, config)
|
|
33
|
+
if (id === 'docs-style') return runDocsStyleGate(root, config)
|
|
34
|
+
if (id !== 'index-freshness') throw new Error(`Unsupported gate "${id}"`)
|
|
35
|
+
|
|
36
|
+
let current: string
|
|
37
|
+
try {
|
|
38
|
+
current = loadDocBridgeIndex(root, config).contentHash
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof IndexNotFoundError) {
|
|
41
|
+
return { id, ok: false, message: error.message }
|
|
42
|
+
}
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const next = buildDocBridgeIndex({ root, config, write: false }).index.contentHash
|
|
47
|
+
if (current !== next) {
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
ok: false,
|
|
51
|
+
message: 'Index is stale. Run: ak-docs index',
|
|
52
|
+
expected: next,
|
|
53
|
+
actual: current,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { id, ok: true, message: 'Index is fresh', expected: next, actual: current }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const runHumanGuideLinksGate = (root: string, config: DocBridgeConfigV1): GateResult => {
|
|
61
|
+
const urls = new Set(scanHumanDocRecords(root, config).map((doc) => doc.url))
|
|
62
|
+
const index = buildDocBridgeIndex({ root, config, write: false }).index
|
|
63
|
+
const localHumanDocs = Object.values(index.handoffs ?? {})
|
|
64
|
+
.map((handoff) => handoff.humanDoc)
|
|
65
|
+
.filter((url): url is string => typeof url === 'string' && url.length > 0)
|
|
66
|
+
.filter((url) => !/^https?:\/\//.test(url))
|
|
67
|
+
const missing = localHumanDocs.filter((url, i, all) => !urls.has(url) && all.indexOf(url) === i)
|
|
68
|
+
|
|
69
|
+
if (missing.length) {
|
|
70
|
+
return {
|
|
71
|
+
id: 'human-guide-links',
|
|
72
|
+
ok: false,
|
|
73
|
+
message: `Broken humanDoc links: ${missing.join(', ')}`,
|
|
74
|
+
expected: 'all local humanDoc links resolve',
|
|
75
|
+
actual: `${missing.length} broken`,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: 'human-guide-links',
|
|
81
|
+
ok: true,
|
|
82
|
+
message: `Resolved ${localHumanDocs.length} humanDoc link(s)`,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const frontmatterType = (markdown: string): string | undefined => {
|
|
87
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(markdown)
|
|
88
|
+
return match?.[1]?.match(/^type:\s*['"]?([^'"\n#]+)['"]?/m)?.[1]?.trim()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const frontmatterField = (markdown: string, field: string): string | undefined => {
|
|
92
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(markdown)
|
|
93
|
+
return match?.[1]?.match(new RegExp(`^${field}:\\s*['"]?([^'\"\\n#]+)['"]?`, 'm'))?.[1]?.trim()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const runOkfTypeGate = (root: string, config: DocBridgeConfigV1): GateResult => {
|
|
97
|
+
const required = config.corpus.agent.okf?.requireType ?? config.gates?.preset === 'strict'
|
|
98
|
+
if (!required) return { id: 'okf-type', ok: true, message: 'OKF type frontmatter not required' }
|
|
99
|
+
|
|
100
|
+
const allowed = config.corpus.agent.okf?.allowedTypes
|
|
101
|
+
const bad = scanAgentCorpus(root, config)
|
|
102
|
+
.filter((doc) => doc.path !== config.corpus.agent.index)
|
|
103
|
+
.map((doc) => ({ path: doc.path, type: frontmatterType(readFileSync(doc.absPath, 'utf8')) }))
|
|
104
|
+
.filter((doc) => !doc.type || (allowed && !allowed.includes(doc.type)))
|
|
105
|
+
|
|
106
|
+
if (bad.length) {
|
|
107
|
+
return {
|
|
108
|
+
id: 'okf-type',
|
|
109
|
+
ok: false,
|
|
110
|
+
message: `Missing or invalid OKF type frontmatter: ${bad.map((doc) => doc.path).join(', ')}`,
|
|
111
|
+
expected: allowed?.length ? `type: ${allowed.join(' | ')}` : 'type frontmatter',
|
|
112
|
+
actual: `${bad.length} invalid`,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { id: 'okf-type', ok: true, message: 'All agent docs have OKF type frontmatter' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type DocsStyleRule =
|
|
120
|
+
| 'title'
|
|
121
|
+
| 'purpose'
|
|
122
|
+
| 'audience'
|
|
123
|
+
| 'task-orientation'
|
|
124
|
+
| 'examples'
|
|
125
|
+
| 'owner-source'
|
|
126
|
+
| 'no-stale-wording'
|
|
127
|
+
|
|
128
|
+
type DocsStyleProfile =
|
|
129
|
+
| 'google-dev-docs'
|
|
130
|
+
| 'playbook-okf'
|
|
131
|
+
| 'playbook-okf-soft'
|
|
132
|
+
| 'title-only'
|
|
133
|
+
| 'custom'
|
|
134
|
+
|
|
135
|
+
const DOCS_STYLE_RULES: Record<Exclude<DocsStyleProfile, 'custom'>, DocsStyleRule[]> = {
|
|
136
|
+
'google-dev-docs': [
|
|
137
|
+
'title',
|
|
138
|
+
'purpose',
|
|
139
|
+
'audience',
|
|
140
|
+
'task-orientation',
|
|
141
|
+
'examples',
|
|
142
|
+
'no-stale-wording',
|
|
143
|
+
],
|
|
144
|
+
/** Full playbook OKF style (strict). */
|
|
145
|
+
'playbook-okf': ['title', 'purpose', 'owner-source', 'no-stale-wording'],
|
|
146
|
+
/** Soft profile for large existing OKF corpora (title + no stale placeholders). */
|
|
147
|
+
'playbook-okf-soft': ['title', 'no-stale-wording'],
|
|
148
|
+
'title-only': ['title'],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
152
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
153
|
+
|
|
154
|
+
const docsStyleOptions = (
|
|
155
|
+
config: DocBridgeConfigV1,
|
|
156
|
+
): { profile: DocsStyleProfile; required: DocsStyleRule[] } => {
|
|
157
|
+
const raw = config.gates?.options?.['docs-style']
|
|
158
|
+
if (!isRecord(raw)) {
|
|
159
|
+
if (config.gates?.preset === 'playbook') {
|
|
160
|
+
return { profile: 'playbook-okf-soft', required: DOCS_STYLE_RULES['playbook-okf-soft'] }
|
|
161
|
+
}
|
|
162
|
+
return { profile: 'playbook-okf', required: DOCS_STYLE_RULES['playbook-okf'] }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const profile =
|
|
166
|
+
raw.profile === 'google-dev-docs' ||
|
|
167
|
+
raw.profile === 'playbook-okf' ||
|
|
168
|
+
raw.profile === 'playbook-okf-soft' ||
|
|
169
|
+
raw.profile === 'title-only' ||
|
|
170
|
+
raw.profile === 'custom'
|
|
171
|
+
? raw.profile
|
|
172
|
+
: 'playbook-okf'
|
|
173
|
+
const customRequired = Array.isArray(raw.required)
|
|
174
|
+
? raw.required.filter((rule): rule is DocsStyleRule =>
|
|
175
|
+
[
|
|
176
|
+
'title',
|
|
177
|
+
'purpose',
|
|
178
|
+
'audience',
|
|
179
|
+
'task-orientation',
|
|
180
|
+
'examples',
|
|
181
|
+
'owner-source',
|
|
182
|
+
'no-stale-wording',
|
|
183
|
+
].includes(String(rule)),
|
|
184
|
+
)
|
|
185
|
+
: []
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
profile,
|
|
189
|
+
required: profile === 'custom' ? customRequired : DOCS_STYLE_RULES[profile],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const missingStyleRules = (markdown: string, rules: readonly DocsStyleRule[]): DocsStyleRule[] => {
|
|
194
|
+
const checks: Record<DocsStyleRule, boolean> = {
|
|
195
|
+
title: /^#\s+\S+/m.test(markdown),
|
|
196
|
+
purpose: Boolean(frontmatterField(markdown, 'purpose') || /^##\s+(Purpose|Goal)\b/im.test(markdown)),
|
|
197
|
+
audience: Boolean(frontmatterField(markdown, 'audience') || /^##\s+Audience\b/im.test(markdown)),
|
|
198
|
+
'task-orientation': /^##\s+(How to|Usage|Workflow|Tasks?)\b/im.test(markdown),
|
|
199
|
+
examples: /^##\s+Examples?\b/im.test(markdown) || /```[\s\S]*?```/.test(markdown),
|
|
200
|
+
'owner-source': Boolean(
|
|
201
|
+
frontmatterField(markdown, 'owner') ||
|
|
202
|
+
frontmatterField(markdown, 'source') ||
|
|
203
|
+
/^(Owner|Source):\s+\S+/im.test(markdown),
|
|
204
|
+
),
|
|
205
|
+
'no-stale-wording': !/\b(TODO|TBD|coming soon|eventually|placeholder)\b/i.test(markdown),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return rules.filter((rule) => !checks[rule])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const runDocsStyleGate = (root: string, config: DocBridgeConfigV1): GateResult => {
|
|
212
|
+
const { profile, required } = docsStyleOptions(config)
|
|
213
|
+
if (!required.length) {
|
|
214
|
+
return { id: 'docs-style', ok: true, message: 'No docs-style rules configured' }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const bad = scanAgentCorpus(root, config)
|
|
218
|
+
.filter((doc) => doc.path !== config.corpus.agent.index)
|
|
219
|
+
.map((doc) => ({
|
|
220
|
+
path: doc.path,
|
|
221
|
+
missing: missingStyleRules(readFileSync(doc.absPath, 'utf8'), required),
|
|
222
|
+
}))
|
|
223
|
+
.filter((doc) => doc.missing.length > 0)
|
|
224
|
+
|
|
225
|
+
if (bad.length) {
|
|
226
|
+
return {
|
|
227
|
+
id: 'docs-style',
|
|
228
|
+
ok: false,
|
|
229
|
+
message: `Docs style issues: ${bad
|
|
230
|
+
.map((doc) => `${doc.path} (${doc.missing.join(', ')})`)
|
|
231
|
+
.join('; ')}`,
|
|
232
|
+
expected: `${profile}: ${required.join(', ')}`,
|
|
233
|
+
actual: `${bad.length} file(s) with style issues`,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
id: 'docs-style',
|
|
239
|
+
ok: true,
|
|
240
|
+
message: `All agent docs pass ${profile} docs-style rules`,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const runGates = (
|
|
245
|
+
root: string,
|
|
246
|
+
config: DocBridgeConfigV1,
|
|
247
|
+
ids?: readonly GateId[],
|
|
248
|
+
): GateRunResult => {
|
|
249
|
+
const results = (ids ?? resolveGateIds(config)).map((id) => runGate(root, config, id))
|
|
250
|
+
return { ok: results.every((result) => result.ok), results }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const resolveGateIds = (config: DocBridgeConfigV1): GateId[] => {
|
|
254
|
+
const preset = config.gates?.preset ?? 'minimal'
|
|
255
|
+
const ids = new Set<GateId>(
|
|
256
|
+
preset === 'minimal'
|
|
257
|
+
? ['index-freshness']
|
|
258
|
+
: preset === 'strict'
|
|
259
|
+
? ['index-freshness', 'human-guide-links', 'okf-type', 'docs-style']
|
|
260
|
+
: preset === 'playbook'
|
|
261
|
+
? // okf-type only — docs-style soft is opt-in via include (large corpora vary)
|
|
262
|
+
['index-freshness', 'okf-type']
|
|
263
|
+
: ['index-freshness', 'human-guide-links'],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
for (const id of config.gates?.include ?? []) {
|
|
267
|
+
if (SUPPORTED_GATES.includes(id as GateId)) ids.add(id as GateId)
|
|
268
|
+
}
|
|
269
|
+
for (const id of config.gates?.exclude ?? []) {
|
|
270
|
+
if (SUPPORTED_GATES.includes(id as GateId)) ids.delete(id as GateId)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return [...ids]
|
|
274
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
2
|
+
import { defaultChecksForTarget } from '../lib/package-manager.js'
|
|
3
|
+
import type { AgentHandoffV1 } from '../schemas/agent-handoff.js'
|
|
4
|
+
import {
|
|
5
|
+
guessAgentDocForPackage,
|
|
6
|
+
ownershipFromCorpus,
|
|
7
|
+
ownershipFromFrontmatter,
|
|
8
|
+
type CorpusDoc,
|
|
9
|
+
} from './scan-corpus.js'
|
|
10
|
+
import type { DiscoveredPackage } from './plugins/pnpm-monorepo.js'
|
|
11
|
+
import type { HumanDocMap } from './plugins/human-markdown.js'
|
|
12
|
+
|
|
13
|
+
export type IndexLookup = {
|
|
14
|
+
packages: string[]
|
|
15
|
+
ownership: Record<string, OwnershipRecord>
|
|
16
|
+
intents: Record<string, IntentRecord>
|
|
17
|
+
changes: Record<string, ChangeRecord>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type OwnershipRecord = {
|
|
21
|
+
id: string
|
|
22
|
+
path: string
|
|
23
|
+
group?: string
|
|
24
|
+
layer?: string
|
|
25
|
+
purpose?: string
|
|
26
|
+
checks: string[]
|
|
27
|
+
agentDoc?: string
|
|
28
|
+
humanDoc?: string
|
|
29
|
+
readme?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type IntentRecord = {
|
|
33
|
+
id: string
|
|
34
|
+
title: string
|
|
35
|
+
paths: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ChangeRecord = {
|
|
39
|
+
id: string
|
|
40
|
+
title: string
|
|
41
|
+
startHere: string
|
|
42
|
+
relatedPackages?: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Merge package identity from:
|
|
47
|
+
* 1. routing.options.ownership (explicit)
|
|
48
|
+
* 2. pnpm / workspace discovery
|
|
49
|
+
* 3. agent-doc frontmatter (package + editRoot)
|
|
50
|
+
* 4. corpus path heuristics (packages/<id>.md, pillars patterns, etc.)
|
|
51
|
+
*/
|
|
52
|
+
export const collectPackages = (
|
|
53
|
+
config: DocBridgeConfigV1,
|
|
54
|
+
discovered: readonly DiscoveredPackage[],
|
|
55
|
+
corpus: readonly CorpusDoc[],
|
|
56
|
+
): DiscoveredPackage[] => {
|
|
57
|
+
const byId = new Map<string, DiscoveredPackage>()
|
|
58
|
+
|
|
59
|
+
for (const [id, entry] of Object.entries(config.routing?.options?.ownership ?? {})) {
|
|
60
|
+
byId.set(id, { id, path: entry.path })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const pkg of discovered) {
|
|
64
|
+
const existing = byId.get(pkg.id)
|
|
65
|
+
if (!existing) {
|
|
66
|
+
byId.set(pkg.id, pkg)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
byId.set(pkg.id, {
|
|
70
|
+
id: pkg.id,
|
|
71
|
+
path: existing.path || pkg.path,
|
|
72
|
+
...(pkg.name ? { name: pkg.name } : existing.name ? { name: existing.name } : {}),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const seeds = [
|
|
77
|
+
...ownershipFromFrontmatter(corpus),
|
|
78
|
+
...(config.routing?.options?.ownershipFromCorpus === false
|
|
79
|
+
? []
|
|
80
|
+
: ownershipFromCorpus(corpus)),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
for (const seed of seeds) {
|
|
84
|
+
const existing = byId.get(seed.id)
|
|
85
|
+
if (!existing) {
|
|
86
|
+
byId.set(seed.id, { id: seed.id, path: seed.path })
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (!existing.path) {
|
|
90
|
+
byId.set(seed.id, { ...existing, path: seed.path })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const resolveHumanDoc = (
|
|
98
|
+
packageId: string,
|
|
99
|
+
override?: string,
|
|
100
|
+
fmHuman?: string,
|
|
101
|
+
humanDocs: HumanDocMap = {},
|
|
102
|
+
): string | undefined => {
|
|
103
|
+
if (override) return override
|
|
104
|
+
if (fmHuman) return fmHuman
|
|
105
|
+
if (humanDocs[packageId]) return humanDocs[packageId]
|
|
106
|
+
// common aliases: scoped package name tail, path segments
|
|
107
|
+
const aliases = [
|
|
108
|
+
packageId,
|
|
109
|
+
packageId.replace(/^@[^/]+\//, ''),
|
|
110
|
+
packageId.replace(/^os-/, ''),
|
|
111
|
+
packageId.replace(/-pattern$/, ''),
|
|
112
|
+
]
|
|
113
|
+
for (const alias of aliases) {
|
|
114
|
+
if (humanDocs[alias]) return humanDocs[alias]
|
|
115
|
+
}
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const buildLookup = (
|
|
120
|
+
config: DocBridgeConfigV1,
|
|
121
|
+
packages: readonly DiscoveredPackage[],
|
|
122
|
+
corpus: readonly CorpusDoc[],
|
|
123
|
+
indexOutFile: string,
|
|
124
|
+
humanDocs: HumanDocMap = {},
|
|
125
|
+
root = process.cwd(),
|
|
126
|
+
): { lookup: IndexLookup; handoffs: Record<string, AgentHandoffV1> } => {
|
|
127
|
+
const ownership: Record<string, OwnershipRecord> = {}
|
|
128
|
+
const handoffs: Record<string, AgentHandoffV1> = {}
|
|
129
|
+
const strict = (config.gates?.preset ?? 'minimal') !== 'minimal'
|
|
130
|
+
const fmSeeds = new Map(
|
|
131
|
+
[...ownershipFromFrontmatter(corpus), ...ownershipFromCorpus(corpus)].map((seed) => [
|
|
132
|
+
seed.id,
|
|
133
|
+
seed,
|
|
134
|
+
]),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for (const pkg of packages) {
|
|
138
|
+
const override = config.routing?.options?.ownership?.[pkg.id]
|
|
139
|
+
const fm = fmSeeds.get(pkg.id)
|
|
140
|
+
const agentDoc =
|
|
141
|
+
override?.agentDoc ??
|
|
142
|
+
fm?.agentDoc ??
|
|
143
|
+
guessAgentDocForPackage(corpus, pkg.id) ??
|
|
144
|
+
config.corpus.agent.index
|
|
145
|
+
const startHere = agentDoc ?? ''
|
|
146
|
+
const purpose = override?.purpose ?? fm?.purpose
|
|
147
|
+
const path = override?.path ?? fm?.path ?? pkg.path
|
|
148
|
+
const checks = [
|
|
149
|
+
...(override?.checks ??
|
|
150
|
+
fm?.checks ??
|
|
151
|
+
defaultChecksForTarget(root, {
|
|
152
|
+
packageId: pkg.id,
|
|
153
|
+
packagePath: path,
|
|
154
|
+
...(pkg.name ? { packageName: pkg.name } : {}),
|
|
155
|
+
strict,
|
|
156
|
+
})),
|
|
157
|
+
]
|
|
158
|
+
const humanDoc = resolveHumanDoc(pkg.id, override?.humanDoc, fm?.humanDoc, humanDocs)
|
|
159
|
+
const record: OwnershipRecord = {
|
|
160
|
+
id: pkg.id,
|
|
161
|
+
path,
|
|
162
|
+
checks,
|
|
163
|
+
...(override?.group ? { group: override.group } : {}),
|
|
164
|
+
...(override?.layer ? { layer: override.layer } : {}),
|
|
165
|
+
...(purpose ? { purpose } : {}),
|
|
166
|
+
...(humanDoc ? { humanDoc } : {}),
|
|
167
|
+
...(agentDoc ? { agentDoc } : {}),
|
|
168
|
+
}
|
|
169
|
+
ownership[pkg.id] = record
|
|
170
|
+
|
|
171
|
+
handoffs[pkg.id] = {
|
|
172
|
+
type: 'agent-handoff',
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
source: indexOutFile,
|
|
175
|
+
target: {
|
|
176
|
+
type: 'package',
|
|
177
|
+
id: pkg.id,
|
|
178
|
+
path: record.path,
|
|
179
|
+
...(record.group ? { group: record.group } : {}),
|
|
180
|
+
...(record.layer ? { layer: record.layer } : {}),
|
|
181
|
+
},
|
|
182
|
+
startHere,
|
|
183
|
+
readBeforeEditing: [agentDoc, 'AGENTS.md'].filter((v, i, a): v is string =>
|
|
184
|
+
Boolean(v) && a.indexOf(v) === i,
|
|
185
|
+
),
|
|
186
|
+
editRoots: [record.path],
|
|
187
|
+
checks: [...record.checks],
|
|
188
|
+
...(record.humanDoc ? { humanDoc: record.humanDoc } : {}),
|
|
189
|
+
notes: record.purpose ? [record.purpose] : [],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const intents: Record<string, IntentRecord> = {}
|
|
194
|
+
for (const intent of config.routing?.options?.intents ?? []) {
|
|
195
|
+
intents[intent.id] = { id: intent.id, title: intent.title, paths: intent.paths }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const changes: Record<string, ChangeRecord> = {}
|
|
199
|
+
for (const change of config.routing?.options?.changes ?? []) {
|
|
200
|
+
changes[change.id] = {
|
|
201
|
+
id: change.id,
|
|
202
|
+
title: change.title,
|
|
203
|
+
startHere: change.startHere,
|
|
204
|
+
...(change.relatedPackages ? { relatedPackages: change.relatedPackages } : {}),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
lookup: {
|
|
210
|
+
packages: packages.map((p) => p.id),
|
|
211
|
+
ownership,
|
|
212
|
+
intents,
|
|
213
|
+
changes,
|
|
214
|
+
},
|
|
215
|
+
handoffs,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import { toPosix } from '../lib/paths.js'
|
|
6
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
7
|
+
import { buildLookup, collectPackages } from './build-handoffs.js'
|
|
8
|
+
import { renderCapabilitiesJson } from './capabilities.js'
|
|
9
|
+
import { sha256NormalizedV1 } from './content-hash.js'
|
|
10
|
+
import { renderLlmsTxt } from './llms-txt.js'
|
|
11
|
+
import { scanHumanDocs } from './human-adapters/index.js'
|
|
12
|
+
import { discoverPnpmPackages } from './plugins/pnpm-monorepo.js'
|
|
13
|
+
import { scanAgentCorpus } from './scan-corpus.js'
|
|
14
|
+
|
|
15
|
+
export type BuildIndexOptions = {
|
|
16
|
+
readonly root?: string
|
|
17
|
+
readonly config: DocBridgeConfigV1
|
|
18
|
+
readonly write?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type BuildIndexResult = {
|
|
22
|
+
readonly index: DocBridgeIndexV1
|
|
23
|
+
readonly indexPath: string
|
|
24
|
+
readonly llmsTxtPath?: string
|
|
25
|
+
readonly capabilitiesPath?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const projectName = (root: string, config: DocBridgeConfigV1): string => {
|
|
29
|
+
if (config.project?.name) return config.project.name
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { name?: string }
|
|
32
|
+
if (pkg.name) return pkg.name
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
return toPosix(root.split('/').pop() ?? 'project')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const existingGeneratedAt = (indexPath: string, contentHash: string): string | undefined => {
|
|
40
|
+
try {
|
|
41
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8')) as {
|
|
42
|
+
contentHash?: unknown
|
|
43
|
+
generatedAt?: unknown
|
|
44
|
+
}
|
|
45
|
+
return index.contentHash === contentHash && typeof index.generatedAt === 'string'
|
|
46
|
+
? index.generatedAt
|
|
47
|
+
: undefined
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const buildDocBridgeIndex = (opts: BuildIndexOptions): BuildIndexResult => {
|
|
54
|
+
const root = opts.root ?? process.cwd()
|
|
55
|
+
const config = opts.config
|
|
56
|
+
const write = opts.write ?? true
|
|
57
|
+
const outFile = config.index?.outFile ?? '.doc-bridge/index.json'
|
|
58
|
+
const indexPath = join(root, outFile)
|
|
59
|
+
|
|
60
|
+
const corpus = scanAgentCorpus(root, config)
|
|
61
|
+
const knowledge = corpus.map(({ absPath: _a, relPath: _r, frontmatter: _f, ...entry }) => entry)
|
|
62
|
+
|
|
63
|
+
const shouldDiscover =
|
|
64
|
+
config.routing?.plugin === 'pnpm-monorepo' ||
|
|
65
|
+
Boolean(config.routing?.options?.packages?.length) ||
|
|
66
|
+
config.routing?.plugin === 'npm-workspaces' ||
|
|
67
|
+
config.routing?.plugin === 'yarn-workspaces'
|
|
68
|
+
|
|
69
|
+
const discovered = shouldDiscover ? discoverPnpmPackages(root, config) : []
|
|
70
|
+
const packages = collectPackages(config, discovered, corpus)
|
|
71
|
+
const humanDocs = scanHumanDocs(root, config)
|
|
72
|
+
|
|
73
|
+
const { lookup, handoffs } = buildLookup(config, packages, corpus, outFile, humanDocs, root)
|
|
74
|
+
|
|
75
|
+
const hashPayload = {
|
|
76
|
+
schemaVersion: 1,
|
|
77
|
+
knowledge,
|
|
78
|
+
handoffs,
|
|
79
|
+
lookup,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const contentHash = sha256NormalizedV1(hashPayload)
|
|
83
|
+
const index: DocBridgeIndexV1 = {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
contentHash,
|
|
86
|
+
contentHashAlgo: 'sha256-normalized-v1',
|
|
87
|
+
generatedAt: existingGeneratedAt(indexPath, contentHash) ?? new Date().toISOString(),
|
|
88
|
+
project: { name: projectName(root, config), root: '.' },
|
|
89
|
+
knowledge,
|
|
90
|
+
handoffs,
|
|
91
|
+
lookup,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (write) {
|
|
95
|
+
mkdirSync(dirname(indexPath), { recursive: true })
|
|
96
|
+
writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let llmsTxtPath: string | undefined
|
|
100
|
+
let llmsTxtRelPath: string | undefined
|
|
101
|
+
if (config.index?.llmsTxt?.enabled !== false) {
|
|
102
|
+
const llmsOut = config.index?.llmsTxt?.outFile ?? 'llms.txt'
|
|
103
|
+
llmsTxtRelPath = toPosix(llmsOut)
|
|
104
|
+
llmsTxtPath = join(root, llmsOut)
|
|
105
|
+
if (write) {
|
|
106
|
+
writeFileSync(
|
|
107
|
+
llmsTxtPath,
|
|
108
|
+
renderLlmsTxt(config, knowledge, index.project?.name ?? 'project'),
|
|
109
|
+
'utf8',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let capabilitiesPath: string | undefined
|
|
115
|
+
if (config.index?.capabilities?.enabled !== false) {
|
|
116
|
+
const capabilitiesOut = config.index?.capabilities?.outFile ?? '.doc-bridge/capabilities.json'
|
|
117
|
+
capabilitiesPath = join(root, capabilitiesOut)
|
|
118
|
+
if (write) {
|
|
119
|
+
mkdirSync(dirname(capabilitiesPath), { recursive: true })
|
|
120
|
+
writeFileSync(
|
|
121
|
+
capabilitiesPath,
|
|
122
|
+
renderCapabilitiesJson(config, index, {
|
|
123
|
+
index: toPosix(outFile),
|
|
124
|
+
...(llmsTxtRelPath ? { llmsTxt: llmsTxtRelPath } : {}),
|
|
125
|
+
}),
|
|
126
|
+
'utf8',
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
index,
|
|
133
|
+
indexPath: toPosix(indexPath),
|
|
134
|
+
...(llmsTxtPath ? { llmsTxtPath: toPosix(llmsTxtPath) } : {}),
|
|
135
|
+
...(capabilitiesPath ? { capabilitiesPath: toPosix(capabilitiesPath) } : {}),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
2
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
3
|
+
import { PACKAGE_VERSION } from '../version.js'
|
|
4
|
+
|
|
5
|
+
export const renderCapabilitiesJson = (
|
|
6
|
+
config: DocBridgeConfigV1,
|
|
7
|
+
index: DocBridgeIndexV1,
|
|
8
|
+
paths: { readonly index: string; readonly llmsTxt?: string },
|
|
9
|
+
): string => {
|
|
10
|
+
const payload = {
|
|
11
|
+
schemaVersion: 1,
|
|
12
|
+
type: 'doc-bridge-capabilities',
|
|
13
|
+
package: '@agentskit/doc-bridge',
|
|
14
|
+
version: PACKAGE_VERSION,
|
|
15
|
+
project: index.project,
|
|
16
|
+
contentHash: index.contentHash,
|
|
17
|
+
artifacts: {
|
|
18
|
+
index: paths.index,
|
|
19
|
+
...(paths.llmsTxt ? { llmsTxt: paths.llmsTxt } : {}),
|
|
20
|
+
},
|
|
21
|
+
capabilities: [
|
|
22
|
+
'index',
|
|
23
|
+
'query',
|
|
24
|
+
'search',
|
|
25
|
+
'agent-handoff',
|
|
26
|
+
'gates',
|
|
27
|
+
...(config.surfaces?.mcp?.enabled === false ? [] : ['mcp']),
|
|
28
|
+
...(config.corpus.human ? ['human-docs'] : []),
|
|
29
|
+
],
|
|
30
|
+
surfaces: {
|
|
31
|
+
cli: config.surfaces?.cli,
|
|
32
|
+
mcp: config.surfaces?.mcp,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${JSON.stringify(payload, null, 2)}\n`
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const sortValue = (value: unknown): unknown => {
|
|
4
|
+
if (Array.isArray(value)) return value.map(sortValue)
|
|
5
|
+
if (value && typeof value === 'object') {
|
|
6
|
+
const record = value as Record<string, unknown>
|
|
7
|
+
return Object.fromEntries(
|
|
8
|
+
Object.keys(record)
|
|
9
|
+
.sort()
|
|
10
|
+
.map((key) => [key, sortValue(record[key])]),
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const sha256NormalizedV1 = (payload: unknown): string => {
|
|
17
|
+
const normalized = JSON.stringify(sortValue(payload))
|
|
18
|
+
return createHash('sha256').update(normalized, 'utf8').digest('hex')
|
|
19
|
+
}
|