@agentskit/doc-bridge 1.11.2 → 1.12.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +12 -2
  2. package/README.md +3 -2
  3. package/action.yml +1 -1
  4. package/dist/cli/program.js +582 -470
  5. package/dist/cli/program.js.map +1 -1
  6. package/dist/index.d.ts +7 -2
  7. package/dist/index.js +500 -388
  8. package/dist/index.js.map +1 -1
  9. package/docs/DESIGN.md +71 -0
  10. package/docs/MARKETPLACE-ECOSYSTEM-PLAN.md +1 -1
  11. package/docs/PRD-enterprise-hardening.md +5 -5
  12. package/docs/PRD-knowledge-retrieval-and-enrichment.md +1 -1
  13. package/docs/knowledge-engine-runbook.md +1 -1
  14. package/docs/loop-workflow.md +15 -15
  15. package/docs/spec/config-v1.md +29 -7
  16. package/docs/spec/documentation-standard-v1.md +5 -0
  17. package/docs/spec/mcp-knowledge-tools-v1.md +1 -1
  18. package/docs/validation-cycle-plan.md +13 -13
  19. package/ecosystem-claims.json +23 -14
  20. package/ecosystem-upstream.json +3 -3
  21. package/ecosystem.json +318 -99
  22. package/mcpb/manifest.json +1 -1
  23. package/package.json +2 -1
  24. package/skills/doc-bridge-handoff/SKILL.md +1 -1
  25. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  26. package/src/conformance/ecosystem-contract.ts +10 -24
  27. package/src/discovery/repository.ts +7 -1
  28. package/src/findings/report.ts +1 -1
  29. package/src/fixes/proposals.ts +4 -2
  30. package/src/gates/run-gates.ts +1 -1
  31. package/src/index-builder/human-adapters/core.ts +10 -4
  32. package/src/lib/ignore-filter.ts +151 -0
  33. package/src/lib/walk.ts +10 -2
  34. package/src/memory/ingest.ts +1 -1
  35. package/src/report/html.ts +1 -1
  36. package/src/safety/repository.ts +9 -0
  37. package/src/version.ts +1 -1
  38. package/docs/DOGFOOD-ROUND2.md +0 -147
  39. package/docs/DOGFOOD-ROUND3.md +0 -79
  40. package/docs/DOGFOOD-V1.md +0 -89
  41. package/docs/DOGFOOD.md +0 -97
@@ -4,7 +4,7 @@ import type { Evidence, FindingStatus, KnowledgeDiagnostic } from '../schemas/kn
4
4
  * The canonical finding, as the rest of the ecosystem consumes it.
5
5
  *
6
6
  * `Finding` and `SEVERITY_ORDER` mirror `@agentskit/core/finding`, which is an optional peer:
7
- * Code Review, AKOS and dashboards read this shape, so a Doc Bridge diagnostic reported in it
7
+ * Code Review and dashboards read this shape, so a Doc Bridge diagnostic reported in it
8
8
  * needs no parser of its own. A test imports the real package and asserts that what is emitted
9
9
  * here is assignable to it and that the severities are drawn from the real order.
10
10
  *
@@ -3,6 +3,7 @@ import { basename, dirname, extname, join, relative, resolve, sep } from 'node:p
3
3
 
4
4
  import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
5
5
  import { FixProposalV1Schema, type FixProposalV1 } from '../schemas/knowledge.js'
6
+ import { createIgnoreFilter, type IgnoreFilter } from '../lib/ignore-filter.js'
6
7
  import { containedPath } from '../safety/repository.js'
7
8
 
8
9
  export type FixProposalOptions = {
@@ -55,10 +56,11 @@ const makeProposal = (root: string, options: FixProposalOptions, changes: readon
55
56
  return FixProposalV1Schema.parse({ ...draft, contentHash: contentHashForArtifactV1(draft) })
56
57
  }
57
58
 
58
- const walkMarkdown = (root: string, directory = root): string[] => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
59
+ const walkMarkdown = (root: string, directory = root, ignored: IgnoreFilter = createIgnoreFilter(root)): string[] => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
59
60
  if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') return []
60
61
  const path = join(directory, entry.name)
61
- if (entry.isDirectory()) return walkMarkdown(root, path)
62
+ if (ignored.isIgnored(path, entry.isDirectory())) return []
63
+ if (entry.isDirectory()) return walkMarkdown(root, path, ignored)
62
64
  return entry.isFile() && ['.md', '.mdx'].includes(extname(entry.name).toLowerCase()) ? [relative(root, path).split(sep).join('/')] : []
63
65
  })
64
66
 
