@agentskit/doc-bridge 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +81 -15
  4. package/SECURITY.md +1 -1
  5. package/action.yml +2 -2
  6. package/dist/cli/program.js +1248 -316
  7. package/dist/cli/program.js.map +1 -1
  8. package/dist/config/index.d.ts +1 -1
  9. package/dist/config/index.js +338 -13
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/{index-CPUJbTbg.d.ts → index-DGI9TBLE.d.ts} +906 -11
  12. package/dist/index.d.ts +65 -11
  13. package/dist/index.js +1084 -171
  14. package/dist/index.js.map +1 -1
  15. package/docs/RELEASE.md +19 -21
  16. package/docs/getting-started.md +27 -2
  17. package/docs/landing/assets/doc-bridge-hero.webp +0 -0
  18. package/docs/landing/assets/doc-bridge-surfaces.webp +0 -0
  19. package/docs/landing/assets/doc-bridge-two-way.webp +0 -0
  20. package/docs/landing/index.html +70 -10
  21. package/docs/playbook/doc-bridge-pattern.md +2 -2
  22. package/docs/recipes/index-pipeline.md +2 -2
  23. package/docs/schemas/memory-candidate-v1.md +10 -1
  24. package/docs/spec/cli.md +1 -0
  25. package/docs/spec/config-v1.md +51 -6
  26. package/docs/spec/documentation-standard-v1.md +131 -0
  27. package/ecosystem-claims.json +187 -0
  28. package/ecosystem-upstream.json +9 -0
  29. package/ecosystem.json +231 -0
  30. package/package.json +14 -2
  31. package/scripts/check-ecosystem-upstream.mjs +50 -0
  32. package/src/cli/program.ts +43 -9
  33. package/src/config/index.ts +7 -1
  34. package/src/config/load-config.ts +4 -14
  35. package/src/config/schema.ts +91 -0
  36. package/src/conformance/documentation-standard-v1.ts +502 -0
  37. package/src/conformance/ecosystem-contract.ts +175 -0
  38. package/src/gates/run-gates.ts +33 -4
  39. package/src/index-builder/human-adapters/core.ts +12 -5
  40. package/src/index-builder/human-adapters/docusaurus.ts +29 -44
  41. package/src/index-builder/human-adapters/index.ts +15 -3
  42. package/src/index-builder/scan-corpus.ts +6 -6
  43. package/src/index.ts +17 -0
  44. package/src/lib/bounded-text.ts +25 -0
  45. package/src/lib/paths.ts +20 -2
  46. package/src/lib/static-js-literal.ts +261 -0
  47. package/src/lib/walk.ts +23 -4
  48. package/src/version.ts +1 -1
