@agentskit/doc-bridge 1.6.4 → 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 +249 -0
- package/CONTRIBUTING.md +6 -4
- package/action.yml +1 -1
- package/dist/cli/program.js +1139 -294
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +43 -5
- package/dist/config/index.js.map +1 -1
- package/dist/index-BUL0q7s8.d.ts +660 -0
- package/dist/index.d.ts +817 -2134
- package/dist/index.js +1154 -244
- package/dist/index.js.map +1 -1
- package/docs/PRD-enterprise-hardening.md +288 -0
- package/docs/RELEASE.md +22 -8
- package/docs/adr/0001-enterprise-verification-contract.md +35 -0
- 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 +30 -2
- package/docs/spec/analyzer-plugin-v1.md +24 -0
- package/docs/spec/benchmark-v1.md +36 -0
- package/docs/spec/config-v1.md +156 -0
- package/docs/validation-cycle-plan.md +255 -0
- package/docs/verification-harness.md +37 -4
- 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 +64 -12
- package/scripts/verification-harness.mjs +216 -14
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +31 -7
- package/src/cli/demo.ts +2 -2
- package/src/cli/program.ts +59 -16
- package/src/config/index.ts +2 -0
- package/src/config/load-config.ts +7 -1
- package/src/config/schema.ts +60 -2
- package/src/conformance/documentation-standard-v1.ts +14 -8
- package/src/discovery/documentation.ts +90 -23
- package/src/discovery/repository.ts +147 -19
- 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/index.ts +29 -0
- package/src/lib/bounded-text.ts +15 -10
- package/src/metrics/benchmark.ts +176 -0
- package/src/plugins/contract.ts +89 -0
- package/src/reconciliation/reconcile.ts +181 -5
- package/src/report/html.ts +318 -88
- package/src/rules/engine.ts +15 -2
- package/src/safety/repository.ts +1 -1
- package/src/schemas/knowledge.ts +21 -3
- package/src/validate.ts +7 -1
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +65 -9
- package/dist/index-DudNuwI5.d.ts +0 -2060
package/src/config/schema.ts
CHANGED
|
@@ -171,18 +171,60 @@ 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
|
|
|
182
182
|
export const ReconciliationConfigSchema = z
|
|
183
183
|
.object({
|
|
184
|
+
/** Semantic comparison level. Raw discovery always keeps file-level relations. */
|
|
185
|
+
scope: z.enum(['file', 'module', 'package']).optional(),
|
|
184
186
|
/** Relation kinds that must have documentation declarations. Omit to require all observed kinds; [] disables this signal. */
|
|
185
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(),
|
|
190
|
+
includeOrphanedDocuments: z.boolean().optional(),
|
|
191
|
+
})
|
|
192
|
+
.strict()
|
|
193
|
+
|
|
194
|
+
export const AnalysisConfigSchema = z
|
|
195
|
+
.object({
|
|
196
|
+
plugins: z
|
|
197
|
+
.array(
|
|
198
|
+
z
|
|
199
|
+
.object({
|
|
200
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/).max(128),
|
|
201
|
+
enabled: z.boolean().optional(),
|
|
202
|
+
order: z.number().int().nonnegative().optional(),
|
|
203
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
204
|
+
reason: z.string().min(1).max(1_024).optional(),
|
|
205
|
+
})
|
|
206
|
+
.strict(),
|
|
207
|
+
)
|
|
208
|
+
.max(128)
|
|
209
|
+
.optional(),
|
|
210
|
+
jsTs: z
|
|
211
|
+
.object({
|
|
212
|
+
runtimeWiringMethods: z.array(z.string().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/).max(64)).max(64).optional(),
|
|
213
|
+
runtimeWiringAdapters: z
|
|
214
|
+
.array(
|
|
215
|
+
z
|
|
216
|
+
.object({
|
|
217
|
+
id: z.string().min(1).max(128),
|
|
218
|
+
methods: z.array(z.string().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/).max(64)).min(1).max(64),
|
|
219
|
+
})
|
|
220
|
+
.strict(),
|
|
221
|
+
)
|
|
222
|
+
.max(32)
|
|
223
|
+
.optional(),
|
|
224
|
+
includeTestRuntimeWiring: z.boolean().optional(),
|
|
225
|
+
})
|
|
226
|
+
.strict()
|
|
227
|
+
.optional(),
|
|
186
228
|
})
|
|
187
229
|
.strict()
|
|
188
230
|
|
|
@@ -203,6 +245,13 @@ export const RepositorySafetyConfigSchema = z
|
|
|
203
245
|
})
|
|
204
246
|
.strict()
|
|
205
247
|
|
|
248
|
+
export const ReportConfigSchema = z
|
|
249
|
+
.object({
|
|
250
|
+
/** Public report privacy mode. Private is the default; anonymized preserves topology without project identity. */
|
|
251
|
+
privacy: z.enum(['private', 'anonymized']).optional(),
|
|
252
|
+
})
|
|
253
|
+
.strict()
|
|
254
|
+
|
|
206
255
|
export const SurfacesConfigSchema = z
|
|
207
256
|
.object({
|
|
208
257
|
cli: z
|
|
@@ -310,6 +359,11 @@ export const IntelligenceConfigSchema = z
|
|
|
310
359
|
agentId: z.string().min(1).max(256).optional(),
|
|
311
360
|
agentRoot: z.string().min(1).max(512).optional(),
|
|
312
361
|
runnerModule: z.string().min(1).max(512).optional(),
|
|
362
|
+
deterministic: z.boolean().optional(),
|
|
363
|
+
timeoutMs: z.number().int().positive().max(600_000).optional(),
|
|
364
|
+
maxTokens: z.number().int().positive().max(1_000_000).optional(),
|
|
365
|
+
maxResponseBytes: z.number().int().positive().max(10_000_000).optional(),
|
|
366
|
+
maxConcurrency: z.number().int().positive().max(64).optional(),
|
|
313
367
|
})
|
|
314
368
|
.strict()
|
|
315
369
|
.optional(),
|
|
@@ -442,9 +496,11 @@ export const DocBridgeConfigV1Schema = z
|
|
|
442
496
|
routing: RoutingConfigSchema.optional(),
|
|
443
497
|
gates: GatesConfigSchema.optional(),
|
|
444
498
|
reconciliation: ReconciliationConfigSchema.optional(),
|
|
499
|
+
analysis: AnalysisConfigSchema.optional(),
|
|
445
500
|
rules: RulesConfigSchema.optional(),
|
|
446
501
|
workflow: WorkflowConfigSchema.optional(),
|
|
447
502
|
safety: RepositorySafetyConfigSchema.optional(),
|
|
503
|
+
report: ReportConfigSchema.optional(),
|
|
448
504
|
surfaces: SurfacesConfigSchema.optional(),
|
|
449
505
|
intelligence: IntelligenceConfigSchema.optional(),
|
|
450
506
|
federation: FederationConfigSchema.optional(),
|
|
@@ -458,8 +514,10 @@ export type HumanCorpusConfig = z.infer<typeof HumanCorpusConfigSchema>
|
|
|
458
514
|
export type DocumentationStandardV1Config = z.infer<typeof DocumentationStandardV1ConfigSchema>
|
|
459
515
|
export type DocumentationStandardRuleId = z.infer<typeof DocumentationStandardRuleIdSchema>
|
|
460
516
|
export type ReconciliationConfig = z.infer<typeof ReconciliationConfigSchema>
|
|
517
|
+
export type AnalysisConfig = z.infer<typeof AnalysisConfigSchema>
|
|
461
518
|
export type RuleId = z.infer<typeof RuleIdSchema>
|
|
462
519
|
export type RuleSeverity = z.infer<typeof RuleSeveritySchema>
|
|
463
520
|
export type RulesConfig = z.infer<typeof RulesConfigSchema>
|
|
464
521
|
export type WorkflowConfig = z.infer<typeof WorkflowConfigSchema>
|
|
465
522
|
export type RepositorySafetyConfig = z.infer<typeof RepositorySafetyConfigSchema>
|
|
523
|
+
export type ReportConfig = z.infer<typeof ReportConfigSchema>
|
|
@@ -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
|
|
|
@@ -22,6 +22,8 @@ export type DocumentationDeclarationInput = {
|
|
|
22
22
|
export type DocumentationDeclarationOptions = {
|
|
23
23
|
readonly snapshot: Pick<DiscoverySnapshotV1, 'entities'>
|
|
24
24
|
readonly documentId?: string
|
|
25
|
+
/** Agent corpus root used for conservative package/app path inference. */
|
|
26
|
+
readonly agentRoot?: string
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export type DocumentationDeclarationResult = {
|
|
@@ -71,6 +73,41 @@ const scalar = (value: string): string => {
|
|
|
71
73
|
return trimmed
|
|
72
74
|
}
|
|
73
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
|
+
|
|
102
|
+
const conventionalPackageReference = (path: string, agentRoot: string): string | undefined => {
|
|
103
|
+
const prefix = `${agentRoot.replace(/\/$/, '')}/`
|
|
104
|
+
if (!path.startsWith(prefix)) return undefined
|
|
105
|
+
const relative = path.slice(prefix.length)
|
|
106
|
+
const [scope, file] = relative.split('/')
|
|
107
|
+
if ((scope !== 'packages' && scope !== 'apps') || !file) return undefined
|
|
108
|
+
return file.replace(/\.mdx?$/, '')
|
|
109
|
+
}
|
|
110
|
+
|
|
74
111
|
const list = (value: string): string[] | undefined => {
|
|
75
112
|
const trimmed = value.trim()
|
|
76
113
|
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return undefined
|
|
@@ -103,7 +140,15 @@ const resolveEntity = (
|
|
|
103
140
|
lineStart: number,
|
|
104
141
|
unresolved: Map<string, KnowledgeEntity>,
|
|
105
142
|
): KnowledgeEntity => {
|
|
106
|
-
const
|
|
143
|
+
const direct = entities.find((entity) => entity.id === reference || entity.aliases?.includes(reference))
|
|
144
|
+
if (direct) return direct
|
|
145
|
+
const packageReference = reference.replace(/^package:/, '')
|
|
146
|
+
const packageCandidates = entities.filter((entity) => {
|
|
147
|
+
if (entity.kind !== 'package') return false
|
|
148
|
+
const pathName = entity.path?.split('/').pop()
|
|
149
|
+
return entity.name === reference || entity.path === reference || pathName === packageReference || entity.name.endsWith(`/${packageReference}`)
|
|
150
|
+
})
|
|
151
|
+
const resolved = packageCandidates.length === 1 ? packageCandidates[0] : undefined
|
|
107
152
|
if (resolved) return resolved
|
|
108
153
|
const id = `unresolved:${reference}`
|
|
109
154
|
const existing = unresolved.get(id)
|
|
@@ -123,9 +168,14 @@ const relationKey = (from: string, to: string, kind: string): string => `${from}
|
|
|
123
168
|
|
|
124
169
|
const parseBlock = (
|
|
125
170
|
input: DocumentationDeclarationInput,
|
|
171
|
+
options: Pick<DocumentationDeclarationOptions, 'agentRoot'> = {},
|
|
126
172
|
): { readonly covers: readonly { value: string; line: number }[]; readonly relations: readonly RelationFields[]; readonly diagnostics: readonly DocumentationDiagnostic[]; readonly hasDocbridge: boolean } => {
|
|
173
|
+
const conventionalPath = conventionalPackageReference(input.path, options.agentRoot ?? 'docs/for-agents')
|
|
127
174
|
const frontmatter = findFrontmatter(input.content)
|
|
128
175
|
if (!frontmatter) {
|
|
176
|
+
if (conventionalPath && !input.content.replace(/^\uFEFF/, '').startsWith('---')) {
|
|
177
|
+
return { covers: [{ value: conventionalPath, line: 1 }], relations: [], diagnostics: [], hasDocbridge: true }
|
|
178
|
+
}
|
|
129
179
|
if (input.content.replace(/^\uFEFF/, '').startsWith('---')) {
|
|
130
180
|
return {
|
|
131
181
|
covers: [],
|
|
@@ -138,8 +188,24 @@ const parseBlock = (
|
|
|
138
188
|
}
|
|
139
189
|
|
|
140
190
|
const { lines, end } = frontmatter
|
|
141
|
-
|
|
142
|
-
|
|
191
|
+
let docbridgeLine = -1
|
|
192
|
+
let typeLine = -1
|
|
193
|
+
let packageLine = -1
|
|
194
|
+
let humanDocLine = -1
|
|
195
|
+
for (let index = 1; index < end; index += 1) {
|
|
196
|
+
const line = lines[index] ?? ''
|
|
197
|
+
if (docbridgeLine < 0 && /^docbridge\s*:/.test(line)) docbridgeLine = index
|
|
198
|
+
if (typeLine < 0 && /^type\s*:/.test(line)) typeLine = index
|
|
199
|
+
if (packageLine < 0 && /^package\s*:/.test(line)) packageLine = index
|
|
200
|
+
if (humanDocLine < 0 && /^humanDoc\s*:/.test(line)) humanDocLine = index
|
|
201
|
+
}
|
|
202
|
+
if (docbridgeLine < 0) {
|
|
203
|
+
const type = typeLine >= 0 ? scalar(lines[typeLine]?.slice('type:'.length) ?? '') : ''
|
|
204
|
+
const packageReference = packageLine >= 0 ? scalar(lines[packageLine]?.slice('package:'.length) ?? '') : conventionalPath ?? ''
|
|
205
|
+
if (type === 'package' && packageReference) return { covers: [{ value: packageReference, line: packageLine >= 0 ? packageLine + 1 : typeLine + 1 }], relations: [], diagnostics: [], hasDocbridge: true }
|
|
206
|
+
if (conventionalPath) return { covers: [{ value: packageReference, line: humanDocLine >= 0 ? humanDocLine + 1 : 1 }], relations: [], diagnostics: [], hasDocbridge: true }
|
|
207
|
+
return { covers: [], relations: [], diagnostics: [], hasDocbridge: false }
|
|
208
|
+
}
|
|
143
209
|
|
|
144
210
|
const diagnostics: DocumentationDiagnostic[] = []
|
|
145
211
|
const covers: { value: string; line: number }[] = []
|
|
@@ -168,11 +234,10 @@ const parseBlock = (
|
|
|
168
234
|
section = undefined
|
|
169
235
|
continue
|
|
170
236
|
}
|
|
171
|
-
|
|
237
|
+
const sectionField = parseIndentedField(raw, 2)
|
|
238
|
+
if (sectionField) {
|
|
172
239
|
finishRelation()
|
|
173
|
-
const
|
|
174
|
-
const key = match?.[1]
|
|
175
|
-
const value = match?.[2] ?? ''
|
|
240
|
+
const { key, value } = sectionField
|
|
176
241
|
if (key !== 'covers' && key !== 'relations') {
|
|
177
242
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown docbridge field: ${key ?? '(missing)'}.`, line)
|
|
178
243
|
section = undefined
|
|
@@ -189,35 +254,36 @@ const parseBlock = (
|
|
|
189
254
|
}
|
|
190
255
|
continue
|
|
191
256
|
}
|
|
192
|
-
|
|
193
|
-
|
|
257
|
+
const listValue = parseListItem(raw, 4)
|
|
258
|
+
if (section === 'covers' && listValue !== undefined) {
|
|
259
|
+
const value = scalar(listValue)
|
|
194
260
|
if (!value) addDiagnostic(diagnostics, input, 'DOCBRIDGE_REFERENCE_MISSING', 'covers entries must not be empty.', line)
|
|
195
261
|
else covers.push({ value, line })
|
|
196
262
|
continue
|
|
197
263
|
}
|
|
198
|
-
if (section === 'relations' &&
|
|
264
|
+
if (section === 'relations' && listValue !== undefined) {
|
|
199
265
|
finishRelation()
|
|
200
|
-
const firstField =
|
|
266
|
+
const firstField = parseIndentedField(` ${listValue}`, 4)
|
|
201
267
|
current = { startLine: line, endLine: line, fields: new Set() }
|
|
202
|
-
if (firstField?.
|
|
203
|
-
current.fields.add(firstField
|
|
204
|
-
current[firstField
|
|
205
|
-
} 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) {
|
|
206
272
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_RELATION_INVALID', 'Relation entries must be field mappings.', line)
|
|
207
273
|
}
|
|
208
274
|
continue
|
|
209
275
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const key = field
|
|
276
|
+
const field = parseIndentedField(raw, 6)
|
|
277
|
+
if (section === 'relations' && current && field) {
|
|
278
|
+
const { key, value } = field
|
|
213
279
|
current.endLine = line
|
|
214
|
-
if (!
|
|
215
|
-
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)
|
|
216
282
|
} else if (current.fields.has(key)) {
|
|
217
283
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_DUPLICATE', `Duplicate relation field: ${key}.`, line)
|
|
218
284
|
} else {
|
|
219
285
|
current.fields.add(key)
|
|
220
|
-
current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(
|
|
286
|
+
current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(value)
|
|
221
287
|
}
|
|
222
288
|
continue
|
|
223
289
|
}
|
|
@@ -231,7 +297,7 @@ export const parseDocumentationDeclarations = (
|
|
|
231
297
|
input: DocumentationDeclarationInput,
|
|
232
298
|
options: DocumentationDeclarationOptions,
|
|
233
299
|
): DocumentationDeclarationResult => {
|
|
234
|
-
const parsed = parseBlock(input)
|
|
300
|
+
const parsed = parseBlock(input, options)
|
|
235
301
|
if (!parsed.hasDocbridge) return { hasDocbridge: false, entities: [], relations: [], diagnostics: [] }
|
|
236
302
|
const diagnostics = [...parsed.diagnostics]
|
|
237
303
|
const unresolved = new Map<string, KnowledgeEntity>()
|
|
@@ -300,13 +366,14 @@ export const parseDocumentationDeclarations = (
|
|
|
300
366
|
export const applyDocumentationDeclarations = (
|
|
301
367
|
snapshot: DiscoverySnapshotV1,
|
|
302
368
|
documents: readonly DocumentationDeclarationInput[],
|
|
369
|
+
options: Pick<DocumentationDeclarationOptions, 'agentRoot'> = {},
|
|
303
370
|
): DocumentationAnalysisResult => {
|
|
304
371
|
const entities = new Map(snapshot.entities.map((entity) => [entity.id, entity]))
|
|
305
372
|
const relations = new Map(snapshot.relations.map((relation) => [relation.id, relation]))
|
|
306
373
|
const diagnostics: DocumentationDiagnostic[] = []
|
|
307
374
|
|
|
308
375
|
for (const document of documents) {
|
|
309
|
-
const result = parseDocumentationDeclarations(document, { snapshot })
|
|
376
|
+
const result = parseDocumentationDeclarations(document, { snapshot, ...options })
|
|
310
377
|
diagnostics.push(...result.diagnostics)
|
|
311
378
|
for (const entity of result.entities) entities.set(entity.id, entity)
|
|
312
379
|
for (const relation of result.relations) relations.set(relation.id, relation)
|
|
@@ -21,6 +21,8 @@ const SOURCE_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts',
|
|
|
21
21
|
const DOCUMENT_EXTENSIONS = ['.md', '.mdx'] as const
|
|
22
22
|
const DEFAULT_MAX_FILES = 10_000
|
|
23
23
|
const EMPTY_HASH = '0'.repeat(64)
|
|
24
|
+
const DEFAULT_RUNTIME_WIRING_METHODS = ['register', 'use', 'mount', 'attach'] as const
|
|
25
|
+
const TEST_MODULE_PATTERN = /(?:\.test|\.spec|__tests__)/
|
|
24
26
|
|
|
25
27
|
type JsonRecord = Record<string, unknown>
|
|
26
28
|
|
|
@@ -42,8 +44,9 @@ type ModuleInfo = {
|
|
|
42
44
|
|
|
43
45
|
type ImportReference = {
|
|
44
46
|
readonly specifier: string
|
|
45
|
-
readonly kind: 'imports' | 're-exports'
|
|
47
|
+
readonly kind: 'imports' | 're-exports' | 'runtime-wiring'
|
|
46
48
|
readonly evidence: Evidence
|
|
49
|
+
readonly detection?: 'dynamic-literal' | 'runtime-wiring-static'
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
type DiscoveryOptions = {
|
|
@@ -97,6 +100,14 @@ const firstLineContaining = (text: string, pattern: string): number | undefined
|
|
|
97
100
|
return line >= 0 ? line + 1 : undefined
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
const documentClassification = (path: string): string => {
|
|
104
|
+
if (/(^|\/)docs\/for-agents(?:\/|$)/.test(path)) return 'agent'
|
|
105
|
+
if (/(^|\/)docs-archive(?:\/|$)/.test(path)) return 'archive'
|
|
106
|
+
if (/(^|\/)docs(?:\/|$)/.test(path)) return 'human'
|
|
107
|
+
if (/(^|\/)(README|CONTRIBUTING|SECURITY|CHANGELOG)(?:\.|$)/i.test(path)) return 'project'
|
|
108
|
+
return 'unclassified'
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
const packageName = (manifest: JsonRecord, fallback: string): string | undefined =>
|
|
101
112
|
typeof manifest.name === 'string' && manifest.name.length > 0 ? manifest.name : fallback || undefined
|
|
102
113
|
|
|
@@ -262,30 +273,114 @@ const moduleReferences = (
|
|
|
262
273
|
root: string,
|
|
263
274
|
path: string,
|
|
264
275
|
sourceFile: ts.SourceFile,
|
|
265
|
-
|
|
276
|
+
runtimeWiringMethods: ReadonlySet<string>,
|
|
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 } => {
|
|
266
278
|
const references: ImportReference[] = []
|
|
279
|
+
const dynamicEvidence: Evidence[] = []
|
|
267
280
|
let hasDynamic = false
|
|
281
|
+
let hasLiteralDynamic = false
|
|
268
282
|
let hasRuntimeWiring = false
|
|
269
|
-
|
|
270
|
-
|
|
283
|
+
let hasUnresolvedRuntimeWiring = false
|
|
284
|
+
const importedBindings = new Map<string, string>()
|
|
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
|
+
}
|
|
298
|
+
const collectStaticStringBindings = (node: ts.Node): void => {
|
|
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)
|
|
301
|
+
const previous = staticStringBindings.get(node.name.text)
|
|
302
|
+
staticStringBindings.set(node.name.text, !staticStringBindings.has(node.name.text) || previous === value ? value : undefined)
|
|
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)
|
|
312
|
+
ts.forEachChild(node, collectStaticStringBindings)
|
|
313
|
+
}
|
|
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
|
+
}
|
|
335
|
+
const addReference = (specifier: ts.StringLiteralLike, kind: ImportReference['kind'], node: ts.Node, detection?: ImportReference['detection']): void => {
|
|
336
|
+
references.push({ specifier: specifier.text, kind, evidence: nodeEvidence(root, path, sourceFile, node), ...(detection ? { detection } : {}) })
|
|
271
337
|
}
|
|
272
338
|
|
|
273
339
|
const visit = (node: ts.Node): void => {
|
|
274
340
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
275
341
|
addReference(node.moduleSpecifier, 'imports', node)
|
|
342
|
+
const clause = node.importClause
|
|
343
|
+
if (clause?.name) importedBindings.set(clause.name.text, node.moduleSpecifier.text)
|
|
344
|
+
if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) importedBindings.set(clause.namedBindings.name.text, node.moduleSpecifier.text)
|
|
345
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
346
|
+
for (const element of clause.namedBindings.elements) importedBindings.set((element.name ?? element.propertyName)?.text ?? '', node.moduleSpecifier.text)
|
|
347
|
+
}
|
|
276
348
|
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
277
349
|
addReference(node.moduleSpecifier, 're-exports', node)
|
|
278
350
|
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && ts.isStringLiteral(node.moduleReference.expression)) {
|
|
279
351
|
addReference(node.moduleReference.expression, 'imports', node)
|
|
352
|
+
importedBindings.set(node.name.text, node.moduleReference.expression.text)
|
|
280
353
|
} else if (ts.isCallExpression(node)) {
|
|
281
354
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
282
|
-
|
|
355
|
+
const specifier = node.arguments[0] ? resolveStaticString(node.arguments[0]) : undefined
|
|
356
|
+
if (specifier !== undefined) {
|
|
357
|
+
hasLiteralDynamic = 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
|
+
}
|
|
283
364
|
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
284
365
|
const argument = node.arguments[0]
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
374
|
+
} else if (ts.isPropertyAccessExpression(node.expression) && runtimeWiringMethods.has(node.expression.name.text)) {
|
|
288
375
|
hasRuntimeWiring = true
|
|
376
|
+
let hasUnresolvedTarget = false
|
|
377
|
+
for (const argument of node.arguments) {
|
|
378
|
+
if (ts.isStringLiteralLike(argument)) continue
|
|
379
|
+
if (addImportedBindingReference(argument, node)) continue
|
|
380
|
+
if (!isKnownLocal(argument)) hasUnresolvedTarget = true
|
|
381
|
+
}
|
|
382
|
+
const receiver = node.expression.expression
|
|
383
|
+
if (hasUnresolvedTarget && !isKnownLocal(receiver)) hasUnresolvedRuntimeWiring = true
|
|
289
384
|
}
|
|
290
385
|
}
|
|
291
386
|
ts.forEachChild(node, visit)
|
|
@@ -294,8 +389,11 @@ const moduleReferences = (
|
|
|
294
389
|
return {
|
|
295
390
|
references,
|
|
296
391
|
exports: exportedNames(sourceFile),
|
|
392
|
+
dynamicEvidence,
|
|
297
393
|
hasDynamic,
|
|
394
|
+
hasLiteralDynamic,
|
|
298
395
|
hasRuntimeWiring,
|
|
396
|
+
hasUnresolvedRuntimeWiring,
|
|
299
397
|
}
|
|
300
398
|
}
|
|
301
399
|
|
|
@@ -394,11 +492,11 @@ const artifact = (root: string, config: DocBridgeConfigV1 | undefined, files: re
|
|
|
394
492
|
sourceRevision: revision.value,
|
|
395
493
|
sourceRevisionKind: revision.kind,
|
|
396
494
|
configurationHash: sha256NormalizedV1(config ?? {}),
|
|
397
|
-
pipelineVersion: '1.
|
|
398
|
-
analyzerVersions: { repository: '1.
|
|
495
|
+
pipelineVersion: '1.1.8',
|
|
496
|
+
analyzerVersions: { repository: '1.1.1', 'js-ts': '1.3.4' },
|
|
399
497
|
entities: [...entities].sort((a, b) => a.id.localeCompare(b.id)),
|
|
400
498
|
relations: [...relations].sort((a, b) => a.id.localeCompare(b.id)),
|
|
401
|
-
coverage: [
|
|
499
|
+
coverage: coverage.map((entry) => ({ ...entry, analyzerVersion: entry.analyzerVersion ?? ({ repository: '1.1.1', 'js-ts': '1.3.4' }[entry.analyzer] ?? '1.0.0') })),
|
|
402
500
|
}
|
|
403
501
|
return DiscoverySnapshotV1Schema.parse({ ...base, contentHash: contentHashForArtifactV1(base) })
|
|
404
502
|
}
|
|
@@ -463,13 +561,13 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
|
|
|
463
561
|
const sourceFile = ts.createSourceFile(absPath, text, ts.ScriptTarget.Latest, true, scriptKind(absPath))
|
|
464
562
|
const exports = exportedNames(sourceFile)
|
|
465
563
|
modules.set(resolve(absPath), { absPath, path, entityId: id, ...(pkg ? { packageId: pkg.id } : {}) })
|
|
466
|
-
addEntity({ id, kind: 'module', name: basename(absPath), path, provenance: 'observed', evidence: [lineEvidence('code', root, absPath, 1, sourceFile.getLineAndCharacterOfPosition(sourceFile.getEnd()).line + 1)], ...(exports.length ? { metadata: { exports, test:
|
|
564
|
+
addEntity({ id, kind: 'module', name: basename(absPath), path, provenance: 'observed', evidence: [lineEvidence('code', root, absPath, 1, sourceFile.getLineAndCharacterOfPosition(sourceFile.getEnd()).line + 1)], ...(exports.length ? { metadata: { exports, test: TEST_MODULE_PATTERN.test(path) } } : {}) })
|
|
467
565
|
if (pkg) addRelation({ id: entityId('relation', `${pkg.id}:contains:${id}`), kind: 'contains', from: pkg.id, to: id, provenance: 'observed', evidence: [lineEvidence('code', root, absPath, 1)] })
|
|
468
566
|
}
|
|
469
567
|
|
|
470
568
|
for (const absPath of documentPaths) {
|
|
471
569
|
const path = relativePath(root, absPath)
|
|
472
|
-
addEntity({ id: entityId('document', path), kind: 'document', name: basename(absPath), path, provenance: 'observed', evidence: [lineEvidence('documentation', root, absPath, 1)] })
|
|
570
|
+
addEntity({ id: entityId('document', path), kind: 'document', name: basename(absPath), path, provenance: 'observed', evidence: [lineEvidence('documentation', root, absPath, 1)], metadata: { classification: documentClassification(path) } })
|
|
473
571
|
}
|
|
474
572
|
|
|
475
573
|
const compiler = readCompilerOptions(root)
|
|
@@ -478,8 +576,8 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
|
|
|
478
576
|
{ analyzer: 'repository', scope: 'package-manager', status: hasPackageManagerMetadata(root, rootManifest) ? 'complete' : 'partial', ...(!hasPackageManagerMetadata(root, rootManifest) ? { reason: `No package manager metadata found; default helper would fall back to ${detectPackageManager(root)}.` } : {}) },
|
|
479
577
|
{ analyzer: 'repository', scope: 'workspace-packages', status: packageResult.coverage.some((item) => item.status === 'partial') ? 'partial' : 'complete', ...(packageResult.coverage.find((item) => item.reason)?.reason ? { reason: packageResult.coverage.find((item) => item.reason)?.reason } : {}) },
|
|
480
578
|
{ analyzer: 'js-ts', scope: 'static-imports-and-exports', status: compiler.error ? 'partial' : 'complete', ...(compiler.error ? { reason: compiler.error } : {}) },
|
|
481
|
-
{ analyzer: 'js-ts', scope: 'dynamic-imports', status: 'not-
|
|
482
|
-
{ analyzer: 'js-ts', scope: 'runtime-wiring', status: 'not-
|
|
579
|
+
{ analyzer: 'js-ts', scope: 'dynamic-imports', status: 'not-applicable', reason: 'No dynamic loading expression was observed.' },
|
|
580
|
+
{ analyzer: 'js-ts', scope: 'runtime-wiring', status: 'not-applicable', reason: 'No configured runtime-wiring call was observed.' },
|
|
483
581
|
{ analyzer: 'js-ts', scope: 'generated-code', status: 'not-analyzed', reason: 'Generated code is not interpreted as source architecture.' },
|
|
484
582
|
]
|
|
485
583
|
|
|
@@ -492,10 +590,28 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
|
|
|
492
590
|
}
|
|
493
591
|
}
|
|
494
592
|
|
|
593
|
+
const dynamicCoverageIndex = coverage.findIndex((entry) => entry.scope === 'dynamic-imports')
|
|
594
|
+
const configuredRuntimeWiringMethods = new Set([
|
|
595
|
+
...(opts.config?.analysis?.jsTs?.runtimeWiringMethods ?? []),
|
|
596
|
+
...(opts.config?.analysis?.jsTs?.runtimeWiringAdapters?.flatMap((adapter) => adapter.methods) ?? []),
|
|
597
|
+
])
|
|
598
|
+
if (!configuredRuntimeWiringMethods.size) for (const method of DEFAULT_RUNTIME_WIRING_METHODS) configuredRuntimeWiringMethods.add(method)
|
|
599
|
+
const includeTestRuntimeWiring = opts.config?.analysis?.jsTs?.includeTestRuntimeWiring ?? false
|
|
600
|
+
let observedLiteralDynamic = false
|
|
601
|
+
let observedUnresolvedDynamic = false
|
|
602
|
+
const observedDynamicEvidence: Evidence[] = []
|
|
603
|
+
let observedRuntimeWiring = false
|
|
604
|
+
let observedUnresolvedRuntimeWiring = false
|
|
495
605
|
for (const module of modules.values()) {
|
|
496
606
|
const text = readFileSync(module.absPath, 'utf8')
|
|
497
607
|
const sourceFile = ts.createSourceFile(module.absPath, text, ts.ScriptTarget.Latest, true, scriptKind(module.absPath))
|
|
498
|
-
const
|
|
608
|
+
const runtimeWiringMethods = includeTestRuntimeWiring || !TEST_MODULE_PATTERN.test(module.path) ? configuredRuntimeWiringMethods : new Set<string>()
|
|
609
|
+
const references = moduleReferences(root, module.absPath, sourceFile, runtimeWiringMethods)
|
|
610
|
+
observedLiteralDynamic ||= references.hasLiteralDynamic
|
|
611
|
+
observedUnresolvedDynamic ||= references.hasDynamic
|
|
612
|
+
observedDynamicEvidence.push(...references.dynamicEvidence)
|
|
613
|
+
observedRuntimeWiring ||= references.hasRuntimeWiring
|
|
614
|
+
observedUnresolvedRuntimeWiring ||= references.hasUnresolvedRuntimeWiring
|
|
499
615
|
for (const reference of references.references) {
|
|
500
616
|
const target = resolveReference(reference, module.absPath, modules, packageResult.packages, compiler.options)
|
|
501
617
|
if (!target) continue
|
|
@@ -503,12 +619,24 @@ export const discoverRepository = (opts: DiscoveryOptions = {}): DiscoverySnapsh
|
|
|
503
619
|
const externalName = target.targetId.replace(/^external:/, '')
|
|
504
620
|
addEntity({ id: target.targetId, kind: 'external', name: externalName, provenance: 'observed', evidence: [reference.evidence] })
|
|
505
621
|
}
|
|
506
|
-
addRelation({ id: entityId('relation', `${module.entityId}:${reference.kind}:${target.targetId}`), kind: reference.kind, from: module.entityId, to: target.targetId, provenance: 'observed', evidence: [reference.evidence] })
|
|
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 } } : {}) })
|
|
507
623
|
}
|
|
508
|
-
if (references.hasDynamic) coverage.push({ analyzer: 'js-ts', scope: `dynamic-imports:${module.path}`, status: 'not-analyzed', reason: 'A dynamic import
|
|
509
|
-
if (references.
|
|
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)] })
|
|
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)] })
|
|
510
626
|
}
|
|
511
627
|
|
|
628
|
+
if (dynamicCoverageIndex >= 0) coverage[dynamicCoverageIndex] = observedUnresolvedDynamic
|
|
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)] }
|
|
630
|
+
: observedLiteralDynamic
|
|
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)] }
|
|
632
|
+
: { analyzer: 'js-ts', scope: 'dynamic-imports', status: 'not-applicable', reason: 'No dynamic loading expression was observed.' }
|
|
633
|
+
const runtimeCoverageIndex = coverage.findIndex((entry) => entry.scope === 'runtime-wiring')
|
|
634
|
+
if (runtimeCoverageIndex >= 0) coverage[runtimeCoverageIndex] = observedUnresolvedRuntimeWiring
|
|
635
|
+
? { analyzer: 'js-ts', scope: 'runtime-wiring', status: 'partial', reason: 'Some configured runtime-wiring calls remain unresolved after static binding analysis.' }
|
|
636
|
+
: observedRuntimeWiring
|
|
637
|
+
? { analyzer: 'js-ts', scope: 'runtime-wiring', status: 'complete', reason: 'All observed configured runtime-wiring calls resolved to static bindings.' }
|
|
638
|
+
: { analyzer: 'js-ts', scope: 'runtime-wiring', status: 'not-applicable', reason: 'No configured runtime-wiring call was observed.' }
|
|
639
|
+
|
|
512
640
|
return artifact(root, opts.config, allFiles, [...entities.values()], [...relations.values()], coverage)
|
|
513
641
|
}
|
|
514
642
|
|