@@ -92,7 +92,7 @@ export const runGate = (
92
92
  return {
93
93
  id,
94
94
  ok: false,
95
- message: `${result.ignored.length} indexed path(s) are ignored by Git, so a clean checkout builds a different index. Add them to safety.exclude.`,
95
+ message: `${result.ignored.length} indexed path(s) are ignored by Git, so a clean checkout builds a different index. Rebuild the index (scans skip ignored paths) or add them to safety.exclude.`,
96
96
  expected: 'every indexed path is committed',
97
97
  actual: sample.join(', '),
98
98
  }
@@ -37,12 +37,15 @@ export const optionString = (
37
37
  }
38
38
 
39
39
  export const parseFrontmatter = (raw: string): Record<string, string> => {
40
- if (!raw.startsWith('---\n')) return {}
41
- const end = raw.indexOf('\n---', 4)
40
+ // Windows-authored (or git autocrlf-checked-out) Markdown commonly uses CRLF;
41
+ // normalize before matching so frontmatter isn't silently dropped there.
42
+ const normalized = raw.includes('\r\n') ? raw.replace(/\r\n/g, '\n') : raw
43
+ if (!normalized.startsWith('---\n')) return {}
44
+ const end = normalized.indexOf('\n---', 4)
42
45
  if (end === -1) return {}
43
46
 
44
47
  const out: Record<string, string> = {}
45
- for (const line of raw.slice(4, end).split('\n')) {
48
+ for (const line of normalized.slice(4, end).split('\n')) {
46
49
  const match = /^([A-Za-z0-9_-]+):\s*(.+?)\s*$/.exec(line)
47
50
  if (match?.[1] && match[2]) out[match[1]] = match[2].replace(/^['"]|['"]$/g, '')
48
51
  }
@@ -89,7 +92,10 @@ export const scanMarkdownDocs = (
89
92
  const canonical = realpathSync.native(abs)
90
93
  const fileRelative = relative(projectRoot, canonical)
91
94
  if (isAbsolute(fileRelative) || fileRelative === '..' || fileRelative.startsWith(`..${sep}`)) continue
92
- const relToHumanRoot = toPosix(abs.replace(`${toPosix(absRoot)}/`, ''))
95
+ // Both sides must be toPosix'd before stripping the prefix: `abs` is native-separator
96
+ // (backslashes on Windows), so comparing it raw against a toPosix'd `absRoot` never
97
+ // matches there, silently leaving relToHumanRoot as the full absolute path.
98
+ const relToHumanRoot = toPosix(abs).replace(`${toPosix(absRoot)}/`, '')
93
99
  const raw = readBoundedText(abs, budget)
94
100
  if (options?.includeRelPath && !options.includeRelPath(relToHumanRoot, raw)) continue
95
101
  out.push({
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Which files a repository scan may see.
3
+ *
4
+ * A committed index is only reproducible while it is a function of committed (or at least
5
+ * non-ignored) content. Build output — `.next/`, `.source/`, generated API docs, files that a dev
6
+ * server writes such as `next-env.d.ts` — exists on one machine and not on the next, so every walk
7
+ * that feeds the index honours the repository's ignore rules in addition to its own excludes.
8
+ *
9
+ * Inside a Git work tree the answer comes from Git itself (`git ls-files --cached --others
10
+ * --exclude-standard`), which applies nested `.gitignore` files, `.git/info/exclude`, and the
11
+ * user's global excludes exactly as `git status` does, and still lists tracked files that happen to
12
+ * match an ignore rule. Outside Git (a tarball, a CI cache, Git not installed) the `.gitignore`
13
+ * files on disk are applied with the same semantics, one directory at a time.
14
+ */
15
+
16
+ import { execFileSync } from 'node:child_process'
17
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
18
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
19
+
20
+ import ignore, { type Ignore } from 'ignore'
21
+
22
+ export type IgnoreFilterMode = 'git' | 'gitignore'
23
+
24
+ export type IgnoreFilter = {
25
+ /** `git` when Git answered for this root, `gitignore` for the on-disk fallback. */
26
+ readonly mode: IgnoreFilterMode
27
+ /** True when `absolutePath` (a file, or a directory when `isDirectory`) must not be scanned. */
28
+ readonly isIgnored: (absolutePath: string, isDirectory: boolean) => boolean
29
+ }
30
+
31
+ const toPosix = (value: string): string => value.split(sep).join('/')
32
+
33
+ const canonical = (path: string): string => {
34
+ try {
35
+ return realpathSync.native(resolve(path))
36
+ } catch {
37
+ return resolve(path)
38
+ }
39
+ }
40
+
41
+ /** Path of `candidate` below `root` in POSIX form, or undefined when it is outside `root`. */
42
+ const below = (root: string, candidate: string): string | undefined => {
43
+ const rel = relative(root, candidate)
44
+ if (rel === '') return ''
45
+ if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) return undefined
46
+ return toPosix(rel)
47
+ }
48
+
49
+ const gitVisibleFiles = (root: string): readonly string[] | undefined => {
50
+ try {
51
+ const inside = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
52
+ cwd: root,
53
+ encoding: 'utf8',
54
+ stdio: ['ignore', 'pipe', 'ignore'],
55
+ }).trim()
56
+ if (inside !== 'true') return undefined
57
+ const output = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], {
58
+ cwd: root,
59
+ encoding: 'utf8',
60
+ maxBuffer: 256 * 1024 * 1024,
61
+ stdio: ['ignore', 'pipe', 'ignore'],
62
+ })
63
+ // Paths are relative to `cwd`. A nested repository shows up as `name/`; its files stay hidden.
64
+ return output.split('\0').filter(Boolean)
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ const gitFilter = (root: string, canonicalRoot: string, visible: readonly string[]): IgnoreFilter => {
71
+ const files = new Set(visible.filter((path) => !path.endsWith('/')))
72
+ const directories = new Set<string>([''])
73
+ for (const file of visible) {
74
+ let parent = file
75
+ for (;;) {
76
+ const slash = parent.lastIndexOf('/')
77
+ if (slash === -1) break
78
+ parent = parent.slice(0, slash)
79
+ if (directories.has(parent)) break
80
+ directories.add(parent)
81
+ }
82
+ }
83
+ return {
84
+ mode: 'git',
85
+ isIgnored: (absolutePath, isDirectory) => {
86
+ const rel = below(root, resolve(absolutePath)) ?? below(canonicalRoot, canonical(absolutePath))
87
+ if (rel === undefined) return false
88
+ return isDirectory ? !directories.has(rel) : !files.has(rel)
89
+ },
90
+ }
91
+ }
92
+
93
+ const gitignoreFilter = (root: string): IgnoreFilter => {
94
+ const rules = new Map<string, Ignore | undefined>()
95
+ const rulesFor = (directory: string): Ignore | undefined => {
96
+ if (rules.has(directory)) return rules.get(directory)
97
+ const file = join(directory, '.gitignore')
98
+ let matcher: Ignore | undefined
99
+ if (existsSync(file)) {
100
+ try {
101
+ matcher = ignore().add(readFileSync(file, 'utf8'))
102
+ } catch {
103
+ matcher = undefined
104
+ }
105
+ }
106
+ rules.set(directory, matcher)
107
+ return matcher
108
+ }
109
+ const ignoredCache = new Map<string, boolean>()
110
+ const isIgnored = (absolutePath: string, isDirectory: boolean): boolean => {
111
+ const target = resolve(absolutePath)
112
+ const rel = below(root, target)
113
+ if (rel === undefined || rel === '') return false
114
+ const key = `${isDirectory ? 'd' : 'f'}:${rel}`
115
+ const cached = ignoredCache.get(key)
116
+ if (cached !== undefined) return cached
117
+ // A path is ignored when an ancestor directory is ignored, or when the nearest applicable rule
118
+ // in any `.gitignore` between the root and its parent matches it (deeper files win).
119
+ const parent = dirname(target)
120
+ let result = parent !== root && below(root, parent) !== undefined ? isIgnored(parent, true) : false
121
+ if (!result) {
122
+ let decided: boolean | undefined
123
+ let directory = root
124
+ const segments = below(root, parent)?.split('/').filter(Boolean) ?? []
125
+ for (let index = 0; index <= segments.length; index += 1) {
126
+ if (index > 0) directory = join(directory, segments[index - 1] as string)
127
+ const matcher = rulesFor(directory)
128
+ if (!matcher) continue
129
+ const local = toPosix(relative(directory, target)) + (isDirectory ? '/' : '')
130
+ const verdict = matcher.test(local)
131
+ if (verdict.ignored) decided = true
132
+ else if (verdict.unignored) decided = false
133
+ }
134
+ result = decided ?? false
135
+ }
136
+ ignoredCache.set(key, result)
137
+ return result
138
+ }
139
+ return { mode: 'gitignore', isIgnored }
140
+ }
141
+
142
+ /**
143
+ * Build the filter for one scan root. Call once per walk: the Git listing reflects the working
144
+ * tree at that moment, so a long-lived watcher rebuilds it on every rescan.
145
+ */
146
+ export const createIgnoreFilter = (root: string): IgnoreFilter => {
147
+ const base = resolve(root)
148
+ const canonicalRoot = canonical(root)
149
+ const visible = gitVisibleFiles(canonicalRoot)
150
+ return visible ? gitFilter(base, canonicalRoot, visible) : gitignoreFilter(base)
151
+ }
package/src/lib/walk.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { lstatSync, readdirSync, realpathSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
+ import { createIgnoreFilter } from './ignore-filter.js'
4
5
  import { toPosix } from './paths.js'
5
6
 
6
7
  const DEFAULT_SKIP = new Set(['node_modules', '.git', 'dist', 'coverage', '.doc-bridge'])
@@ -12,12 +13,19 @@ export const walkFiles = (
12
13
  readonly extensions?: readonly string[]
13
14
  readonly skipDirs?: ReadonlySet<string>
14
15
  readonly maxFiles?: number
16
+ /**
17
+ * Skip what the repository ignores (`.gitignore`, `.git/info/exclude`, global excludes) on top
18
+ * of `skipDirs`. On by default; only walks of non-repository input, such as an agent memory
19
+ * directory, turn it off.
20
+ */
21
+ readonly respectIgnore?: boolean
15
22
  },
16
23
  ): string[] => {
17
24
  const extensions = opts?.extensions ?? ['.md']
18
25
  const skip = opts?.skipDirs ?? DEFAULT_SKIP
19
26
  const out: string[] = []
20
27
  const visited = new Set<string>()
28
+ const ignored = opts?.respectIgnore === false ? undefined : createIgnoreFilter(root)
21
29
 
22
30
  const visit = (dir: string) => {
23
31
  let canonicalDir: string
@@ -45,12 +53,12 @@ export const walkFiles = (
45
53
  }
46
54
  if (st.isSymbolicLink()) continue
47
55
  if (st.isDirectory()) {
48
- if (skip.has(name)) continue
56
+ if (skip.has(name) || ignored?.isIgnored(abs, true)) continue
49
57
  visit(abs)
50
58
  continue
51
59
  }
52
60
  if (!st.isFile()) continue
53
- if (extensions.some((ext) => name.endsWith(ext))) {
61
+ if (extensions.some((ext) => name.endsWith(ext)) && !ignored?.isIgnored(abs, false)) {
54
62
  if (out.length >= (opts?.maxFiles ?? DEFAULT_MAX_FILES)) {
55
63
  throw new Error(`Documentation corpus exceeds the ${opts?.maxFiles ?? DEFAULT_MAX_FILES} file limit.`)
56
64
  }
@@ -20,7 +20,7 @@ const ingestMarkdownDir = (
20
20
  ): MemoryCandidateV1[] => {
21
21
  if (!existsSync(dir)) return []
22
22
 
23
- return walkFiles(dir, { extensions: ['.md', '.mdc'] }).map((abs) => {
23
+ return walkFiles(dir, { extensions: ['.md', '.mdc'], respectIgnore: false }).map((abs) => {
24
24
  const rel = relativePath(root, abs)
25
25
  const raw = readFileSync(abs, 'utf8')
26
26
  const id = slugFromPath(rel)
@@ -424,7 +424,7 @@ const findingMatches=(finding)=>{const query=state.query.toLowerCase();return(!q
424
424
  const findingScope=(finding)=>data.view?.diagnosticGroup?.[finding.id]?.[0]||(()=>{const ids=[...(finding.entityIds||[])];(finding.relationIds||[]).forEach((id)=>{const relation=relationById.get(id);if(relation)ids.push(relation.from,relation.to)});return ids.map(groupFor).sort()[0]||"repository"})();
425
425
  const findingGroups=(findings)=>{const groups=new Map();findings.forEach((finding)=>{const scope=findingScope(finding),key=[scope,finding.code,finding.status,finding.severity].join("|"),group=groups.get(key)||{key,scope,code:finding.code,status:finding.status,severity:finding.severity,findings:[]};group.findings.push(finding);groups.set(key,group)});return[...groups.values()].sort((left,right)=>right.findings.length-left.findings.length||left.code.localeCompare(right.code)||left.key.localeCompare(right.key))};
426
426
  const renderFinding=(finding)=>"<article class=\"finding\" id=\""+esc("diagnostic-"+finding.id.replace(/[^A-Za-z0-9_-]+/g,"-"))+"\"><div class=\"finding-head\"><h3>"+esc(finding.code)+"</h3><span><span class=\"tag "+esc(finding.severity)+"\">"+esc(finding.severity)+"</span> <span class=\"tag\">"+esc(finding.status)+"</span></span></div><p>"+esc(finding.message)+"</p>"+(finding.evidence.length?"<ul>"+finding.evidence.slice(0,4).map((item)=>"<li>"+esc(item.path+(item.lineStart?":"+item.lineStart:"")+(item.context?" — "+item.context:""))+"</li>").join("")+"</ul>":"")+(finding.remediation?"<p><strong>Next check:</strong> "+esc(finding.remediation)+"</p>":"")+"</article>";
427
- const renderFindings=()=>{let findings=data.diagnostics.filter(findingMatches);if(state.lens==="risks")findings=findings.filter((finding)=>finding.severity==="error"||finding.severity==="warn");if(state.lens==="evidence")findings=findings.filter((finding)=>finding.evidence.length);if(state.lens==="drift")findings=findings.filter((finding)=>finding.status!=="confirmed");const itemLabel=state.lens==="evidence"?"evidence checks":"findings",groups=findingGroups(findings),active=groups.find((group)=>group.key===state.findingGroup);if(!active){state.findingGroup=null;state.findingPage=0}syncHash();const selected=active||null,pageSize=40,pageCount=selected?Math.ceil(selected.findings.length/pageSize):0,page=Math.max(0,Math.min(state.findingPage,Math.max(0,pageCount-1))),start=page*pageSize;document.querySelector("#finding-count").textContent=findings.length+" "+itemLabel+" · "+groups.length+" groups";document.querySelector("#findings").innerHTML=findings.length?"<div class=\"finding-groups\">"+groups.map((group)=>"<article class=\"finding-group\"><div class=\"finding-head\"><h3>"+esc(group.code)+"</h3><span><span class=\"tag "+esc(group.severity)+"\">"+esc(group.severity)+"</span> <span class=\"tag\">"+esc(group.status)+"</span></span></div><p><strong>"+group.findings.length+"</strong> "+itemLabel+" · "+esc(groupById.get(group.scope)?.name||group.scope)+"</p><p>"+esc(group.findings[0].message)+"</p><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(group.key)+"\" aria-expanded=\""+String(selected?.key===group.key)+"\">Inspect group</button></article>").join("")+"</div>"+(selected?"<div class=\"finding-detail\"><div class=\"finding-head\"><h3>"+esc(selected.code)+" · "+esc(groupById.get(selected.scope)?.name||selected.scope)+"</h3><span class=\"subtle\">"+selected.findings.length+" "+itemLabel+"</span></div>"+selected.findings.slice(start,start+pageSize).map(renderFinding).join("")+"<div class=\"finding-actions\"><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(selected.key)+"\" data-finding-page=\""+(page-1)+"\" "+(page===0?"disabled":"")+">Previous</button><span class=\"subtle\">Showing "+(start+1)+"–"+Math.min(start+pageSize,selected.findings.length)+" of "+selected.findings.length+" · page "+(page+1)+"/"+pageCount+"</span><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(selected.key)+"\" data-finding-page=\""+(page+1)+"\" "+(page+1>=pageCount?"disabled":"")+">Next</button></div></div>":""):"<p class=\"empty\">No "+itemLabel+" match the current lens and filters.</p>"};
427
+ const renderFindings=()=>{let findings=data.diagnostics.filter(findingMatches);if(state.lens==="risks")findings=findings.filter((finding)=>finding.severity==="error"||finding.severity==="warn");if(state.lens==="evidence")findings=findings.filter((finding)=>finding.evidence.length);if(state.lens==="drift")findings=findings.filter((finding)=>finding.status!=="confirmed");const itemLabel=state.lens==="evidence"?"evidence checks":"findings",groups=findingGroups(findings),active=groups.find((group)=>group.key===state.findingGroup);if(!active){state.findingGroup=null;state.findingPage=0}syncHash();const selected=active||null,pageSize=40,pageCount=selected?Math.ceil(selected.findings.length/pageSize):0,page=Math.max(0,Math.min(state.findingPage,Math.max(0,pageCount-1))),start=page*pageSize;document.querySelector("#finding-count").textContent=findings.length+" "+itemLabel+" · "+groups.length+" groups";document.querySelector("#findings").innerHTML=findings.length?"<div class=\"finding-groups\">"+groups.map((group)=>"<article class=\"finding-group\"><div class=\"finding-head\"><h3>"+esc(group.code)+"</h3><span><span class=\"tag "+esc(group.severity)+"\">"+esc(group.severity)+"</span> <span class=\"tag\">"+esc(group.status)+"</span></span></div><p><strong>"+group.findings.length+"</strong> "+itemLabel+" · "+esc(groupById.get(group.scope)?.name||group.scope)+"</p><p>"+esc(group.findings[0].message)+"</p><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(group.key)+"\" aria-expanded=\""+String(selected?.key===group.key)+"\">Inspect group</button></article>").join("")+"</div>"+(selected?"<div class=\"finding-detail\"><div class=\"finding-head\"><h3>"+esc(selected.code)+" · "+esc(groupById.get(selected.scope)?.name||selected.scope)+"</h3><span class=\"subtle\">"+selected.findings.length+" "+itemLabel+"</span></div>"+selected.findings.slice(start,start+pageSize).map(renderFinding).join("")+"<div class=\"finding-actions\"><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(selected.key)+"\" data-finding-page=\""+(page - 1)+"\" "+(page===0?"disabled":"")+">Previous</button><span class=\"subtle\">Showing "+(start+1)+"–"+Math.min(start+pageSize,selected.findings.length)+" of "+selected.findings.length+" · page "+(page+1)+"/"+pageCount+"</span><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(selected.key)+"\" data-finding-page=\""+(page+1)+"\" "+(page+1>=pageCount?"disabled":"")+">Next</button></div></div>":""):"<p class=\"empty\">No "+itemLabel+" match the current lens and filters.</p>"};
428
428
  const renderCoverage=()=>{document.querySelector("#coverage-list").innerHTML=(data.coverage||[]).map((entry)=>{const width=entry.status==="complete"?100:entry.status==="partial"?55:12;return"<div class=\"coverage-row\"><div><b>"+esc(entry.analyzer)+"</b><br><span class=\"subtle\">"+esc(entry.scope)+"</span></div><div class=\"bar\"><i class=\""+(entry.status==="complete"?"":entry.status==="partial"?"partial":"none")+"\" style=\"width:"+width+"%\"></i></div><div>"+esc(entry.status)+"</div></div>"}).join("")||"<p class=\"empty\">No coverage metadata.</p>"};
429
429
  const deferFindings=()=>{if(findingsLoaded())return;const actionable=data.actionableCount??data.diagnosticCount??0,confirmed=data.confirmedCount??0;document.querySelector("#finding-count").textContent=actionable+" actionable findings · "+confirmed+" confirmed checks available on demand";document.querySelector("#findings").innerHTML="<div class=\"empty\"><p>Findings stay out of the first paint so large repositories remain responsive. Confirmed checks are evidence, not issues.</p><button id=\"load-findings\" class=\"tab\" type=\"button\">Load findings</button></div>"};
430
430
  const scopePrompt=()=>{if((state.level==="module"||state.level==="file")&&!state.selected){document.querySelector("#map-note").textContent="Select an app or package to inspect this level.";document.querySelector("#graph").innerHTML="<text x=\"500\" y=\"270\" text-anchor=\"middle\" class=\"subtle\">Select an app or package to expand this view.</text>"}};
@@ -3,6 +3,8 @@ import { isAbsolute, relative, resolve, sep } from 'node:path'
3
3
 
4
4
  import { minimatch } from 'minimatch'
5
5
 
6
+ import { createIgnoreFilter } from '../lib/ignore-filter.js'
7
+
6
8
  export const DEFAULT_SAFETY_EXCLUDES = ['**/.git/**', '**/node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/.doc-bridge/**', '**/.next/**', '**/out/**', '**/.turbo/**', '**/.svelte-kit/**', '**/.mcpb-build/**', '**/.mcpb-output/**', '**/.env', '**/.env.*', '**/*secret*', '**/*credential*', '**/*.pem', '**/*.key'] as const
7
9
 
8
10
  export type SafeWalkOptions = {
@@ -12,6 +14,11 @@ export type SafeWalkOptions = {
12
14
  readonly maxBytes?: number
13
15
  readonly maxTimeMs?: number
14
16
  readonly maxMemoryMb?: number
17
+ /**
18
+ * Skip what the repository ignores (Git ignore rules, or `.gitignore` files outside Git) in
19
+ * addition to `exclude`, so build output never reaches the index. Defaults to true.
20
+ */
21
+ readonly respectIgnore?: boolean
15
22
  }
16
23
 
17
24
  export type SafeWalkResult = {
@@ -43,6 +50,7 @@ export const safeWalkFiles = (root: string, options: SafeWalkOptions = {}): Safe
43
50
  let reason: string | undefined
44
51
  const started = Date.now()
45
52
  const matchesExclude = (path: string): boolean => excludes.some((pattern) => minimatch(path, pattern, { dot: true }))
53
+ const ignored = options.respectIgnore === false ? undefined : createIgnoreFilter(projectRoot)
46
54
  const visit = (directory: string): void => {
47
55
  if (reason) return
48
56
  if (options.maxTimeMs !== undefined && Date.now() - started >= options.maxTimeMs) { reason = `Repository scan exceeded the ${options.maxTimeMs} ms time limit.`; return }
@@ -56,6 +64,7 @@ export const safeWalkFiles = (root: string, options: SafeWalkOptions = {}): Safe
56
64
  let stats
57
65
  try { stats = lstatSync(absolute) } catch { continue }
58
66
  if (stats.isSymbolicLink()) continue
67
+ if (ignored?.isIgnored(absolute, stats.isDirectory())) continue
59
68
  if (stats.isDirectory()) { visit(absolute); if (reason) return; continue }
60
69
  if (!stats.isFile() || (extensions.length > 0 && !extensions.some((extension) => name.endsWith(extension)))) continue
61
70
  if (files.length >= (options.maxFiles ?? 10_000)) { reason = `Repository scan exceeded the ${options.maxFiles ?? 10_000} file limit.`; return }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '1.11.2'
1
+ export const PACKAGE_VERSION = '1.12.0'
@@ -1,147 +0,0 @@
1
- ---
2
- title: Dogfood round 2
3
- description: Validation evidence from the second published Doc Bridge alpha.
4
- ---
5
-
6
- # Dogfood round 2 — published `@agentskit/doc-bridge@0.1.0-alpha.2`
7
-
8
- **Date:** 2026-07-09
9
- **Install:** npm registry tag `alpha` → `0.1.0-alpha.2`
10
- **Consumers:** agentskit · agentskit-os · agents-playbook · agentskit-registry
11
-
12
- ## Scoreboard
13
-
14
- | Repo | Install | Version | Knowledge | Handoffs | Gate | Sample handoff quality |
15
- |------|---------|---------|-----------|----------|------|------------------------|
16
- | **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` |
17
- | **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) |
18
- | **agents-playbook** | `pnpm add -D` ✅ | 0.1.0-alpha.2 | 127 | 81 | ✅ freshness + okf-type | Pattern ownership + `check:okf-type` ✅ |
19
- | **agentskit-registry** | needs `--legacy-peer-deps` ⚠️ | 0.1.0-alpha.2 | 36 | 36 | ✅ freshness | Full agent ownership + humanDoc ✅ |
20
-
21
- ### npm dist-tags (observed)
22
-
23
- | Tag | Version |
24
- |-----|---------|
25
- | `latest` | `0.1.0-alpha.1` |
26
- | `alpha` | `0.1.0-alpha.2` |
27
-
28
- **Recommendation:** keep prereleases only on `alpha` until stable; document `npm i -D @agentskit/doc-bridge@alpha`.
29
-
30
- ---
31
-
32
- ## What works well (validated)
33
-
34
- 1. **Published binary works** — `ak-docs 0.1.0-alpha.2` from registry / npx.
35
- 2. **Monorepo handoffs** — `editRoots` + `pnpm --filter @agentskit/<pkg> test|lint` on agentskit and AKOS.
36
- 3. **Playbook preset** — gates green without crushing large OKF corpus.
37
- 4. **Registry ownership** — all 36 agents resolve; checks use `npm run validate`.
38
- 5. **Fumadocs humanDoc** on agentskit points at **reference** docs, not nested for-agents.
39
- 6. **Pattern checks** — playbook patterns emit `pnpm run check:okf-type`.
40
-
41
- ---
42
-
43
- ## Issues found (prioritized)
44
-
45
- ### P0 — correctness / ranking (agents will take wrong action)
46
-
47
- | ID | Issue | Evidence | Suggested fix |
48
- |----|--------|----------|----------------|
49
- | **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 |
50
- | **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 |
51
- | **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 |
52
-
53
- ### P1 — install / peer DX
54
-
55
- | ID | Issue | Evidence | Suggested fix |
56
- |----|--------|----------|----------------|
57
- | **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 |
58
- | **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 |
59
- | **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 |
60
- | **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 |
61
-
62
- ### P2 — search / retrieval quality
63
-
64
- | ID | Issue | Evidence | Suggested fix |
65
- |----|--------|----------|----------------|
66
- | **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 |
67
- | **R2-9** | **Playbook `search handoff` → 0** | term may only appear deep in body | Same as R2-8; chunk full markdown into search corpus |
68
- | **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 |
69
- | **R2-11** | **Text search UX** | tab-separated dense lines | Columnar/TTY formatting (`type id path summary`) |
70
-
71
- ### P3 — bridge coverage & product polish
72
-
73
- | ID | Issue | Evidence | Suggested fix |
74
- |----|--------|----------|----------------|
75
- | **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. |
76
- | **R2-13** | **Duplicate knowledge+ownership rows in search** | same path appears as ownership + knowledge | Deduplicate by path; show ownership preferred |
77
- | **R2-14** | **Default checks for patterns without okf script** | N/A playbook ok | Fine; document convention |
78
- | **R2-15** | **MCP not re-smoked this round** | — | Keep in CI smoke; optional round 3 |
79
-
80
- ---
81
-
82
- ## Sample outputs (good)
83
-
84
- ### agentskit — `query package core --agent`
85
-
86
- ```json
87
- {
88
- "startHere": "apps/docs-next/content/docs/for-agents/core.mdx",
89
- "editRoots": ["packages/core"],
90
- "checks": [
91
- "pnpm --filter @agentskit/core test",
92
- "pnpm --filter @agentskit/core lint"
93
- ],
94
- "humanDoc": "/docs/reference/packages/core"
95
- }
96
- ```
97
-
98
- ### playbook — pattern handoff
99
-
100
- ```json
101
- {
102
- "startHere": "content/docs/pillars/ai-collaboration/open-knowledge-format-pattern.md",
103
- "checks": ["pnpm run check:okf-type"],
104
- "humanDoc": "/docs/pillars/ai-collaboration/open-knowledge-format-pattern"
105
- }
106
- ```
107
-
108
- ### registry — `docs-chat`
109
-
110
- ```json
111
- {
112
- "startHere": "registry/docs-chat/README.md",
113
- "editRoots": ["registry/docs-chat"],
114
- "checks": ["npm run validate", "npm test"],
115
- "humanDoc": "/agents/docs-chat"
116
- }
117
- ```
118
-
119
- ---
120
-
121
- ## Recommended next alpha (0.1.0-alpha.3) scope
122
-
123
- 1. **Ranking:** exact id / basename boost; package-intent heuristics for `ask`
124
- 2. **Search corpus:** full-text over body (not only description); dedupe paths
125
- 3. **Descriptions:** full purpose from frontmatter or first complete paragraph
126
- 4. **Peers:** widen optional peer ranges; Layer 0 install never requires AgentsKit packages
127
- 5. **Federation:** soft-fail missing `llms.txt` sources
128
- 6. **Publish:** keep `latest` free of broken prereleases; doc install as `@alpha`
129
-
130
- ---
131
-
132
- ## Consumer follow-ups (not doc-bridge code)
133
-
134
- | Repo | Action |
135
- |------|--------|
136
- | agentskit | Bump `@agentskit/core` to 1.10+ or live with peer warning |
137
- | agentskit-os | Align pnpm store / commit lock with published dep; add CI `docs:bridge:gate` |
138
- | agents-playbook | Optional CI workflow for gate |
139
- | agentskit-registry | Publish `public/llms.txt` at site root (404 today); use `--legacy-peer-deps` until peer ranges loosen |
140
- | all | After alpha.3: switch dep to `@agentskit/doc-bridge@alpha` only |
141
-
142
- ---
143
-
144
- ## Conclusion
145
-
146
- **Round 2 proves Layer 0 is useful on real monorepos and OKF/registry corpora with the published package.**
147
- 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.
@@ -1,79 +0,0 @@
1
- ---
2
- title: Dogfood round 3
3
- description: Validation evidence from the third published Doc Bridge alpha.
4
- ---
5
-
6
- # Dogfood round 3 — `@agentskit/doc-bridge@0.1.0-alpha.3`
7
-
8
- **Date:** 2026-07-09
9
- **npm:** `alpha` → `0.1.0-alpha.3` · `latest` still `0.1.0-alpha.1`
10
-
11
- ## Scoreboard
12
-
13
- | Repo | Install | Ver | K | Handoffs | Gate | Ranking | Ask | Retrieve |
14
- |------|---------|-----|---|----------|------|---------|-----|----------|
15
- | **agentskit** | `pnpm add -Dw` ✅ | 0.1.0-alpha.3 | 25 | 24 | ✅ 24 humanDoc | `core` #1 (683) ✅ | ownership core ✅ | ✅ soft, 8 chunks |
16
- | **agentskit-os** | `npx` (store issue) ✅ | 0.1.0-alpha.3 | 170 | 92 | ✅ | `os-core` #1 (689) ✅ | ownership os-core ✅ | ✅ 8 chunks |
17
- | **playbook** | `pnpm add -D` ✅ | 0.1.0-alpha.3 | 127 | 81 | ✅ okf | OKF pattern #1 ✅ | pattern ownership ✅ | ✅ |
18
- | **registry** | `--legacy-peer-deps` ⚠️ | 0.1.0-alpha.3 | 36 | 36 | ✅ | `docs-chat` #1 (593) ✅ | docs-chat ✅ | ✅ |
19
-
20
- ## Round-2 issues — validation
21
-
22
- | Item (R2) | Status | Evidence |
23
- |-----------|--------|----------|
24
- | Exact id ranking | **Fixed** | agentskit `search core` → best=`core` not angular |
25
- | ask wrong package | **Fixed** | “change the core package?” → ownership core |
26
- | Notes truncated | **Fixed** | full purpose sentences on core / os-core |
27
- | Full-text search | **Improved** | `search documentation` → 20 matches (was 0) |
28
- | Federation 404 hard-fail | **Fixed** | retrieve exit 0, no error, local chunks only |
29
- | Peer install friction | **Improved** | no peer warning on agentskit; registry still needs legacy-peer-deps |
30
- | Text UX | **Fixed** | multi-line `[ownership] id score=` format |
31
- | Path dedupe | **OK** | top lists are unique ownership rows |
32
- | AKOS humanDoc sparse | **Still open** | os-core handoff has no `humanDoc` (product docs thin) |
33
- | pnpm store AKOS | **Still open** | environment; npx works |
34
- | `latest` on old alpha | **Still open** | publish hygiene |
35
-
36
- ## Sample outputs (alpha.3)
37
-
38
- ### agentskit — ranking
39
-
40
- ```json
41
- { "term": "core", "best": "core", "top": ["core:683", "angular:27", "ink:27"] }
42
- ```
43
-
44
- ### agentskit — handoff
45
-
46
- ```json
47
- {
48
- "startHere": "apps/docs-next/content/docs/for-agents/core.mdx",
49
- "editRoots": ["packages/core"],
50
- "checks": ["pnpm --filter @agentskit/core test", "pnpm --filter @agentskit/core lint"],
51
- "humanDoc": "/docs/reference/packages/core",
52
- "notes": ["Stable TypeScript contracts (Adapter, Tool, Memory, Retriever, Skill, Runtime) + `createChatController` + primitives. Target: <10 KB gzipped, zero runtime deps."]
53
- }
54
- ```
55
-
56
- ### agentskit-os — ranking
57
-
58
- ```json
59
- { "best": "os-core", "top": ["os-core:689", "command-palette:36", "os-blob:36"] }
60
- ```
61
-
62
- ### registry — ranking
63
-
64
- ```json
65
- { "best": "docs-chat", "top": ["docs-chat:593", "agency-brief-generator:15"] }
66
- ```
67
-
68
- ## Remaining improvements (lower urgency)
69
-
70
- 1. **AKOS humanDoc coverage** — enrich for-agents frontmatter `humanDoc` or map product Fumadocs more densely (content work more than code).
71
- 2. **npm install without legacy-peer-deps** on registry — may still need peerOptional tuning vs npm ERESOLVE; document install flags.
72
- 3. **HTML entities in notes** — `&lt;10 KB` from source MD; optional decode on index.
73
- 4. **AKOS lockfile install path** — fix local pnpm store / commit published dep version.
74
- 5. **CI wire-up** — `docs:bridge:gate` on each consumer after merge of dogfood branches.
75
- 6. **Publish tags** — keep `latest` free of prereleases or retag intentionally.
76
-
77
- ## Verdict
78
-
79
- **alpha.3 is a successful dogfood pass.** P0/P1 product issues from round 2 are validated fixed on published package across monorepo + playbook + registry. Remaining items are polish, content, or consumer-side install/CI.