@@ -0,0 +1,261 @@
1
+ const stripComments = (source: string): string => {
2
+ let out = ''
3
+ let index = 0
4
+ let quote: string | undefined
5
+ while (index < source.length) {
6
+ const char = source[index] ?? ''
7
+ const next = source[index + 1] ?? ''
8
+ if (quote) {
9
+ out += char
10
+ if (char === '\\') {
11
+ out += next
12
+ index += 2
13
+ continue
14
+ }
15
+ if (char === quote) quote = undefined
16
+ index += 1
17
+ continue
18
+ }
19
+ if (char === '"' || char === "'" || char === '`') {
20
+ quote = char
21
+ out += char
22
+ index += 1
23
+ continue
24
+ }
25
+ if (char === '/' && next === '/') {
26
+ while (index < source.length && source[index] !== '\n') index += 1
27
+ out += '\n'
28
+ index += 1
29
+ continue
30
+ }
31
+ if (char === '/' && next === '*') {
32
+ index += 2
33
+ while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) index += 1
34
+ index += 2
35
+ out += ' '
36
+ continue
37
+ }
38
+ out += char
39
+ index += 1
40
+ }
41
+ if (quote) throw new Error('Unterminated string in static JavaScript literal.')
42
+ return out
43
+ }
44
+
45
+ class LiteralParser {
46
+ private index = 0
47
+
48
+ constructor(private readonly source: string) {}
49
+
50
+ parseAt(index: number): unknown {
51
+ this.index = index
52
+ const value = this.value()
53
+ this.space()
54
+ return value
55
+ }
56
+
57
+ private space(): void {
58
+ while (/\s/.test(this.source[this.index] ?? '')) this.index += 1
59
+ }
60
+
61
+ private value(): unknown {
62
+ this.space()
63
+ const char = this.source[this.index]
64
+ if (char === '{') return this.object()
65
+ if (char === '[') return this.array()
66
+ if (char === '"' || char === "'") return this.string()
67
+ const word = this.word()
68
+ if (word === 'true') return true
69
+ if (word === 'false') return false
70
+ if (word === 'null') return null
71
+ if (/^-?\d+(?:\.\d+)?$/.test(word)) return Number(word)
72
+ throw new Error(`Unsupported dynamic sidebar expression near "${word || char || 'EOF'}".`)
73
+ }
74
+
75
+ private object(): Record<string, unknown> {
76
+ const result: Record<string, unknown> = {}
77
+ this.index += 1
78
+ this.space()
79
+ while (this.source[this.index] !== '}') {
80
+ const key = this.source[this.index] === '"' || this.source[this.index] === "'" ? this.string() : this.word()
81
+ if (!key) throw new Error('Static sidebar object key is required.')
82
+ this.space()
83
+ if (this.source[this.index] !== ':') throw new Error(`Static sidebar key "${key}" requires a literal value.`)
84
+ this.index += 1
85
+ result[key] = this.value()
86
+ this.space()
87
+ if (this.source[this.index] === ',') {
88
+ this.index += 1
89
+ this.space()
90
+ continue
91
+ }
92
+ if (this.source[this.index] !== '}') throw new Error('Expected comma or closing brace in static sidebar.')
93
+ }
94
+ this.index += 1
95
+ return result
96
+ }
97
+
98
+ private array(): unknown[] {
99
+ const result: unknown[] = []
100
+ this.index += 1
101
+ this.space()
102
+ while (this.source[this.index] !== ']') {
103
+ result.push(this.value())
104
+ this.space()
105
+ if (this.source[this.index] === ',') {
106
+ this.index += 1
107
+ this.space()
108
+ continue
109
+ }
110
+ if (this.source[this.index] !== ']') throw new Error('Expected comma or closing bracket in static sidebar.')
111
+ }
112
+ this.index += 1
113
+ return result
114
+ }
115
+
116
+ private string(): string {
117
+ const quote = this.source[this.index] ?? ''
118
+ this.index += 1
119
+ let result = ''
120
+ while (this.index < this.source.length) {
121
+ const char = this.source[this.index] ?? ''
122
+ if (char === quote) {
123
+ this.index += 1
124
+ return result
125
+ }
126
+ if (char === '\\') {
127
+ const escaped = this.source[this.index + 1]
128
+ if (escaped === undefined) break
129
+ const escapes: Record<string, string> = { n: '\n', r: '\r', t: '\t' }
130
+ result += escapes[escaped] ?? escaped
131
+ this.index += 2
132
+ continue
133
+ }
134
+ result += char
135
+ this.index += 1
136
+ }
137
+ throw new Error('Unterminated string in static sidebar.')
138
+ }
139
+
140
+ private word(): string {
141
+ this.space()
142
+ const start = this.index
143
+ while (/[A-Za-z0-9_$.-]/.test(this.source[this.index] ?? '')) this.index += 1
144
+ return this.source.slice(start, this.index)
145
+ }
146
+ }
147
+
148
+ const maskStrings = (source: string): string => {
149
+ const chars = [...source]
150
+ let quote: string | undefined
151
+ let previousToken: string | undefined
152
+ const regexPrefixKeywords = new Set([
153
+ 'await', 'case', 'delete', 'do', 'else', 'in', 'instanceof', 'new',
154
+ 'of', 'return', 'throw', 'typeof', 'void', 'yield',
155
+ ])
156
+ const canStartRegex = (): boolean => previousToken === undefined
157
+ || '=(:,[!&|?{};'.includes(previousToken)
158
+ || previousToken === '=>'
159
+ || regexPrefixKeywords.has(previousToken)
160
+ for (let index = 0; index < chars.length; index += 1) {
161
+ const char = chars[index] ?? ''
162
+ if (quote) {
163
+ chars[index] = ' '
164
+ if (char === '\\') {
165
+ if (index + 1 < chars.length) chars[index + 1] = ' '
166
+ index += 1
167
+ } else if (char === quote) {
168
+ quote = undefined
169
+ previousToken = 'literal'
170
+ }
171
+ continue
172
+ }
173
+ if (char === '"' || char === "'" || char === '`') {
174
+ quote = char
175
+ chars[index] = ' '
176
+ continue
177
+ }
178
+ if (char === '/' && canStartRegex()) {
179
+ chars[index] = ' '
180
+ let inClass = false
181
+ for (index += 1; index < chars.length; index += 1) {
182
+ const regexChar = chars[index] ?? ''
183
+ chars[index] = ' '
184
+ if (regexChar === '\\') {
185
+ if (index + 1 < chars.length) chars[index + 1] = ' '
186
+ index += 1
187
+ } else if (regexChar === '[') {
188
+ inClass = true
189
+ } else if (regexChar === ']') {
190
+ inClass = false
191
+ } else if (regexChar === '/' && !inClass) {
192
+ while (/[A-Za-z]/.test(chars[index + 1] ?? '')) {
193
+ index += 1
194
+ chars[index] = ' '
195
+ }
196
+ break
197
+ }
198
+ }
199
+ previousToken = 'literal'
200
+ continue
201
+ }
202
+ if (/[A-Za-z_$]/.test(char)) {
203
+ const start = index
204
+ while (/[A-Za-z0-9_$]/.test(chars[index + 1] ?? '')) index += 1
205
+ previousToken = chars.slice(start, index + 1).join('')
206
+ continue
207
+ }
208
+ if (char === '=' && chars[index + 1] === '>') {
209
+ previousToken = '=>'
210
+ index += 1
211
+ continue
212
+ }
213
+ if (!/\s/.test(char)) previousToken = char
214
+ }
215
+ return chars.join('')
216
+ }
217
+
218
+ const nonSpace = (source: string, start: number): number => {
219
+ let index = start
220
+ while (/\s/.test(source[index] ?? '')) index += 1
221
+ return index
222
+ }
223
+
224
+ const escapedRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
225
+
226
+ export const parseStaticJsObject = (source: string): unknown => {
227
+ const clean = stripComments(source)
228
+ const mask = maskStrings(clean)
229
+ const parseBinding = (identifier: string): unknown => {
230
+ const declaration = new RegExp(`\\b(?:const|let|var)\\s+${escapedRegex(identifier)}(?:\\s*:[^=;]+)?\\s*=`).exec(mask)
231
+ if (!declaration) throw new Error(`Exported binding "${identifier}" must reference a static object declaration.`)
232
+ const start = nonSpace(mask, declaration.index + declaration[0].length)
233
+ if (mask[start] !== '{') throw new Error(`Exported binding "${identifier}" must be a static object literal.`)
234
+ return new LiteralParser(clean).parseAt(start)
235
+ }
236
+ const commonJs = /\bmodule\.exports\s*=/.exec(mask)
237
+ if (commonJs) {
238
+ const start = nonSpace(mask, commonJs.index + commonJs[0].length)
239
+ if (mask[start] === '{') return new LiteralParser(clean).parseAt(start)
240
+ const identifier = /^[A-Za-z_$][\w$]*/.exec(mask.slice(start))?.[0]
241
+ if (identifier) return parseBinding(identifier)
242
+ throw new Error('module.exports must be assigned a static object literal or binding.')
243
+ }
244
+
245
+ const exported = /\bexport\s+default\b/.exec(mask)
246
+ if (exported) {
247
+ let start = nonSpace(mask, exported.index + exported[0].length)
248
+ if (mask.slice(start).startsWith('defineConfig')) {
249
+ start = nonSpace(mask, start + 'defineConfig'.length)
250
+ if (mask[start] !== '(') throw new Error('defineConfig must be called with a static object literal.')
251
+ start = nonSpace(mask, start + 1)
252
+ if (mask[start] !== '{') throw new Error('defineConfig must be called with a static object literal.')
253
+ return new LiteralParser(clean).parseAt(start)
254
+ }
255
+ if (mask[start] === '{') return new LiteralParser(clean).parseAt(start)
256
+
257
+ const identifier = /^[A-Za-z_$][\w$]*/.exec(mask.slice(start))?.[0]
258
+ if (identifier) return parseBinding(identifier)
259
+ }
260
+ throw new Error('Docusaurus sidebar must export a static object or assign one to a const.')
261
+ }
package/src/lib/walk.ts CHANGED
@@ -1,19 +1,34 @@
1
- import { readdirSync, statSync } from 'node:fs'
1
+ import { lstatSync, readdirSync, realpathSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
4
  import { toPosix } from './paths.js'
5
5
 
6
6
  const DEFAULT_SKIP = new Set(['node_modules', '.git', 'dist', 'coverage', '.doc-bridge'])
7
+ const DEFAULT_MAX_FILES = 10_000
7
8
 
8
9
  export const walkFiles = (
9
10
  root: string,
10
- opts?: { readonly extensions?: readonly string[]; readonly skipDirs?: ReadonlySet<string> },
11
+ opts?: {
12
+ readonly extensions?: readonly string[]
13
+ readonly skipDirs?: ReadonlySet<string>
14
+ readonly maxFiles?: number
15
+ },
11
16
  ): string[] => {
12
17
  const extensions = opts?.extensions ?? ['.md']
13
18
  const skip = opts?.skipDirs ?? DEFAULT_SKIP
14
19
  const out: string[] = []
20
+ const visited = new Set<string>()
15
21
 
16
22
  const visit = (dir: string) => {
23
+ let canonicalDir: string
24
+ try {
25
+ canonicalDir = realpathSync.native(dir)
26
+ } catch {
27
+ return
28
+ }
29
+ if (visited.has(canonicalDir)) return
30
+ visited.add(canonicalDir)
31
+
17
32
  let entries: string[] = []
18
33
  try {
19
34
  entries = readdirSync(dir)
@@ -24,10 +39,11 @@ export const walkFiles = (
24
39
  const abs = join(dir, name)
25
40
  let st
26
41
  try {
27
- st = statSync(abs)
42
+ st = lstatSync(abs)
28
43
  } catch {
29
44
  continue
30
45
  }
46
+ if (st.isSymbolicLink()) continue
31
47
  if (st.isDirectory()) {
32
48
  if (skip.has(name)) continue
33
49
  visit(abs)
@@ -35,6 +51,9 @@ export const walkFiles = (
35
51
  }
36
52
  if (!st.isFile()) continue
37
53
  if (extensions.some((ext) => name.endsWith(ext))) {
54
+ if (out.length >= (opts?.maxFiles ?? DEFAULT_MAX_FILES)) {
55
+ throw new Error(`Documentation corpus exceeds the ${opts?.maxFiles ?? DEFAULT_MAX_FILES} file limit.`)
56
+ }
38
57
  out.push(toPosix(abs))
39
58
  }
40
59
  }
@@ -42,4 +61,4 @@ export const walkFiles = (
42
61
 
43
62
  visit(root)
44
63
  return out.sort()
45
- }
64
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '1.0.0'
1
+ export const PACKAGE_VERSION = '1.1.1'