@agentskit/doc-bridge 1.7.44 → 1.7.45

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 (55) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/CONTRIBUTING.md +6 -4
  3. package/action.yml +1 -1
  4. package/dist/cli/program.js +378 -189
  5. package/dist/cli/program.js.map +1 -1
  6. package/dist/config/index.d.ts +1 -1
  7. package/dist/config/index.js +5 -3
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/index-BUL0q7s8.d.ts +660 -0
  10. package/dist/index.d.ts +637 -2724
  11. package/dist/index.js +328 -149
  12. package/dist/index.js.map +1 -1
  13. package/docs/RELEASE.md +22 -8
  14. package/docs/agent-corpus/INDEX.md +2 -2
  15. package/docs/agent-corpus/chat.md +2 -2
  16. package/docs/agent-corpus/cli.md +2 -2
  17. package/docs/agent-corpus/conformance.md +2 -2
  18. package/docs/agent-corpus/doc-bridge.md +1 -1
  19. package/docs/agent-corpus/doctor.md +2 -2
  20. package/docs/agent-corpus/gates.md +2 -2
  21. package/docs/agent-corpus/mcp.md +2 -2
  22. package/docs/agent-corpus/memory.md +2 -2
  23. package/docs/agent-corpus/query.md +2 -2
  24. package/docs/knowledge-engine-runbook.md +13 -1
  25. package/docs/spec/benchmark-v1.md +6 -0
  26. package/docs/spec/config-v1.md +45 -0
  27. package/docs/validation-cycle-plan.md +19 -0
  28. package/docs/verification-harness.md +4 -0
  29. package/mcpb/manifest.json +1 -1
  30. package/package.json +68 -70
  31. package/scripts/check-ecosystem-upstream.mjs +3 -2
  32. package/scripts/report-visual-check.mjs +20 -3
  33. package/scripts/verification-harness.mjs +0 -1
  34. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  35. package/src/cli/demo.ts +2 -2
  36. package/src/cli/program.ts +15 -5
  37. package/src/config/load-config.ts +7 -1
  38. package/src/config/schema.ts +4 -2
  39. package/src/conformance/documentation-standard-v1.ts +14 -8
  40. package/src/discovery/documentation.ts +44 -18
  41. package/src/discovery/repository.ts +77 -28
  42. package/src/doctor/run-doctor.ts +2 -15
  43. package/src/federation/llms.ts +72 -20
  44. package/src/fixes/proposals.ts +4 -3
  45. package/src/index-builder/human-adapters/fumadocs.ts +1 -1
  46. package/src/index-builder/watch-index.ts +1 -1
  47. package/src/lib/bounded-text.ts +15 -10
  48. package/src/reconciliation/reconcile.ts +47 -5
  49. package/src/report/html.ts +21 -15
  50. package/src/rules/engine.ts +15 -2
  51. package/src/safety/repository.ts +1 -1
  52. package/src/schemas/knowledge.ts +5 -2
  53. package/src/validate.ts +7 -1
  54. package/src/version.ts +1 -1
  55. package/dist/index-C2PCQSrB.d.ts +0 -2251
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, relative, resolve } from 'node:path'
3
3
  import { createInterface } from 'node:readline/promises'
4
4
 
@@ -25,7 +25,7 @@ import { ingestMemoryCandidates } from '../memory/ingest.js'
25
25
  import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
26
26
  import { promoteMemoryToGithubPr } from '../memory/github-pr.js'
27
27
  import { watchDocBridgeIndex } from '../index-builder/watch-index.js'
28
- import { loadWorkflowManifest, loadWorkflowStepOutput, runWorkflow, type WorkflowExecutionResult } from '../workflow/engine.js'
28
+ import { loadWorkflowStepOutput, runWorkflow, type WorkflowExecutionResult } from '../workflow/engine.js'
29
29
  import {
30
30
  formatDoctorBadgeJson,
31
31
  formatDoctorBadgeMarkdown,
@@ -500,6 +500,7 @@ const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExe
500
500
  const report = reconcileKnowledge(snapshot, declared, {
501
501
  ...(config.reconciliation?.scope === undefined ? {} : { scope: config.reconciliation.scope }),
502
502
  ...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
503
+ ...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
503
504
  ...(config.reconciliation?.includeOrphanedDocuments === undefined ? {} : { includeOrphanedDocuments: config.reconciliation.includeOrphanedDocuments }),
504
505
  })
505
506
  return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }, { pipelineVersion: snapshot.pipelineVersion, analyzerVersions: snapshot.analyzerVersions }))
@@ -720,10 +721,19 @@ const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string
720
721
  }
721
722
 
722
723
  const writeIfMissing = (path: string, contents: string): boolean => {
723
- if (existsSync(path)) return false
724
724
  mkdirSync(dirname(path), { recursive: true })
725
- writeFileSync(path, contents, 'utf8')
726
- return true
725
+ try {
726
+ const fd = openSync(path, 'wx')
727
+ try {
728
+ writeFileSync(fd, contents, 'utf8')
729
+ return true
730
+ } finally {
731
+ closeSync(fd)
732
+ }
733
+ } catch (error) {
734
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') return false
735
+ throw error
736
+ }
727
737
  }
