function modulePath(p, depth) {
const parts = (p || '').split('/');
if (parts.length <= depth) {
const lastSlash = (p || '').lastIndexOf('/');
return lastSlash < 0 ? (p || '') : p.slice(0, lastSlash);
}
return parts.slice(0, depth).join('/');
}
function aggregateImportsAt(imports, depth) {
const ee = {};
const nn = {};
for (var i = 0; i < imports.length; i++) {
const imp = imports[i];
if (!imp.target_path) continue;
const s = modulePath(imp.src_path, depth);
const t = modulePath(imp.target_path, depth);
if (!s || !t || s === t) continue;
const key = s + '\x00' + t;
ee[key] = (ee[key] || 0) + 1;
nn[s] = true;
nn[t] = true;
}
return { edges: ee, nodes: nn };
}
function aggregateCouplingAt(coupling, depth) {
const cc = {};
for (var i = 0; i < coupling.length; i++) {
const row = coupling[i];
const a = modulePath(row.entity_a, depth);
const b = modulePath(row.entity_b, depth);
if (!a || !b || a === b) continue;
const key = (a < b) ? (a + '\x00' + b) : (b + '\x00' + a);
const deg = (typeof row.degree === 'number') ? row.degree : 0;
if (!(key in cc) || deg > cc[key]) cc[key] = deg;
}
return cc;
}
function renderArchGraph(imports, violations, unstable, roles) {
violations = violations || [];
unstable = unstable || [];
roles = roles || [];
const container = document.getElementById('widget-arch-graph-body');
if (!container) return;
if (!imports.length) {
container.innerHTML = '<div class="empty">No resolved import edges yet. The resolver covers Rust, Python, and JS/TS today; Java FQN→file mapping is not attempted.</div>';
return;
}
const MIN_NODES_FOR_USEFUL_GRAPH = 8;
const archLayout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const userArchDepth = archLayout ? archLayout.archGraphDepth : 'auto';
var edges = {};
var nodes = {};
var chosenDepth = (typeof userArchDepth === 'number') ? userArchDepth : 6;
if (typeof userArchDepth === 'number') {
const result = aggregateImportsAt(imports, userArchDepth);
edges = result.edges;
nodes = result.nodes;
} else {
for (var depth = 2; depth <= 6; depth++) {
const result = aggregateImportsAt(imports, depth);
edges = result.edges;
nodes = result.nodes;
chosenDepth = depth;
if (Object.keys(nodes).length >= MIN_NODES_FOR_USEFUL_GRAPH) break;
}
}
const violEdges = {};
for (var vi = 0; vi < violations.length; vi++) {
const v = violations[vi];
const vs = modulePath(v.entity_a, chosenDepth);
const vt = modulePath(v.entity_b, chosenDepth);
if (!vs || !vt || vs === vt) continue;
const vkey = vs + '\x00' + vt;
const vd = (typeof v.degree === 'number') ? v.degree : 0;
if (!(vkey in violEdges) || vd > violEdges[vkey]) violEdges[vkey] = vd;
nodes[vs] = true;
nodes[vt] = true;
}
const unstableModules = {};
for (var ui = 0; ui < unstable.length; ui++) {
const um = modulePath(unstable[ui].path, chosenDepth);
if (!um) continue;
unstableModules[um] = (unstableModules[um] || 0) + 1;
nodes[um] = true;
}
const ROLE_RANK = { core: 3, control: 2, shared: 1, periphery: 0 };
const moduleRole = {};
const moduleInCycle = {};
const moduleLevel = {};
var vfoSum = 0;
var fileCount = 0;
var filesInCycles = 0;
for (var rri = 0; rri < roles.length; rri++) {
const rr = roles[rri];
fileCount += 1;
vfoSum += (typeof rr.vfo === 'number') ? rr.vfo : 0;
if (rr.in_cycle) filesInCycles += 1;
const rm = modulePath(rr.path, chosenDepth);
if (!rm) continue;
const cur = moduleRole[rm];
if (cur === undefined || (ROLE_RANK[rr.role] || 0) > (ROLE_RANK[cur] || 0)) {
moduleRole[rm] = rr.role;
}
if (rr.in_cycle) moduleInCycle[rm] = true;
const lv = (typeof rr.level === 'number') ? rr.level : 1e9;
if (moduleLevel[rm] === undefined || lv < moduleLevel[rm]) moduleLevel[rm] = lv;
}
const propagationCost = fileCount > 0 ? (vfoSum / (fileCount * fileCount)) : 0;
if (!Object.keys(nodes).length) {
container.innerHTML = '<div class="empty">All resolved imports stay intra-module — no inter-module edges to graph.</div>';
return;
}
const roleColors = {
core: token('--color-error') || '#dc2626',
control: token('--color-warning') || '#d97706',
shared: token('--color-info') || '#2563eb',
periphery: token('--fg-dim') || '#6b7280',
};
const violColor = roleColors.control;
const cycleRing = getCssVar('--fg') || '#111827';
const ROLE_ORDER = ['core', 'control', 'shared', 'periphery'];
const nodeArr = Object.keys(nodes).map(function (n) {
const role = moduleRole[n] || 'periphery';
const isUnstable = !!unstableModules[n];
const inCycle = !!moduleInCycle[n];
const cat = ROLE_ORDER.indexOf(role);
return {
name: n,
symbol: isUnstable ? 'diamond' : 'circle',
symbolSize: isUnstable ? 42 : 30,
category: cat < 0 ? 3 : cat,
itemStyle: inCycle ? { borderColor: cycleRing, borderWidth: 3 } : undefined,
};
});
const layoutMode = (archLayout && archLayout.archGraphLayout === 'layered')
? 'layered' : 'force';
if (layoutMode === 'layered') {
const W = container.clientWidth || 900;
const padX = 64;
const padTop = 40;
const padBot = 40;
const rowGap = 96;
const minSpacingX = 96; const usableW = W - 2 * padX;
const perRow = Math.max(1, Math.floor(usableW / minSpacingX));
const byLevel = {};
nodeArr.forEach(function (nd) {
const lv = (moduleLevel[nd.name] === undefined) ? 0 : moduleLevel[nd.name];
(byLevel[lv] = byLevel[lv] || []).push(nd);
});
const levelsPresent = Object.keys(byLevel)
.map(Number)
.sort(function (a, b) { return a - b; });
var rowCursor = 0;
levelsPresent.forEach(function (lv) {
const band = byLevel[lv].sort(function (a, c) { return a.name < c.name ? -1 : 1; });
const subRows = Math.max(1, Math.ceil(band.length / perRow));
for (var sr = 0; sr < subRows; sr++) {
const slice = band.slice(sr * perRow, (sr + 1) * perRow);
const k = slice.length;
const yRow = padTop + (rowCursor + sr) * rowGap;
slice.forEach(function (nd, i) {
nd.x = (k === 1) ? (W / 2) : (padX + (i / (k - 1)) * usableW);
nd.y = yRow;
});
}
rowCursor += subRows;
});
const totalRows = Math.max(rowCursor, 1);
container.style.height = (padTop + padBot + (totalRows - 1) * rowGap + 40) + 'px';
} else {
container.style.height = '380px';
}
const structuralLinks = Object.keys(edges).map(function (k) {
const parts = k.split('\x00');
return { source: parts[0], target: parts[1], value: edges[k], _kind: 'import' };
});
const violationLinks = Object.keys(violEdges).map(function (k) {
const parts = k.split('\x00');
return {
source: parts[0],
target: parts[1],
value: violEdges[k],
_kind: 'violation',
lineStyle: { color: violColor, type: 'dashed', opacity: 0.9, width: 2, curveness: 0.2 },
};
});
const edgeArr = structuralLinks.concat(violationLinks);
const cycleModuleCount = Object.keys(moduleInCycle).length;
setChartAriaLabel(container,
'Architecture graph, ' + nodeArr.length + ' modules coloured by role ' +
'(core/control/shared/periphery), ' + structuralLinks.length + ' import edges, ' +
violationLinks.length + ' dashed modularity-violation edges, ' +
Object.keys(unstableModules).length + ' unstable-interface modules shown as diamonds, ' +
cycleModuleCount + ' modules in dependency cycles shown ringed. Propagation cost ' +
(propagationCost * 100).toFixed(1) + ' percent.');
const chart = mountEcharts(container);
const titleText = fileCount > 0
? 'Propagation cost ' + (propagationCost * 100).toFixed(1) + '% · ' +
filesInCycles + ' files in cycles'
: '';
chart.setOption({
title: {
text: titleText,
left: 'center',
top: 4,
textStyle: { color: getCssVar('--fg-dim'), fontSize: 12, fontWeight: 'normal' },
},
tooltip: {
trigger: 'item',
formatter: function (p) {
if (p.dataType === 'edge') {
if (p.data && p.data._kind === 'violation') {
return 'Modularity violation — co-change, no import<br/>' +
escapeHtml(p.data.source) + ' ↔ ' + escapeHtml(p.data.target) +
'<br/>coupling degree ' + (Number(p.data.value) || 0).toFixed(1) + '%';
}
return 'Imports: ' + escapeHtml(p.data.source) + ' → ' + escapeHtml(p.data.target) +
' (' + (Number(p.data.value) || 0) + ')';
}
return escapeHtml(p.name) + '<br/>role: ' + (moduleRole[p.name] || 'periphery') +
(moduleInCycle[p.name] ? ' · in cycle' : '') +
(unstableModules[p.name] ? ' · unstable interface' : '');
},
},
legend: [{
data: ROLE_ORDER,
textStyle: { color: getCssVar('--fg-dim') },
bottom: 0,
}],
series: [{
type: 'graph',
layout: 'force',
categories: [
{ name: 'core', itemStyle: { color: roleColors.core } },
{ name: 'control', itemStyle: { color: roleColors.control } },
{ name: 'shared', itemStyle: { color: roleColors.shared } },
{ name: 'periphery', itemStyle: { color: roleColors.periphery } },
],
data: nodeArr,
links: edgeArr,
roam: true,
layout: layoutMode === 'layered' ? 'none' : 'force',
force: { repulsion: 200, edgeLength: 80 },
edgeSymbol: layoutMode === 'layered' ? ['none', 'arrow'] : 'none',
edgeSymbolSize: 7,
label: {
show: true,
color: getCssVar('--fg-dim'),
fontSize: 11,
formatter: layoutMode === 'layered'
? function (p) { const s = String(p.name).split('/'); return s[s.length - 1]; }
: undefined,
},
lineStyle: { color: token('--color-info'), opacity: 0.6, width: 1.5 },
emphasis: { focus: 'adjacency', lineStyle: { width: 3 } },
}],
});
window._codeloreResetZoomHandlers['widget-arch-graph'] = function () {
renderArchGraph(imports, violations, unstable, roles);
};
}
function renderArchTrend(rows) {
const container = document.getElementById('widget-arch-trend-body');
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="empty">No architecture-trend data — repo too small, or the historical scan was skipped.</div>';
return;
}
const dates = rows.map(function (r) { return r.date; });
const propagation = rows.map(function (r) {
return Number(((r.propagation_cost || 0) * 100).toFixed(2));
});
const cycles = rows.map(function (r) { return r.cycle_count || 0; });
const infoColor = token('--color-info') || '#2563eb';
const errColor = token('--color-error') || '#dc2626';
const dim = getCssVar('--fg-dim');
setChartAriaLabel(container,
'Architecture decay trend over ' + rows.length +
' sampled revisions: propagation cost (percent) and dependency-cycle count.');
const chart = mountEcharts(container);
chart.setOption({
tooltip: { trigger: 'axis' },
legend: {
data: ['Propagation cost %', 'Dependency cycles'],
textStyle: { color: dim },
bottom: 0,
},
grid: { left: 24, right: 8, top: 24, bottom: 48, containLabel: true },
xAxis: {
type: 'category',
data: dates,
axisLabel: { color: dim, rotate: 30, fontSize: 10 },
},
yAxis: [
{
type: 'value',
name: 'Propagation %',
position: 'left',
nameGap: 10,
axisLabel: { color: dim },
nameTextStyle: { color: dim, fontSize: 10, align: 'left' },
splitLine: { lineStyle: { color: getCssVar('--border') } },
},
{
type: 'value',
name: 'Cycles',
position: 'right',
minInterval: 1,
nameGap: 10,
axisLabel: { color: dim },
nameTextStyle: { color: dim, fontSize: 10, align: 'left' },
splitLine: { show: false },
},
],
series: [
{
name: 'Propagation cost %',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
yAxisIndex: 0,
data: propagation,
lineStyle: { color: infoColor, width: 2 },
itemStyle: { color: infoColor },
areaStyle: { color: infoColor, opacity: 0.08 },
},
{
name: 'Dependency cycles',
type: 'line',
step: 'end',
symbol: 'circle',
symbolSize: 6,
yAxisIndex: 1,
data: cycles,
lineStyle: { color: errColor, width: 2, type: 'dashed' },
itemStyle: { color: errColor },
},
],
});
}
function healthTrendBands(errColor, warnColor, okColor) {
var opts = data.options || {};
var gMin = (opts.health_green_min != null) ? opts.health_green_min : 70;
var yMin = (opts.health_yellow_min != null) ? opts.health_yellow_min : 40;
var zoneColors = [errColor, warnColor, okColor];
return {
silent: true,
data: [
[{ yAxis: 0 }, { yAxis: yMin }],
[{ yAxis: yMin }, { yAxis: gMin }],
[{ yAxis: gMin }, { yAxis: 100 }],
],
itemStyle: {
color: function (params) {
return zoneColors[params.dataIndex] || zoneColors[0];
},
opacity: 0.06,
},
};
}
function renderHealthTrend(rows, mode) {
const container = document.getElementById('widget-health-trend-body');
if (!container) return;
if (rows.length < 2) {
container.innerHTML =
'<div class="empty">Not enough history for a health timeline — need at least 2 commits.</div>';
return;
}
const view = mode || 'overlay';
const dates = rows.map(function (r) { return r.date; });
const arch = rows.map(function (r) { return Number((r.arch_health || 0).toFixed(2)); });
const code = rows.map(function (r) { return Number((r.code_health || 0).toFixed(2)); });
const combined = rows.map(function (r) { return Number((r.combined_health || 0).toFixed(2)); });
const okColor = token('--color-success') || '#16a34a';
const warnColor = token('--color-warning') || '#ca8a04';
const errColor = token('--color-error') || '#dc2626';
const fgColor = getCssVar('--fg') || '#e6edf3';
const dim = getCssVar('--fg-dim');
const bands = healthTrendBands(errColor, warnColor, okColor);
container.innerHTML =
'<div class="widget-toolbar"><button id="ht-toggle" class="wt-btn">' +
(view === 'overlay' ? 'Split view' : 'Overlay view') +
'</button></div><div id="ht-charts"></div>';
const toggle = document.getElementById('ht-toggle');
if (toggle) {
toggle.onclick = function () {
renderHealthTrend(rows, view === 'overlay' ? 'split' : 'overlay');
};
}
const host = document.getElementById('ht-charts');
const baseAxis = {
tooltip: { trigger: 'axis' },
grid: { left: 8, right: 8, top: 28, bottom: 28, containLabel: true },
xAxis: {
type: 'category',
data: dates,
boundaryGap: false,
axisLabel: { color: dim, rotate: 30, fontSize: 10 },
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLabel: { color: dim },
splitLine: { lineStyle: { color: getCssVar('--border') } },
},
};
if (view === 'overlay') {
setChartAriaLabel(container,
'Repo health timeline over ' + rows.length +
' sampled revisions: combined, architectural, and code health (0–100).');
const chart = mountEcharts(host);
chart.setOption(Object.assign({}, baseAxis, {
legend: {
data: ['Combined', 'Architectural', 'Code'],
textStyle: { color: dim },
bottom: 0,
},
series: [
{
name: 'Architectural',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
data: arch,
lineStyle: { color: okColor, width: 1.5, opacity: 0.7 },
itemStyle: { color: okColor },
},
{
name: 'Code',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
data: code,
lineStyle: { color: warnColor, width: 1.5, opacity: 0.7 },
itemStyle: { color: warnColor },
},
{
name: 'Combined',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: combined,
lineStyle: { color: fgColor, width: 3 },
itemStyle: { color: fgColor },
markArea: bands,
},
],
}));
return;
}
const panels = [
{ label: 'Combined', series: combined, color: fgColor },
{ label: 'Architectural', series: arch, color: okColor },
{ label: 'Code', series: code, color: warnColor },
];
host.innerHTML = panels
.map(function (p, i) { return '<div id="ht-sm-' + i + '" class="ht-sm"></div>'; })
.join('');
panels.forEach(function (p, i) {
const el = document.getElementById('ht-sm-' + i);
if (!el) return;
const isLast = i === panels.length - 1;
const c = mountEcharts(el);
c.setOption(Object.assign({}, baseAxis, {
title: {
text: p.label,
left: 8,
top: 4,
textStyle: { fontSize: 12, color: getCssVar('--fg') },
},
grid: {
left: 8,
right: 8,
top: 36,
bottom: isLast ? 24 : 8,
containLabel: true,
},
xAxis: {
type: 'category',
data: dates,
boundaryGap: false,
axisLabel: {
show: isLast,
color: dim,
rotate: 30,
fontSize: 10,
},
},
yAxis: {
type: 'value',
min: 0,
max: 100,
interval: 50,
axisLabel: { color: dim, fontSize: 10 },
splitLine: { lineStyle: { color: getCssVar('--border') } },
},
series: [
{
name: p.label,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
data: p.series,
lineStyle: { color: p.color, width: 2 },
itemStyle: { color: p.color },
markArea: bands,
},
],
}));
});
}
function classifyCells(structEdges, couplingAgg) {
const edgeClasses = {};
Object.keys(structEdges).forEach(function (k) {
const parts = k.split('\x00');
const canon = (parts[0] < parts[1]) ? k : (parts[1] + '\x00' + parts[0]);
const deg = couplingAgg[canon];
edgeClasses[k] = (deg === undefined)
? { cls: 'struct-only' }
: { cls: 'agree', degree: deg };
});
const extra = [];
Object.keys(couplingAgg).forEach(function (k) {
const parts = k.split('\x00'); const fwd = k;
const bwd = parts[1] + '\x00' + parts[0];
if (structEdges[fwd] !== undefined || structEdges[bwd] !== undefined) return;
extra.push({ a: parts[0], b: parts[1], degree: couplingAgg[k] });
});
return { edgeClasses: edgeClasses, extra: extra };
}
function archMatrixLegendHtml(fwdColor, violColor, backColor) {
function item(color, opacity, label) {
return '<span style="display:inline-flex;align-items:center;gap:4px;margin-right:12px;">' +
'<i style="display:inline-block;width:10px;height:10px;border-radius:2px;' +
'background:' + color + ';opacity:' + opacity + '"></i>' + label + '</span>';
}
return item(fwdColor, 0.85, 'agree — import + co-change') +
item(fwdColor, 0.35, 'structural only') +
item(violColor, 0.85, 'co-change only (modularity violation)') +
item(backColor, 0.95, 'back-edge (cycle)');
}
function renderArchMatrix(imports, roles, coupling) {
roles = roles || [];
coupling = coupling || [];
const outer = document.getElementById('widget-arch-matrix-body');
if (!outer) return;
if (!imports.length) {
outer.innerHTML = '<div class="empty">No resolved import edges to matrix yet (Rust + Python + JS/TS).</div>';
return;
}
const archLayout = (window.Alpine && window.Alpine.store)
? window.Alpine.store('layout') : null;
const userDepth = archLayout ? archLayout.archGraphDepth : 'auto';
var edges = {};
var nodes = {};
var chosenDepth = (typeof userDepth === 'number') ? userDepth : 6;
if (typeof userDepth === 'number') {
const r = aggregateImportsAt(imports, userDepth);
edges = r.edges;
nodes = r.nodes;
} else {
for (var d = 2; d <= 6; d++) {
const r = aggregateImportsAt(imports, d);
edges = r.edges;
nodes = r.nodes;
chosenDepth = d;
if (Object.keys(nodes).length >= 8) break;
}
}
const mods = Object.keys(nodes);
if (!mods.length) {
outer.innerHTML = '<div class="empty">All resolved imports stay intra-module — no inter-module matrix.</div>';
return;
}
const moduleLevel = {};
for (var ri = 0; ri < roles.length; ri++) {
const m = modulePath(roles[ri].path, chosenDepth);
if (!m || !nodes[m]) continue;
const lv = (typeof roles[ri].level === 'number') ? roles[ri].level : 1e9;
if (moduleLevel[m] === undefined || lv < moduleLevel[m]) moduleLevel[m] = lv;
}
const order = mods.slice().sort(function (a, b) {
const la = (moduleLevel[a] === undefined) ? 1e9 : moduleLevel[a];
const lb = (moduleLevel[b] === undefined) ? 1e9 : moduleLevel[b];
return la - lb || (a < b ? -1 : (a > b ? 1 : 0));
});
const idxOf = {};
order.forEach(function (m, i) { idxOf[m] = i; });
const n = order.length;
const labels = order.map(function (m) {
return m.length > 24 ? '…' + m.slice(-23) : m;
});
const fwdColor = token('--color-info') || '#2563eb';
const backColor = token('--color-error') || '#dc2626';
const violColor = token('--color-warning') || '#d97706';
var maxCount = 1;
Object.keys(edges).forEach(function (k) { if (edges[k] > maxCount) maxCount = edges[k]; });
const mode = (archLayout && archLayout.archMatrixMode === 'fusion') ? 'fusion' : 'structure';
const couplingAgg = aggregateCouplingAt(coupling, chosenDepth);
const hasCoupling = Object.keys(couplingAgg).length > 0;
const effectiveMode = (mode === 'fusion' && !hasCoupling) ? 'structure' : mode;
outer.innerHTML =
'<div class="widget-toolbar"><button id="wam-mode-toggle" class="wt-btn">' +
(mode === 'fusion' ? 'Structure' : 'Fusion') + '</button></div>' +
(mode === 'fusion' && !hasCoupling
? '<div style="font-size:11px;color:' + getCssVar('--fg-dim') + ';margin-bottom:6px;">No co-change data — showing structure only</div>'
: '') +
'<div id="wam-legend" style="font-size:11px;color:' + getCssVar('--fg-dim') + ';margin-bottom:6px;' +
(effectiveMode === 'fusion' ? '' : 'display:none;') + '"></div>' +
'<div id="wam-chart-host"></div>';
const toggleBtn = document.getElementById('wam-mode-toggle');
if (toggleBtn) {
toggleBtn.onclick = function () {
if (archLayout) archLayout.archMatrixMode = (mode === 'fusion') ? 'structure' : 'fusion';
renderArchMatrix(imports, roles, coupling);
};
}
const legendHost = document.getElementById('wam-legend');
if (legendHost && effectiveMode === 'fusion') {
legendHost.innerHTML = archMatrixLegendHtml(fwdColor, violColor, backColor);
}
const container = document.getElementById('wam-chart-host');
if (!container) return;
const cellMeta = {};
const cells = [];
var backEdges = 0;
if (effectiveMode !== 'fusion') {
Object.keys(edges).forEach(function (k) {
const parts = k.split('\x00');
const r = idxOf[parts[0]]; const c = idxOf[parts[1]]; if (r === undefined || c === undefined) return;
const count = edges[k];
const isBack = r > c; if (isBack) backEdges += 1;
cells.push({
value: [c, r, count],
itemStyle: {
color: isBack ? backColor : fwdColor,
opacity: isBack ? 0.95 : (0.5 + 0.45 * (count / maxCount)),
},
});
});
} else {
var maxCouplingDegree = 1;
Object.keys(couplingAgg).forEach(function (k) {
if (couplingAgg[k] > maxCouplingDegree) maxCouplingDegree = couplingAgg[k];
});
const classified = classifyCells(edges, couplingAgg);
Object.keys(edges).forEach(function (k) {
const parts = k.split('\x00');
const r = idxOf[parts[0]];
const c = idxOf[parts[1]];
if (r === undefined || c === undefined) return;
const count = edges[k];
const isBack = r > c;
if (isBack) {
backEdges += 1;
cells.push({ value: [c, r, count], itemStyle: { color: backColor, opacity: 0.95 } });
cellMeta[c + '\x00' + r] = { cls: 'back-edge', count: count };
return;
}
const info = classified.edgeClasses[k] || { cls: 'struct-only' };
const opacity = (info.cls === 'agree')
? (0.45 + 0.5 * ((info.degree || 0) / maxCouplingDegree))
: 0.35;
cells.push({ value: [c, r, count], itemStyle: { color: fwdColor, opacity: opacity } });
cellMeta[c + '\x00' + r] = { cls: info.cls, count: count, degree: info.degree };
});
classified.extra.forEach(function (ex) {
const ia = idxOf[ex.a];
const ib = idxOf[ex.b];
if (ia === undefined || ib === undefined) return;
const r = Math.min(ia, ib);
const c = Math.max(ia, ib);
cells.push({ value: [c, r, -2], itemStyle: { color: violColor, opacity: 0.85 } });
cellMeta[c + '\x00' + r] = { cls: 'temporal-only', count: 0, degree: ex.degree };
});
}
for (var di = 0; di < n; di++) {
cells.push({
value: [di, di, -1],
itemStyle: { color: getCssVar('--fg-dim') || '#888', opacity: 0.22 },
});
}
setChartAriaLabel(container,
'Dependency structure matrix, ' + n + ' modules ordered by architectural layer, ' +
Object.keys(edges).length + ' dependency cells, ' + backEdges +
' below-diagonal back-edges (dependency cycles / layering violations) in red' +
(effectiveMode === 'fusion' ? '. Fusion mode: cells classified by structure×history agreement.' : '.'));
const cell = Math.max(11, Math.min(26, Math.round(620 / Math.max(n, 1))));
const padTop = 128; const padLeft = 156; const span = n * cell;
const cw = container.clientWidth || 900;
const gridLeft = Math.max(padLeft, Math.round((cw - span) / 2));
container.style.height = (padTop + span + 10) + 'px';
outer.style.height = 'auto';
const chart = mountEcharts(container);
chart.setOption({
tooltip: {
position: 'top',
formatter: function (p) {
const c = p.value[0];
const r = p.value[1];
const v = p.value[2];
if (r === c) return escapeHtml(order[r]) + '<br/><span style="opacity:.7">diagonal (self)</span>';
if (effectiveMode !== 'fusion') {
return escapeHtml(order[r]) + ' → ' + escapeHtml(order[c]) +
'<br/>' + v + ' import' + (v === 1 ? '' : 's') +
(r > c ? '<br/><strong>back-edge — dependency cycle / layering violation</strong>' : '');
}
const meta = cellMeta[c + '\x00' + r] || { cls: 'struct-only', count: v };
const label = {
'agree': 'agree — import + co-change',
'struct-only': 'structural only',
'temporal-only': 'co-change only — modularity violation',
'back-edge': 'back-edge — dependency cycle / layering violation',
}[meta.cls] || 'structural only';
const importsTxt = 'imports: ' + (meta.count || 0);
const coTxt = (typeof meta.degree === 'number')
? ('co-change degree: ' + meta.degree.toFixed(1) + '%')
: 'co-change degree: n/a';
return escapeHtml(order[r]) + ' → ' + escapeHtml(order[c]) +
' — ' + label + '<br/>' + importsTxt + ', ' + coTxt;
},
},
grid: { left: gridLeft, top: padTop, width: span, height: span, containLabel: false },
xAxis: {
type: 'category', data: labels, position: 'top',
axisTick: { show: false },
axisLabel: { rotate: 55, fontSize: 9, color: getCssVar('--fg-dim'), margin: 8 },
splitArea: { show: true, areaStyle: { color: ['transparent', 'rgba(128,128,128,0.05)'] } },
},
yAxis: {
type: 'category', data: labels, inverse: true,
axisTick: { show: false },
axisLabel: { fontSize: 9, color: getCssVar('--fg-dim'), margin: 8 },
splitArea: { show: false },
},
series: [{
type: 'heatmap',
data: cells,
label: { show: false },
itemStyle: { borderColor: getCssVar('--bg'), borderWidth: 0.5 },
emphasis: { itemStyle: { borderColor: getCssVar('--fg'), borderWidth: 1 } },
}],
});
window._codeloreRegisterSelectionListener('dsm', function (selectedPath) {
chart.dispatchAction({ type: 'downplay' });
if (!selectedPath) return;
const mod = modulePath(selectedPath, chosenDepth);
const idx = idxOf[mod];
if (idx === undefined) return;
const indices = [];
for (var k = 0; k < cells.length; k++) {
if (cells[k].value[0] === idx || cells[k].value[1] === idx) indices.push(k);
}
chart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: indices });
});
}