fix(web-probe): flag mdtodo hwpod and pane gap regressions

This commit is contained in:
Codex
2026-06-27 05:27:32 +00:00
parent d6b3079208
commit 8fd0fe0226
3 changed files with 205 additions and 5 deletions
@@ -19,17 +19,25 @@ sentinel:
promptSetRef: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet
reportViewRef: config/hwlab-web-probe-sentinel/report-views.yaml#sentinel.reportViews
commandSequence:
- type: goto
path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_0db4dc4e46adf188/tasks/R1
- type: screenshot
label: mdtodo-few-task-gap
- type: gotoProjectMdtodo
- type: selectMdtodoFile
filename: 20260609_频率判断_用户反馈.md
- type: screenshot
label: mdtodo-mobile-selected
- type: openMdtodoReportPreview
task: R1
link: R1
- type: selectMdtodoTask
task: R14
- type: screenshot
label: mdtodo-report-preview
label: mdtodo-r14-selected
- type: openMdtodoReportPreview
task: R14
link: R14
- type: screenshot
label: mdtodo-r14-report-preview
- type: toggleMdtodoReportFullscreen
text: toggle
- type: screenshot
label: mdtodo-report-fullscreen
label: mdtodo-r14-report-fullscreen
@@ -808,6 +808,16 @@ function compactProjectManagementSample(value) {
launchButtonText: value.launchButtonText ?? null,
blockerCount: value.blockerCount ?? 0,
blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 6) : [],
paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({
name: item?.name ?? null,
visible: item?.visible === true,
widthPx: item?.widthPx ?? null,
heightPx: item?.heightPx ?? null,
bottomGapPx: item?.bottomGapPx ?? null,
bottomGapRatio: item?.bottomGapRatio ?? null,
contentNodeCount: item?.contentNodeCount ?? null,
valuesRedacted: true
})) : [],
workbenchLinkCount: value.workbenchLinkCount ?? 0,
valuesRedacted: true
};
@@ -1042,6 +1052,10 @@ function buildProjectManagementReport(samples, control, network, pagePerformance
const reportLinkSamples = projectSamples.filter((sample) => Number(sample.projectManagement?.reportLinkCount ?? 0) > 0);
const reportPreviewSamples = projectSamples.filter((sample) => sample.projectManagement?.reportPreviewVisible === true && Number(sample.projectManagement?.reportPreview?.textBytes ?? 0) > 0);
const reportFullscreenSamples = projectSamples.filter((sample) => sample.projectManagement?.reportFullscreenVisible === true);
const hwpodBlockerSamples = projectManagementHwpodBlockerRows(projectSamples);
const projectionReportSamples = projectManagementProjectionReportRows(projectSamples);
const hwpodApiFailures = projectManagementHwpodApiFailureRows(projectApiFailures);
const severePaneGapSamples = projectManagementPaneGapRows(projectSamples);
const previewCommands = commandRows.filter((item) => item.type === "openMdtodoReportPreview" || item.type === "toggleMdtodoReportFullscreen");
const launchNonEmpty = launchSuccess.filter((item) => item.chatObserved === true && (Number(item.workbenchMessageCount ?? 0) > 0 || Number(item.workbenchTraceRowCount ?? 0) > 0));
const launchEmpty = launchSuccess.filter((item) => item.chatObserved !== true || (Number(item.workbenchMessageCount ?? 0) === 0 && Number(item.workbenchTraceRowCount ?? 0) === 0));
@@ -1081,6 +1095,12 @@ function buildProjectManagementReport(samples, control, network, pagePerformance
maxReportLinkCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.reportLinkCount)),
reportPreviewVisibleSamples: reportPreviewSamples.length,
reportFullscreenVisibleSamples: reportFullscreenSamples.length,
hwpodBlockerSampleCount: hwpodBlockerSamples.length,
projectionReportSampleCount: projectionReportSamples.length,
hwpodApiFailureCount: hwpodApiFailures.length,
severePaneGapSampleCount: severePaneGapSamples.length,
maxPaneBottomGapPx: maxNumber(severePaneGapSamples.map((item) => item.maxBottomGapPx)),
maxPaneBottomGapRatio: maxNumber(severePaneGapSamples.map((item) => item.maxBottomGapRatio)),
launchButtonVisibleSamples: launchVisibleSamples.length,
launchButtonEnabledSamples: launchEnabledSamples.length,
launchButtonDisabledSamples: Math.max(0, launchVisibleSamples.length - launchEnabledSamples.length),
@@ -1128,6 +1148,7 @@ function buildProjectManagementReport(samples, control, network, pagePerformance
reportPreviewVisible: sample.projectManagement?.reportPreviewVisible === true,
reportPreviewBytes: sample.projectManagement?.reportPreview?.textBytes ?? 0,
reportFullscreenVisible: sample.projectManagement?.reportFullscreenVisible === true,
paneGaps: Array.isArray(sample.projectManagement?.paneGaps) ? sample.projectManagement.paneGaps.slice(0, 4) : [],
launchButtonVisible: sample.projectManagement?.launchButtonVisible === true,
launchButtonEnabled: sample.projectManagement?.launchButtonEnabled === true,
blockerCount: sample.projectManagement?.blockerCount ?? 0,
@@ -1140,6 +1161,10 @@ function buildProjectManagementReport(samples, control, network, pagePerformance
projectApiByPath,
projectApiFailures: projectApiFailures.slice(0, 40),
projectApiRequestFailed: projectApiFailedRequests.slice(0, 40),
hwpodBlockerSamples: hwpodBlockerSamples.slice(0, 40),
projectionReportSamples: projectionReportSamples.slice(0, 40),
hwpodApiFailures: hwpodApiFailures.slice(0, 40),
severePaneGapSamples: severePaneGapSamples.slice(0, 40),
projectApiPerformance,
slowProjectApiPerformance,
valuesRedacted: true
@@ -1204,12 +1229,29 @@ function compactProjectManagementForOutput(report) {
slowSamples: Array.isArray(item?.slowSamples) ? item.slowSamples.slice(0, 3).map(compactSlowSample) : [],
valuesRedacted: true
});
const compactSample = (item) => ({
seq: item?.seq ?? null,
ts: item?.ts ?? null,
pageRole: item?.pageRole ?? null,
path: item?.path ?? null,
selectedTaskRefHash: item?.selectedTaskRefHash ?? null,
selectedFileLabelPreview: item?.selectedFileLabelPreview ?? null,
severePaneCount: item?.severePaneCount ?? null,
maxBottomGapPx: item?.maxBottomGapPx ?? null,
maxBottomGapRatio: item?.maxBottomGapRatio ?? null,
paneGaps: Array.isArray(item?.paneGaps) ? item.paneGaps.slice(0, 4) : undefined,
valuesRedacted: true
});
return {
summary: report.summary ?? null,
samples: Array.isArray(report.samples) ? report.samples.slice(-8) : [],
commands: Array.isArray(report.commands) ? report.commands.slice(-8).map(compactCommand) : [],
launchCommands: Array.isArray(report.launchCommands) ? report.launchCommands.slice(-8).map(compactCommand) : [],
projectApiByPath: Array.isArray(report.projectApiByPath) ? report.projectApiByPath.slice(0, 8).map(compactApiGroup) : [],
hwpodBlockerSamples: Array.isArray(report.hwpodBlockerSamples) ? report.hwpodBlockerSamples.slice(0, 8).map(compactSample) : [],
projectionReportSamples: Array.isArray(report.projectionReportSamples) ? report.projectionReportSamples.slice(0, 8).map(compactSample) : [],
hwpodApiFailures: Array.isArray(report.hwpodApiFailures) ? report.hwpodApiFailures.slice(0, 8).map(compactApiGroup) : [],
severePaneGapSamples: Array.isArray(report.severePaneGapSamples) ? report.severePaneGapSamples.slice(0, 8).map(compactSample) : [],
projectApiPerformance: Array.isArray(report.projectApiPerformance) ? report.projectApiPerformance.slice(0, 8).map(compactPerformance) : [],
slowProjectApiPerformance: Array.isArray(report.slowProjectApiPerformance) ? report.slowProjectApiPerformance.slice(0, 8).map(compactPerformance) : [],
valuesRedacted: true
@@ -1290,6 +1332,110 @@ function projectManagementPerformanceRows(pagePerformance, config) {
.map((item) => ({ ...item, projectSlowBudgetMs: config?.slowApiBudgetMs ?? null }));
}
function projectManagementDigestText(value) {
if (!value || typeof value !== "object") return "";
return String(value.textPreview ?? value.preview ?? value.text ?? "").trim();
}
function projectManagementSampleRef(sample) {
return {
seq: sample?.seq ?? null,
ts: sample?.ts ?? null,
pageRole: sample?.pageRole ?? null,
path: sample?.path ?? null,
selectedTaskRefHash: sample?.projectManagement?.selectedTaskRef?.hash ?? null,
selectedFileLabelPreview: sample?.projectManagement?.selectedFileLabel?.textPreview ?? null,
valuesRedacted: true
};
}
function projectManagementHwpodBlockerRows(projectSamples) {
const pattern = /(?:no outbound WebSocket hwpod-node|HWLAB_HWPOD_NODE_OPS_URL|hwpod-node-ops contract)/iu;
const rows = [];
for (const sample of projectSamples || []) {
const blockers = Array.isArray(sample?.projectManagement?.blockers) ? sample.projectManagement.blockers : [];
const matched = blockers
.filter((item) => pattern.test(projectManagementDigestText(item)))
.map((item) => ({
index: item?.index ?? null,
testId: item?.testId ?? null,
role: item?.role ?? null,
textHash: item?.textHash ?? null,
textPreview: item?.textPreview ?? null,
valuesRedacted: true
}));
if (matched.length > 0) rows.push({ ...projectManagementSampleRef(sample), blockers: matched.slice(0, 4), valuesRedacted: true });
}
return rows;
}
function projectManagementProjectionReportRows(projectSamples) {
const pattern = /(?:报告索引待刷新|projection-only|任务投影确认存在报告链接)/iu;
return (projectSamples || [])
.filter((sample) => pattern.test(projectManagementDigestText(sample?.projectManagement?.reportPreview)))
.map((sample) => ({
...projectManagementSampleRef(sample),
reportPreviewHash: sample?.projectManagement?.reportPreview?.textHash ?? null,
reportPreviewPreview: sample?.projectManagement?.reportPreview?.textPreview ?? null,
reportPreviewBytes: sample?.projectManagement?.reportPreview?.textBytes ?? null,
valuesRedacted: true
}));
}
function projectManagementHwpodApiFailureRows(projectApiFailures) {
const pattern = /^\/v1\/project-management\/mdtodo\/(?:task-detail|report-preview)\b/u;
return (projectApiFailures || [])
.filter((item) => pattern.test(String(item?.path || "")) && Number(item?.status ?? 0) >= 500)
.map((item) => ({
ts: item?.ts ?? null,
method: item?.method ?? null,
path: item?.path ?? null,
status: item?.status ?? null,
type: item?.type ?? null,
failureKind: item?.failureKind ?? null,
valuesRedacted: true
}));
}
function projectManagementPaneGapRows(projectSamples) {
const rows = [];
for (const sample of projectSamples || []) {
const paneGaps = Array.isArray(sample?.projectManagement?.paneGaps) ? sample.projectManagement.paneGaps : [];
const severeGaps = paneGaps
.filter((item) => item?.visible === true)
.filter((item) => {
const bottomGapPx = Number(item?.bottomGapPx ?? 0);
const bottomGapRatio = Number(item?.bottomGapRatio ?? 0);
const heightPx = Number(item?.heightPx ?? 0);
return heightPx >= 120 && bottomGapPx >= 180 && bottomGapRatio >= 0.28;
})
.map((item) => ({
name: item?.name ?? null,
widthPx: item?.widthPx ?? null,
heightPx: item?.heightPx ?? null,
bottomGapPx: item?.bottomGapPx ?? null,
bottomGapRatio: item?.bottomGapRatio ?? null,
contentNodeCount: item?.contentNodeCount ?? null,
valuesRedacted: true
}));
if (severeGaps.length === 0) continue;
const maxGapPx = maxNumber(severeGaps.map((item) => item.bottomGapPx));
const maxGapRatio = maxNumber(severeGaps.map((item) => item.bottomGapRatio));
const multiPane = severeGaps.length >= 2;
const singleExtreme = maxGapPx >= 240 && maxGapRatio >= 0.45;
if (!multiPane && !singleExtreme) continue;
rows.push({
...projectManagementSampleRef(sample),
severePaneCount: severeGaps.length,
maxBottomGapPx: maxGapPx,
maxBottomGapRatio: maxGapRatio,
paneGaps: severeGaps.slice(0, 4),
valuesRedacted: true
});
}
return rows;
}
function buildProjectManagementFindings(report) {
if (!report?.enabled) return [];
const findings = [];
@@ -1312,6 +1458,12 @@ function buildProjectManagementFindings(report) {
if (Number(summary.selectedTaskSampleCount ?? 0) > 0 && Number(summary.selectedTaskBodyVisibleSamples ?? 0) === 0) {
findings.push({ id: "mdtodo-task-body-not-visible", severity: "red", summary: "selected MDTODO task was sampled but no rendered task body was visible", count: summary.selectedTaskSampleCount, samples: report.samples.filter((item) => item.selectedTaskRefHash).slice(-12), valuesRedacted: true });
}
if (Number(summary.hwpodBlockerSampleCount ?? 0) > 0) {
findings.push({ id: "mdtodo-hwpod-node-disconnected", severity: "red", summary: "MDTODO surfaced the hwpod-node disconnected / HWLAB_HWPOD_NODE_OPS_URL fallback blocker", count: summary.hwpodBlockerSampleCount, samples: report.hwpodBlockerSamples.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.projectionReportSampleCount ?? 0) > 0) {
findings.push({ id: "mdtodo-report-projection-only", severity: "red", summary: "MDTODO report preview is projection-only instead of opening the full markdown report from the HWPOD source", count: summary.projectionReportSampleCount, samples: report.projectionReportSamples.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.maxReportLinkCount ?? 0) > 0 && Number(summary.reportPreviewVisibleSamples ?? 0) === 0) {
const severity = Number(summary.reportPreviewCommandCount ?? 0) > 0 ? "red" : "amber";
findings.push({ id: "mdtodo-report-preview-missing", severity, summary: "MDTODO report links were visible but no markdown report preview was sampled", count: summary.maxReportLinkCount, previewCommandCount: summary.reportPreviewCommandCount, samples: report.samples.filter((item) => Number(item.reportLinkCount ?? 0) > 0).slice(-12), valuesRedacted: true });
@@ -1325,6 +1477,12 @@ function buildProjectManagementFindings(report) {
if (Number(summary.mdtodoTaskCountMin ?? 0) > 0 && Number(summary.mdtodoTaskCountMax ?? 0) > 0 && (Number(summary.mdtodoTaskCountMax) - Number(summary.mdtodoTaskCountMin) >= 10 || Number(summary.mdtodoTaskCountMax) / Math.max(1, Number(summary.mdtodoTaskCountMin)) >= 2)) {
findings.push({ id: "mdtodo-task-count-diverged", severity: "amber", summary: "MDTODO task count varied sharply during observation; compare control commands and observer samples for projection divergence", minTaskCount: summary.mdtodoTaskCountMin, maxTaskCount: summary.mdtodoTaskCountMax, samples: report.samples.slice(-20), valuesRedacted: true });
}
if (Number(summary.severePaneGapSampleCount ?? 0) > 0) {
findings.push({ id: "mdtodo-pane-bottom-gap", severity: "red", summary: "MDTODO task tree, main detail, or report sidebar left large unused bottom gaps when the visible task set was small", count: summary.severePaneGapSampleCount, maxBottomGapPx: summary.maxPaneBottomGapPx, maxBottomGapRatio: summary.maxPaneBottomGapRatio, samples: report.severePaneGapSamples.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.hwpodApiFailureCount ?? 0) > 0) {
findings.push({ id: "project-management-hwpod-api-failed", severity: "red", summary: "HWPOD-backed MDTODO task detail or report preview API returned a server error during natural page use", count: summary.hwpodApiFailureCount, failures: report.hwpodApiFailures.slice(0, 12), valuesRedacted: true });
}
if (Number(summary.projectApiFailureCount ?? 0) > 0 || Number(summary.projectApiRequestFailedCount ?? 0) > 0) {
findings.push({ id: "project-management-api-failed", severity: "amber", summary: "natural project-management or Workbench launch API requests failed during observation", count: Number(summary.projectApiFailureCount ?? 0) + Number(summary.projectApiRequestFailedCount ?? 0), groups: report.projectApiByPath.slice(0, 12), valuesRedacted: true });
}
@@ -3448,6 +3448,29 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
text: trim(element.textContent || "", 260),
})).filter((item) => item.text);
const workbenchLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-link-summary"] li, a[href*="/workbench/sessions/"]')).filter(visible);
const measurePaneGap = (name, paneSelector, contentSelector) => {
const pane = document.querySelector(paneSelector);
if (!visible(pane)) return { name, visible: false };
const rect = pane.getBoundingClientRect();
const contentNodes = Array.from(pane.querySelectorAll(contentSelector)).filter(visible);
const contentBottom = Math.max(rect.top, ...contentNodes.map((element) => element.getBoundingClientRect().bottom));
const bottomGapPx = Math.max(0, Math.round(rect.bottom - contentBottom));
const heightPx = Math.max(0, Math.round(rect.height));
return {
name,
visible: true,
widthPx: Math.max(0, Math.round(rect.width)),
heightPx,
bottomGapPx,
bottomGapRatio: heightPx > 0 ? Number((bottomGapPx / heightPx).toFixed(3)) : 0,
contentNodeCount: contentNodes.length,
};
};
const paneGaps = [
measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', '[data-task-ref], [role="treeitem"], [role="listitem"], li, button, input, select, .task-row-shell, .task-tools'),
measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .task-status-stack > *, .task-document-footer'),
measurePaneGap("report-sidebar", '[data-testid="mdtodo-report-sidebar"]', '[data-testid="mdtodo-report-preview"] > *, [data-testid="mdtodo-report-error"], [data-testid="mdtodo-report-fullscreen"], [data-testid="mdtodo-report-close"], .report-sidebar-header, .report-preview .markdown-body > *'),
];
return {
pageKind: mdtodoVisible || path.startsWith("/projects/mdtodo") ? "project-management-mdtodo" : rootVisible || path === "/projects" || path.startsWith("/projects/") ? "project-management-root" : "project-management-unknown",
configuredPath,
@@ -3480,6 +3503,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
launchButtonText: trim(launch?.textContent || "", 120),
blockerCount: blockers.length,
blockers,
paneGaps,
workbenchLinkCount: workbenchLinks.length,
valuesRedacted: true,
};
@@ -3706,6 +3730,16 @@ function digestProjectManagement(value) {
role: item?.role ?? null,
...textDigest(item?.text || "", 160),
})) : [],
paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({
name: item?.name ?? null,
visible: item?.visible === true,
widthPx: Number.isFinite(Number(item?.widthPx)) ? Number(item.widthPx) : null,
heightPx: Number.isFinite(Number(item?.heightPx)) ? Number(item.heightPx) : null,
bottomGapPx: Number.isFinite(Number(item?.bottomGapPx)) ? Number(item.bottomGapPx) : null,
bottomGapRatio: Number.isFinite(Number(item?.bottomGapRatio)) ? Number(item.bottomGapRatio) : null,
contentNodeCount: Number.isFinite(Number(item?.contentNodeCount)) ? Number(item.contentNodeCount) : null,
valuesRedacted: true,
})) : [],
workbenchLinkCount: Number.isFinite(Number(value.workbenchLinkCount)) ? Number(value.workbenchLinkCount) : 0,
valuesRedacted: true
};