728
738
 
729
739
  const demoOwnership = {
@@ -79,7 +79,13 @@ const parseConfig = (input: unknown): DocBridgeConfigV1 => {
79
79
  if (result.success) return result.data
80
80
  throw new Error(
81
81
  `Invalid doc-bridge config:\n${result.error.issues.map((issue) =>
82
- ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`,
82
+ ` - ${issue.path.join('.') || '(root)'}: ${
83
+ issue.code === 'invalid_type' && issue.message.endsWith('received undefined')
84
+ ? 'Required'
85
+ : issue.code === 'invalid_value' && 'values' in issue
86
+ ? 'Invalid enum value'
87
+ : issue.message
88
+ }`,
83
89
  ).join('\n')}`,
84
90
  )
85
91
  }
@@ -171,11 +171,11 @@ export const RuleSeveritySchema = z.enum(['off', 'info', 'warn', 'error'])
171
171
  export const RulesConfigSchema = z
172
172
  .object({
173
173
  mode: z.enum(['default', 'recommended', 'strict']).optional(),
174
- severity: z.record(RuleIdSchema, RuleSeveritySchema).optional(),
174
+ severity: z.partialRecord(RuleIdSchema, RuleSeveritySchema).optional(),
175
175
  ignore: z.array(RuleIdSchema).max(128).optional(),
176
176
  criticalEntities: z.array(z.string().min(1).max(256)).max(128).optional(),
177
177
  criticalPaths: z.array(z.string().min(1).max(512)).max(128).optional(),
178
- warningThresholds: z.record(RuleIdSchema, z.number().int().min(1).max(100_000)).optional(),
178
+ warningThresholds: z.partialRecord(RuleIdSchema, z.number().int().min(1).max(100_000)).optional(),
179
179
  })
180
180
  .strict()
181
181
 
@@ -185,6 +185,8 @@ export const ReconciliationConfigSchema = z
185
185
  scope: z.enum(['file', 'module', 'package']).optional(),
186
186
  /** Relation kinds that must have documentation declarations. Omit to require all observed kinds; [] disables this signal. */
187
187
  requiredRelationKinds: z.array(z.string().min(1).max(128)).max(128).optional(),
188
+ /** Limit missing-declaration findings to relations whose endpoints are internal project entities. */
189
+ requiredRelationTargets: z.enum(['all', 'internal']).optional(),
188
190
  includeOrphanedDocuments: z.boolean().optional(),
189
191
  })
190
192
  .strict()
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
1
+ import { closeSync, existsSync, fstatSync, openSync, readFileSync, realpathSync } from 'node:fs'
2
2
  import { isAbsolute, relative, resolve, sep } from 'node:path'
3
3
 
4
4
  import type {
@@ -95,11 +95,10 @@ const fileEvidence = (
95
95
  evidence: { path, detail: 'Path escapes the project root.' },
96
96
  }
97
97
  }
98
- if (!existsSync(abs)) {
99
- return { exists: false, content: '', evidence: { path, detail: 'File does not exist.' } }
100
- }
98
+ let fd: number | undefined
101
99
  try {
102
- const stat = statSync(abs)
100
+ fd = openSync(abs, 'r')
101
+ const stat = fstatSync(fd)
103
102
  if (!stat.isFile()) {
104
103
  return { exists: false, content: '', evidence: { path, detail: 'Path is not a regular file.' } }
105
104
  }
@@ -120,7 +119,7 @@ const fileEvidence = (
120
119
  evidence: { path, detail: `Text evidence exceeds ${MAX_TEXT_EVIDENCE_BYTES} bytes.` },
121
120
  }
122
121
  }
123
- const content = readFileSync(abs, 'utf8')
122
+ const content = readFileSync(fd, 'utf8')
124
123
  return {
125
124
  exists: content.trim().length > 0,
126
125
  content,
@@ -129,8 +128,15 @@ const fileEvidence = (
129
128
  detail: content.trim().length > 0 ? 'File exists and is non-empty.' : 'File is empty.',
130
129
  },
131
130
  }
132
- } catch {
133
- return { exists: false, content: '', evidence: { path, detail: 'File is not readable text.' } }
131
+ } catch (error) {
132
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined
133
+ return {
134
+ exists: false,
135
+ content: '',
136
+ evidence: { path, detail: code === 'ENOENT' ? 'File does not exist.' : 'File is not readable text.' },
137
+ }
138
+ } finally {
139
+ if (fd !== undefined) closeSync(fd)
134
140
  }
135
141
  }
136
142
 
@@ -73,6 +73,32 @@ const scalar = (value: string): string => {
73
73
  return trimmed
74
74
  }
75
75
 
76
+ const isFieldName = (value: string): boolean => {
77
+ if (!/^[A-Za-z]/.test(value)) return false
78
+ for (const character of value.slice(1)) {
79
+ if (!/[A-Za-z0-9_-]/.test(character)) return false
80
+ }
81
+ return true
82
+ }
83
+
84
+ const parseIndentedField = (raw: string, indentation: number): { readonly key: string; readonly value: string } | undefined => {
85
+ const prefix = ' '.repeat(indentation)
86
+ if (!raw.startsWith(prefix) || raw[indentation] === ' ') return undefined
87
+ const body = raw.slice(indentation)
88
+ const separator = body.indexOf(':')
89
+ if (separator <= 0) return undefined
90
+ const key = body.slice(0, separator).trim()
91
+ return isFieldName(key) ? { key, value: body.slice(separator + 1).trim() } : undefined
92
+ }
93
+
94
+ const parseListItem = (raw: string, indentation: number): string | undefined => {
95
+ const prefix = `${' '.repeat(indentation)}-`
96
+ if (!raw.startsWith(prefix)) return undefined
97
+ const rest = raw.slice(prefix.length)
98
+ if (rest && !/\s/.test(rest[0] ?? '')) return undefined
99
+ return rest.trim()
100
+ }
101
+
76
102
  const conventionalPackageReference = (path: string, agentRoot: string): string | undefined => {
77
103
  const prefix = `${agentRoot.replace(/\/$/, '')}/`
78
104
  if (!path.startsWith(prefix)) return undefined
@@ -208,11 +234,10 @@ const parseBlock = (
208
234
  section = undefined
209
235
  continue
210
236
  }
211
- if (/^ {2}[A-Za-z][A-Za-z0-9_-]*\s*:/.test(raw)) {
237
+ const sectionField = parseIndentedField(raw, 2)
238
+ if (sectionField) {
212
239
  finishRelation()
213
- const match = /^ {2}([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(raw)
214
- const key = match?.[1]
215
- const value = match?.[2] ?? ''
240
+ const { key, value } = sectionField
216
241
  if (key !== 'covers' && key !== 'relations') {
217
242
  addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown docbridge field: ${key ?? '(missing)'}.`, line)
