@agentskit/doc-bridge 1.6.3 → 1.7.44
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/action.yml +1 -1
- package/dist/cli/program.js +793 -137
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +38 -2
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DudNuwI5.d.ts → index-C2PCQSrB.d.ts} +216 -25
- package/dist/index.d.ts +837 -67
- package/dist/index.js +858 -127
- package/dist/index.js.map +1 -1
- package/docs/PRD-enterprise-hardening.md +288 -0
- package/docs/adr/0001-enterprise-verification-contract.md +35 -0
- package/docs/knowledge-engine-runbook.md +18 -2
- package/docs/spec/analyzer-plugin-v1.md +24 -0
- package/docs/spec/benchmark-v1.md +30 -0
- package/docs/spec/config-v1.md +111 -0
- package/docs/validation-cycle-plan.md +236 -0
- package/docs/verification-harness.md +33 -4
- package/mcpb/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/report-visual-check.mjs +63 -17
- package/scripts/verification-harness.mjs +216 -13
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +31 -7
- package/src/cli/program.ts +44 -11
- package/src/config/index.ts +2 -0
- package/src/config/schema.ts +56 -0
- package/src/discovery/documentation.ts +46 -5
- package/src/discovery/repository.ts +95 -16
- package/src/index.ts +29 -0
- package/src/metrics/benchmark.ts +176 -0
- package/src/plugins/contract.ts +89 -0
- package/src/reconciliation/reconcile.ts +137 -3
- package/src/report/html.ts +302 -78
- package/src/schemas/knowledge.ts +16 -1
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +65 -9
package/src/report/html.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type OfflineReportInput = {
|
|
|
9
9
|
|
|
10
10
|
export type OfflineReportOptions = {
|
|
11
11
|
readonly includeSnippets?: boolean
|
|
12
|
+
readonly privacy?: 'private' | 'anonymized'
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export type OfflineReportArtifact = {
|
|
@@ -33,8 +34,15 @@ type ReportViewModel = {
|
|
|
33
34
|
readonly nodes: readonly ReportViewNode[]
|
|
34
35
|
readonly edges: readonly { from: string; to: string; count: number; kinds: readonly string[]; relationIds: readonly string[] }[]
|
|
35
36
|
}
|
|
36
|
-
readonly groups: Readonly<Record<string, { kind: string; name: string; path: string | undefined; members: readonly string[] }>>
|
|
37
|
+
readonly groups: Readonly<Record<string, { kind: string; name: string; path: string | undefined; members: readonly string[]; moduleCount: number }>>
|
|
37
38
|
readonly entityGroup: Readonly<Record<string, string>>
|
|
39
|
+
readonly diagnosticGroup?: Readonly<Record<string, readonly string[]>>
|
|
40
|
+
readonly diagnosticRelationFindings?: Readonly<Record<string, readonly { status: string; severity: string }[]>>
|
|
41
|
+
readonly levelChunks: {
|
|
42
|
+
readonly default: string
|
|
43
|
+
readonly groups: Readonly<Record<string, string>>
|
|
44
|
+
readonly packages: readonly string[]
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
const pathParts = (value: string | undefined): string[] => (value ?? '').split('/').filter(Boolean)
|
|
@@ -56,6 +64,8 @@ const reportGroupKey = (entity: KnowledgeEntity, appLayout: boolean): string =>
|
|
|
56
64
|
|
|
57
65
|
const titleCase = (value: string): string => value.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ')
|
|
58
66
|
|
|
67
|
+
const levelChunkName = (scope: 'group' | 'package', id: string): string => `chunks/levels-${scope}-${sha256NormalizedV1(id).slice(0, 12)}.js`
|
|
68
|
+
|
|
59
69
|
const buildReportViewModel = (snapshot: DiscoverySnapshotV1): ReportViewModel => {
|
|
60
70
|
const entities = new Map(snapshot.entities.map((entity) => [entity.id, entity]))
|
|
61
71
|
const packages = snapshot.entities.filter((entity) => entity.kind === 'package')
|
|
@@ -71,6 +81,7 @@ const buildReportViewModel = (snapshot: DiscoverySnapshotV1): ReportViewModel =>
|
|
|
71
81
|
const appLayout = packages.some((entity) => pathParts(entity.path)[0] === 'apps')
|
|
72
82
|
const groups = new Map<string, { kind: string; name: string; path: string | undefined; members: string[] }>()
|
|
73
83
|
const entityGroup: Record<string, string> = {}
|
|
84
|
+
const groupModuleCounts = new Map<string, number>()
|
|
74
85
|
|
|
75
86
|
for (const entity of packages) {
|
|
76
87
|
const family = reportGroupKey(entity, appLayout)
|
|
@@ -89,6 +100,7 @@ const buildReportViewModel = (snapshot: DiscoverySnapshotV1): ReportViewModel =>
|
|
|
89
100
|
for (const [entityId, packageId] of packageForEntity) {
|
|
90
101
|
const groupId = entityGroup[packageId]
|
|
91
102
|
if (groupId) entityGroup[entityId] = groupId
|
|
103
|
+
if (groupId && entities.get(entityId)?.kind === 'module') groupModuleCounts.set(groupId, (groupModuleCounts.get(groupId) ?? 0) + 1)
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
if (externalIds.size) {
|
|
@@ -136,8 +148,13 @@ const buildReportViewModel = (snapshot: DiscoverySnapshotV1): ReportViewModel =>
|
|
|
136
148
|
.map((edge) => ({ ...edge, kinds: [...edge.kinds].sort(), relationIds: [...edge.relationIds].sort() }))
|
|
137
149
|
.sort((left, right) => right.count - left.count || left.from.localeCompare(right.from) || left.to.localeCompare(right.to)),
|
|
138
150
|
},
|
|
139
|
-
groups: Object.fromEntries([...groups.entries()].map(([id, group]) => [id, { ...group, members: [...group.members].sort() }])),
|
|
151
|
+
groups: Object.fromEntries([...groups.entries()].map(([id, group]) => [id, { ...group, members: [...group.members].sort(), moduleCount: groupModuleCounts.get(id) ?? 0 }])),
|
|
140
152
|
entityGroup,
|
|
153
|
+
levelChunks: {
|
|
154
|
+
default: 'chunks/levels-packages.js',
|
|
155
|
+
groups: Object.fromEntries([...groups.keys()].map((id) => [id, levelChunkName('group', id)])),
|
|
156
|
+
packages: [...packages].sort((left, right) => left.id.localeCompare(right.id)).map((entity) => levelChunkName('package', entity.id)),
|
|
157
|
+
},
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
|
|
@@ -154,54 +171,169 @@ const embeddedJson = (value: unknown): string => JSON.stringify(value).replaceAl
|
|
|
154
171
|
|
|
155
172
|
const errorPage = (message: string): string => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Doc Bridge report error</title><style>body{font:16px system-ui;margin:3rem;color:#311}main{max-width:60rem;margin:auto;border:1px solid #d99;padding:2rem;border-radius:8px;background:#fff8f8}code{white-space:pre-wrap}</style></head><body><main><h1>Doc Bridge report unavailable</h1><p>The saved snapshot/report could not be rendered.</p><code>${escapeHtml(message)}</code><p>Run <code>ak-docs check</code> to regenerate valid artifacts.</p></main></body></html>`
|
|
156
173
|
|
|
157
|
-
const reportData = (snapshot: DiscoverySnapshotV1, report: ReconciliationReportV1, includeSnippets: boolean) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
174
|
+
const reportData = (snapshot: DiscoverySnapshotV1, report: ReconciliationReportV1, includeSnippets: boolean, privacy: OfflineReportOptions['privacy'] = 'private') => {
|
|
175
|
+
const evidence = (items: readonly { source: string; path: string; lineStart?: number | undefined; lineEnd?: number | undefined; context?: string | undefined }[]) => items.map(({ context, ...item }) => includeSnippets && context ? { ...item, context: redactSecrets(context) } : item)
|
|
176
|
+
const base = {
|
|
177
|
+
project: snapshot.project,
|
|
178
|
+
revision: snapshot.sourceRevision,
|
|
179
|
+
revisionKind: snapshot.sourceRevisionKind,
|
|
180
|
+
snapshotHash: snapshot.contentHash,
|
|
181
|
+
reportHash: report.contentHash,
|
|
182
|
+
configurationHash: snapshot.configurationHash,
|
|
183
|
+
pipelineVersion: snapshot.pipelineVersion,
|
|
184
|
+
analyzerVersions: snapshot.analyzerVersions,
|
|
185
|
+
diagnosticCount: report.diagnostics.length,
|
|
186
|
+
requiredRelationKinds: report.summary.requiredRelationKinds,
|
|
187
|
+
diagnosticSummary: {
|
|
188
|
+
errors: report.diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length,
|
|
189
|
+
warnings: report.diagnostics.filter((diagnostic) => diagnostic.severity === 'warn').length,
|
|
190
|
+
undocumented: report.diagnostics.filter((diagnostic) => diagnostic.status === 'undocumented').length,
|
|
191
|
+
drift: report.diagnostics.filter((diagnostic) => diagnostic.status === 'stale-or-unverified' || diagnostic.status === 'conflict').length,
|
|
192
|
+
},
|
|
193
|
+
entities: snapshot.entities.map((entity, index) => ({
|
|
194
|
+
id: entity.id,
|
|
195
|
+
anchor: entity.id.length > 64 ? `entity-n${index}` : anchor('entity', entity.id),
|
|
196
|
+
kind: entity.kind,
|
|
197
|
+
name: entity.name,
|
|
198
|
+
path: entity.path,
|
|
199
|
+
provenance: entity.provenance,
|
|
200
|
+
evidence: evidence(entity.evidence),
|
|
201
|
+
})),
|
|
202
|
+
relations: snapshot.relations.map((relation) => ({
|
|
203
|
+
id: relation.id,
|
|
204
|
+
kind: relation.kind,
|
|
205
|
+
from: relation.from,
|
|
206
|
+
to: relation.to,
|
|
207
|
+
provenance: relation.provenance,
|
|
208
|
+
evidence: evidence(relation.evidence),
|
|
209
|
+
})),
|
|
210
|
+
diagnostics: report.diagnostics.map((diagnostic) => ({
|
|
211
|
+
id: diagnostic.id,
|
|
212
|
+
code: diagnostic.code,
|
|
213
|
+
status: diagnostic.status,
|
|
214
|
+
severity: diagnostic.severity,
|
|
215
|
+
message: diagnostic.message,
|
|
216
|
+
entityIds: diagnostic.entityIds ?? [],
|
|
217
|
+
relationIds: diagnostic.relationIds ?? [],
|
|
218
|
+
remediation: diagnostic.remediation,
|
|
219
|
+
evidence: evidence(diagnostic.evidence),
|
|
220
|
+
})),
|
|
221
|
+
coverage: snapshot.coverage,
|
|
222
|
+
view: buildReportViewModel(snapshot),
|
|
223
|
+
}
|
|
224
|
+
if (privacy !== 'anonymized') return base
|
|
225
|
+
|
|
226
|
+
const pad = (value: number): string => String(value).padStart(3, '0')
|
|
227
|
+
const sortedEntities = [...base.entities].sort((left, right) => left.id.localeCompare(right.id))
|
|
228
|
+
const entityOrdinals = new Map<string, number>()
|
|
229
|
+
const entityIds = new Map(sortedEntities.map((entity) => {
|
|
230
|
+
const next = (entityOrdinals.get(entity.kind) ?? 0) + 1
|
|
231
|
+
entityOrdinals.set(entity.kind, next)
|
|
232
|
+
return [entity.id, `entity:${entity.kind}-${pad(next)}`] as const
|
|
233
|
+
}))
|
|
234
|
+
const entityNames = new Map<string, number>()
|
|
235
|
+
const nameFor = (kind: string): string => {
|
|
236
|
+
const prefix = kind === 'external' ? 'dependency' : kind
|
|
237
|
+
const next = (entityNames.get(prefix) ?? 0) + 1
|
|
238
|
+
entityNames.set(prefix, next)
|
|
239
|
+
return `${prefix}-${pad(next)}`
|
|
240
|
+
}
|
|
241
|
+
const sortedRelations = [...base.relations].sort((left, right) => left.id.localeCompare(right.id))
|
|
242
|
+
const relationIds = new Map(sortedRelations.map((relation, index) => [relation.id, `relation:${pad(index + 1)}`]))
|
|
243
|
+
const sortedDiagnostics = [...base.diagnostics].sort((left, right) => left.id.localeCompare(right.id))
|
|
244
|
+
const diagnosticIds = new Map(sortedDiagnostics.map((diagnostic, index) => [diagnostic.id, `finding:${pad(index + 1)}`]))
|
|
245
|
+
const genericEvidence = (items: readonly { source: string; lineStart?: number | undefined; lineEnd?: number | undefined }[]) => items.map((item, index) => ({
|
|
246
|
+
source: item.source,
|
|
247
|
+
path: `evidence-${pad(index + 1)}`,
|
|
248
|
+
...(item.lineStart === undefined ? {} : { lineStart: item.lineStart }),
|
|
249
|
+
...(item.lineEnd === undefined ? {} : { lineEnd: item.lineEnd }),
|
|
250
|
+
}))
|
|
251
|
+
const groups = Object.entries(base.view.groups).sort(([left], [right]) => left.localeCompare(right))
|
|
252
|
+
const groupIds = new Map(groups.map(([id], index) => [id, `group:${pad(index + 1)}`]))
|
|
253
|
+
const groupNames = new Map<string, number>()
|
|
254
|
+
const groupName = (kind: string): string => {
|
|
255
|
+
if (kind === 'external') return 'External dependencies'
|
|
256
|
+
if (kind === 'shared') return 'Shared packages'
|
|
257
|
+
if (kind === 'workspace') return 'Workspace'
|
|
258
|
+
const next = (groupNames.get(kind) ?? 0) + 1
|
|
259
|
+
groupNames.set(kind, next)
|
|
260
|
+
return `${kind.charAt(0).toUpperCase()}${kind.slice(1)} ${next}`
|
|
261
|
+
}
|
|
262
|
+
const sanitizedGroups = Object.fromEntries(groups.map(([id, group]) => [groupIds.get(id) as string, {
|
|
263
|
+
kind: group.kind,
|
|
264
|
+
name: groupName(group.kind),
|
|
265
|
+
path: undefined,
|
|
266
|
+
members: group.members.map((member) => entityIds.get(member) as string),
|
|
267
|
+
moduleCount: group.moduleCount,
|
|
268
|
+
}]))
|
|
269
|
+
const sanitizedOverviewNodes = base.view.overview.nodes.map((node) => ({
|
|
270
|
+
id: groupIds.get(node.id) as string,
|
|
271
|
+
kind: node.kind,
|
|
272
|
+
name: sanitizedGroups[groupIds.get(node.id) as string]?.name ?? 'Scope',
|
|
273
|
+
path: undefined,
|
|
274
|
+
memberCount: node.memberCount,
|
|
275
|
+
}))
|
|
276
|
+
const sanitizedView = {
|
|
277
|
+
overview: {
|
|
278
|
+
nodes: sanitizedOverviewNodes,
|
|
279
|
+
edges: base.view.overview.edges.map((edge) => ({
|
|
280
|
+
from: groupIds.get(edge.from) as string,
|
|
281
|
+
to: groupIds.get(edge.to) as string,
|
|
282
|
+
count: edge.count,
|
|
283
|
+
kinds: edge.kinds,
|
|
284
|
+
relationIds: edge.relationIds.map((id) => relationIds.get(id) as string),
|
|
285
|
+
})),
|
|
286
|
+
},
|
|
287
|
+
groups: sanitizedGroups,
|
|
288
|
+
entityGroup: Object.fromEntries(Object.entries(base.view.entityGroup).map(([id, group]) => [entityIds.get(id) as string, groupIds.get(group) as string])),
|
|
289
|
+
levelChunks: {
|
|
290
|
+
default: base.view.levelChunks.default,
|
|
291
|
+
groups: Object.fromEntries([...groupIds.values()].map((id) => [id, levelChunkName('group', id)])),
|
|
292
|
+
packages: [...entityIds.values()].filter((id) => id.startsWith('entity:package-')).sort().map((id) => levelChunkName('package', id)),
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
...base,
|
|
297
|
+
project: { name: 'Anonymized repository' },
|
|
298
|
+
revision: 'redacted',
|
|
299
|
+
entities: base.entities.map((entity) => ({
|
|
300
|
+
id: entityIds.get(entity.id) as string,
|
|
301
|
+
anchor: anchor('entity', entityIds.get(entity.id) as string),
|
|
302
|
+
kind: entity.kind,
|
|
303
|
+
name: nameFor(entity.kind),
|
|
304
|
+
path: undefined,
|
|
305
|
+
provenance: entity.provenance,
|
|
306
|
+
evidence: genericEvidence(entity.evidence),
|
|
307
|
+
})),
|
|
308
|
+
relations: base.relations.map((relation) => ({
|
|
309
|
+
id: relationIds.get(relation.id) as string,
|
|
310
|
+
kind: relation.kind,
|
|
311
|
+
from: entityIds.get(relation.from) as string,
|
|
312
|
+
to: entityIds.get(relation.to) as string,
|
|
313
|
+
provenance: relation.provenance,
|
|
314
|
+
evidence: genericEvidence(relation.evidence),
|
|
315
|
+
})),
|
|
316
|
+
diagnostics: base.diagnostics.map((diagnostic) => ({
|
|
317
|
+
id: diagnosticIds.get(diagnostic.id) as string,
|
|
318
|
+
code: diagnostic.code,
|
|
319
|
+
status: diagnostic.status,
|
|
320
|
+
severity: diagnostic.severity,
|
|
321
|
+
message: `Finding ${diagnostic.code}`,
|
|
322
|
+
entityIds: diagnostic.entityIds?.map((id) => entityIds.get(id) as string) ?? [],
|
|
323
|
+
relationIds: diagnostic.relationIds?.map((id) => relationIds.get(id) as string) ?? [],
|
|
324
|
+
evidence: genericEvidence(diagnostic.evidence),
|
|
325
|
+
})),
|
|
326
|
+
coverage: base.coverage.map((entry, index) => ({
|
|
327
|
+
analyzer: entry.analyzer,
|
|
328
|
+
analyzerVersion: entry.analyzerVersion,
|
|
329
|
+
scope: `scope-${pad(index + 1)}`,
|
|
330
|
+
status: entry.status,
|
|
331
|
+
...(entry.reason ? { reason: 'Coverage boundary reported by analyzer.' } : {}),
|
|
332
|
+
...(entry.evidence ? { evidence: genericEvidence(entry.evidence) } : {}),
|
|
333
|
+
})),
|
|
334
|
+
view: sanitizedView,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
205
337
|
|
|
206
338
|
let styles = String.raw`
|
|
207
339
|
:root{color-scheme:light;--ink:#17251f;--muted:#60706a;--paper:#f5f7f3;--panel:#fff;--line:#d9e0da;--green:#18794e;--blue:#236b83;--amber:#a9640b;--red:#b33c35;--shadow:0 12px 32px rgba(23,37,31,.08)}
|
|
@@ -223,6 +355,7 @@ styles += String.raw`body[data-report-view="architecture"] #report-dashboard{dis
|
|
|
223
355
|
styles += String.raw`:root{--ink:#17251f;--muted:#586b63;--paper:#f2f5f1;--panel:#fbfcf9;--line:#c9d5cc;--green:#13734a;--blue:#155f78;--amber:#995d08;--red:#a52e2b}html,body{max-width:100%;overflow-x:hidden}body{font-size:15px;line-height:1.55}.shell{width:100%;max-width:1600px;margin:0 auto;padding:28px clamp(16px,3vw,48px) 56px}.masthead{gap:32px;padding:0 0 24px;align-items:flex-start}.masthead h1{font-size:clamp(2.8rem,5.5vw,5.4rem);line-height:.94;margin:12px 0 16px;max-width:100%;overflow-wrap:anywhere}.lede{max-width:60ch;font-size:16px}.run-meta{min-width:0;max-width:300px}.summary{margin:0;padding:18px 0 20px;grid-template-columns:repeat(5,minmax(0,1fr));gap:0}.metric{min-width:0;padding:0 16px;border-inline-start:1px solid var(--line)}.metric:first-child{padding-inline-start:0;border-inline-start:0}.metric b{font-size:clamp(1.8rem,3vw,3rem);max-width:100%;overflow-wrap:anywhere}.metric span{font-size:10px;letter-spacing:.06em;line-height:1.25}.lens-bar{margin:24px 0 4px;align-items:flex-end}.lens-bar>.subtle{max-width:52ch}.report-tabs{max-width:100%;overflow-x:auto}.tab,.level{min-height:40px;padding:8px 12px}.filters{max-width:100%;align-items:center}.filters label{min-width:0}.filters input,.filters select{max-width:100%;min-width:0}.workspace{min-width:0;grid-template-columns:minmax(0,1fr) minmax(280px,340px);gap:20px}.panel{min-width:0}.map-wrap{min-width:0;max-width:100%;width:100%;overflow:hidden;contain:layout paint}#graph{display:block;width:100%!important;min-width:0!important;max-width:100%;height:clamp(480px,60vh,680px)!important}.side{min-width:0}.detail-title,.path,.list,.finding,.finding-group,.report-table{min-width:0;overflow-wrap:anywhere}.report-dashboard{min-width:0}.report-table{table-layout:fixed}.report-table th,.report-table td{overflow-wrap:anywhere}.bar-item{min-width:0}.bar-item span{min-width:0;overflow-wrap:anywhere}.tab:focus-visible,.level:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--blue);outline-offset:2px}@media(max-width:1100px){.workspace{grid-template-columns:1fr}.side{position:static}.summary{grid-template-columns:repeat(3,minmax(0,1fr));row-gap:16px}.summary .metric:nth-child(4){border-inline-start:0;padding-inline-start:0}}@media(max-width:700px){.shell{padding:12px 16px 32px}.masthead{padding-bottom:14px}.masthead h1{font-size:clamp(2.2rem,11vw,3.4rem);margin:6px 0 10px}.lede{font-size:14px;line-height:1.35}.run-meta{display:none}.summary{padding:10px 0 12px;grid-template-columns:repeat(2,minmax(0,1fr));row-gap:8px}.summary .metric:nth-child(even){border-inline-start:1px solid var(--line);padding-inline-start:12px}.summary .metric:nth-child(odd){border-inline-start:0;padding-inline-start:0}.metric{padding-inline:10px}.metric b{font-size:1.65rem}.lens-bar{display:block;margin-top:14px}.lens-bar>.subtle{display:none}.filters{display:grid;grid-template-columns:1fr;gap:10px}.filters label{display:grid;grid-template-columns:1fr;gap:5px}.filters input,.filters select,.filters button{width:100%;min-height:44px}.panel{padding:16px}.panel-head{display:block}.panel-head>.tabs{margin-top:12px}.finding-groups{grid-template-columns:1fr}.report-dashboard{grid-template-columns:1fr}#graph{height:520px!important}.lens-bar + #lens-caption{margin:8px 0}.lens-bar + #lens-caption + #insights{margin-top:8px}}@media(prefers-color-scheme:dark){:root{--ink:#edf5ef;--muted:#b7c6ba;--paper:#111815;--panel:#1a2821;--line:#405248;--green:#73d19d;--blue:#8bd1e6;--amber:#f0bd70;--red:#ff9389}}`
|
|
224
356
|
|
|
225
357
|
styles += String.raw`@media(max-width:700px){.map-wrap{overflow:auto;contain:layout paint}#graph{width:720px!important;min-width:720px!important;max-width:none!important}}`
|
|
358
|
+
styles += String.raw`.map-wrap.dense{overflow:auto}.map-wrap.dense #graph{width:2000px!important;max-width:none!important}`
|
|
226
359
|
styles += String.raw`#lens-caption + p.subtle{display:none}`
|
|
227
360
|
|
|
228
361
|
const script = String.raw`
|
|
@@ -231,22 +364,26 @@ data.entities=data.entities||[];data.relations=data.relations||[];data.diagnosti
|
|
|
231
364
|
const policySuffix=data.requiredRelationKinds===undefined?"Policy: all observed relation kinds require declarations.":data.requiredRelationKinds.length?"Policy: declarations required for "+data.requiredRelationKinds.join(", ")+".":"Policy: missing relation declarations disabled; stale, conflicting, and unresolved declarations remain checked.";
|
|
232
365
|
const ensurePolicyNote=()=>{const note=document.querySelector("#map-note");if(note&&!note.textContent.includes(policySuffix))note.textContent=(note.textContent+" · "+policySuffix).trim()};
|
|
233
366
|
new MutationObserver(ensurePolicyNote).observe(document.querySelector("#map-note"),{childList:true,characterData:true});
|
|
234
|
-
const lazyChunks=globalThis.__DOC_BRIDGE_LAZY_CHUNKS__||[],loadedChunks=new Set(),loadingChunks=new Map();
|
|
367
|
+
const lazyChunks=globalThis.__DOC_BRIDGE_LAZY_CHUNKS__||[],hasLevelChunks=globalThis.__DOC_BRIDGE_HAS_LEVEL_CHUNKS__===true,loadedChunks=new Set(),loadingChunks=new Map();
|
|
235
368
|
const esc=(value)=>String(value??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll("\"",""").replaceAll("'","'");
|
|
236
|
-
const byId=new Map(),parent=new Map(),relationById=new Map(),packageMembers=new Map(),findingIndex=new Map(),relationFindingIndex=new Map(),relationsByEntity=new Map(),groupEntityIds=new Map(),groupFindingCounts=new Map(),packageCache=new Map(),nodeIssueCache=new Map();
|
|
369
|
+
const byId=new Map(),parent=new Map(),relationById=new Map(),packageMembers=new Map(),findingIndex=new Map(),relationFindingIndex=new Map(),relationsByEntity=new Map(),groupEntityIds=new Map(),groupFindingIndex=new Map(),groupFindingCounts=new Map(),packageCache=new Map(),nodeIssueCache=new Map();let graphCacheKey="",graphCache;
|
|
370
|
+
let activeLevelChunk="";
|
|
237
371
|
const groupById=new Map(Object.entries(data.view?.groups||{}));
|
|
372
|
+
const packageChunkNameById=new Map([...groupById.values()].flatMap((group)=>group.members).filter((id)=>data.entities.find((entity)=>entity.id===id)?.kind==="package").sort().map((id,index)=>[id,data.view?.levelChunks?.packages?.[index]]));
|
|
238
373
|
const addFinding=(index,id,finding)=>{const values=index.get(id)||[];values.push(finding);index.set(id,values)};
|
|
239
374
|
const addRelation=(id,relation)=>{const values=relationsByEntity.get(id)||[];values.push(relation);relationsByEntity.set(id,values)};
|
|
240
|
-
const hydrate=()=>{byId.clear();parent.clear();relationById.clear();packageMembers.clear();findingIndex.clear();relationFindingIndex.clear();relationsByEntity.clear();groupEntityIds.clear();groupFindingCounts.clear();packageCache.clear();nodeIssueCache.clear();data.entities.forEach((entity)=>{byId.set(entity.id,entity);const groupId=data.view?.entityGroup?.[entity.id];if(groupId){const ids=groupEntityIds.get(groupId)||new Set();ids.add(entity.id);groupEntityIds.set(groupId,ids)}});data.relations.forEach((relation)=>{relationById.set(relation.id,relation);addRelation(relation.from,relation);addRelation(relation.to,relation);if(relation.kind==="contains"){if(!parent.has(relation.to))parent.set(relation.to,relation.from);const members=packageMembers.get(relation.from)||new Set();members.add(relation.to);packageMembers.set(relation.from,members)}});data.diagnostics.forEach((finding)=>{(finding.entityIds||[]).forEach((id)=>addFinding(findingIndex,id,finding));(finding.relationIds||[]).forEach((id)=>addFinding(relationFindingIndex,id,finding));const scopes=
|
|
241
|
-
const
|
|
242
|
-
const
|
|
375
|
+
const hydrate=()=>{byId.clear();parent.clear();relationById.clear();packageMembers.clear();findingIndex.clear();relationFindingIndex.clear();relationsByEntity.clear();groupEntityIds.clear();groupFindingIndex.clear();groupFindingCounts.clear();packageCache.clear();nodeIssueCache.clear();data.entities.forEach((entity)=>{byId.set(entity.id,entity);const groupId=data.view?.entityGroup?.[entity.id];if(groupId){const ids=groupEntityIds.get(groupId)||new Set();ids.add(entity.id);groupEntityIds.set(groupId,ids)}});data.relations.forEach((relation)=>{relationById.set(relation.id,relation);addRelation(relation.from,relation);addRelation(relation.to,relation);if(relation.kind==="contains"){if(!parent.has(relation.to))parent.set(relation.to,relation.from);const members=packageMembers.get(relation.from)||new Set();members.add(relation.to);packageMembers.set(relation.from,members)}});data.diagnostics.forEach((finding)=>{(finding.entityIds||[]).forEach((id)=>addFinding(findingIndex,id,finding));(finding.relationIds||[]).forEach((id)=>addFinding(relationFindingIndex,id,finding));const scopes=data.view?.diagnosticGroup?.[finding.id]||[];scopes.forEach((scope)=>{addFinding(groupFindingIndex,scope,finding);groupFindingCounts.set(scope,(groupFindingCounts.get(scope)||0)+1)})})};
|
|
376
|
+
const isLevelChunk=(name)=>name.startsWith("chunks/levels-");
|
|
377
|
+
const isDetailChunk=(name)=>name.startsWith("chunks/details-");
|
|
378
|
+
const hasChunk=(name)=>isLevelChunk(name)?activeLevelChunk===name:isDetailChunk(name)?loadedChunks.has(name):!lazyChunks.includes(name)||loadedChunks.has(name);
|
|
379
|
+
const loadChunk=(name)=>{if(hasChunk(name))return Promise.resolve();if(loadingChunks.has(name))return loadingChunks.get(name);const promise=new Promise((resolve,reject)=>{const element=document.createElement("script");element.src=name;element.onload=()=>{try{if(isLevelChunk(name)){const payload=globalThis.__DOC_BRIDGE_LEVEL_PAYLOAD__;if(!payload)throw new Error("Offline report level payload is missing: "+name);data.entities=payload.entities||[];data.relations=payload.relations||[];activeLevelChunk=name;delete globalThis.__DOC_BRIDGE_LEVEL_PAYLOAD__}else if(isDetailChunk(name)){const payload=globalThis.__DOC_BRIDGE_DETAIL_PAYLOAD__;if(!payload)throw new Error("Offline report detail payload is missing: "+name);const entities=new Map((payload.entities||[]).map((entity)=>[entity.id,entity]));const relations=new Map((payload.relations||[]).map((relation)=>[relation.id,relation]));data.entities=data.entities.map((entity)=>entities.get(entity.id)||entity);data.relations=data.relations.map((relation)=>relations.get(relation.id)||relation);delete globalThis.__DOC_BRIDGE_DETAIL_PAYLOAD__}loadedChunks.add(name);hydrate();loadingChunks.delete(name);resolve()}catch(error){loadingChunks.delete(name);reject(error)}};element.onerror=()=>{loadingChunks.delete(name);reject(new Error("Unable to load offline report chunk: "+name))};document.head.append(element)});loadingChunks.set(name,promise);return promise};
|
|
243
380
|
const readHash=()=>new URLSearchParams(location.hash.slice(1));
|
|
244
381
|
const hash=readHash();
|
|
245
382
|
const state={lens:hash.get("lens")||"architecture",level:hash.get("level")||"overview",selected:hash.get("selected"),query:hash.get("query")||"",status:hash.get("status")||"",severity:hash.get("severity")||"",findingGroup:hash.get("findingGroup"),findingPage:Number(hash.get("findingPage")||0)};
|
|
246
383
|
const syncHash=()=>{const params=new URLSearchParams();[["lens",state.lens,"architecture"],["level",state.level,"overview"],["selected",state.selected,""],["query",state.query,""],["status",state.status,""],["severity",state.severity,""],["findingGroup",state.findingGroup,""],["findingPage",state.findingPage,"0"]].forEach(([key,value,defaultValue])=>{if(value&&value!==defaultValue)params.set(key,String(value))});const next=params.toString();try{if(location.hash.slice(1)!==next)history.replaceState(null,"",next?"#"+next:"")}catch{}};
|
|
247
384
|
const rootOf=(id)=>{let current=id;const seen=new Set();while(parent.has(current)&&!seen.has(current)){seen.add(current);current=parent.get(current)}return current};
|
|
248
385
|
const packageOf=(id)=>{if(packageCache.has(id))return packageCache.get(id);let current=id;const seen=new Set();while(current&&!seen.has(current)){seen.add(current);if(byId.get(current)?.kind==="package"){packageCache.set(id,current);return current}current=parent.get(current)}packageCache.set(id,id);return id};
|
|
249
|
-
const groupFor=(id)=>data.view?.entityGroup?.[id]||id;
|
|
386
|
+
const groupFor=(id)=>{const direct=data.view?.entityGroup?.[id];if(direct)return direct;const entity=byId.get(id);return entity?.kind==="module"||entity?.kind==="file"?groupFor(packageOf(id)):id};
|
|
250
387
|
const packageNodes=()=>data.entities.filter((entity)=>entity.kind==="package");
|
|
251
388
|
const viewLabels={architecture:"Architecture",drift:"Insights",risks:"Findings",evidence:"Coverage"};
|
|
252
389
|
const ensureReportChrome=()=>{document.querySelectorAll(".tab[data-lens]").forEach((tab)=>{tab.textContent=viewLabels[tab.dataset.lens]||tab.dataset.lens;tab.closest(".tabs")?.classList.add("report-tabs")});let breadcrumbs=document.querySelector("#breadcrumbs");if(!breadcrumbs){breadcrumbs=document.createElement("nav");breadcrumbs.id="breadcrumbs";breadcrumbs.className="breadcrumbs";breadcrumbs.setAttribute("aria-label","Report location");document.querySelector(".workspace")?.before(breadcrumbs)}return breadcrumbs};
|
|
@@ -254,28 +391,32 @@ const label=(entity)=>entity?.name||entity?.id||"Unknown";
|
|
|
254
391
|
const short=(value,max)=>{const text=String(value??""),limit=text==="External dependencies"?16:max;return text.length>limit?text.slice(0,limit-1)+"…":text};
|
|
255
392
|
const renderBreadcrumbs=()=>{const breadcrumbs=ensureReportChrome(),items=[{label:"Repository",selected:"",level:"overview"}];if(state.selected){const group=groupById.get(state.selected),entity=byId.get(state.selected);if(group)items.push({label:group.name,selected:group.id,level:"package"});else if(entity){const packageId=packageOf(entity.id),packageEntity=byId.get(packageId),groupId=groupFor(packageId);if(groupById.has(groupId))items.push({label:groupById.get(groupId).name,selected:groupId,level:"package"});if(packageEntity&&packageEntity.id!==entity.id)items.push({label:label(packageEntity),selected:packageEntity.id,level:"module"});items.push({label:label(entity),selected:entity.id,level:state.level})}}breadcrumbs.innerHTML=items.map((item,index)=>{const separator="<span aria-hidden=\"true\">"+(index?" / ":"")+"</span>";if(index===items.length-1)return separator+"<span class=\"current\">"+esc(item.label)+"</span>";return separator+"<button type=\"button\" data-breadcrumb-selected=\""+esc(item.selected)+"\" data-breadcrumb-level=\""+esc(item.level)+"\">"+esc(item.label)+"</button>"}).join("")};
|
|
256
393
|
const degreeMap=(relations)=>{const degrees=new Map();relations.forEach((relation)=>{degrees.set(relation.from,(degrees.get(relation.from)||0)+1);degrees.set(relation.to,(degrees.get(relation.to)||0)+1)});return degrees};
|
|
257
|
-
const relationHealth=(relation)=>{if(!relation)return "";const findings=
|
|
258
|
-
const nodeIssues=(nodeId)=>{if(nodeIssueCache.has(nodeId))return nodeIssueCache.get(nodeId);const ids=groupEntityIds.get(nodeId)||new Set([nodeId]),findings=new Set(),relations=new Set();for(const id of ids){(findingIndex.get(id)||[]).forEach((finding)=>findings.add(finding));(relationsByEntity.get(id)||[]).forEach((relation)=>relations.add(relation))}relations.forEach((relation)=>(relationFindingIndex.get(relation.id)||[]).forEach((finding)=>findings.add(finding)));const result=[...findings];nodeIssueCache.set(nodeId,result);return result};
|
|
394
|
+
const relationHealth=(relation)=>{if(!relation)return "";const findings=relationFindings(relation.id);return findings.some((finding)=>finding.severity==="error")?"error":findings.length?"warn":""};
|
|
395
|
+
const nodeIssues=(nodeId)=>{if(nodeIssueCache.has(nodeId))return nodeIssueCache.get(nodeId);const ids=groupEntityIds.get(nodeId)||new Set([nodeId]),findings=new Set(groupFindingIndex.get(nodeId)||[]),relations=new Set();for(const id of ids){(findingIndex.get(id)||[]).forEach((finding)=>findings.add(finding));(relationsByEntity.get(id)||[]).forEach((relation)=>relations.add(relation))}relations.forEach((relation)=>(relationFindingIndex.get(relation.id)||[]).forEach((finding)=>findings.add(finding)));const result=[...findings];nodeIssueCache.set(nodeId,result);return result};
|
|
259
396
|
const findingInLens=(finding)=>state.lens==="architecture"||(state.lens==="drift"&&finding.status!=="confirmed")||(state.lens==="risks"&&(finding.severity==="error"||finding.severity==="warn"))||(state.lens==="evidence"&&finding.evidence.length>0);
|
|
260
|
-
const
|
|
397
|
+
const relationFindings=(id)=>relationFindingIndex.get(id)||data.view?.diagnosticRelationFindings?.[id]||[];
|
|
398
|
+
const relationInLens=(id)=>relationFindings(id).some(findingInLens);
|
|
261
399
|
const externalFor=(ids)=>{const externalIds=new Set();for(const id of ids)for(const relation of relationsByEntity.get(id)||[]){const other=relation.from===id?relation.to:relation.from;if(byId.get(other)?.kind==="external")externalIds.add(other)}return [...externalIds].map((id)=>byId.get(id)).filter(Boolean)};
|
|
400
|
+
const levelChunkName=()=>{if(state.selected&&groupById.has(state.selected))return data.view?.levelChunks?.groups?.[state.selected]||data.view?.levelChunks?.default;const selected=state.selected?byId.get(state.selected):null;const packageId=selected?.kind==="package"?selected.id:selected?packageOf(selected.id):state.selected?.startsWith("package:")?state.selected:null;const packageChunk=packageId&&packageChunkNameById.get(packageId);if(packageChunk)return packageChunk;const selectedGroup=state.selected?data.view?.entityGroup?.[state.selected]:null;if(selectedGroup&&data.view?.levelChunks?.groups?.[selectedGroup])return data.view.levelChunks.groups[selectedGroup];return data.view?.levelChunks?.default};
|
|
401
|
+
const detailChunkName=()=>{const entity=state.selected?byId.get(state.selected):null;const packageId=entity?.kind==="package"?entity.id:entity?packageOf(entity.id):null;const levelName=packageId&&packageChunkNameById.get(packageId);return levelName?.replace("chunks/levels-package-","chunks/details-")||null};
|
|
262
402
|
const nodesFor=()=>{if(state.level==="overview")return data.view?.overview?.nodes||[];const packages=packageNodes(),selectedEntity=state.selected?byId.get(state.selected):null,groupScope=state.selected&&groupById.has(state.selected)?state.selected:null,packageScope=selectedEntity?.kind==="package"?selectedEntity.id:state.selected&&!groupScope?packageOf(state.selected):null;if(state.level==="package"&&!state.selected){const packageIds=new Set(packages.map((node)=>node.id));return packages.concat(state.lens==="architecture"?[]:externalFor(packageIds))}if((state.level==="module"||state.level==="file")&&!state.selected)return[];let nodes=data.entities.filter((entity)=>{if(state.level==="package")return entity.kind==="package"&&(!groupScope||groupFor(entity.id)===groupScope)&&(!packageScope||entity.id===packageScope);if(state.level==="module")return entity.kind==="module"&&(groupScope?groupFor(entity.id)===groupScope:packageOf(entity.id)===packageScope);return Boolean(entity.path)&&(groupScope?groupFor(entity.id)===groupScope:packageOf(entity.id)===packageScope)});if(state.level==="module"&&selectedEntity?.kind==="module"){const neighborhood=new Set([selectedEntity.id]);(relationsByEntity.get(selectedEntity.id)||[]).forEach((relation)=>{if(!["imports","re-exports"].includes(relation.kind))return;if(relation.from===selectedEntity.id)neighborhood.add(relation.to);if(relation.to===selectedEntity.id)neighborhood.add(relation.from)});nodes=data.entities.filter((entity)=>entity.kind==="module"&&neighborhood.has(entity.id))}const scopedIds=new Set(nodes.map((node)=>node.id)),external=state.lens==="architecture"?[]:externalFor(scopedIds),degrees=degreeMap(data.relations);return nodes.concat(external).sort((a,b)=>(degrees.get(b.id)||0)-(degrees.get(a.id)||0)||a.id.localeCompare(b.id)).slice(0,state.level==="file"?80:60)};
|
|
263
|
-
const
|
|
403
|
+
const graphModelUncached=()=>{if(state.level==="overview"){const overviewNodes=data.view?.overview?.nodes||[],overviewNodeIds=new Set(overviewNodes.map((node)=>node.id)),base=(data.view?.overview?.edges||[]).filter((edge)=>overviewNodeIds.has(edge.from)&&overviewNodeIds.has(edge.to)),edges=state.lens==="architecture"?base:base.filter((edge)=>edge.relationIds.some(relationInLens)),visible=new Set(edges.flatMap((edge)=>[edge.from,edge.to]));return{nodes:state.lens==="architecture"?overviewNodes:overviewNodes.filter((node)=>visible.has(node.id)||nodeIssues(node.id).some((finding)=>findingInLens(finding))),edges:edges.map((edge)=>({...edge,health:edge.relationIds.some((id)=>relationHealth(relationById.get(id)))?"error":""}))}}const nodes=nodesFor(),ids=new Set(nodes.map((node)=>node.id)),aggregate=state.level==="package"&&(!state.selected||state.selected.startsWith("group:")),edges=new Map();data.relations.forEach((relation)=>{const essential=state.level==="package"?relation.kind==="depends-on":relation.kind==="imports"||relation.kind==="re-exports";if(relation.kind==="contains"|| (state.lens==="architecture"&&!essential)||(state.lens!=="architecture"&&!relationInLens(relation.id)))return;const from=aggregate?packageOf(relation.from):relation.from,to=aggregate?packageOf(relation.to):relation.to;if(from===to||!ids.has(from)||!ids.has(to))return;const key=from+"→"+to,current=edges.get(key)||{from,to,count:0,kinds:new Set(),relationIds:new Set(),health:""};current.count++;current.kinds.add(relation.kind);current.relationIds.add(relation.id);current.health=current.health==="error"||relationHealth(relation)==="error"?"error":current.health||relationHealth(relation);edges.set(key,current)});const visibleEdges=[...edges.values()].sort((a,b)=>b.count-a.count||a.from.localeCompare(b.from)||a.to.localeCompare(b.to));const visible=new Set(visibleEdges.flatMap((edge)=>[edge.from,edge.to]));return{nodes:state.lens==="architecture"?nodes:nodes.filter((node)=>visible.has(node.id)||nodeIssues(node.id).some((finding)=>findingInLens(finding))),edges:visibleEdges}};
|
|
404
|
+
const graphModel=()=>{const key=state.lens+"|"+state.level+"|"+(state.selected||"");if(graphCacheKey===key&&graphCache)return graphCache;graphCacheKey=key;return graphCache=graphModelUncached()};
|
|
264
405
|
const renderInsights=()=>{const summary=data.diagnosticSummary||{},undocumented=summary.undocumented??data.diagnostics.filter((finding)=>finding.status==="undocumented").length,drift=summary.drift??data.diagnostics.filter((finding)=>finding.status==="stale-or-unverified"||finding.status==="conflict").length,unsupported=(data.coverage||[]).filter((entry)=>entry.status==="not-analyzed"||entry.status==="partial").length,model=graphModel(),degrees=degreeMap(model.edges),values=[...degrees.values()].sort((a,b)=>a-b),median=values.length?values[Math.floor(values.length/2)]:0,hot=[...degrees.values()].filter((value)=>value>=Math.max(4,median*2)).length,isolated=model.nodes.filter((node)=>!model.edges.some((edge)=>edge.from===node.id||edge.to===node.id)).length;document.querySelector("#insights").innerHTML=[["Documentation drift",undocumented+drift,"Relations or docs needing comparison.",""],["Unanalyzed scope",unsupported,"Coverage gaps are explicit.",""],["Connectivity hotspots",hot,"Heuristic: unusually connected nodes.","heuristic"],["Disconnected nodes",isolated,"Heuristic: no visible edge at this level.","heuristic"]].map(([title,count,copy,tag])=>"<article class=\"insight\"><h3>"+esc(title)+" <span class=\"tag "+tag+"\">"+(tag?"heuristic":"signal")+"</span></h3><p><strong>"+count+"</strong> · "+esc(copy)+"</p></article>").join("")};
|
|
265
|
-
const renderInsightsDashboard=()=>{let dashboard=document.querySelector("#report-dashboard");if(!dashboard){dashboard=document.createElement("section");dashboard.id="report-dashboard";dashboard.className="report-dashboard";document.querySelector(".filters")?.before(dashboard)}if(!data.diagnostics.length){dashboard.innerHTML="<p class=\"empty\">Open Findings or load the findings data to calculate insights.</p>";return}const degrees=degreeMap(data.relations),groups=[...groupById.entries()].filter(([,group])=>group.kind!=="external"),rows=groups.map(([id,group])=>{const members=groupEntityIds.get(id)||new Set(),modules=[...members].filter((entityId)=>byId.get(entityId)?.kind==="module").length;return{name:group.name,packages:group.members.length,modules,findings:groupFindingCounts.get(id)||0}}).sort((a,b)=>b.findings-a.findings||a.name.localeCompare(b.name)),topPackages=[...data.entities].filter((entity)=>entity.kind==="package").map((entity)=>({name:label(entity),degree:degrees.get(entity.id)||0})).sort((a,b)=>b.degree-a.degree||a.name.localeCompare(b.name)).slice(0,10),max=Math.max(1,...topPackages.map((row)=>row.degree));dashboard.innerHTML="<section><div class=\"eyebrow\">Attention by scope</div><h2>Where the map needs a closer look</h2><table class=\"report-table\"><thead><tr><th>Scope</th><th>Packages</th><th>Modules</th><th>Findings</th></tr></thead><tbody>"+rows.map((row)=>"<tr><td>"+esc(row.name)+"</td><td>"+row.packages+"</td><td>"+row.modules+"</td><td><strong>"+row.findings+"</strong></td></tr>").join("")+"</tbody></table></section><section><div class=\"eyebrow\">Connectivity</div><h2>Most connected packages</h2><div class=\"bar-list\">"+topPackages.map((row)=>"<div class=\"bar-item\"><span>"+esc(row.name)+"</span><i><b style=\"width:"+Math.round(row.degree/max*100)+"%\"></b></i><strong>"+row.degree+"</strong></div>").join("")+"</div></section>"};
|
|
406
|
+
const renderInsightsDashboard=()=>{let dashboard=document.querySelector("#report-dashboard");if(!dashboard){dashboard=document.createElement("section");dashboard.id="report-dashboard";dashboard.className="report-dashboard";document.querySelector(".filters")?.before(dashboard)}if(!data.diagnostics.length){dashboard.innerHTML="<p class=\"empty\">Open Findings or load the findings data to calculate insights.</p>";return}const degrees=degreeMap(data.relations),groups=[...groupById.entries()].filter(([,group])=>group.kind!=="external"),rows=groups.map(([id,group])=>{const members=groupEntityIds.get(id)||new Set(),modules=group.moduleCount||[...members].filter((entityId)=>byId.get(entityId)?.kind==="module").length;return{name:group.name,packages:group.members.length,modules,findings:groupFindingCounts.get(id)||0}}).sort((a,b)=>b.findings-a.findings||a.name.localeCompare(b.name)),topPackages=[...data.entities].filter((entity)=>entity.kind==="package").map((entity)=>({name:label(entity),degree:degrees.get(entity.id)||0})).sort((a,b)=>b.degree-a.degree||a.name.localeCompare(b.name)).slice(0,10),max=Math.max(1,...topPackages.map((row)=>row.degree));dashboard.innerHTML="<section><div class=\"eyebrow\">Attention by scope</div><h2>Where the map needs a closer look</h2><table class=\"report-table\"><thead><tr><th>Scope</th><th>Packages</th><th>Modules</th><th>Findings</th></tr></thead><tbody>"+rows.map((row)=>"<tr><td>"+esc(row.name)+"</td><td>"+row.packages+"</td><td>"+row.modules+"</td><td><strong>"+row.findings+"</strong></td></tr>").join("")+"</tbody></table></section><section><div class=\"eyebrow\">Connectivity</div><h2>Most connected packages</h2><div class=\"bar-list\">"+topPackages.map((row)=>"<div class=\"bar-item\"><span>"+esc(row.name)+"</span><i><b style=\"width:"+Math.round(row.degree/max*100)+"%\"></b></i><strong>"+row.degree+"</strong></div>").join("")+"</div></section>"};
|
|
266
407
|
const mapState={scale:1,dragging:false,x:0,y:0};
|
|
267
|
-
const applyMapTransform=()=>{const svg=document.querySelector("#graph");if(!svg)return;svg.style.transform="translate("+mapState.x+"px,"+mapState.y+"px) scale("+mapState.scale+")";svg.style.transformOrigin="center center"};
|
|
408
|
+
const applyMapTransform=()=>{const svg=document.querySelector("#graph");if(!svg)return;const dense=svg.closest(".map-wrap")?.classList.contains("dense");svg.setAttribute("preserveAspectRatio",dense?"xMinYMin meet":"xMidYMid meet");svg.style.transform="translate("+mapState.x+"px,"+mapState.y+"px) scale("+mapState.scale+")";svg.style.transformOrigin="center center"};
|
|
268
409
|
const routeGraphEdges=()=>{const svg=document.querySelector("#graph");if(!svg)return;svg.querySelectorAll("line.edge").forEach((line,index)=>{const x1=Number(line.getAttribute("x1")),y1=Number(line.getAttribute("y1")),x2=Number(line.getAttribute("x2")),y2=Number(line.getAttribute("y2")),midX=(x1+x2)/2+((index%5)-2)*14,path=document.createElementNS("http://www.w3.org/2000/svg","path");path.setAttribute("class",line.getAttribute("class")||"edge");path.setAttribute("fill","none");path.setAttribute("marker-end",line.getAttribute("marker-end")||"url(#arrow)");path.setAttribute("d","M "+x1+" "+y1+" H "+midX+" V "+y2+" H "+x2);const label=line.parentElement?.querySelector("text.edge-label");if(label){label.setAttribute("x",String(midX+3));label.setAttribute("y",String((y1+y2)/2))}line.replaceWith(path)})};
|
|
269
410
|
const enterNode=(id)=>{const group=groupById.get(id),entity=byId.get(id);if(group){state.selected=id;state.level="package"}else if(entity?.kind==="package"){state.selected=id;state.level="module"}else if(entity?.kind==="module"){state.selected=id;state.level="file"}else return;mapState.scale=1;mapState.x=0;mapState.y=0;render()};
|
|
270
411
|
let nodeClickTimer;
|
|
271
412
|
document.addEventListener("dblclick",(event)=>{const node=event.target.closest?.("[data-node]");if(!node)return;event.preventDefault();event.stopImmediatePropagation();clearTimeout(nodeClickTimer);enterNode(node.dataset.node)},true);
|
|
272
|
-
document.addEventListener("click",(event)=>{const node=event.target.closest?.("[data-node]");if(!node)return;event.preventDefault();event.stopImmediatePropagation();clearTimeout(nodeClickTimer);
|
|
413
|
+
document.addEventListener("click",(event)=>{const node=event.target.closest?.("[data-node]");if(!node)return;event.preventDefault();event.stopImmediatePropagation();clearTimeout(nodeClickTimer);nodeClickTimer=setTimeout(async()=>{state.selected=node.dataset.node;render();const chunk=detailChunkName();if(chunk&&!hasChunk(chunk)){await loadChunk(chunk);render()}},380)},true);
|
|
273
414
|
document.addEventListener("click",(event)=>{const crumb=event.target.closest?.("[data-breadcrumb-level]");if(!crumb)return;event.preventDefault();event.stopImmediatePropagation();state.level=crumb.dataset.breadcrumbLevel;state.selected=crumb.dataset.breadcrumbSelected||null;mapState.scale=1;mapState.x=0;mapState.y=0;render()},true);
|
|
274
415
|
const diagnosticsFor=(id)=>findingIndex.get(id)||[];
|
|
275
416
|
const renderDetails=()=>{const panel=document.querySelector("#details"),entity=state.selected?byId.get(state.selected):null,group=state.selected?groupById.get(state.selected):null;if(group){const members=group.members.map((id)=>byId.get(id)).filter(Boolean),relations=new Set(),ids=groupEntityIds.get(state.selected)||new Set();ids.forEach((id)=>(relationsByEntity.get(id)||[]).forEach((relation)=>relations.add(relation)));const findings=nodeIssues(state.selected),evidence=members.flatMap((member)=>member.evidence||[]).slice(0,8),unit=group.kind==="external"?"dependencies":"packages";panel.innerHTML="<div class=\"eyebrow\">Selected group</div><h2 class=\"detail-title\">"+esc(group.name)+"</h2><span class=\"tag\">"+esc(group.kind)+"</span><p class=\"path\">"+esc(group.path||"Derived from stable package identity and repository structure")+"</p><div class=\"detail-grid\"><div><b>"+members.length+"</b><span>"+unit+"</span></div><div><b>"+relations.size+"</b><span>relations</span></div><div><b>"+findings.length+"</b><span>findings</span></div><div><b>"+evidence.length+"</b><span>evidence items</span></div></div><h3>Members</h3><ul class=\"list\">"+(members.length?members.slice(0,8).map((member)=>"<li>"+esc(label(member))+"</li>").join(""):"<li>No members recorded.</li>")+(members.length>8?"<li class=\"subtle\">+"+(members.length-8)+" more — choose Package level to inspect all.</li>":"")+"</ul>"+(findings.length?"<h3 style=\"margin-top:16px\">Attention</h3><ul class=\"list\">"+findings.slice(0,5).map((finding)=>"<li><span class=\"tag "+esc(finding.severity)+"\">"+esc(finding.severity)+"</span> "+esc(finding.code)+"</li>").join("")+"</ul>":"");return}if(!entity){panel.innerHTML="<p class=\"subtle\">Select a node in the map to inspect its evidence, connectivity, and findings.</p>";return}const relations=relationsByEntity.get(entity.id)||[],incoming=relations.filter((relation)=>relation.to===entity.id),outgoing=relations.filter((relation)=>relation.from===entity.id),findings=diagnosticsFor(entity.id),evidence=[...(entity.evidence||[]),...incoming.flatMap((relation)=>relation.evidence||[]),...outgoing.flatMap((relation)=>relation.evidence||[])].slice(0,8);panel.innerHTML="<div class=\"eyebrow\">Selected entity</div><h2 class=\"detail-title\">"+esc(label(entity))+"</h2><span class=\"tag\">"+esc(entity.kind)+"</span><p class=\"path\">"+esc(entity.path||entity.id)+"</p><div class=\"detail-grid\"><div><b>"+incoming.length+"</b><span>incoming</span></div><div><b>"+outgoing.length+"</b><span>outgoing</span></div><div><b>"+findings.length+"</b><span>findings</span></div><div><b>"+evidence.length+"</b><span>evidence items</span></div></div><h3>Evidence</h3><ul class=\"list\">"+(evidence.length?evidence.map((item)=>"<li>"+esc(item.path+(item.lineStart?":"+item.lineStart:"")+(item.context?" — "+item.context:""))+"</li>").join(""):"<li>No evidence recorded.</li>")+"</ul>"+(findings.length?"<h3 style=\"margin-top:16px\">Attention</h3><ul class=\"list\">"+findings.slice(0,5).map((finding)=>"<li><span class=\"tag "+esc(finding.severity)+"\">"+esc(finding.severity)+"</span> "+esc(finding.code)+"</li>").join("")+"</ul>":"")};
|
|
276
|
-
const renderGraph=()=>{const model=graphModel(),svg=document.querySelector("#graph");if(!model.nodes.length){svg.innerHTML="<text x=\"500\" y=\"270\" text-anchor=\"middle\" class=\"subtle\">Select an app or package to expand this view.</text>";return}const ids=new Set(model.nodes.map((node)=>node.id)),incoming=new Map(model.nodes.map((node)=>[node.id,0])),outgoing=new Map(model.nodes.map((node)=>[node.id,[]]));
|
|
417
|
+
const renderGraph=()=>{const model=graphModel(),svg=document.querySelector("#graph"),dense=model.edges.length>64,edges=dense?[...model.edges].sort((a,b)=>(b.health?1:0)-(a.health?1:0)||b.count-a.count||a.from.localeCompare(b.from)||a.to.localeCompare(b.to)).slice(0,64):model.edges;if(!model.nodes.length){svg.innerHTML="<text x=\"500\" y=\"270\" text-anchor=\"middle\" class=\"subtle\">Select an app or package to expand this view.</text>";return}const ids=new Set(model.nodes.map((node)=>node.id)),incoming=new Map(model.nodes.map((node)=>[node.id,0])),outgoing=new Map(model.nodes.map((node)=>[node.id,[]]));edges.forEach((edge)=>{if(!ids.has(edge.from)||!ids.has(edge.to))return;incoming.set(edge.to,(incoming.get(edge.to)||0)+1);outgoing.get(edge.from).push(edge.to)});const rank=new Map(),work=[];model.nodes.filter((node)=>(incoming.get(node.id)||0)===0).sort((a,b)=>label(a).localeCompare(label(b))||a.id.localeCompare(b.id)).forEach((node)=>{rank.set(node.id,0);work.push(node)});for(let index=0;index<work.length;index++){const node=work[index];for(const next of outgoing.get(node.id)||[]){const nextRank=Math.max(rank.get(next)||0,(rank.get(node.id)||0)+1);rank.set(next,nextRank);if(!work.some((item)=>item.id===next))work.push(byId.get(next)||{id:next,name:next})}}model.nodes.forEach((node)=>{if(!rank.has(node.id))rank.set(node.id,0)});const columns=new Map();model.nodes.forEach((node)=>{const items=columns.get(rank.get(node.id))||[];items.push(node);columns.set(rank.get(node.id),items)});for(const items of columns.values())items.sort((a,b)=>label(a).localeCompare(label(b))||a.id.localeCompare(b.id));if(dense){columns.clear();model.nodes.forEach((node,index)=>{const column=Math.floor(index/8),items=columns.get(column)||[];items.push(node);columns.set(column,items)})}const maxRows=Math.max(1,...[...columns.values()].map((items)=>items.length)),cellWidth=190,cellHeight=82,pad=28,columnCount=Math.max(1,...columns.keys())+1,width=Math.max(620,columnCount*cellWidth+pad*2,Math.min(1000,svg.clientWidth*1.05)),height=Math.max(520,maxRows*cellHeight+pad*2);svg.setAttribute("viewBox","0 0 "+width+" "+height);svg.style.width=dense?Math.max(1000,width)+"px":"100%";svg.style.minWidth="0";svg.style.height="100%";document.querySelector(".map-wrap")?.classList.toggle("dense",dense);const positions=new Map();for(const [column,items] of columns)items.forEach((node,row)=>positions.set(node.id,{x:pad+column*cellWidth+78,y:pad+row*cellHeight+27}));const defs="<defs><marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"7\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L0,6 L7,3 z\" fill=\"#789087\"/></marker></defs>",edgeMarkup=edges.map((edge)=>{const from=positions.get(edge.from),to=positions.get(edge.to);if(!from||!to)return "";const midX=(from.x+to.x)/2,midY=(from.y+to.y)/2;return "<g><line class=\"edge "+(edge.health?"alert":"")+"\" x1=\""+from.x+"\" y1=\""+from.y+"\" x2=\""+to.x+"\" y2=\""+to.y+"\" marker-end=\"url(#arrow)\"/><text class=\"edge-label\" x=\""+midX+"\" y=\""+midY+"\">"+esc(edge.count>1?edge.count+"×":"")+"</text></g>"}).join(""),nodes=model.nodes.map((node)=>{const pos=positions.get(node.id),issues=nodeIssues(node.id),degree=degreeMap(edges).get(node.id)||0,selected=state.selected===node.id?" selected":"",issue=issues.length?" issue":"",nodeLabel=label(node),nodeKind=node.kind==="domain"?(node.memberCount||0)+" packages":node.kind;return "<g class=\"graph-node"+selected+issue+"\" transform=\"translate("+(pos.x-78)+","+(pos.y-27)+")\"><title>"+esc(nodeLabel)+" · "+esc(nodeKind)+"</title><rect width=\"156\" height=\"54\" rx=\"8\" role=\"button\" tabindex=\"0\" aria-label=\""+esc(nodeLabel+" "+nodeKind)+"\" data-node=\""+esc(node.id)+"\"></rect><text class=\"node-label\" x=\"10\" y=\"19\">"+esc(short(nodeLabel,22))+"</text><text class=\"node-kind\" x=\"10\" y=\"34\">"+esc(short(nodeKind,22))+"</text><text class=\"node-count\" x=\"144\" y=\"19\" text-anchor=\"end\">"+(degree||"")+"</text></g>"}).join("");svg.innerHTML=defs+edgeMarkup+nodes;routeGraphEdges();applyMapTransform();document.querySelector("#map-note").textContent=(state.level==="overview"?"Bounded domain view. ":state.level+" view. ")+"Directional edges are aggregated from canonical relations; "+model.edges.length+" visible connection groups"+(dense?" (showing "+edges.length+" prioritized of "+model.edges.length+").":".");};
|
|
277
418
|
const findingMatches=(finding)=>{const query=state.query.toLowerCase();return(!query||[finding.id,finding.code,finding.message,...finding.entityIds,...finding.relationIds].join(" ").toLowerCase().includes(query))&&(!state.status||finding.status===state.status)&&(!state.severity||finding.severity===state.severity)};
|
|
278
|
-
const findingScope=(finding)=>{const ids=[...(finding.entityIds||[])];(finding.relationIds||[]).forEach((id)=>{const relation=relationById.get(id);if(relation)ids.push(relation.from,relation.to)});return ids.map(groupFor).sort()[0]||"repository"};
|
|
419
|
+
const findingScope=(finding)=>data.view?.diagnosticGroup?.[finding.id]?.[0]||(()=>{const ids=[...(finding.entityIds||[])];(finding.relationIds||[]).forEach((id)=>{const relation=relationById.get(id);if(relation)ids.push(relation.from,relation.to)});return ids.map(groupFor).sort()[0]||"repository"})();
|
|
279
420
|
const findingGroups=(findings)=>{const groups=new Map();findings.forEach((finding)=>{const scope=findingScope(finding),key=[scope,finding.code,finding.status,finding.severity].join("|"),group=groups.get(key)||{key,scope,code:finding.code,status:finding.status,severity:finding.severity,findings:[]};group.findings.push(finding);groups.set(key,group)});return[...groups.values()].sort((left,right)=>right.findings.length-left.findings.length||left.code.localeCompare(right.code)||left.key.localeCompare(right.key))};
|
|
280
421
|
const renderFinding=(finding)=>"<article class=\"finding\" id=\""+esc("diagnostic-"+finding.id.replace(/[^A-Za-z0-9_-]+/g,"-"))+"\"><div class=\"finding-head\"><h3>"+esc(finding.code)+"</h3><span><span class=\"tag "+esc(finding.severity)+"\">"+esc(finding.severity)+"</span> <span class=\"tag\">"+esc(finding.status)+"</span></span></div><p>"+esc(finding.message)+"</p>"+(finding.evidence.length?"<ul>"+finding.evidence.slice(0,4).map((item)=>"<li>"+esc(item.path+(item.lineStart?":"+item.lineStart:"")+(item.context?" — "+item.context:""))+"</li>").join("")+"</ul>":"")+(finding.remediation?"<p><strong>Next check:</strong> "+esc(finding.remediation)+"</p>":"")+"</article>";
|
|
281
422
|
const renderFindings=()=>{let findings=data.diagnostics.filter(findingMatches);if(state.lens==="risks")findings=findings.filter((finding)=>finding.severity==="error"||finding.severity==="warn");if(state.lens==="evidence")findings=findings.filter((finding)=>finding.evidence.length);if(state.lens==="drift")findings=findings.filter((finding)=>finding.status!=="confirmed");const groups=findingGroups(findings),active=groups.find((group)=>group.key===state.findingGroup);if(!active){state.findingGroup=null;state.findingPage=0}syncHash();const selected=active||null,pageSize=40,pageCount=selected?Math.ceil(selected.findings.length/pageSize):0,page=Math.max(0,Math.min(state.findingPage,Math.max(0,pageCount-1))),start=page*pageSize;document.querySelector("#finding-count").textContent=findings.length+" findings · "+groups.length+" groups";document.querySelector("#findings").innerHTML=findings.length?"<div class=\"finding-groups\">"+groups.map((group)=>"<article class=\"finding-group\"><div class=\"finding-head\"><h3>"+esc(group.code)+"</h3><span><span class=\"tag "+esc(group.severity)+"\">"+esc(group.severity)+"</span> <span class=\"tag\">"+esc(group.status)+"</span></span></div><p><strong>"+group.findings.length+"</strong> finding"+(group.findings.length===1?"":"s")+" · "+esc(groupById.get(group.scope)?.name||group.scope)+"</p><p>"+esc(group.findings[0].message)+"</p><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(group.key)+"\" aria-expanded=\""+String(selected?.key===group.key)+"\">Inspect group</button></article>").join("")+"</div>"+(selected?"<div class=\"finding-detail\"><div class=\"finding-head\"><h3>"+esc(selected.code)+" · "+esc(groupById.get(selected.scope)?.name||selected.scope)+"</h3><span class=\"subtle\">"+selected.findings.length+" total</span></div>"+selected.findings.slice(start,start+pageSize).map(renderFinding).join("")+"<div class=\"finding-actions\"><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(selected.key)+"\" data-finding-page=\""+(page-1)+"\" "+(page===0?"disabled":"")+">Previous</button><span class=\"subtle\">Showing "+(start+1)+"–"+Math.min(start+pageSize,selected.findings.length)+" of "+selected.findings.length+" · page "+(page+1)+"/"+pageCount+"</span><button class=\"tab\" type=\"button\" data-finding-group=\""+esc(selected.key)+"\" data-finding-page=\""+(page+1)+"\" "+(page+1>=pageCount?"disabled":"")+">Next</button></div></div>":""):"<p class=\"empty\">No findings match the current lens and filters.</p>"};
|
|
@@ -283,11 +424,11 @@ const renderCoverage=()=>{document.querySelector("#coverage-list").innerHTML=(da
|
|
|
283
424
|
const deferFindings=()=>{if(findingsLoaded())return;const count=data.diagnosticCount||0;document.querySelector("#finding-count").textContent=count+" findings · available on demand";document.querySelector("#findings").innerHTML="<div class=\"empty\"><p>Findings stay out of the first paint so large repositories remain responsive.</p><button id=\"load-findings\" class=\"tab\" type=\"button\">Load findings</button></div>"};
|
|
284
425
|
const scopePrompt=()=>{if((state.level==="module"||state.level==="file")&&!state.selected){document.querySelector("#map-note").textContent="Select an app or package to inspect this level.";document.querySelector("#graph").innerHTML="<text x=\"500\" y=\"270\" text-anchor=\"middle\" class=\"subtle\">Select an app or package to expand this view.</text>"}};
|
|
285
426
|
const findingsLoaded=()=>!lazyChunks.includes("chunks/findings.js")||loadedChunks.has("chunks/findings.js");
|
|
286
|
-
const levelsLoaded=()=>!
|
|
427
|
+
const levelsLoaded=()=>!hasLevelChunks||state.level==="overview"||activeLevelChunk===levelChunkName();
|
|
287
428
|
const setReportView=()=>{document.body.dataset.reportView={architecture:"architecture",drift:"insights",risks:"findings",evidence:"coverage"}[state.lens]||"architecture";ensureReportChrome()};
|
|
288
|
-
const
|
|
289
|
-
document.
|
|
290
|
-
document.addEventListener("click",(event)=>{const
|
|
429
|
+
const measureRender=(name,renderFn)=>{const started=performance.now(),value=renderFn(),timings=JSON.parse(document.documentElement.dataset.docBridgeRenderTimings||"{}");timings[name]=Math.round(performance.now()-started);document.documentElement.dataset.docBridgeRenderTimings=JSON.stringify(timings);return value};
|
|
430
|
+
const render=()=>{if(state.level!=="overview"&&!levelsLoaded()){document.querySelector("#map-note").textContent="Loading canonical entities…";loadChunk(levelChunkName()).then(render).catch((error)=>{document.querySelector("#map-note").textContent=error.message});return}if(state.lens!=="architecture"&&!findingsLoaded()){document.querySelector("#lens-caption").textContent="Loading findings for this lens…";loadChunk("chunks/findings.js").then(render).catch((error)=>{document.querySelector("#lens-caption").textContent=error.message});return}syncHash();setReportView();renderBreadcrumbs();document.querySelectorAll(".tab[data-lens]").forEach((tab)=>tab.setAttribute("aria-selected",String(tab.dataset.lens===state.lens)));document.querySelectorAll(".level").forEach((button)=>button.setAttribute("aria-pressed",String(button.dataset.level===state.level)));document.querySelector("#lens-caption").textContent=state.lens==="architecture"?"The repository topology at the selected level.":state.lens==="drift"?"Where canonical code relations and documentation declarations need attention.":state.lens==="risks"?"Signals that deserve human review; connectivity warnings are heuristics, not architectural proof.":"What the analyzers observed, declared, or could not analyze.";document.documentElement.dataset.docBridgeRenderTimings="{}";measureRender("insights",renderInsights);if(state.lens!=="architecture")measureRender("insightsDashboard",renderInsightsDashboard);measureRender("graph",renderGraph);measureRender("details",renderDetails);if(state.lens!=="architecture")measureRender("findings",renderFindings);else document.querySelector("#findings")?.replaceChildren();deferFindings();measureRender("coverage",renderCoverage)};
|
|
431
|
+
document.addEventListener("click",async(event)=>{const target=event.target.closest?.("[data-level],[data-lens],#load-findings");if(!target)return;if(target.id==="load-findings"){event.preventDefault();event.stopImmediatePropagation();await loadChunk("chunks/findings.js");render();return}if(target.dataset.level&&!levelsLoaded()){event.preventDefault();event.stopImmediatePropagation();state.level=target.dataset.level;await loadChunk(levelChunkName());render();return}if(target.dataset.lens&&target.dataset.lens!=="architecture"&&!findingsLoaded()){event.preventDefault();event.stopImmediatePropagation();state.lens=target.dataset.lens;state.findingGroup=null;await loadChunk("chunks/findings.js");render()}},true);
|
|
291
432
|
document.addEventListener("click",(event)=>{const crumb=event.target.closest?.("[data-breadcrumb-level]");if(!crumb)return;event.preventDefault();event.stopImmediatePropagation();state.level=crumb.dataset.breadcrumbLevel;state.selected=crumb.dataset.breadcrumbSelected||null;mapState.scale=1;mapState.x=0;mapState.y=0;render()},true);
|
|
292
433
|
document.addEventListener("input",async(event)=>{const target=event.target;if(target?.id!=="search"||findingsLoaded())return;event.stopImmediatePropagation();state.query=target.value;state.findingGroup=null;await loadChunk("chunks/findings.js");render()},true);
|
|
293
434
|
document.addEventListener("change",async(event)=>{const target=event.target;if(!["status","severity"].includes(target?.id)||findingsLoaded())return;event.stopImmediatePropagation();state[target.id]=target.value;state.findingGroup=null;await loadChunk("chunks/findings.js");render()},true);
|
|
@@ -319,7 +460,7 @@ type InternalOfflineReportOptions = OfflineReportOptions & {
|
|
|
319
460
|
const render = (input: OfflineReportInput, options: InternalOfflineReportOptions): string => {
|
|
320
461
|
const { snapshot, report } = input
|
|
321
462
|
const includeSnippets = options.includeSnippets === true
|
|
322
|
-
const data = reportData(snapshot, report, includeSnippets)
|
|
463
|
+
const data = reportData(snapshot, report, includeSnippets, options.privacy)
|
|
323
464
|
const scriptBody = script.replace('${' + 'DATA}', options.dataExpression ?? embeddedJson(data))
|
|
324
465
|
const largeNote = snapshot.entities.length > 500 || report.diagnostics.length > 1_000
|
|
325
466
|
? 'Large snapshots are rendered from compact canonical data with progressive graph levels.'
|
|
@@ -332,7 +473,7 @@ const render = (input: OfflineReportInput, options: InternalOfflineReportOptions
|
|
|
332
473
|
? `Missing relation declarations are checked for: ${report.summary.requiredRelationKinds.join(', ')}.`
|
|
333
474
|
: 'Missing relation declarations are disabled by configuration; stale, conflicting, and unresolved declarations remain checked.'
|
|
334
475
|
const statusOptions = ['confirmed', 'undocumented', 'stale-or-unverified', 'conflict', 'unresolved', 'not-analyzed']
|
|
335
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Doc Bridge — ${escapeHtml(
|
|
476
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Doc Bridge — ${escapeHtml(data.project.name)}</title><style>${styles}</style></head><body>${options.dataScripts ?? ''}<main class="shell"><header class="masthead"><div><div class="eyebrow">Doc Bridge / Knowledge report</div><h1>${escapeHtml(data.project.name)}</h1><p class="lede">A read-only architecture and documentation map. Start broad, then follow evidence to the exact entity, relation, or finding.</p></div><div class="run-meta"><span class="read-only">Read-only snapshot</span><strong>${escapeHtml(data.revision)}</strong><span>${escapeHtml(data.revisionKind)} · pipeline ${escapeHtml(data.pipelineVersion)}</span></div></header><section class="summary" aria-label="Snapshot summary"><div class="metric"><b>${snapshot.entities.length}</b><span>entities</span></div><div class="metric"><b>${snapshot.relations.length}</b><span>canonical relations</span></div><div class="metric ${issueCount?'warn':''}"><b>${issueCount}</b><span>warn/error findings</span></div><div class="metric ${report.diagnostics.length?'warn':''}"><b>${report.diagnostics.length}</b><span>total findings</span></div><div class="metric ${unsupportedCount?'bad':''}"><b>${unsupportedCount}</b><span>partial / unanalyzed scopes</span></div></section><div class="lens-bar"><nav class="tabs" aria-label="Report lenses"><button class="tab" data-lens="architecture" aria-selected="true">Architecture</button><button class="tab" data-lens="drift" aria-selected="false">Documentation drift</button><button class="tab" data-lens="risks" aria-selected="false">Risks & hotspots</button><button class="tab" data-lens="evidence" aria-selected="false">Evidence</button></nav><span class="subtle">${escapeHtml(largeNote)}</span></div><p id="lens-caption" class="subtle">The repository topology at the selected level.</p><p class="subtle">${escapeHtml(policyNote)}</p><section id="insights" class="insights" aria-label="Attention signals"></section><section class="filters" aria-label="Finding filters"><label>Search <input id="search" type="search" placeholder="Press / to search findings"></label><label>Status <select id="status"><option value="">Any status</option>${statusOptions.map((value) => `<option>${escapeHtml(value)}</option>`).join('')}</select></label><label>Severity <select id="severity"><option value="">Any severity</option><option>error</option><option>warn</option><option>info</option></select></label><button id="reset" type="button">Reset filters</button></section><div class="workspace"><section class="panel" aria-labelledby="architecture-title"><div class="panel-head"><div><h2 id="architecture-title">Architecture map</h2><p id="map-note" class="subtle">Grouped package/external view.</p></div><div class="tabs" aria-label="Graph level"><button class="level" data-level="overview" aria-pressed="true">Overview</button><button class="level" data-level="package" aria-pressed="false">Package</button><button class="level" data-level="module" aria-pressed="false">Module</button><button class="level" data-level="file" aria-pressed="false">File</button></div></div><div class="map-wrap"><svg id="graph" viewBox="0 0 1000 560" role="img" aria-label="Interactive architecture graph"></svg></div><p class="subtle">Node number = visible connection degree. Amber nodes/edges have findings. Click or focus a node to inspect its evidence, connectivity, and findings. Grouped edges preserve canonical relation direction and count.</p></section><aside class="panel side" aria-labelledby="details-title"><div class="panel-head"><div><div class="eyebrow">Evidence trail</div><h2 id="details-title">Details</h2></div><button id="clear-selection" class="tab" type="button">Clear</button></div><div id="details"><p class="subtle">Select a node in the map to inspect its evidence, connectivity, and findings.</p></div></aside></div><section class="panel run" aria-labelledby="run-title"><div class="panel-head"><div><div class="eyebrow">Jest-like diagnostics</div><h2 id="run-title">Run report</h2></div><span id="finding-count" class="subtle">${report.diagnostics.length} shown</span></div><div class="run-summary"><span class="chip"><b>${report.diagnostics.filter((item) => item.severity === 'error').length}</b> errors</span><span class="chip"><b>${report.diagnostics.filter((item) => item.severity === 'warn').length}</b> warnings</span><span class="chip"><b>${report.diagnostics.filter((item) => item.status === 'undocumented').length}</b> undocumented</span><span class="chip"><b>${report.diagnostics.filter((item) => item.status === 'stale-or-unverified' || item.status === 'conflict').length}</b> drift/conflict</span></div><div id="findings"></div></section><section class="panel run" aria-labelledby="coverage-title"><div class="panel-head"><div><div class="eyebrow">Analyzer boundaries</div><h2 id="coverage-title">Coverage & unsupported areas</h2></div><span class="subtle">Explicit limits are part of the evidence</span></div><div id="coverage-list"></div></section><section class="metadata" aria-label="Run metadata"><div><b>Snapshot</b>${escapeHtml(data.snapshotHash)}</div><div><b>Report</b>${escapeHtml(data.reportHash)}</div><div><b>Configuration</b>${escapeHtml(data.configurationHash)}</div><div><b>Analyzers</b>${escapeHtml(Object.entries(data.analyzerVersions).map(([name, version]) => `${name} ${version}`).join(', '))}</div><div><b>Source revision</b>${escapeHtml(data.revision)} (${escapeHtml(data.revisionKind)})</div><div><b>Mode</b>${options.privacy === 'anonymized' ? 'Anonymized read-only browser viewer; evidence paths and project identity are redacted.' : 'Read-only browser viewer; approvals and fixes remain outside this artifact.'}</div></section></main><script>${scriptBody}</script></body></html>`
|
|
336
477
|
}
|
|
337
478
|
|
|
338
479
|
const parseReportInput = (input: unknown): OfflineReportInput => {
|
|
@@ -345,6 +486,8 @@ const parseReportInput = (input: unknown): OfflineReportInput => {
|
|
|
345
486
|
}
|
|
346
487
|
|
|
347
488
|
const chunkScript = (payload: unknown): string => `globalThis.__DOC_BRIDGE_DATA__=Object.assign(globalThis.__DOC_BRIDGE_DATA__||{},${embeddedJson(payload)});`
|
|
489
|
+
const levelChunkScript = (payload: unknown): string => `globalThis.__DOC_BRIDGE_LEVEL_PAYLOAD__=${embeddedJson(payload)};`
|
|
490
|
+
const detailChunkScript = (payload: unknown): string => `globalThis.__DOC_BRIDGE_DETAIL_PAYLOAD__=${embeddedJson(payload)};`
|
|
348
491
|
const byteLength = (value: string): number => new TextEncoder().encode(value).byteLength
|
|
349
492
|
|
|
350
493
|
export const renderOfflineReport = (input: unknown, options: OfflineReportOptions = {}): string => {
|
|
@@ -358,24 +501,105 @@ export const renderOfflineReport = (input: unknown, options: OfflineReportOption
|
|
|
358
501
|
export const renderOfflineReportArtifact = (input: unknown, options: OfflineReportOptions & { readonly thresholdBytes?: number } = {}): OfflineReportArtifact => {
|
|
359
502
|
const parsed = parseReportInput(input)
|
|
360
503
|
const thresholdBytes = options.thresholdBytes ?? DEFAULT_LARGE_REPORT_THRESHOLD_BYTES
|
|
361
|
-
const renderOptions = { includeSnippets: options.includeSnippets === true, thresholdBytes }
|
|
504
|
+
const renderOptions = { includeSnippets: options.includeSnippets === true, privacy: options.privacy ?? 'private', thresholdBytes }
|
|
362
505
|
const singleFile = render(parsed, options)
|
|
363
506
|
if (byteLength(singleFile) <= thresholdBytes) {
|
|
364
507
|
const files = { 'index.html': sha256NormalizedV1(singleFile) }
|
|
365
|
-
const manifestPayload = { schemaVersion: 1, mode: 'single-file', generatedBy: '@agentskit/doc-bridge', snapshotHash: parsed.snapshot.contentHash, reportHash: parsed.report.contentHash, configurationHash: parsed.snapshot.configurationHash, sourceRevision: parsed.snapshot.sourceRevision, renderOptions, files }
|
|
508
|
+
const manifestPayload = { schemaVersion: 1, mode: 'single-file', generatedBy: '@agentskit/doc-bridge', snapshotHash: parsed.snapshot.contentHash, reportHash: parsed.report.contentHash, configurationHash: parsed.snapshot.configurationHash, sourceRevision: options.privacy === 'anonymized' ? 'redacted' : parsed.snapshot.sourceRevision, renderOptions, files }
|
|
366
509
|
const manifest = JSON.stringify({ ...manifestPayload, artifactHash: sha256NormalizedV1(manifestPayload) }, null, 2)
|
|
367
510
|
return { mode: 'single-file', indexHtml: singleFile, files: { 'index.html': singleFile }, manifest }
|
|
368
511
|
}
|
|
369
512
|
|
|
370
|
-
const data = reportData(parsed.snapshot, parsed.report, options.includeSnippets === true)
|
|
513
|
+
const data = reportData(parsed.snapshot, parsed.report, options.includeSnippets === true, options.privacy)
|
|
514
|
+
const entitiesById = new Map(data.entities.map((entity) => [entity.id, entity]))
|
|
515
|
+
const packageEntities = data.entities.filter((entity) => entity.kind === 'package')
|
|
516
|
+
const packageIds = new Set(packageEntities.map((entity) => entity.id))
|
|
517
|
+
const packageRelations = data.relations.filter((relation) => packageIds.has(relation.from) && packageIds.has(relation.to))
|
|
518
|
+
const diagnosticGroup = Object.fromEntries(data.diagnostics.map((diagnostic) => {
|
|
519
|
+
const groups = new Set<string>()
|
|
520
|
+
for (const entityId of diagnostic.entityIds ?? []) {
|
|
521
|
+
const groupId = data.view.entityGroup[entityId]
|
|
522
|
+
if (groupId) groups.add(groupId)
|
|
523
|
+
}
|
|
524
|
+
for (const relationId of diagnostic.relationIds ?? []) {
|
|
525
|
+
const relation = data.relations.find((candidate) => candidate.id === relationId)
|
|
526
|
+
if (!relation) continue
|
|
527
|
+
for (const endpoint of [relation.from, relation.to]) {
|
|
528
|
+
const groupId = data.view.entityGroup[endpoint]
|
|
529
|
+
if (groupId) groups.add(groupId)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return [diagnostic.id, [...groups].sort()] as const
|
|
533
|
+
}))
|
|
534
|
+
const diagnosticRelationFindings = new Map<string, { status: string; severity: string }[]>()
|
|
535
|
+
for (const diagnostic of data.diagnostics) {
|
|
536
|
+
for (const relationId of diagnostic.relationIds ?? []) {
|
|
537
|
+
const findings = diagnosticRelationFindings.get(relationId) ?? []
|
|
538
|
+
findings.push({ status: diagnostic.status, severity: diagnostic.severity })
|
|
539
|
+
diagnosticRelationFindings.set(relationId, findings)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const containsRelations = data.relations.filter((relation) => relation.kind === 'contains')
|
|
543
|
+
const collectScope = (roots: readonly string[]): Set<string> => {
|
|
544
|
+
const ids = new Set(roots)
|
|
545
|
+
let changed = true
|
|
546
|
+
while (changed) {
|
|
547
|
+
changed = false
|
|
548
|
+
for (const relation of containsRelations) {
|
|
549
|
+
if (ids.has(relation.from) && !ids.has(relation.to)) {
|
|
550
|
+
ids.add(relation.to)
|
|
551
|
+
changed = true
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return ids
|
|
556
|
+
}
|
|
557
|
+
const scopedPayload = (ids: ReadonlySet<string>) => ({
|
|
558
|
+
entities: data.entities.filter((entity) => ids.has(entity.id)),
|
|
559
|
+
relations: data.relations.filter((relation) => ids.has(relation.from) && ids.has(relation.to)),
|
|
560
|
+
})
|
|
561
|
+
const packagePayload = (ids: ReadonlySet<string>) => ({
|
|
562
|
+
entities: data.entities.filter((entity) => ids.has(entity.id) && entity.kind === 'package'),
|
|
563
|
+
relations: data.relations.filter((relation) => ids.has(relation.from) && ids.has(relation.to) && entitiesById.get(relation.from)?.kind === 'package' && entitiesById.get(relation.to)?.kind === 'package'),
|
|
564
|
+
})
|
|
565
|
+
const compactPayload = (payload: ReturnType<typeof scopedPayload>) => ({
|
|
566
|
+
entities: payload.entities.map(({ evidence: _evidence, anchor: _anchor, provenance: _provenance, ...entity }) => entity),
|
|
567
|
+
relations: payload.relations.map(({ evidence: _evidence, provenance: _provenance, ...relation }) => relation),
|
|
568
|
+
})
|
|
569
|
+
const levelFiles: Record<string, string> = {}
|
|
570
|
+
const detailFiles: Record<string, string> = {}
|
|
571
|
+
for (const [groupId, group] of Object.entries(data.view.groups)) {
|
|
572
|
+
const ids = new Set(group.members.filter((id) => entitiesById.get(id)?.kind === 'package'))
|
|
573
|
+
const chunkName = data.view.levelChunks.groups[groupId]
|
|
574
|
+
if (chunkName) levelFiles[chunkName] = levelChunkScript(packagePayload(ids))
|
|
575
|
+
}
|
|
576
|
+
const packageIdList = packageEntities.map((entity) => entity.id)
|
|
577
|
+
const packageIdSet = new Set(packageIdList)
|
|
578
|
+
levelFiles[data.view.levelChunks.default] = levelChunkScript(scopedPayload(packageIdSet))
|
|
579
|
+
const sortedPackageIds = [...packageIds].sort()
|
|
580
|
+
for (const [index, packageId] of sortedPackageIds.entries()) {
|
|
581
|
+
const chunkName = data.view.levelChunks.packages[index]
|
|
582
|
+
const packageScope = scopedPayload(collectScope([packageId]))
|
|
583
|
+
if (chunkName) levelFiles[chunkName] = levelChunkScript(compactPayload(packageScope))
|
|
584
|
+
const detailName = chunkName?.replace('chunks/levels-package-', 'chunks/details-')
|
|
585
|
+
if (detailName) detailFiles[detailName] = detailChunkScript(packageScope)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const overviewView = {
|
|
589
|
+
...data.view,
|
|
590
|
+
entityGroup: Object.fromEntries(Object.entries(data.view.entityGroup).filter(([id]) => data.entities.find((entity) => entity.id === id)?.kind === 'package')),
|
|
591
|
+
diagnosticGroup,
|
|
592
|
+
diagnosticRelationFindings: Object.fromEntries(diagnosticRelationFindings),
|
|
593
|
+
}
|
|
371
594
|
const files = {
|
|
372
|
-
'chunks/overview.js': chunkScript({ project: data.project, revision: data.revision, revisionKind: data.revisionKind, snapshotHash: data.snapshotHash, reportHash: data.reportHash, configurationHash: data.configurationHash, pipelineVersion: data.pipelineVersion, analyzerVersions: data.analyzerVersions, coverage: data.coverage, diagnosticCount: data.diagnosticCount, diagnosticSummary: data.diagnosticSummary, requiredRelationKinds: data.requiredRelationKinds,
|
|
373
|
-
|
|
595
|
+
'chunks/overview.js': chunkScript({ project: data.project, revision: data.revision, revisionKind: data.revisionKind, snapshotHash: data.snapshotHash, reportHash: data.reportHash, configurationHash: data.configurationHash, pipelineVersion: data.pipelineVersion, analyzerVersions: data.analyzerVersions, coverage: data.coverage, diagnosticCount: data.diagnosticCount, diagnosticSummary: data.diagnosticSummary, requiredRelationKinds: data.requiredRelationKinds, entities: packageEntities, relations: packageRelations, view: overviewView }),
|
|
596
|
+
...levelFiles,
|
|
597
|
+
...detailFiles,
|
|
374
598
|
'chunks/findings.js': chunkScript({ diagnostics: data.diagnostics }),
|
|
375
599
|
}
|
|
376
|
-
const indexHtml = render(parsed, { ...options, dataExpression: 'globalThis.__DOC_BRIDGE_DATA__', dataScripts: `<script>globalThis.__DOC_BRIDGE_LAZY_CHUNKS__
|
|
600
|
+
const indexHtml = render(parsed, { ...options, dataExpression: 'globalThis.__DOC_BRIDGE_DATA__', dataScripts: `<script>globalThis.__DOC_BRIDGE_HAS_LEVEL_CHUNKS__=true;globalThis.__DOC_BRIDGE_LAZY_CHUNKS__=["chunks/findings.js"]</script><script src="chunks/overview.js"></script>` })
|
|
377
601
|
const fileHashes = Object.fromEntries(Object.entries({ ...files, 'index.html': indexHtml }).map(([file, content]) => [file, sha256NormalizedV1(content)]))
|
|
378
|
-
const manifestPayload = { schemaVersion: 1, mode: 'directory', generatedBy: '@agentskit/doc-bridge', snapshotHash: parsed.snapshot.contentHash, reportHash: parsed.report.contentHash, configurationHash: parsed.snapshot.configurationHash, sourceRevision: parsed.snapshot.sourceRevision, renderOptions, files: fileHashes }
|
|
602
|
+
const manifestPayload = { schemaVersion: 1, mode: 'directory', generatedBy: '@agentskit/doc-bridge', snapshotHash: parsed.snapshot.contentHash, reportHash: parsed.report.contentHash, configurationHash: parsed.snapshot.configurationHash, sourceRevision: options.privacy === 'anonymized' ? 'redacted' : parsed.snapshot.sourceRevision, renderOptions, files: fileHashes }
|
|
379
603
|
const manifest = JSON.stringify({ ...manifestPayload, artifactHash: sha256NormalizedV1(manifestPayload) }, null, 2)
|
|
380
604
|
return { mode: 'directory', indexHtml, files: { ...files, 'index.html': indexHtml }, manifest }
|
|
381
605
|
}
|