518 lines
34 KiB
TypeScript
518 lines
34 KiB
TypeScript
// SPEC: PJ2026-01040111 long-running Workbench observation.
|
|
// Responsibility: Analyzer Project Management/MDTODO report and finding source fragment.
|
|
|
|
export function nodeWebObserveAnalyzerProjectSource(): string {
|
|
return String.raw`function buildProjectManagementReport(samples, control, network, pagePerformance, config) {
|
|
const enabled = config?.enabled === true;
|
|
const targetPathSamples = (samples || []).filter((sample) => enabled && config.targetPaths.some((target) => String(sample?.path || "").startsWith(target)));
|
|
const projectSamples = (samples || []).filter((sample) => sample?.projectManagement && typeof sample.projectManagement === "object");
|
|
const latest = projectSamples[projectSamples.length - 1] || null;
|
|
const latestProject = latest?.projectManagement || null;
|
|
const pageKindCounts = countBy(projectSamples.map((sample) => sample.projectManagement?.pageKind).filter(Boolean));
|
|
const latestTaskStatusCounts = latestProject?.taskStatusCounts && typeof latestProject.taskStatusCounts === "object" ? latestProject.taskStatusCounts : {};
|
|
const commandRows = projectManagementCommandRows(control, config);
|
|
const launchCommands = commandRows.filter((item) => item.type === "launchWorkbenchFromTask" || item.type === "launchWorkbenchFromMdtodo");
|
|
const launchSuccess = launchCommands.filter((item) => item.phase === "completed" && Number(item.launchStatus ?? 0) >= 200 && Number(item.launchStatus ?? 0) < 300);
|
|
const launchFailed = launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400);
|
|
const projectApiEvents = projectManagementNetworkRows(network, config);
|
|
const projectApiResponses = projectApiEvents.filter((item) => item.type === "response");
|
|
const projectApiFailures = projectApiResponses.filter((item) => Number(item.status ?? 0) >= 400);
|
|
const projectApiFailedRequests = projectApiEvents.filter((item) => item.type === "requestfailed");
|
|
const projectApiByPath = groupProjectApiEvents(projectApiEvents);
|
|
const projectApiPerformance = projectManagementPerformanceRows(pagePerformance, config);
|
|
const slowProjectApiPerformance = projectApiPerformance.filter((item) => Number(item.overBudgetCount ?? 0) > 0 || Number(item.p95Ms ?? 0) > Number(config?.slowApiBudgetMs ?? 0));
|
|
const selectedTaskSamples = projectSamples.filter((sample) => sample.projectManagement?.selectedTaskRef?.hash);
|
|
const launchEnabledSamples = projectSamples.filter((sample) => sample.projectManagement?.launchButtonEnabled === true);
|
|
const launchVisibleSamples = projectSamples.filter((sample) => sample.projectManagement?.launchButtonVisible === true);
|
|
const mdtodoSamples = projectSamples.filter((sample) => sample.projectManagement?.pageKind === "project-management-mdtodo");
|
|
const selectedFileLabelBadSamples = projectSamples.filter((sample) => sample.projectManagement?.selectedFileLabel && sample.projectManagement?.selectedFileLabelLooksDirect === false);
|
|
const suspiciousFileLabelSamples = projectSamples.filter((sample) => Number(sample.projectManagement?.fileOptionSuspiciousLabelCount ?? 0) > 0);
|
|
const bodyVisibleSamples = selectedTaskSamples.filter((sample) => sample.projectManagement?.taskBodyVisible === true && Number(sample.projectManagement?.taskBody?.textBytes ?? 0) > 0);
|
|
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 paneGapRows = projectManagementPaneGapRows(projectSamples);
|
|
const severePaneGapSamples = paneGapRows.actionable;
|
|
const ignoredPaneGapSamples = paneGapRows.ignored;
|
|
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));
|
|
const minMdtodoTaskCount = minNumber(mdtodoSamples.map((sample) => sample.projectManagement?.taskCount));
|
|
const maxMdtodoTaskCount = maxNumber(mdtodoSamples.map((sample) => sample.projectManagement?.taskCount));
|
|
return {
|
|
enabled,
|
|
config: config || null,
|
|
summary: {
|
|
enabled,
|
|
targetPathSampleCount: targetPathSamples.length,
|
|
projectSampleCount: projectSamples.length,
|
|
mdtodoSampleCount: mdtodoSamples.length,
|
|
pageKindCounts,
|
|
latestPageKind: latestProject?.pageKind ?? null,
|
|
latestPath: latest?.path ?? null,
|
|
latestSeq: latest?.seq ?? null,
|
|
latestTs: latest?.ts ?? null,
|
|
latestSourceCount: latestProject?.sourceCount ?? null,
|
|
latestFileCount: latestProject?.fileCount ?? null,
|
|
latestTaskCount: latestProject?.taskCount ?? null,
|
|
maxSourceCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.sourceCount)),
|
|
maxFileCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.fileCount)),
|
|
maxTaskCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.taskCount)),
|
|
taskRefMissingMax: maxNumber(projectSamples.map((sample) => sample.projectManagement?.taskRefMissingCount)),
|
|
latestSelectedTaskRefHash: latestProject?.selectedTaskRef?.hash ?? null,
|
|
latestSelectedTaskRefPreview: latestProject?.selectedTaskRef?.preview ?? null,
|
|
latestSelectedFileLabelPreview: latestProject?.selectedFileLabel?.textPreview ?? null,
|
|
latestSelectedFileLabelLooksDirect: latestProject?.selectedFileLabelLooksDirect ?? null,
|
|
selectedFileLabelBadSampleCount: selectedFileLabelBadSamples.length,
|
|
fileOptionSuspiciousLabelSampleCount: suspiciousFileLabelSamples.length,
|
|
maxFileOptionSuspiciousLabelCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.fileOptionSuspiciousLabelCount)),
|
|
latestSelectedTaskStatus: latestProject?.selectedTaskStatus ?? null,
|
|
latestTaskStatusCounts,
|
|
selectedTaskBodyVisibleSamples: bodyVisibleSamples.length,
|
|
reportLinkVisibleSamples: reportLinkSamples.length,
|
|
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,
|
|
ignoredPaneGapSampleCount: ignoredPaneGapSamples.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),
|
|
latestWorkbenchLinkCount: latestProject?.workbenchLinkCount ?? null,
|
|
maxWorkbenchLinkCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.workbenchLinkCount)),
|
|
maxBlockerCount: maxNumber(projectSamples.map((sample) => sample.projectManagement?.blockerCount)),
|
|
selectedTaskSampleCount: selectedTaskSamples.length,
|
|
projectCommandCount: commandRows.length,
|
|
launchCommandCount: launchCommands.length,
|
|
launchSuccessCount: launchSuccess.length,
|
|
launchFailureCount: launchFailed.length,
|
|
launchNonEmptyCount: launchNonEmpty.length,
|
|
launchEmptyCount: launchEmpty.length,
|
|
launchWithOtelTraceHeaderCount: launchSuccess.filter((item) => item.otelTraceId).length,
|
|
reportPreviewCommandCount: previewCommands.length,
|
|
mdtodoTaskCountMin: minMdtodoTaskCount,
|
|
mdtodoTaskCountMax: maxMdtodoTaskCount,
|
|
projectApiEventCount: projectApiEvents.length,
|
|
projectApiResponseCount: projectApiResponses.length,
|
|
projectApiFailureCount: projectApiFailures.length,
|
|
projectApiRequestFailedCount: projectApiFailedRequests.length,
|
|
projectApiSlowPathCount: slowProjectApiPerformance.length,
|
|
slowApiBudgetMs: config?.slowApiBudgetMs ?? null,
|
|
valuesRedacted: true
|
|
},
|
|
latest: latestProject,
|
|
samples: projectSamples.slice(-80).map((sample) => ({
|
|
seq: sample.seq ?? null,
|
|
ts: sample.ts ?? null,
|
|
pageRole: sample.pageRole ?? null,
|
|
path: sample.path ?? null,
|
|
pageKind: sample.projectManagement?.pageKind ?? null,
|
|
sourceCount: sample.projectManagement?.sourceCount ?? null,
|
|
fileCount: sample.projectManagement?.fileCount ?? null,
|
|
taskCount: sample.projectManagement?.taskCount ?? null,
|
|
taskRefMissingCount: sample.projectManagement?.taskRefMissingCount ?? null,
|
|
selectedTaskRefHash: sample.projectManagement?.selectedTaskRef?.hash ?? null,
|
|
selectedFileLabelPreview: sample.projectManagement?.selectedFileLabel?.textPreview ?? null,
|
|
selectedFileLabelLooksDirect: sample.projectManagement?.selectedFileLabelLooksDirect ?? null,
|
|
fileOptionSuspiciousLabelCount: sample.projectManagement?.fileOptionSuspiciousLabelCount ?? 0,
|
|
selectedTaskStatus: sample.projectManagement?.selectedTaskStatus ?? null,
|
|
taskBodyVisible: sample.projectManagement?.taskBodyVisible === true,
|
|
taskBodyBytes: sample.projectManagement?.taskBody?.textBytes ?? 0,
|
|
reportLinkCount: sample.projectManagement?.reportLinkCount ?? 0,
|
|
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,
|
|
workbenchLinkCount: sample.projectManagement?.workbenchLinkCount ?? 0,
|
|
valuesRedacted: true
|
|
})),
|
|
targetPathWithoutProjectSummary: targetPathSamples.filter((sample) => !sample.projectManagement).slice(0, 20).map(ref),
|
|
commands: commandRows,
|
|
launchCommands,
|
|
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),
|
|
ignoredPaneGapSamples: ignoredPaneGapSamples.slice(0, 40),
|
|
projectApiPerformance,
|
|
slowProjectApiPerformance,
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function compactProjectManagementForOutput(report) {
|
|
if (!report || typeof report !== "object") return null;
|
|
const compactCommand = (item) => ({
|
|
ts: item?.ts ?? null,
|
|
phase: item?.phase ?? null,
|
|
type: item?.type ?? null,
|
|
commandId: item?.commandId ?? null,
|
|
afterPath: item?.afterPath ?? null,
|
|
launchStatus: item?.launchStatus ?? null,
|
|
sessionId: item?.sessionId ?? null,
|
|
workbenchUrl: item?.workbenchUrl ?? null,
|
|
otelTraceId: item?.otelTraceId ?? null,
|
|
chatObserved: item?.chatObserved ?? null,
|
|
chatStatus: item?.chatStatus ?? null,
|
|
chatTraceId: item?.chatTraceId ?? null,
|
|
workbenchMessageCount: item?.workbenchMessageCount ?? null,
|
|
workbenchTraceRowCount: item?.workbenchTraceRowCount ?? null,
|
|
contractVersion: item?.contractVersion ?? null,
|
|
selectedTaskRefHash: item?.selectedTaskRefHash ?? null,
|
|
errorMessageHash: item?.errorMessageHash ?? null,
|
|
message: item?.message ? limitText(item.message, 180) : null,
|
|
valuesRedacted: true
|
|
});
|
|
const compactApiGroup = (item) => ({
|
|
method: item?.method ?? null,
|
|
path: item?.path ?? item?.urlPath ?? null,
|
|
status: item?.status ?? null,
|
|
type: item?.type ?? null,
|
|
count: item?.count ?? item?.sampleCount ?? null,
|
|
firstAt: item?.firstAt ?? null,
|
|
lastAt: item?.lastAt ?? null,
|
|
failureKinds: Array.isArray(item?.failureKinds) ? item.failureKinds.slice(0, 4) : [],
|
|
valuesRedacted: true
|
|
});
|
|
const compactSlowSample = (item) => ({
|
|
ts: item?.ts ?? null,
|
|
seq: item?.seq ?? null,
|
|
path: item?.path ?? item?.rawPath ?? null,
|
|
durationMs: item?.durationMs ?? null,
|
|
requestToResponseStartMs: item?.requestToResponseStartMs ?? item?.streamOpenMs ?? null,
|
|
responseTransferMs: item?.responseTransferMs ?? null,
|
|
timingStatus: item?.timingStatus ?? null,
|
|
initiatorType: item?.initiatorType ?? null,
|
|
nextHopProtocol: item?.nextHopProtocol ?? null,
|
|
serverTimingNames: Array.isArray(item?.serverTimingNames) ? item.serverTimingNames.slice(0, 4) : [],
|
|
otelTraceId: item?.otelTraceId ?? null,
|
|
valuesRedacted: true
|
|
});
|
|
const compactPerformance = (item) => ({
|
|
path: item?.path ?? item?.route ?? null,
|
|
sampleCount: item?.sampleCount ?? null,
|
|
p95Ms: item?.p95Ms ?? item?.p95 ?? null,
|
|
maxMs: item?.maxMs ?? item?.max ?? null,
|
|
budgetMs: item?.projectSlowBudgetMs ?? item?.budgetMs ?? report.summary?.slowApiBudgetMs ?? null,
|
|
overBudgetCount: item?.overBudgetCount ?? item?.overFiveSecondCount ?? null,
|
|
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,
|
|
pageKind: item?.pageKind ?? null,
|
|
reason: item?.reason ?? 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) : [],
|
|
ignoredPaneGapSamples: Array.isArray(report.ignoredPaneGapSamples) ? report.ignoredPaneGapSamples.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
|
|
};
|
|
}
|
|
|
|
function projectManagementCommandRows(control, config) {
|
|
const allowed = new Set(config?.commandAllowlist || []);
|
|
const mdtodoCommandTypes = new Set(["gotoProjectMdtodo", "openMdtodoSourceConfig", "configureMdtodoHwpodSource", "probeMdtodoSource", "reindexMdtodoSource", "expandMdtodoTask", "openMdtodoReportPreview", "toggleMdtodoReportFullscreen", "editMdtodoTaskInline", "editMdtodoTaskTitle", "editMdtodoTaskBody", "toggleMdtodoTaskStatus", "addMdtodoRootTask", "addMdtodoSubTask", "continueMdtodoTask", "deleteMdtodoTask", "launchWorkbenchFromMdtodo"]);
|
|
return (control || [])
|
|
.filter((item) => allowed.has(item?.type) || mdtodoCommandTypes.has(item?.type) || String(item?.type || "").startsWith("selectMdtodo") || item?.type === "selectProjectSource" || item?.type === "launchWorkbenchFromTask")
|
|
.filter((item) => item.phase === "completed" || item.phase === "failed")
|
|
.map((item) => {
|
|
const detail = item.detail && typeof item.detail === "object" ? item.detail : {};
|
|
const error = detail.error && typeof detail.error === "object" ? detail.error : {};
|
|
return {
|
|
ts: item.ts ?? null,
|
|
phase: item.phase ?? null,
|
|
type: item.type ?? null,
|
|
commandId: item.commandId ?? null,
|
|
afterPath: urlPath(item.afterUrl),
|
|
launchStatus: detail.launchStatus ?? error.details?.launchStatus ?? null,
|
|
sessionId: detail.sessionId ?? error.details?.sessionId ?? null,
|
|
workbenchUrl: detail.workbenchUrl ?? error.details?.workbenchUrl ?? null,
|
|
otelTraceId: detail.otelTraceId ?? error.details?.otelTraceId ?? null,
|
|
chatObserved: detail.chatObserved ?? error.details?.chatObserved ?? null,
|
|
chatStatus: detail.chatStatus ?? error.details?.chatStatus ?? null,
|
|
chatSessionId: detail.chatSessionId ?? error.details?.chatSessionId ?? null,
|
|
chatTraceId: detail.chatTraceId ?? error.details?.chatTraceId ?? null,
|
|
chatOtelTraceId: detail.chatOtelTraceId ?? error.details?.chatOtelTraceId ?? null,
|
|
workbenchMessageCount: detail.workbenchSnapshot?.messageCount ?? error.details?.workbenchSnapshot?.messageCount ?? null,
|
|
workbenchTraceRowCount: detail.workbenchSnapshot?.traceRowCount ?? error.details?.workbenchSnapshot?.traceRowCount ?? null,
|
|
workbenchComposerReady: detail.workbenchSnapshot?.composerReady ?? error.details?.workbenchSnapshot?.composerReady ?? null,
|
|
contractVersion: detail.contractVersion ?? error.details?.contractVersion ?? null,
|
|
selectedTaskRefHash: detail.selectedTask?.hash ?? detail.projectBeforeClick?.selectedTaskRef?.hash ?? null,
|
|
errorName: error.name ?? null,
|
|
errorMessageHash: error.message ? sha256(error.message) : null,
|
|
message: error.message ? limitText(error.message, 180) : null,
|
|
valuesRedacted: true
|
|
};
|
|
});
|
|
}
|
|
|
|
function projectManagementNetworkRows(network, config) {
|
|
const prefixes = config?.naturalApiPathPrefixes || [];
|
|
return (network || [])
|
|
.filter((item) => item?.observerInitiated !== true)
|
|
.map((item) => ({
|
|
ts: item.ts ?? null,
|
|
type: item.type ?? null,
|
|
method: String(item.method || "GET").toUpperCase(),
|
|
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
|
|
path: urlPath(item.url),
|
|
failureKind: item.failure ? limitText(item.failure, 120) : null,
|
|
valuesRedacted: true
|
|
}))
|
|
.filter((item) => prefixes.some((prefix) => String(item.path || "").startsWith(prefix)));
|
|
}
|
|
|
|
function groupProjectApiEvents(events) {
|
|
const groups = new Map();
|
|
for (const item of events || []) {
|
|
const key = [item.method, item.path, item.status ?? "-", item.type].join(" ");
|
|
const existing = groups.get(key) || { method: item.method, path: item.path, status: item.status, type: item.type, count: 0, firstAt: item.ts, lastAt: item.ts, failureKinds: [], valuesRedacted: true };
|
|
existing.count += 1;
|
|
existing.lastAt = item.ts;
|
|
if (item.failureKind && !existing.failureKinds.includes(item.failureKind)) existing.failureKinds.push(item.failureKind);
|
|
groups.set(key, existing);
|
|
}
|
|
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.path).localeCompare(String(b.path)));
|
|
}
|
|
|
|
function projectManagementPerformanceRows(pagePerformance, config) {
|
|
const prefixes = config?.naturalApiPathPrefixes || [];
|
|
const rows = Array.isArray(pagePerformance?.sameOriginApiByPath) ? pagePerformance.sameOriginApiByPath : [];
|
|
return rows
|
|
.filter((item) => prefixes.some((prefix) => String(item?.path || "").startsWith(prefix)))
|
|
.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,
|
|
pageKind: sample?.projectManagement?.pageKind ?? 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 actionable = [];
|
|
const ignored = [];
|
|
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;
|
|
const selectedTaskRefHash = sample?.projectManagement?.selectedTaskRef?.hash ?? null;
|
|
const isMdtodo = sample?.projectManagement?.pageKind === "project-management-mdtodo";
|
|
const isInitialEmptyDetail = isMdtodo && !selectedTaskRefHash;
|
|
const row = {
|
|
...projectManagementSampleRef(sample),
|
|
severePaneCount: severeGaps.length,
|
|
maxBottomGapPx: maxGapPx,
|
|
maxBottomGapRatio: maxGapRatio,
|
|
paneGaps: severeGaps.slice(0, 4),
|
|
valuesRedacted: true
|
|
};
|
|
if (isInitialEmptyDetail) {
|
|
ignored.push({
|
|
...row,
|
|
reason: "mdtodo-initial-empty-detail-no-selected-task",
|
|
valuesRedacted: true
|
|
});
|
|
continue;
|
|
}
|
|
actionable.push(row);
|
|
}
|
|
return { actionable, ignored, valuesRedacted: true };
|
|
}
|
|
|
|
function buildProjectManagementFindings(report) {
|
|
if (!report?.enabled) return [];
|
|
const findings = [];
|
|
const summary = report.summary || {};
|
|
if (Number(summary.targetPathSampleCount ?? 0) > 0 && Number(summary.projectSampleCount ?? 0) === 0) {
|
|
findings.push({ id: "project-management-route-not-ready", severity: "red", summary: "project management target path was sampled but no project-management DOM summary was detected", count: summary.targetPathSampleCount, samples: report.targetPathWithoutProjectSummary, valuesRedacted: true });
|
|
}
|
|
if (Number(summary.taskRefMissingMax ?? 0) > 0) {
|
|
findings.push({ id: "mdtodo-taskref-missing", severity: "red", summary: "mdtodo task rows were visible without stable data-task-ref; Workbench launch must bind by opaque public task id", count: summary.taskRefMissingMax, samples: report.samples.filter((item) => Number(item.taskRefMissingCount ?? 0) > 0).slice(0, 20), valuesRedacted: true });
|
|
}
|
|
if (Number(summary.mdtodoSampleCount ?? 0) > 0 && Number(summary.latestTaskCount ?? 0) > 0 && Number(summary.launchButtonEnabledSamples ?? 0) === 0) {
|
|
findings.push({ id: "workbench-launch-button-unavailable", severity: "red", summary: "mdtodo tasks were sampled but the Workbench launch button was never enabled", count: summary.mdtodoSampleCount, latest: report.latest, valuesRedacted: true });
|
|
}
|
|
if (Number(summary.selectedFileLabelBadSampleCount ?? 0) > 0 || summary.latestSelectedFileLabelLooksDirect === false) {
|
|
findings.push({ id: "mdtodo-file-label-not-filename", severity: "red", summary: "MDTODO file dropdown selected label is not a direct markdown filename", count: summary.selectedFileLabelBadSampleCount, latestSelectedFileLabelPreview: summary.latestSelectedFileLabelPreview, samples: report.samples.filter((item) => item.selectedFileLabelLooksDirect === false).slice(0, 12), valuesRedacted: true });
|
|
}
|
|
if (Number(summary.maxFileOptionSuspiciousLabelCount ?? 0) > 0) {
|
|
findings.push({ id: "mdtodo-nondirect-files-visible", severity: "red", summary: "MDTODO file dropdown includes non-direct or report-like markdown labels; docs/MDTODO discovery must be direct files only", count: summary.maxFileOptionSuspiciousLabelCount, samples: report.samples.filter((item) => Number(item.fileOptionSuspiciousLabelCount ?? 0) > 0).slice(0, 12), valuesRedacted: true });
|
|
}
|
|
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 });
|
|
}
|
|
if (Number(summary.reportPreviewCommandCount ?? 0) > 0 && Number(summary.reportFullscreenVisibleSamples ?? 0) === 0 && report.commands.some((item) => item.type === "toggleMdtodoReportFullscreen" && item.phase === "completed")) {
|
|
findings.push({ id: "mdtodo-report-fullscreen-missing", severity: "red", summary: "toggleMdtodoReportFullscreen command completed but fullscreen report dialog was never sampled", count: summary.reportPreviewCommandCount, commands: report.commands.filter((item) => item.type === "toggleMdtodoReportFullscreen").slice(-8), valuesRedacted: true });
|
|
}
|
|
if (Number(summary.launchEmptyCount ?? 0) > 0) {
|
|
findings.push({ id: "mdtodo-workbench-launch-empty", severity: "red", summary: "MDTODO Workbench launch created a session without observing agent chat or visible message/trace content", count: summary.launchEmptyCount, commands: report.launchCommands.filter((item) => item.chatObserved !== true || (Number(item.workbenchMessageCount ?? 0) === 0 && Number(item.workbenchTraceRowCount ?? 0) === 0)).slice(0, 12), valuesRedacted: true });
|
|
}
|
|
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 in actionable selected-task samples", count: summary.severePaneGapSampleCount, ignoredInitialEmptyDetailCount: summary.ignoredPaneGapSampleCount, 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 });
|
|
}
|
|
if (Number(summary.projectApiSlowPathCount ?? 0) > 0) {
|
|
findings.push({ id: "project-management-api-slow", severity: "red", summary: "project-management API resource timing exceeded YAML projectManagement.slowApiBudgetMs", count: summary.projectApiSlowPathCount, budgetMs: summary.slowApiBudgetMs, groups: report.slowProjectApiPerformance.slice(0, 12), valuesRedacted: true });
|
|
}
|
|
if (Number(summary.launchFailureCount ?? 0) > 0) {
|
|
findings.push({ id: "mdtodo-workbench-launch-failed", severity: "red", summary: "MDTODO Workbench launch command failed or returned an HTTP error", count: summary.launchFailureCount, commands: report.launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400).slice(0, 12), valuesRedacted: true });
|
|
}
|
|
if (Number(summary.launchSuccessCount ?? 0) > 0 && Number(summary.launchWithOtelTraceHeaderCount ?? 0) === 0) {
|
|
findings.push({ id: "mdtodo-workbench-launch-otel-trace-missing", severity: "amber", summary: "Workbench launch succeeded but no x-hwlab-otel-trace-id header was captured for Tempo drill-down", count: summary.launchSuccessCount, commands: report.launchCommands.slice(0, 12), valuesRedacted: true });
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
function countBy(values) {
|
|
const out = {};
|
|
for (const value of values || []) out[value] = (out[value] || 0) + 1;
|
|
return out;
|
|
}
|
|
|
|
function maxNumber(values) {
|
|
const numeric = (values || []).map((value) => Number(value)).filter(Number.isFinite);
|
|
return numeric.length > 0 ? Math.max(...numeric) : 0;
|
|
}
|
|
|
|
function minNumber(values) {
|
|
const numeric = (values || []).map((value) => Number(value)).filter(Number.isFinite);
|
|
return numeric.length > 0 ? Math.min(...numeric) : 0;
|
|
}
|
|
`;
|
|
}
|