218
243
  section = undefined
@@ -229,35 +254,36 @@ const parseBlock = (
229
254
  }
230
255
  continue
231
256
  }
232
- if (section === 'covers' && /^ {4}-\s*/.test(raw)) {
233
- const value = scalar(raw.replace(/^ {4}-\s*/, ''))
257
+ const listValue = parseListItem(raw, 4)
258
+ if (section === 'covers' && listValue !== undefined) {
259
+ const value = scalar(listValue)
234
260
  if (!value) addDiagnostic(diagnostics, input, 'DOCBRIDGE_REFERENCE_MISSING', 'covers entries must not be empty.', line)
235
261
  else covers.push({ value, line })
236
262
  continue
237
263
  }
238
- if (section === 'relations' && /^ {4}-\s*/.test(raw)) {
264
+ if (section === 'relations' && listValue !== undefined) {
239
265
  finishRelation()
240
- const firstField = /^ {4}-\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(raw)
266
+ const firstField = parseIndentedField(` ${listValue}`, 4)
241
267
  current = { startLine: line, endLine: line, fields: new Set() }
242
- if (firstField?.[1]) {
243
- current.fields.add(firstField[1])
244
- current[firstField[1] as 'from' | 'to' | 'kind' | 'detection'] = scalar(firstField[2] ?? '')
245
- } else if (raw.replace(/^ {4}-\s*/, '').trim()) {
268
+ if (firstField?.key) {
269
+ current.fields.add(firstField.key)
270
+ current[firstField.key as 'from' | 'to' | 'kind' | 'detection'] = scalar(firstField.value)
271
+ } else if (listValue) {
246
272
  addDiagnostic(diagnostics, input, 'DOCBRIDGE_RELATION_INVALID', 'Relation entries must be field mappings.', line)
247
273
  }
248
274
  continue
249
275
  }
250
- if (section === 'relations' && current && /^ {6}[A-Za-z][A-Za-z0-9_-]*\s*:/.test(raw)) {
251
- const field = /^ {6}([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(raw)
252
- const key = field?.[1]
276
+ const field = parseIndentedField(raw, 6)
277
+ if (section === 'relations' && current && field) {
278
+ const { key, value } = field
253
279
  current.endLine = line
254
- if (!key || !['from', 'to', 'kind', 'detection'].includes(key)) {
255
- addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown relation field: ${key ?? '(missing)'}.`, line)
280
+ if (!['from', 'to', 'kind', 'detection'].includes(key)) {
281
+ addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown relation field: ${key}.`, line)
256
282
  } else if (current.fields.has(key)) {
257
283
  addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_DUPLICATE', `Duplicate relation field: ${key}.`, line)
258
284
  } else {
259
285
  current.fields.add(key)
260
- current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(field?.[2] ?? '')
286
+ current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(value)
261
287
  }
262
288
  continue
263
289
  }
@@ -102,6 +102,7 @@ const firstLineContaining = (text: string, pattern: string): number | undefined
102
102
 
103
103
  const documentClassification = (path: string): string => {
104
104
  if (/(^|\/)docs\/for-agents(?:\/|$)/.test(path)) return 'agent'
105
+ if (/(^|\/)docs-archive(?:\/|$)/.test(path)) return 'archive'
105
106
  if (/(^|\/)docs(?:\/|$)/.test(path)) return 'human'
106
107
  if (/(^|\/)(README|CONTRIBUTING|SECURITY|CHANGELOG)(?:\.|$)/i.test(path)) return 'project'
107
108
  return 'unclassified'
@@ -273,22 +274,64 @@ const moduleReferences = (
273
274
  path: string,
274
275
  sourceFile: ts.SourceFile,
275
276
  runtimeWiringMethods: ReadonlySet<string>,
276
- ): { readonly references: readonly ImportReference[]; readonly exports: readonly string[]; readonly hasDynamic: boolean; readonly hasLiteralDynamic: boolean; readonly hasRuntimeWiring: boolean; readonly hasUnresolvedRuntimeWiring: boolean } => {
277
+ ): { readonly references: readonly ImportReference[]; readonly exports: readonly string[]; readonly dynamicEvidence: readonly Evidence[]; readonly hasDynamic: boolean; readonly hasLiteralDynamic: boolean; readonly hasRuntimeWiring: boolean; readonly hasUnresolvedRuntimeWiring: boolean } => {
277
278
  const references: ImportReference[] = []
279
+ const dynamicEvidence: Evidence[] = []
278
280
  let hasDynamic = false
279
281
  let hasLiteralDynamic = false
280
282
  let hasRuntimeWiring = false
281
283
  let hasUnresolvedRuntimeWiring = false
282
284
  const importedBindings = new Map<string, string>()
283
285
  const staticStringBindings = new Map<string, string | undefined>()
286
+ const localBindings = new Set<string>()
287
+ const resolveStaticString = (expression: ts.Expression): string | undefined => {
288
+ if (ts.isStringLiteralLike(expression)) return expression.text
289
+ if (ts.isIdentifier(expression)) return staticStringBindings.get(expression.text)
290
+ if (ts.isParenthesizedExpression(expression)) return resolveStaticString(expression.expression)
291
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.PlusToken) {
292
+ const left = resolveStaticString(expression.left)
293
+ const right = resolveStaticString(expression.right)
294
+ return left !== undefined && right !== undefined ? left + right : undefined
295
+ }
296
+ return undefined
297
+ }
284
298
  const collectStaticStringBindings = (node: ts.Node): void => {
285
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isStringLiteralLike(node.initializer) && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0) {
299
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0) {
300
+ const value = resolveStaticString(node.initializer)
286
301
  const previous = staticStringBindings.get(node.name.text)
287
- staticStringBindings.set(node.name.text, !staticStringBindings.has(node.name.text) || previous === node.initializer.text ? node.initializer.text : undefined)
302
+ staticStringBindings.set(node.name.text, !staticStringBindings.has(node.name.text) || previous === value ? value : undefined)
288
303
  }
304
+ if (
305
+ (ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node)) &&
306
+ ts.isIdentifier(node.name)
307
+ ) localBindings.add(node.name.text)
308
+ if (
309
+ (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) &&
310
+ node.name
311
+ ) localBindings.add(node.name.text)
289
312
  ts.forEachChild(node, collectStaticStringBindings)
290
313
  }
291
314
  collectStaticStringBindings(sourceFile)
315
+ const addImportedBindingReference = (expression: ts.Expression, node: ts.Node): boolean => {
316
+ if (ts.isIdentifier(expression)) {
317
+ const specifier = importedBindings.get(expression.text)
318
+ if (specifier) {
319
+ addReference({ text: specifier } as ts.StringLiteralLike, 'runtime-wiring', node, 'runtime-wiring-static')
320
+ return true
321
+ }
322
+ return false
323
+ }
324
+ if (ts.isPropertyAccessExpression(expression)) return addImportedBindingReference(expression.expression, node)
325
+ if (ts.isCallExpression(expression)) return addImportedBindingReference(expression.expression, node)
326
+ return false
327
+ }
328
+ const isKnownLocal = (expression: ts.Expression): boolean => {
329
+ if (ts.isIdentifier(expression)) return localBindings.has(expression.text) || importedBindings.has(expression.text)
330
+ if (expression.kind === ts.SyntaxKind.ThisKeyword) return true
331
+ if (ts.isPropertyAccessExpression(expression)) return isKnownLocal(expression.expression)
332
+ if (ts.isCallExpression(expression)) return isKnownLocal(expression.expression)
333
+ return ts.isStringLiteralLike(expression)
334
+ }
292
335
  const addReference = (specifier: ts.StringLiteralLike, kind: ImportReference['kind'], node: ts.Node, detection?: ImportReference['detection']): void => {
293
336
  references.push({ specifier: specifier.text, kind, evidence: nodeEvidence(root, path, sourceFile, node), ...(detection ? { detection } : {}) })
294
337
  }
@@ -309,32 +352,35 @@ const moduleReferences = (
309
352
  importedBindings.set(node.name.text, node.moduleReference.expression.text)
310
353
  } else if (ts.isCallExpression(node)) {
311
354
  if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
312
- if (node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) {
313
- hasLiteralDynamic = true
314
- addReference(node.arguments[0], 'imports', node, 'dynamic-literal')
315
- } else if (node.arguments[0] && ts.isIdentifier(node.arguments[0]) && staticStringBindings.get(node.arguments[0].text)) {
355
+ const specifier = node.arguments[0] ? resolveStaticString(node.arguments[0]) : undefined
356
+ if (specifier !== undefined) {
316
357
  hasLiteralDynamic = true
317
- addReference({ text: staticStringBindings.get(node.arguments[0].text)! } as ts.StringLiteralLike, 'imports', node, 'dynamic-literal')
318
- } else hasDynamic = true
358
+ dynamicEvidence.push(nodeEvidence(root, path, sourceFile, node))
359
+ addReference({ text: specifier } as ts.StringLiteralLike, 'imports', node, 'dynamic-literal')
360
+ } else {
361
+ hasDynamic = true
362
+ dynamicEvidence.push(nodeEvidence(root, path, sourceFile, node))
363
+ }
319
364
  } else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
320
365
  const argument = node.arguments[0]
321
- if (argument && ts.isStringLiteralLike(argument)) addReference(argument, 'imports', node)
322
- else if (argument && ts.isIdentifier(argument) && staticStringBindings.get(argument.text)) addReference({ text: staticStringBindings.get(argument.text)! } as ts.StringLiteralLike, 'imports', node)
323
- else hasDynamic = true
366
+ const specifier = argument ? resolveStaticString(argument) : undefined
367
+ if (specifier !== undefined) {
368
+ dynamicEvidence.push(nodeEvidence(root, path, sourceFile, node))
369
+ addReference({ text: specifier } as ts.StringLiteralLike, 'imports', node)
370
+ } else {
371
+ hasDynamic = true
372
+ dynamicEvidence.push(nodeEvidence(root, path, sourceFile, node))
373
+ }
324
374
  } else if (ts.isPropertyAccessExpression(node.expression) && runtimeWiringMethods.has(node.expression.name.text)) {
325
375
  hasRuntimeWiring = true
326
- const before = references.length
327
- let hasPotentialTargetArgument = false
376
+ let hasUnresolvedTarget = false
328
377
  for (const argument of node.arguments) {
329
- if (ts.isIdentifier(argument)) {
330
- hasPotentialTargetArgument = true
331
- const specifier = importedBindings.get(argument.text)
332
- if (specifier) addReference({ text: specifier } as ts.StringLiteralLike, 'runtime-wiring', node, 'runtime-wiring-static')
333
- } else if (ts.isPropertyAccessExpression(argument) || ts.isCallExpression(argument)) {
334
- hasPotentialTargetArgument = true
335
- }
378
+ if (ts.isStringLiteralLike(argument)) continue
379
+ if (addImportedBindingReference(argument, node)) continue
380
+ if (!isKnownLocal(argument)) hasUnresolvedTarget = true
336
381
  }
337
- if (hasPotentialTargetArgument && references.length === before) hasUnresolvedRuntimeWiring = true
382
+ const receiver = node.expression.expression
383
+ if (hasUnresolvedTarget && !isKnownLocal(receiver)) hasUnresolvedRuntimeWiring = true
338
384
  }
339
385
  }
340
386
  ts.forEachChild(node, visit)
@@ -343,6 +389,7 @@ const moduleReferences = (
343
389
  return {
344
390
  references,
345
391
  exports: exportedNames(sourceFile),
392
+ dynamicEvidence,
346
393
  hasDynamic,
347
394
  hasLiteralDynamic,
348
395
  hasRuntimeWiring,
@@ -445,11 +492,11 @@ const artifact = (root: string, config: DocBridgeConfigV1 | undefined, files: re
445
492
  sourceRevision: revision.value,
446
493
  sourceRevisionKind: revision.kind,
447
494
  configurationHash: sha256NormalizedV1(config ?? {}),
448
- pipelineVersion: '1.0.0',
449
- analyzerVersions: { repository: '1.0.0', 'js-ts': '1.3.0' },
495
+ pipelineVersion: '1.1.8',
496
+ analyzerVersions: { repository: '1.1.1', 'js-ts': '1.3.4' },
450
497
  entities: [...entities].sort((a, b) => a.id.localeCompare(b.id)),
451
498
  relations: [...relations].sort((a, b) => a.id.localeCompare(b.id)),
452
- coverage: coverage.map((entry) => ({ ...entry, analyzerVersion: entry.analyzerVersion ?? ({ repository: '1.0.0', 'js-ts': '1.3.0' }[entry.analyzer] ?? '1.0.0') })),
499
+ coverage: coverage.map((entry) => ({ ...entry, analyzerVersion: entry.analyzerVersion ?? ({ repository: '1.1.1', 'js-ts': '1.3.4' }[entry.analyzer] ?? '1.0.0') })),
453
500
  }
454
501
  return DiscoverySnapshotV1Schema.parse({ ...base, contentHash: contentHashForArtifactV1(base) })
455
502
  }
@@ -552,6 +599,7 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
552
599
  const includeTestRuntimeWiring = opts.config?.analysis?.jsTs?.includeTestRuntimeWiring ?? false
553
600
  let observedLiteralDynamic = false
554
601
  let observedUnresolvedDynamic = false
602
+ const observedDynamicEvidence: Evidence[] = []
555
603
  let observedRuntimeWiring = false
556
604
  let observedUnresolvedRuntimeWiring = false
557
605
  for (const module of modules.values()) {
@@ -561,6 +609,7 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
561
609
  const references = moduleReferences(root, module.absPath, sourceFile, runtimeWiringMethods)
562
610
  observedLiteralDynamic ||= references.hasLiteralDynamic
563
611
  observedUnresolvedDynamic ||= references.hasDynamic
612
+ observedDynamicEvidence.push(...references.dynamicEvidence)
564
613
  observedRuntimeWiring ||= references.hasRuntimeWiring
565
614
  observedUnresolvedRuntimeWiring ||= references.hasUnresolvedRuntimeWiring
566
615
  for (const reference of references.references) {
@@ -572,14 +621,14 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
572
621
  }
573
622
  addRelation({ id: entityId('relation', `${module.entityId}:${reference.kind}:${target.targetId}`), kind: reference.kind, from: module.entityId, to: target.targetId, provenance: 'observed', evidence: [reference.evidence], ...(reference.detection ? { metadata: { detection: reference.detection } } : {}) })
574
623
  }
575
- if (references.hasLiteralDynamic || references.hasDynamic) coverage.push({ analyzer: 'js-ts', scope: `dynamic-imports:${module.path}`, status: references.hasDynamic ? 'not-analyzed' : 'complete', reason: references.hasDynamic ? 'A non-literal dynamic import was found; the target is unresolved.' : 'Literal dynamic imports were resolved.', evidence: [lineEvidence('code', root, module.absPath)] })
624
+ if (references.hasLiteralDynamic || references.hasDynamic) coverage.push({ analyzer: 'js-ts', scope: `dynamic-imports:${module.path}`, status: references.hasDynamic ? 'not-analyzed' : 'complete', reason: references.hasDynamic ? 'A non-literal dynamic import was found; the target is unresolved.' : 'Literal dynamic imports were resolved.', evidence: [...references.dynamicEvidence.slice(0, 32)] })
576
625
  if (references.hasUnresolvedRuntimeWiring) coverage.push({ analyzer: 'js-ts', scope: `runtime-wiring:${module.path}`, status: 'not-analyzed', reason: 'A runtime registration/wiring call was found without a statically imported target.', evidence: [lineEvidence('code', root, module.absPath)] })
577
626
  }
578
627
 
579
628
  if (dynamicCoverageIndex >= 0) coverage[dynamicCoverageIndex] = observedUnresolvedDynamic
580
- ? { analyzer: 'js-ts', scope: 'dynamic-imports', status: 'partial', reason: 'Literal dynamic imports are resolved; non-literal import expressions and require calls remain unresolved.' }
629
+ ? { analyzer: 'js-ts', scope: 'dynamic-imports', status: 'partial', reason: 'Literal dynamic imports are resolved; non-literal import expressions and require calls remain unresolved. Evidence lists representative dynamic loading sites.', evidence: [...observedDynamicEvidence.slice(0, 32)] }
581
630
  : observedLiteralDynamic
582
- ? { analyzer: 'js-ts', scope: 'dynamic-imports', status: 'complete', reason: 'All observed dynamic imports used literal targets and were resolved.' }
631
+ ? { analyzer: 'js-ts', scope: 'dynamic-imports', status: 'complete', reason: 'All observed dynamic imports used literal targets and were resolved.', evidence: [...observedDynamicEvidence.slice(0, 32)] }
583
632
  : { analyzer: 'js-ts', scope: 'dynamic-imports', status: 'not-applicable', reason: 'No dynamic loading expression was observed.' }
584
633
  const runtimeCoverageIndex = coverage.findIndex((entry) => entry.scope === 'runtime-wiring')
585
634
  if (runtimeCoverageIndex >= 0) coverage[runtimeCoverageIndex] = observedUnresolvedRuntimeWiring
@@ -52,17 +52,6 @@ const gradeForScore = (score: number): DoctorReport['grade'] => {
52
52
  return 'F'
53
53
  }
54
54
 
55
- const agentDocPaths = (index: DocBridgeIndexV1): Set<string> => {
56
- const paths = new Set<string>()
57
- for (const owner of Object.values(index.lookup?.ownership ?? {})) {
58
- if (owner.agentDoc) paths.add(owner.agentDoc)
59
- }
60
- for (const handoff of Object.values(index.handoffs ?? {})) {
61
- if (handoff.startHere) paths.add(handoff.startHere)
62
- }
63
- return paths
64
- }
65
-
66
55
  const computeScore = (coverage: DoctorCoverage): number => {
67
56
  let score = 0
68
57
 
@@ -166,7 +155,7 @@ export const runDoctor = (root: string, config: DocBridgeConfigV1): DoctorReport
166
155
  let index: DocBridgeIndexV1 | undefined
167
156
  let hasIndex = true
168
157
  let freshnessOk = false
169
- let freshnessMessage = 'Index is fresh'
158
+ let freshnessMessage: string
170
159
 
171
160
  try {
172
161
  index = loadDocBridgeIndex(root, config)
@@ -191,8 +180,6 @@ export const runDoctor = (root: string, config: DocBridgeConfigV1): DoctorReport
191
180
  const missingHumanDoc = ownership.filter(([, owner]) => !owner.humanDoc).map(([id]) => id)
192
181
 
193
182
  const indexedPaths = new Set(index.knowledge.map((entry) => entry.path))
194
- const expectedAgentDocs = agentDocPaths(index)
195
- const unindexed = [...expectedAgentDocs].filter((path) => !indexedPaths.has(path))
196
183
 
197
184
  const corpusDocs = scanAgentCorpus(root, config).filter(
198
185
  (doc) => doc.path !== config.corpus.agent.index,
@@ -283,4 +270,4 @@ export const formatDoctorText = (report: DoctorReport): string[] => {
283
270
 
284
271
  lines.push('', 'Next actions', ...report.nextActions.map((action) => ` → ${action}`))
285
272
  return lines
286
- }
273
+ }
@@ -31,13 +31,25 @@ const defaultFetchText: FetchText = async (url) => {
31
31
  return res.text()
32
32
  }
33
33
 
34
+ const httpUrl = (value: string): string | undefined => {
35
+ try {
36
+ const parsed = new URL(value)
37
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined
38
+ if (parsed.username || parsed.password) return undefined
39
+ return parsed.href
40
+ } catch {
41
+ return undefined
42
+ }
43
+ }
44
+
34
45
  const sourceText = async (
35
46
  root: string,
36
47
  source: string,
37
48
  fetchText: FetchText,
38
49
  ): Promise<string | null> => {
39
50
  try {
40
- if (/^https?:\/\//.test(source)) return await fetchText(source)
51
+ const remote = httpUrl(source)
52
+ if (remote) return await fetchText(remote)
41
53
  const path = resolve(root, source)
42
54
  if (!existsSync(path)) return null
43
55
  return readFileSync(path, 'utf8')
@@ -52,34 +64,72 @@ const sameOrigin = (base: string, target: string): boolean => {
52
64
  }
53
65
 
54
66
  export const parseLlmsTxtLinks = (raw: string): { title: string; url: string; description?: string }[] => {
55
- const links = [...raw.matchAll(/\[([^\]]+)\]\(([^)]+)\)(?::\s*([^\n]+))?/g)].map((match) => ({
56
- title: match[1] ?? match[2] ?? 'link',
57
- url: match[2] ?? '',
58
- ...(match[3]?.trim() ? { description: match[3].trim() } : {}),
59
- })).filter((link) => link.url)
60
-
61
- for (const match of raw.matchAll(/(?:^|\s)(?:Raw|llms\.txt|Full bundle|ZIP bundle)?:?\s*(https?:\/\/\S+)/gi)) {
62
- const url = match[1]?.replace(/[),.;]+$/, '')
63
- if (url && !links.some((link) => link.url === url)) links.push({ title: slugFromPath(url), url })
67
+ const links: { title: string; url: string; description?: string }[] = []
68
+ for (const line of raw.split(/\r?\n/)) {
69
+ let cursor = 0
70
+ while (cursor < line.length) {
71
+ const open = line.indexOf('[', cursor)
72
+ if (open < 0) break
73
+ const titleEnd = line.indexOf(']', open + 1)
74
+ const urlStart = titleEnd < 0 ? -1 : line.indexOf('(', titleEnd + 1)
75
+ const urlEnd = urlStart < 0 ? -1 : line.indexOf(')', urlStart + 1)
76
+ if (titleEnd < 0 || urlStart !== titleEnd + 1 || urlEnd < 0) {
77
+ cursor = open + 1
78
+ continue
79
+ }
80
+ const title = line.slice(open + 1, titleEnd).trim()
81
+ const url = line.slice(urlStart + 1, urlEnd).trim()
82
+ const description = line.slice(urlEnd + 1).trim().replace(/^:\s*/, '')
83
+ if (url) links.push({ title: title || url, url, ...(description ? { description } : {}) })
84
+ cursor = urlEnd + 1
85
+ }
86
+
87
+ for (const token of line.split(/\s+/)) {
88
+ let end = token.length
89
+ while (end > 0 && '),.;:'.includes(token[end - 1] ?? '')) end -= 1
90
+ const url = token.slice(0, end)
91
+ if (!/^https?:\/\//i.test(url) || links.some((link) => link.url === url)) continue
92
+ links.push({ title: slugFromPath(url), url })
93
+ }
64
94
  }
65
95
  return links
66
96
  }
67
97
 
98
+ const firstMatchingLine = (section: string, predicate: (line: string) => boolean): string | undefined =>
99
+ section.split(/\r?\n/).find((line) => predicate(line.trim()))?.trim()
100
+
101
+ const sectionTitle = (section: string, sourceUrl: string): string => {
102
+ const titleLine = firstMatchingLine(section, (line) => line.startsWith('title:'))
103
+ if (titleLine) return titleLine.slice('title:'.length).trim()
104
+ const urlLine = firstMatchingLine(section, (line) => /^https?:\/\//i.test(line))
105
+ if (urlLine) {
106
+ try {
107
+ return new URL(urlLine).pathname.split('/').filter(Boolean).at(-1) ?? slugFromPath(sourceUrl)
108
+ } catch {
109
+ return slugFromPath(sourceUrl)
110
+ }
111
+ }
112
+ const heading = firstMatchingLine(section, (line) => line.startsWith('#'))
113
+ if (heading) return heading.replace(/^#+\s*/, '').trim()
114
+ return slugFromPath(sourceUrl)
115
+ }
116
+
68
117
  export const chunksFromMarkdown = (
69
118
  property: string,
70
119
  raw: string,
71
120
  sourceUrl: string,
72
121
  ): DocBridgeRetrievedChunk[] => {
73
- const searchable = raw.includes('\n==== ') ? raw : raw.replace(/^---\n[\s\S]*?\n---\n?/, '')
122
+ const frontmatterEnd = raw.startsWith('---\n') ? raw.indexOf('\n---', 4) : -1
123
+ const searchable = raw.includes('\n==== ')
124
+ ? raw
125
+ : frontmatterEnd >= 0
126
+ ? raw.slice(frontmatterEnd + '\n---'.length).replace(/^\n/, '')
127
+ : raw
74
128
  const sections = searchable.includes('\n==== ')
75
129
  ? searchable.split(/\n====\s+/).filter((section) => section.trim())
76
130
  : searchable.split(/\n(?=##?\s+)/)
77
131
  return sections.map((section, index) => {
78
- const title =
79
- /^title:\s*(.+)$/m.exec(section)?.[1]?.trim() ??
80
- /^https?:\/\/\S+\/([^/\s]+)$/m.exec(section)?.[1]?.trim() ??
81
- /^#+\s+(.+)$/m.exec(section)?.[1]?.trim() ??
82
- slugFromPath(sourceUrl)
132
+ const title = sectionTitle(section, sourceUrl)
83
133
  const id = slugFromPath(title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) || `${index}`
84
134
  return {
85
135
  chunkKey: `${property}:federated:${id}`,
@@ -112,10 +162,12 @@ export const loadFederatedChunks = async (
112
162
  chunks.push(...chunksFromMarkdown(source.id, llms, source.llmsTxt))
113
163
  const links = parseLlmsTxtLinks(llms)
114
164
  for (const link of links) {
115
- const url = !/^https?:\/\//.test(link.url) && source.rawBaseUrl
116
- ? `${source.rawBaseUrl.replace(/\/$/, '')}/${link.url.replace(/^\//, '')}`
117
- : link.url
118
- if (!/\.(md|txt)(?:$|\?)/.test(url)) continue
165
+ const linkUrl = httpUrl(link.url)
166
+ const baseUrl = source.rawBaseUrl ? httpUrl(source.rawBaseUrl) : undefined
167
+ const url = linkUrl ?? (baseUrl ? new URL(link.url, baseUrl).href : link.url)
168
+ let pathname = url
169
+ try { pathname = new URL(url).pathname } catch { /* local source */ }
170
+ if (!/\.(md|txt)$/i.test(pathname)) continue
119
171
  if (!sameOrigin(source.llmsTxt, url)) continue
120
172
  const raw = await sourceText(root, url, fetchText)
121
173
  if (raw) chunks.push(...chunksFromMarkdown(source.id, raw, url))
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
2
2
  import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path'
3
3
 
4
4
  import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
@@ -99,8 +99,9 @@ export const createArtifactNormalizationProposal = (root: string, artifactPath:
99
99
  const projectRoot = realpathSync.native(resolve(root))
100
100
  const path = artifactPath.split(sep).join('/')
101
101
  const absolute = containedPath(projectRoot, path)
102
- if (!absolute || !existsSync(absolute) || !statSync(absolute).isFile()) return undefined
103
- const before = readFileSync(absolute, 'utf8')
102
+ if (!absolute) return undefined
103
+ let before: string
104
+ try { before = readFileSync(absolute, 'utf8') } catch { return undefined }
104
105
  let after: string
105
106
  try { after = `${JSON.stringify(sortJson(JSON.parse(before) as unknown), null, 2)}\n` } catch { return undefined }
106
107
  return after === before ? undefined : makeProposal(projectRoot, options, [{ path: relative(projectRoot, absolute).split(sep).join('/'), before, after }], ['The artifact contains valid JSON.'], ['The artifact is valid canonical JSON with one trailing newline.'])
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
- import { dirname, join } from 'node:path'
2
+ import { join } from 'node:path'
3
3
 
4
4
  import { optionString, scanMarkdownDocs, type HumanAdapter } from './core.js'
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { existsSync, watch } from 'node:fs'
2
- import { dirname, join, resolve } from 'node:path'
2
+ import { dirname, resolve } from 'node:path'
3
3
 
4
4
  import type { DocBridgeConfigV1 } from '../config/schema.js'
5
5
  import { buildDocBridgeIndex } from './build-index.js'