@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.
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +6 -4
- package/action.yml +1 -1
- package/dist/cli/program.js +378 -189
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +5 -3
- package/dist/config/index.js.map +1 -1
- package/dist/index-BUL0q7s8.d.ts +660 -0
- package/dist/index.d.ts +637 -2724
- package/dist/index.js +328 -149
- package/dist/index.js.map +1 -1
- package/docs/RELEASE.md +22 -8
- package/docs/agent-corpus/INDEX.md +2 -2
- package/docs/agent-corpus/chat.md +2 -2
- package/docs/agent-corpus/cli.md +2 -2
- package/docs/agent-corpus/conformance.md +2 -2
- package/docs/agent-corpus/doc-bridge.md +1 -1
- package/docs/agent-corpus/doctor.md +2 -2
- package/docs/agent-corpus/gates.md +2 -2
- package/docs/agent-corpus/mcp.md +2 -2
- package/docs/agent-corpus/memory.md +2 -2
- package/docs/agent-corpus/query.md +2 -2
- package/docs/knowledge-engine-runbook.md +13 -1
- package/docs/spec/benchmark-v1.md +6 -0
- package/docs/spec/config-v1.md +45 -0
- package/docs/validation-cycle-plan.md +19 -0
- package/docs/verification-harness.md +4 -0
- package/mcpb/manifest.json +1 -1
- package/package.json +68 -70
- package/scripts/check-ecosystem-upstream.mjs +3 -2
- package/scripts/report-visual-check.mjs +20 -3
- package/scripts/verification-harness.mjs +0 -1
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/cli/demo.ts +2 -2
- package/src/cli/program.ts +15 -5
- package/src/config/load-config.ts +7 -1
- package/src/config/schema.ts +4 -2
- package/src/conformance/documentation-standard-v1.ts +14 -8
- package/src/discovery/documentation.ts +44 -18
- package/src/discovery/repository.ts +77 -28
- package/src/doctor/run-doctor.ts +2 -15
- package/src/federation/llms.ts +72 -20
- package/src/fixes/proposals.ts +4 -3
- package/src/index-builder/human-adapters/fumadocs.ts +1 -1
- package/src/index-builder/watch-index.ts +1 -1
- package/src/lib/bounded-text.ts +15 -10
- package/src/reconciliation/reconcile.ts +47 -5
- package/src/report/html.ts +21 -15
- package/src/rules/engine.ts +15 -2
- package/src/safety/repository.ts +1 -1
- package/src/schemas/knowledge.ts +5 -2
- package/src/validate.ts +7 -1
- package/src/version.ts +1 -1
- package/dist/index-C2PCQSrB.d.ts +0 -2251
package/src/cli/program.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
726
|
-
|
|
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)'}: ${
|
|
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
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
99
|
-
return { exists: false, content: '', evidence: { path, detail: 'File does not exist.' } }
|
|
100
|
-
}
|
|
98
|
+
let fd: number | undefined
|
|
101
99
|
try {
|
|
102
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
237
|
+
const sectionField = parseIndentedField(raw, 2)
|
|
238
|
+
if (sectionField) {
|
|
212
239
|
finishRelation()
|
|
213
|
-
const
|
|
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
|
-
|
|
233
|
-
|
|
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' &&
|
|
264
|
+
if (section === 'relations' && listValue !== undefined) {
|
|
239
265
|
finishRelation()
|
|
240
|
-
const firstField =
|
|
266
|
+
const firstField = parseIndentedField(` ${listValue}`, 4)
|
|
241
267
|
current = { startLine: line, endLine: line, fields: new Set() }
|
|
242
|
-
if (firstField?.
|
|
243
|
-
current.fields.add(firstField
|
|
244
|
-
current[firstField
|
|
245
|
-
} else if (
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
const key = field
|
|
276
|
+
const field = parseIndentedField(raw, 6)
|
|
277
|
+
if (section === 'relations' && current && field) {
|
|
278
|
+
const { key, value } = field
|
|
253
279
|
current.endLine = line
|
|
254
|
-
if (!
|
|
255
|
-
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown relation field: ${key
|
|
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(
|
|
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.
|
|
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 ===
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
327
|
-
let hasPotentialTargetArgument = false
|
|
376
|
+
let hasUnresolvedTarget = false
|
|
328
377
|
for (const argument of node.arguments) {
|
|
329
|
-
if (ts.
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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.
|
|
449
|
-
analyzerVersions: { repository: '1.
|
|
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.
|
|
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: [
|
|
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
|
package/src/doctor/run-doctor.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/federation/llms.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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))
|
package/src/fixes/proposals.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, renameSync,
|
|
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
|
|
103
|
-
|
|
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.'])
|