@agentskit/doc-bridge 1.6.4 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/action.yml +1 -1
  3. package/dist/cli/program.js +793 -137
  4. package/dist/cli/program.js.map +1 -1
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +38 -2
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/{index-DudNuwI5.d.ts → index-C2PCQSrB.d.ts} +216 -25
  9. package/dist/index.d.ts +837 -67
  10. package/dist/index.js +858 -127
  11. package/dist/index.js.map +1 -1
  12. package/docs/PRD-enterprise-hardening.md +288 -0
  13. package/docs/adr/0001-enterprise-verification-contract.md +35 -0
  14. package/docs/knowledge-engine-runbook.md +18 -2
  15. package/docs/spec/analyzer-plugin-v1.md +24 -0
  16. package/docs/spec/benchmark-v1.md +30 -0
  17. package/docs/spec/config-v1.md +111 -0
  18. package/docs/validation-cycle-plan.md +236 -0
  19. package/docs/verification-harness.md +33 -4
  20. package/mcpb/manifest.json +1 -1
  21. package/package.json +1 -1
  22. package/scripts/report-visual-check.mjs +45 -10
  23. package/scripts/verification-harness.mjs +216 -13
  24. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  25. package/src/agents/registry-adapter.ts +31 -7
  26. package/src/cli/program.ts +44 -11
  27. package/src/config/index.ts +2 -0
  28. package/src/config/schema.ts +56 -0
  29. package/src/discovery/documentation.ts +46 -5
  30. package/src/discovery/repository.ts +95 -16
  31. package/src/index.ts +29 -0
  32. package/src/metrics/benchmark.ts +176 -0
  33. package/src/plugins/contract.ts +89 -0
  34. package/src/reconciliation/reconcile.ts +137 -3
  35. package/src/report/html.ts +302 -78
  36. package/src/schemas/knowledge.ts +16 -1
  37. package/src/version.ts +1 -1
  38. package/src/workflow/engine.ts +65 -9
