Refactor L0 quality and diffusion graph gating for balanced recall

This commit is contained in:
2026-02-14 17:12:03 +08:00
parent 15640d48f2
commit e8383570e0
3 changed files with 169 additions and 46 deletions

View File

@@ -63,6 +63,8 @@ const SYSTEM_PROMPT = `你是场景摘要器。从一轮对话中提取1-2个场
- 只从正文内容中识别角色名,不要把标签名(如 user、assistant当作角色 - 只从正文内容中识别角色名,不要把标签名(如 user、assistant当作角色
- r 使用动作模板短语:“动作+对象/结果”(例:“提出交易条件”、“拒绝对方请求”、“当众揭露秘密”、“安抚对方情绪”) - r 使用动作模板短语:“动作+对象/结果”(例:“提出交易条件”、“拒绝对方请求”、“当众揭露秘密”、“安抚对方情绪”)
- r 不要写人名,不要复述整句,不要写心理描写或评价词 - r 不要写人名,不要复述整句,不要写心理描写或评价词
- r 正例(合格):提出交易条件、拒绝对方请求、当众揭露秘密、安抚对方情绪、强行打断发言、转移谈话焦点
- r 反例不合格我觉得她现在很害怕、他突然非常生气地大喊起来、user开始说话、assistant解释了很多细节
- 每个锚点 1-3 条 - 每个锚点 1-3 条
## where ## where
@@ -87,6 +89,46 @@ const JSON_PREFILL = '{"anchors":[';
const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const ACTION_STRIP_WORDS = [
'突然', '非常', '有些', '有点', '轻轻', '悄悄', '缓缓', '立刻',
'马上', '然后', '并且', '而且', '开始', '继续', '再次', '正在',
];
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function sanitizeActionPhrase(raw) {
let text = String(raw || '')
.normalize('NFKC')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.trim();
if (!text) return '';
text = text
.replace(/[,。!?、;:,.!?;:"'“”‘’()[\]{}<>《》]/g, '')
.replace(/\s+/g, '');
for (const word of ACTION_STRIP_WORDS) {
text = text.replaceAll(word, '');
}
text = text.replace(/(地|得|了|着|过)+$/g, '');
if (text.length < 2) return '';
if (text.length > 12) text = text.slice(0, 12);
return text;
}
function calcAtomQuality(scene, edges, where) {
const sceneLen = String(scene || '').length;
const sceneScore = clamp(sceneLen / 80, 0, 1);
const edgeScore = clamp((edges?.length || 0) / 3, 0, 1);
const whereScore = where ? 1 : 0;
const quality = 0.55 * sceneScore + 0.35 * edgeScore + 0.10 * whereScore;
return Number(quality.toFixed(3));
}
// ============================================================================ // ============================================================================
// 清洗与构建 // 清洗与构建
// ============================================================================ // ============================================================================
@@ -103,7 +145,7 @@ function sanitizeEdges(raw) {
.map(e => ({ .map(e => ({
s: String(e.s || '').trim(), s: String(e.s || '').trim(),
t: String(e.t || '').trim(), t: String(e.t || '').trim(),
r: String(e.r || '').trim().slice(0, 30), r: sanitizeActionPhrase(e.r),
})) }))
.filter(e => e.s && e.t && e.r) .filter(e => e.s && e.t && e.r)
.slice(0, 3); .slice(0, 3);
@@ -127,6 +169,7 @@ function anchorToAtom(anchor, aiFloor, idx) {
if (scene.length < 15) return null; if (scene.length < 15) return null;
const edges = sanitizeEdges(anchor.edges); const edges = sanitizeEdges(anchor.edges);
const where = String(anchor.where || '').trim(); const where = String(anchor.where || '').trim();
const quality = calcAtomQuality(scene, edges, where);
return { return {
atomId: `atom-${aiFloor}-${idx}`, atomId: `atom-${aiFloor}-${idx}`,
@@ -139,6 +182,7 @@ function anchorToAtom(anchor, aiFloor, idx) {
// ═══ 图结构层(扩散的 key ═══ // ═══ 图结构层(扩散的 key ═══
edges, edges,
where, where,
quality,
}; };
} }

View File

@@ -47,20 +47,23 @@ const CONFIG = {
MAX_ITER: 50, // hard iteration cap (typically converges in 15-25) MAX_ITER: 50, // hard iteration cap (typically converges in 15-25)
// Edge weight channel coefficients // Edge weight channel coefficients
// No standalone WHO channel: rely on interaction/action/location only. // Candidate generation uses WHAT/HOW only.
// WHO/WHERE are reweight-only signals.
GAMMA: { GAMMA: {
what: 0.55, // interaction pair overlap — Szymkiewicz-Simpson what: 0.45, // interaction pair overlap — Szymkiewicz-Simpson
where: 0.15, // location exact match — binary
how: 0.30, // action-term co-occurrence — Jaccard how: 0.30, // action-term co-occurrence — Jaccard
who: 0.15, // endpoint entity overlap — Jaccard (reweight-only)
where: 0.10, // location exact match — damped (reweight-only)
}, },
WHERE_MAX_GROUP_SIZE: 16, // skip location-only pair expansion for over-common places WHERE_MAX_GROUP_SIZE: 16, // skip location-only pair expansion for over-common places
WHERE_FREQ_DAMP_PIVOT: 6, // location freq <= pivot keeps full WHERE score WHERE_FREQ_DAMP_PIVOT: 6, // location freq <= pivot keeps full WHERE score
WHERE_FREQ_DAMP_MIN: 0.20, // lower bound for damped WHERE contribution WHERE_FREQ_DAMP_MIN: 0.20, // lower bound for damped WHERE contribution
HOW_MAX_GROUP_SIZE: 24, // skip ultra-common action terms to avoid dense pair explosion
// Post-verification (Cosine Gate) // Post-verification (Cosine Gate)
COSINE_GATE: 0.45, // min cosine(queryVector, stateVector) COSINE_GATE: 0.48, // min cosine(queryVector, stateVector)
SCORE_FLOOR: 0.10, // min finalScore = PPR_normalized × cosine SCORE_FLOOR: 0.12, // min finalScore = PPR_normalized × cosine
DIFFUSION_CAP: 100, // max diffused nodes (excluding seeds) DIFFUSION_CAP: 80, // max diffused nodes (excluding seeds)
}; };
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -226,25 +229,27 @@ function extractAllFeatures(allAtoms, excludeEntities = new Set()) {
/** /**
* Build inverted index: value → list of atom indices * Build inverted index: value → list of atom indices
* @param {object[]} features * @param {object[]} features
* @returns {{ entityIndex: Map, locationIndex: Map }} * @returns {{ whatIndex: Map, howIndex: Map, locationFreq: Map }}
*/ */
function buildInvertedIndices(features) { function buildInvertedIndices(features) {
const entityIndex = new Map(); const whatIndex = new Map();
const locationIndex = new Map(); const howIndex = new Map();
const locationFreq = new Map();
for (let i = 0; i < features.length; i++) { for (let i = 0; i < features.length; i++) {
for (const e of features[i].entities) { for (const pair of features[i].interactionPairs) {
if (!entityIndex.has(e)) entityIndex.set(e, []); if (!whatIndex.has(pair)) whatIndex.set(pair, []);
entityIndex.get(e).push(i); whatIndex.get(pair).push(i);
}
for (const action of features[i].actionTerms) {
if (!howIndex.has(action)) howIndex.set(action, []);
howIndex.get(action).push(i);
} }
const loc = features[i].location; const loc = features[i].location;
if (loc) { if (loc) locationFreq.set(loc, (locationFreq.get(loc) || 0) + 1);
if (!locationIndex.has(loc)) locationIndex.set(loc, []);
locationIndex.get(loc).push(i);
}
} }
return { entityIndex, locationIndex }; return { whatIndex, howIndex, locationFreq };
} }
/** /**
@@ -277,30 +282,32 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
const T0 = performance.now(); const T0 = performance.now();
const features = extractAllFeatures(allAtoms, excludeEntities); const features = extractAllFeatures(allAtoms, excludeEntities);
const { entityIndex, locationIndex } = buildInvertedIndices(features); const { whatIndex, howIndex, locationFreq } = buildInvertedIndices(features);
const locationFreq = new Map();
for (const [loc, indices] of locationIndex.entries()) {
locationFreq.set(loc, indices.length);
}
// Candidate pairs: share ≥1 entity or same location // Candidate pairs: only WHAT/HOW can create edges
const pairSetByWhat = new Set();
const pairSetByHow = new Set();
const pairSet = new Set(); const pairSet = new Set();
collectPairsFromIndex(entityIndex, pairSet, N); collectPairsFromIndex(whatIndex, pairSetByWhat, N);
let skippedLocationGroups = 0; let skippedHowGroups = 0;
for (const [loc, indices] of locationIndex.entries()) { for (const [term, indices] of howIndex.entries()) {
if (!loc) continue; if (!term) continue;
if (indices.length > CONFIG.WHERE_MAX_GROUP_SIZE) { if (indices.length > CONFIG.HOW_MAX_GROUP_SIZE) {
skippedLocationGroups++; skippedHowGroups++;
continue; continue;
} }
const oneLocMap = new Map([[loc, indices]]); const oneHowMap = new Map([[term, indices]]);
collectPairsFromIndex(oneLocMap, pairSet, N); collectPairsFromIndex(oneHowMap, pairSetByHow, N);
} }
for (const p of pairSetByWhat) pairSet.add(p);
for (const p of pairSetByHow) pairSet.add(p);
// Compute three-channel edge weights for all candidates // Compute edge weights for all candidates
const neighbors = Array.from({ length: N }, () => []); const neighbors = Array.from({ length: N }, () => []);
let edgeCount = 0; let edgeCount = 0;
const channelStats = { what: 0, where: 0, how: 0 }; const channelStats = { what: 0, where: 0, how: 0, who: 0 };
let reweightWhoUsed = 0;
let reweightWhereUsed = 0;
for (const packed of pairSet) { for (const packed of pairSet) {
const i = Math.floor(packed / N); const i = Math.floor(packed / N);
@@ -310,6 +317,8 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
const fj = features[j]; const fj = features[j];
const wWhat = overlapCoefficient(fi.interactionPairs, fj.interactionPairs); const wWhat = overlapCoefficient(fi.interactionPairs, fj.interactionPairs);
const wHow = jaccard(fi.actionTerms, fj.actionTerms);
const wWho = jaccard(fi.entities, fj.entities);
let wWhere = 0.0; let wWhere = 0.0;
if (fi.location && fi.location === fj.location) { if (fi.location && fi.location === fj.location) {
const freq = locationFreq.get(fi.location) || 1; const freq = locationFreq.get(fi.location) || 1;
@@ -319,12 +328,12 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
); );
wWhere = damp; wWhere = damp;
} }
const wHow = jaccard(fi.actionTerms, fj.actionTerms);
const weight = const weight =
CONFIG.GAMMA.what * wWhat + CONFIG.GAMMA.what * wWhat +
CONFIG.GAMMA.where * wWhere + CONFIG.GAMMA.how * wHow +
CONFIG.GAMMA.how * wHow; CONFIG.GAMMA.who * wWho +
CONFIG.GAMMA.where * wWhere;
if (weight > 0) { if (weight > 0) {
neighbors[i].push({ target: j, weight }); neighbors[i].push({ target: j, weight });
@@ -332,8 +341,11 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
edgeCount++; edgeCount++;
if (wWhat > 0) channelStats.what++; if (wWhat > 0) channelStats.what++;
if (wWhere > 0) channelStats.where++;
if (wHow > 0) channelStats.how++; if (wHow > 0) channelStats.how++;
if (wWho > 0) channelStats.who++;
if (wWhere > 0) channelStats.where++;
if (wWho > 0) reweightWhoUsed++;
if (wWhere > 0) reweightWhereUsed++;
} }
} }
@@ -341,12 +353,28 @@ function buildGraph(allAtoms, excludeEntities = new Set()) {
xbLog.info(MODULE_ID, xbLog.info(MODULE_ID,
`Graph: ${N} nodes, ${edgeCount} edges ` + `Graph: ${N} nodes, ${edgeCount} edges ` +
`(what=${channelStats.what} where=${channelStats.where} how=${channelStats.how}) ` + `(candidate_by_what=${pairSetByWhat.size} candidate_by_how=${pairSetByHow.size}) ` +
`(whereSkippedGroups=${skippedLocationGroups}) ` + `(what=${channelStats.what} how=${channelStats.how} who=${channelStats.who} where=${channelStats.where}) ` +
`(reweight_who_used=${reweightWhoUsed} reweight_where_used=${reweightWhereUsed}) ` +
`(howSkippedGroups=${skippedHowGroups}) ` +
`(${buildTime}ms)` `(${buildTime}ms)`
); );
return { neighbors, edgeCount, channelStats, buildTime }; const totalPairs = N > 1 ? (N * (N - 1)) / 2 : 0;
const edgeDensity = totalPairs > 0 ? Number((edgeCount / totalPairs * 100).toFixed(2)) : 0;
return {
neighbors,
edgeCount,
channelStats,
buildTime,
candidatePairs: pairSet.size,
pairsFromWhat: pairSetByWhat.size,
pairsFromHow: pairSetByHow.size,
reweightWhoUsed,
reweightWhereUsed,
edgeDensity,
};
} }
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -673,6 +701,12 @@ export function diffuseFromSeeds(seeds, allAtoms, stateVectors, queryVector, met
graphNodes: N, graphNodes: N,
graphEdges: 0, graphEdges: 0,
channelStats: graph.channelStats, channelStats: graph.channelStats,
candidatePairs: graph.candidatePairs,
pairsFromWhat: graph.pairsFromWhat,
pairsFromHow: graph.pairsFromHow,
edgeDensity: graph.edgeDensity,
reweightWhoUsed: graph.reweightWhoUsed,
reweightWhereUsed: graph.reweightWhereUsed,
time: graph.buildTime, time: graph.buildTime,
}); });
xbLog.info(MODULE_ID, 'No graph edges — skipping diffusion'); xbLog.info(MODULE_ID, 'No graph edges — skipping diffusion');
@@ -719,6 +753,12 @@ export function diffuseFromSeeds(seeds, allAtoms, stateVectors, queryVector, met
graphNodes: N, graphNodes: N,
graphEdges: graph.edgeCount, graphEdges: graph.edgeCount,
channelStats: graph.channelStats, channelStats: graph.channelStats,
candidatePairs: graph.candidatePairs,
pairsFromWhat: graph.pairsFromWhat,
pairsFromHow: graph.pairsFromHow,
edgeDensity: graph.edgeDensity,
reweightWhoUsed: graph.reweightWhoUsed,
reweightWhereUsed: graph.reweightWhereUsed,
buildTime: graph.buildTime, buildTime: graph.buildTime,
iterations, iterations,
convergenceError: finalError, convergenceError: finalError,
@@ -726,6 +766,9 @@ export function diffuseFromSeeds(seeds, allAtoms, stateVectors, queryVector, met
cosineGatePassed: gateStats.passed, cosineGatePassed: gateStats.passed,
cosineGateFiltered: gateStats.filtered, cosineGateFiltered: gateStats.filtered,
cosineGateNoVector: gateStats.noVector, cosineGateNoVector: gateStats.noVector,
postGatePassRate: pprActivated > 0
? Math.round((gateStats.passed / pprActivated) * 100)
: 0,
finalCount: diffused.length, finalCount: diffused.length,
scoreDistribution: diffused.length > 0 scoreDistribution: diffused.length > 0
? calcScoreStats(diffused.map(d => d.finalScore)) ? calcScoreStats(diffused.map(d => d.finalScore))
@@ -783,7 +826,14 @@ function fillMetricsEmpty(metrics) {
cosineGateNoVector: 0, cosineGateNoVector: 0,
finalCount: 0, finalCount: 0,
scoreDistribution: { min: 0, max: 0, mean: 0 }, scoreDistribution: { min: 0, max: 0, mean: 0 },
byChannel: { what: 0, where: 0, how: 0 }, byChannel: { what: 0, where: 0, how: 0, who: 0 },
candidatePairs: 0,
pairsFromWhat: 0,
pairsFromHow: 0,
edgeDensity: 0,
reweightWhoUsed: 0,
reweightWhereUsed: 0,
postGatePassRate: 0,
time: 0, time: 0,
}; };
} }
@@ -803,9 +853,16 @@ function fillMetrics(metrics, data) {
cosineGatePassed: data.cosineGatePassed || 0, cosineGatePassed: data.cosineGatePassed || 0,
cosineGateFiltered: data.cosineGateFiltered || 0, cosineGateFiltered: data.cosineGateFiltered || 0,
cosineGateNoVector: data.cosineGateNoVector || 0, cosineGateNoVector: data.cosineGateNoVector || 0,
postGatePassRate: data.postGatePassRate || 0,
finalCount: data.finalCount || 0, finalCount: data.finalCount || 0,
scoreDistribution: data.scoreDistribution || { min: 0, max: 0, mean: 0 }, scoreDistribution: data.scoreDistribution || { min: 0, max: 0, mean: 0 },
byChannel: data.channelStats || { what: 0, where: 0, how: 0 }, byChannel: data.channelStats || { what: 0, where: 0, how: 0, who: 0 },
candidatePairs: data.candidatePairs || 0,
pairsFromWhat: data.pairsFromWhat || 0,
pairsFromHow: data.pairsFromHow || 0,
edgeDensity: data.edgeDensity || 0,
reweightWhoUsed: data.reweightWhoUsed || 0,
reweightWhereUsed: data.reweightWhereUsed || 0,
time: data.time || 0, time: data.time || 0,
}; };
} }

View File

@@ -118,15 +118,22 @@ export function createMetrics() {
seedCount: 0, seedCount: 0,
graphNodes: 0, graphNodes: 0,
graphEdges: 0, graphEdges: 0,
candidatePairs: 0,
pairsFromWhat: 0,
pairsFromHow: 0,
edgeDensity: 0,
reweightWhoUsed: 0,
reweightWhereUsed: 0,
iterations: 0, iterations: 0,
convergenceError: 0, convergenceError: 0,
pprActivated: 0, pprActivated: 0,
cosineGatePassed: 0, cosineGatePassed: 0,
cosineGateFiltered: 0, cosineGateFiltered: 0,
cosineGateNoVector: 0, cosineGateNoVector: 0,
postGatePassRate: 0,
finalCount: 0, finalCount: 0,
scoreDistribution: { min: 0, max: 0, mean: 0 }, scoreDistribution: { min: 0, max: 0, mean: 0 },
byChannel: { what: 0, where: 0, how: 0 }, byChannel: { what: 0, where: 0, how: 0, who: 0 },
time: 0, time: 0,
}, },
@@ -169,6 +176,7 @@ export function createMetrics() {
eventPrecisionProxy: 0, eventPrecisionProxy: 0,
l1AttachRate: 0, l1AttachRate: 0,
rerankRetentionRate: 0, rerankRetentionRate: 0,
diffusionEffectiveRate: 0,
potentialIssues: [], potentialIssues: [],
}, },
}; };
@@ -368,9 +376,12 @@ export function formatMetricsLog(metrics) {
lines.push('[Diffusion] PPR Spreading Activation'); lines.push('[Diffusion] PPR Spreading Activation');
lines.push(`├─ seeds: ${m.diffusion.seedCount}`); lines.push(`├─ seeds: ${m.diffusion.seedCount}`);
lines.push(`├─ graph: ${m.diffusion.graphNodes} nodes, ${m.diffusion.graphEdges} edges`); lines.push(`├─ graph: ${m.diffusion.graphNodes} nodes, ${m.diffusion.graphEdges} edges`);
lines.push(`├─ candidate_pairs: ${m.diffusion.candidatePairs || 0} (what=${m.diffusion.pairsFromWhat || 0}, how=${m.diffusion.pairsFromHow || 0})`);
lines.push(`├─ edge_density: ${m.diffusion.edgeDensity || 0}%`);
if (m.diffusion.graphEdges > 0) { if (m.diffusion.graphEdges > 0) {
const ch = m.diffusion.byChannel || {}; const ch = m.diffusion.byChannel || {};
lines.push(`─ by_channel: what=${ch.what || 0}, where=${ch.where || 0}, how=${ch.how || 0}`); lines.push(`─ by_channel: what=${ch.what || 0}, how=${ch.how || 0}, who=${ch.who || 0}, where=${ch.where || 0}`);
lines.push(`│ └─ reweight_used: who=${m.diffusion.reweightWhoUsed || 0}, where=${m.diffusion.reweightWhereUsed || 0}`);
} }
if (m.diffusion.iterations > 0) { if (m.diffusion.iterations > 0) {
lines.push(`├─ ppr: ${m.diffusion.iterations} iterations, ε=${Number(m.diffusion.convergenceError).toExponential(1)}`); lines.push(`├─ ppr: ${m.diffusion.iterations} iterations, ε=${Number(m.diffusion.convergenceError).toExponential(1)}`);
@@ -378,8 +389,10 @@ export function formatMetricsLog(metrics) {
lines.push(`├─ activated (excl seeds): ${m.diffusion.pprActivated}`); lines.push(`├─ activated (excl seeds): ${m.diffusion.pprActivated}`);
if (m.diffusion.pprActivated > 0) { if (m.diffusion.pprActivated > 0) {
lines.push(`├─ cosine_gate: ${m.diffusion.cosineGatePassed} passed, ${m.diffusion.cosineGateFiltered} filtered`); lines.push(`├─ cosine_gate: ${m.diffusion.cosineGatePassed} passed, ${m.diffusion.cosineGateFiltered} filtered`);
const passPrefix = m.diffusion.cosineGateNoVector > 0 ? '│ ├─' : '│ └─';
lines.push(`${passPrefix} pass_rate: ${m.diffusion.postGatePassRate || 0}%`);
if (m.diffusion.cosineGateNoVector > 0) { if (m.diffusion.cosineGateNoVector > 0) {
lines.push(`─ no_vector: ${m.diffusion.cosineGateNoVector}`); lines.push(`─ no_vector: ${m.diffusion.cosineGateNoVector}`);
} }
} }
lines.push(`├─ final_injected: ${m.diffusion.finalCount}`); lines.push(`├─ final_injected: ${m.diffusion.finalCount}`);
@@ -435,6 +448,7 @@ export function formatMetricsLog(metrics) {
lines.push(`├─ event_precision_proxy: ${m.quality.eventPrecisionProxy}`); lines.push(`├─ event_precision_proxy: ${m.quality.eventPrecisionProxy}`);
lines.push(`├─ l1_attach_rate: ${m.quality.l1AttachRate}%`); lines.push(`├─ l1_attach_rate: ${m.quality.l1AttachRate}%`);
lines.push(`├─ rerank_retention_rate: ${m.quality.rerankRetentionRate}%`); lines.push(`├─ rerank_retention_rate: ${m.quality.rerankRetentionRate}%`);
lines.push(`├─ diffusion_effective_rate: ${m.quality.diffusionEffectiveRate}%`);
if (m.quality.potentialIssues && m.quality.potentialIssues.length > 0) { if (m.quality.potentialIssues && m.quality.potentialIssues.length > 0) {
lines.push(`└─ potential_issues:`); lines.push(`└─ potential_issues:`);
@@ -642,6 +656,10 @@ export function detectIssues(metrics) {
issues.push('All PPR-activated nodes failed cosine gate - graph structure diverged from query semantics'); issues.push('All PPR-activated nodes failed cosine gate - graph structure diverged from query semantics');
} }
m.quality.diffusionEffectiveRate = m.diffusion.pprActivated > 0
? Math.round((m.diffusion.finalCount / m.diffusion.pprActivated) * 100)
: 0;
if (m.diffusion.cosineGateNoVector > 5) { if (m.diffusion.cosineGateNoVector > 5) {
issues.push(`${m.diffusion.cosineGateNoVector} PPR nodes missing vectors - L0 vectorization may be incomplete`); issues.push(`${m.diffusion.cosineGateNoVector} PPR nodes missing vectors - L0 vectorization may be incomplete`);
} }
@@ -650,5 +668,9 @@ export function detectIssues(metrics) {
issues.push(`Slow diffusion (${m.diffusion.time}ms) - graph may be too dense`); issues.push(`Slow diffusion (${m.diffusion.time}ms) - graph may be too dense`);
} }
if (m.diffusion.pprActivated > 0 && (m.diffusion.postGatePassRate < 20 || m.diffusion.postGatePassRate > 60)) {
issues.push(`Diffusion post-gate pass rate out of target (${m.diffusion.postGatePassRate}%)`);
}
return issues; return issues;
} }