@@ -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
- project: snapshot.project,
159
- revision: snapshot.sourceRevision,
160
- revisionKind: snapshot.sourceRevisionKind,
161
- snapshotHash: snapshot.contentHash,
162
- reportHash: report.contentHash,
163
- configurationHash: snapshot.configurationHash,
164
- pipelineVersion: snapshot.pipelineVersion,
165
- analyzerVersions: snapshot.analyzerVersions,
166
- diagnosticCount: report.diagnostics.length,
167
- requiredRelationKinds: report.summary.requiredRelationKinds,
168
- diagnosticSummary: {
169
- errors: report.diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length,
170
- warnings: report.diagnostics.filter((diagnostic) => diagnostic.severity === 'warn').length,
171
- undocumented: report.diagnostics.filter((diagnostic) => diagnostic.status === 'undocumented').length,
172
- drift: report.diagnostics.filter((diagnostic) => diagnostic.status === 'stale-or-unverified' || diagnostic.status === 'conflict').length,
173
- },
174
- entities: snapshot.entities.map((entity, index) => ({
175
- id: entity.id,
176
- anchor: entity.id.length > 64 ? `entity-n${index}` : anchor('entity', entity.id),
177
- kind: entity.kind,
178
- name: entity.name,
179
- path: entity.path,
180
- provenance: entity.provenance,
181
- evidence: entity.evidence.map(({ context, ...item }) => includeSnippets && context ? { ...item, context: redactSecrets(context) } : item),
182
- })),
183
- relations: snapshot.relations.map((relation) => ({
184
- id: relation.id,
185
- kind: relation.kind,
186
- from: relation.from,
187
- to: relation.to,
188
- provenance: relation.provenance,
189
- evidence: relation.evidence.map(({ context, ...item }) => includeSnippets && context ? { ...item, context: redactSecrets(context) } : item),
190
- })),
191
- diagnostics: report.diagnostics.map((diagnostic) => ({
192
- id: diagnostic.id,
193
- code: diagnostic.code,
194
- status: diagnostic.status,
195
- severity: diagnostic.severity,
196
- message: diagnostic.message,
197
- entityIds: diagnostic.entityIds ?? [],
198
- relationIds: diagnostic.relationIds ?? [],
199
- remediation: diagnostic.remediation,
200
- evidence: diagnostic.evidence.map(({ context, ...item }) => includeSnippets && context ? { ...item, context: redactSecrets(context) } : item),
201
- })),
202
- coverage: snapshot.coverage,
203
- view: buildReportViewModel(snapshot),
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("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll("\"","&quot;").replaceAll("'","&#39;");
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=new Set();(finding.entityIds||[]).forEach((id)=>{const scope=data.view?.entityGroup?.[id];if(scope)scopes.add(scope)});(finding.relationIds||[]).forEach((id)=>{const relation=relationById.get(id);if(!relation)return;[relation.from,relation.to].forEach((endpoint)=>{const scope=data.view?.entityGroup?.[endpoint];if(scope)scopes.add(scope)})});scopes.forEach((scope)=>groupFindingCounts.set(scope,(groupFindingCounts.get(scope)||0)+1))})};
241
- const hasChunk=(name)=>!lazyChunks.includes(name)||loadedChunks.has(name);
242
- 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=()=>{loadedChunks.add(name);hydrate();resolve()};element.onerror=()=>reject(new Error("Unable to load offline report chunk: "+name));document.head.append(element)});loadingChunks.set(name,promise);return promise};
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=relationFindingIndex.get(relation.id)||[];return findings.some((finding)=>finding.severity==="error")?"error":findings.length?"warn":""};
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 relationInLens=(id)=>{const findings=relationFindingIndex.get(id)||[];return findings.some(findingInLens)};
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 graphModel=()=>{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}};
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);if(event.detail>=2){enterNode(node.dataset.node);return}nodeClickTimer=setTimeout(()=>{state.selected=node.dataset.node;render()},380)},true);
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,[]]));model.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));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="100%";svg.style.minWidth="0";svg.style.height="100%";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>",edges=model.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(model.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+edges+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."};
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=()=>!lazyChunks.includes("chunks/levels.js")||loadedChunks.has("chunks/levels.js");
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 render=()=>{if(state.level!=="overview"&&!levelsLoaded()){document.querySelector("#map-note").textContent="Loading canonical entities…";loadChunk("chunks/levels.js").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.";renderInsights();renderInsightsDashboard();renderGraph();renderDetails();renderFindings();deferFindings();renderCoverage()};
289
- 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("chunks/levels.js");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);
290
- document.addEventListener("click",(event)=>{const node=event.target.closest?.("[data-node]");if(!node)return;event.preventDefault();event.stopImmediatePropagation();clearTimeout(nodeClickTimer);if(event.detail>=2){enterNode(node.dataset.node);return}nodeClickTimer=setTimeout(()=>{state.selected=node.dataset.node;render()},380)},true);
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(snapshot.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(snapshot.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(snapshot.sourceRevision)}</strong><span>${escapeHtml(snapshot.sourceRevisionKind)} · pipeline ${escapeHtml(snapshot.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 &amp; 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 &amp; 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(snapshot.contentHash)}</div><div><b>Report</b>${escapeHtml(report.contentHash)}</div><div><b>Configuration</b>${escapeHtml(snapshot.configurationHash)}</div><div><b>Analyzers</b>${escapeHtml(Object.entries(snapshot.analyzerVersions).map(([name, version]) => `${name} ${version}`).join(', '))}</div><div><b>Source revision</b>${escapeHtml(snapshot.sourceRevision)} (${escapeHtml(snapshot.sourceRevisionKind)})</div><div><b>Mode</b>Read-only browser viewer; approvals and fixes remain outside this artifact.</div></section></main><script>${scriptBody}</script></body></html>`
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 &amp; 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 &amp; 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, view: data.view }),
373
- 'chunks/levels.js': chunkScript({ entities: data.entities, relations: data.relations }),
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__=${embeddedJson(['chunks/levels.js', 'chunks/findings.js'])}</script><script src="chunks/overview.js"></script>` })
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
  }