Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 388 additions & 0 deletions pmoves/ui/app/api/audit/summary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
import { NextRequest, NextResponse } from "next/server";
import { promises as fs } from "fs";
import path from "path";

import { checkAllServices, getHealthPercentage } from "@/lib/serviceHealth";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

type ExecutiveMetrics = Record<string, string>;

interface AuditBlocker {
id: string;
blocker: string;
severity: string;
status: string;
nextAction: string;
}

interface ReleaseGate {
id: string;
status: string;
notes: string;
}

interface PrMonitorSummary {
available: boolean;
source: string | null;
count: number;
totalBlockers: number;
totalPendingChecks: number;
totalFailedChecks: number;
}

interface GraphitiSummary {
available: boolean;
source: string | null;
generatedAt: string | null;
}

interface RuntimeHealthSummary {
total: number;
healthy: number;
unhealthy: number;
unknown: number;
percentage: number;
timestamp: Date;
}

interface PrMonitorItemShape {
blockers?: unknown;
checks?: {
pending?: number;
failed?: number;
};
}

function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}

function asNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}

function asString(value: unknown): string {
return typeof value === "string" ? value : "";
}

async function exists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}

async function findDocsRoot(): Promise<string | null> {
const cwd = process.cwd();
const candidates = [
path.resolve(cwd, "../docs"), // when running from pmoves/ui
path.resolve(cwd, "pmoves/docs"), // when running from repo root
path.resolve(cwd, "docs"), // fallback
];

for (const candidate of candidates) {
if (await exists(candidate)) {
return candidate;
}
}
return null;
}

async function readText(filePath: string): Promise<string | null> {
try {
return await fs.readFile(filePath, "utf8");
} catch {
return null;
}
}

async function readJson<T>(filePath: string): Promise<T | null> {
const text = await readText(filePath);
if (!text) return null;
try {
return JSON.parse(text) as T;
} catch {
return null;
}
}

function parseLabelValue(markdown: string, label: string): string | null {
const pattern = new RegExp(`\\*\\*${label}:\\*\\*\\s*([^\\n]+)`, "i");
const match = markdown.match(pattern);
if (!match) return null;
return match[1].replace(/`/g, "").trim();
}

function parseMetricTable(markdown: string): ExecutiveMetrics {
const metrics: ExecutiveMetrics = {};
for (const line of markdown.split(/\r?\n/)) {
if (!line.startsWith("|")) continue;
const cols = line
.split("|")
.map((c) => c.trim())
.filter(Boolean);
if (cols.length < 2) continue;
if (cols[0].toLowerCase() === "metric") continue;
if (cols[0].startsWith("---")) continue;
if (!cols[0] || !cols[1]) continue;
metrics[cols[0]] = cols[1];
}
return metrics;
Comment on lines +120 to +134

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope parseMetricTable to the metrics table only.

Line 122 currently parses all markdown table rows, so rows from other tables can leak into executiveMetrics and corrupt the summary.

Proposed fix
 function parseMetricTable(markdown: string): ExecutiveMetrics {
   const metrics: ExecutiveMetrics = {};
+  let inMetricsTable = false;
+
   for (const line of markdown.split(/\r?\n/)) {
-    if (!line.startsWith("|")) continue;
+    if (!inMetricsTable) {
+      if (/^\|\s*Metric\s*\|\s*Value\s*\|/i.test(line)) inMetricsTable = true;
+      continue;
+    }
+    if (!line.startsWith("|")) break;
+
     const cols = line
       .split("|")
       .map((c) => c.trim())
       .filter(Boolean);
     if (cols.length < 2) continue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/audit/summary/route.ts` around lines 120 - 134, The
parseMetricTable function currently processes every markdown table row and can
pick up rows from unrelated tables; change it to first locate the metrics table
header (a row where the first column lowercased is "metric" and there is a
second column like "value"/"metric value"), then only parse subsequent table
rows until the table ends (stop when a non-table line or a blank line occurs).
Update parseMetricTable to: scan lines to find the header row, begin parsing
only after that header, skip the header/separator (e.g., lines starting with
"---"), and use the existing cols[0]/cols[1] assignment to populate
ExecutiveMetrics so unrelated table rows cannot leak in.

}

function parseActiveBlockers(markdown: string): AuditBlocker[] {
const blockers: AuditBlocker[] = [];
const lines = markdown.split(/\r?\n/);
let inTable = false;

for (const line of lines) {
if (line.startsWith("| ID | Blocker |")) {
inTable = true;
continue;
}
if (!inTable) continue;
if (!line.startsWith("|")) break;
if (line.includes("---")) continue;

const cols = line
.split("|")
.map((c) => c.trim())
.filter(Boolean);
if (cols.length < 6) continue;
if (!/^AB-\d+$/i.test(cols[0])) continue;

blockers.push({
id: cols[0],
blocker: cols[1],
severity: cols[3].replace(/\*/g, ""),
status: cols[4],
nextAction: cols[5],
});
}

return blockers;
}

function parseReleaseGateRows(markdown: string): ReleaseGate[] {
const gates: ReleaseGate[] = [];
for (const line of markdown.split(/\r?\n/)) {
if (!line.startsWith("| RG-")) continue;
const cols = line
.split("|")
.map((c) => c.trim())
.filter(Boolean);
if (cols.length < 3) continue;
gates.push({
id: cols[0],
status: cols[1],
notes: cols[2],
});
}
return gates;
}

async function latestReleaseGatePath(docsRoot: string): Promise<string | null> {
const auditDir = path.join(docsRoot, "audit");
if (!(await exists(auditDir))) return null;
const files = await fs.readdir(auditDir);
const matches = files.filter((name) => /^RELEASE_GATE_CHECK_\d{4}-\d{2}-\d{2}\.md$/i.test(name));
if (!matches.length) return null;
matches.sort((a, b) => b.localeCompare(a));
return path.join(auditDir, matches[0]);
}
Comment on lines +188 to +196

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n -C3 'fs\.readdir\(' pmoves/ui/app/api/audit/summary/route.ts

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 870


🏁 Script executed:

cat -n pmoves/ui/app/api/audit/summary/route.ts | sed -n '180,220p'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 1699


🏁 Script executed:

cat -n pmoves/ui/app/api/audit/summary/route.ts | sed -n '1,100p'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2977


🏁 Script executed:

rg -n 'export.*GET|async.*GET' pmoves/ui/app/api/audit/summary/route.ts -A 50

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 2199


🏁 Script executed:

cat -n pmoves/ui/app/api/audit/summary/route.ts | sed -n '290,360p'

Repository: POWERFULMOVES/PMOVES.AI

Length of output: 3324


Guard fs.readdir calls to preserve fail-soft behavior.

Lines 191 and 209 use fs.readdir without error handling. The existence checks do not prevent all failures—permission errors, concurrent deletions, and other I/O issues can still occur and crash the route. The codebase pattern (see readText at lines 95-100 and exists at lines 70-77) consistently wraps file operations in try/catch to degrade gracefully by adding warnings. These two calls should follow the same pattern.

Proposed fix
 async function latestReleaseGatePath(docsRoot: string): Promise<string | null> {
   const auditDir = path.join(docsRoot, "audit");
   if (!(await exists(auditDir))) return null;
-  const files = await fs.readdir(auditDir);
+  let files: string[] = [];
+  try {
+    files = await fs.readdir(auditDir);
+  } catch {
+    return null;
+  }
   const matches = files.filter((name) => /^RELEASE_GATE_CHECK_\d{4}-\d{2}-\d{2}\.md$/i.test(name));
   if (!matches.length) return null;
   matches.sort((a, b) => b.localeCompare(a));
   return path.join(auditDir, matches[0]);
 }
@@
     const fallbackDir = path.join(docsRoot, "evidence", "pr_monitor");
     if (await exists(fallbackDir)) {
-      const files = (await fs.readdir(fallbackDir)).filter((f) => f.endsWith("-latest.json"));
-      files.sort((a, b) => b.localeCompare(a));
-      if (files.length > 0) {
-        source = path.join(fallbackDir, files[0]);
+      let files: string[] = [];
+      try {
+        files = await fs.readdir(fallbackDir);
+      } catch {
+        files = [];
+      }
+      const latest = files.filter((f) => f.endsWith("-latest.json"));
+      latest.sort((a, b) => b.localeCompare(a));
+      if (latest.length > 0) {
+        source = path.join(fallbackDir, latest[0]);
         payload = await readJson<unknown>(source);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/audit/summary/route.ts` around lines 188 - 196,
latestReleaseGatePath (and the other spot using fs.readdir) calls fs.readdir
without try/catch which can throw despite the prior exists() check; wrap each
fs.readdir invocation in a try/catch that logs a warning (following the
readText/exists pattern) and returns null (or an empty result) to preserve
fail-soft behavior. Locate latestReleaseGatePath and the other function using
fs.readdir, add a try/catch around the await fs.readdir(...) call, on catch call
the module's logger/warning mechanism with contextual details and return null so
the route degrades gracefully.


async function readPrMonitorSummary(docsRoot: string): Promise<PrMonitorSummary> {
const primary = path.join(docsRoot, "logs", "pr_monitor_latest.json");
let source: string | null = null;
let payload: unknown = null;

if (await exists(primary)) {
payload = await readJson<unknown>(primary);
source = primary;
} else {
const fallbackDir = path.join(docsRoot, "evidence", "pr_monitor");
if (await exists(fallbackDir)) {
const files = (await fs.readdir(fallbackDir)).filter((f) => f.endsWith("-latest.json"));
files.sort((a, b) => b.localeCompare(a));
if (files.length > 0) {
source = path.join(fallbackDir, files[0]);
payload = await readJson<unknown>(source);
}
}
}

if (!payload || !source) {
return {
available: false,
source: null,
count: 0,
totalBlockers: 0,
totalPendingChecks: 0,
totalFailedChecks: 0,
};
}

const payloadRecord = asRecord(payload);
const items = Array.isArray(payloadRecord?.items) ? (payloadRecord.items as PrMonitorItemShape[]) : [];
if (items.length > 0) {
return {
available: true,
source,
count: items.length,
totalBlockers: items.reduce(
(acc: number, item) => acc + (Array.isArray(item?.blockers) ? item.blockers.length : 0),
0,
),
totalPendingChecks: items.reduce(
(acc: number, item) => acc + asNumber(item?.checks?.pending),
0,
),
totalFailedChecks: items.reduce(
(acc: number, item) => acc + asNumber(item?.checks?.failed),
0,
),
};
}

const summary = asRecord(payloadRecord?.summary);
return {
available: true,
source,
count: asNumber(summary?.checks_total) > 0 ? 1 : 0,
totalBlockers: 0,
totalPendingChecks: asNumber(summary?.checks_pending),
totalFailedChecks: asNumber(summary?.checks_failed),
};
}

async function readGraphitiSummary(docsRoot: string): Promise<GraphitiSummary> {
const primary = path.join(docsRoot, "logs", "graphiti_signed_latest.json");
const payload = await readJson<unknown>(primary);
const payloadRecord = asRecord(payload);
if (!payload) {
return {
available: false,
source: null,
generatedAt: null,
};
}
return {
available: true,
source: primary,
generatedAt: asString(payloadRecord?.generated_at ?? payloadRecord?.timestamp) || null,
};
}

function parsePlanLastUpdated(markdown: string): string | null {
const firstLine = markdown.split(/\r?\n/).find((line) => /last updated/i.test(line));
if (!firstLine) return null;
const cleaned = firstLine
.replace(/^[_*\s]+|[_*\s]+$/g, "")
.replace(/^[^:]+:\s*/i, "")
.trim();
return cleaned || null;
}

export async function GET(request: NextRequest) {
const includeHealth = request.nextUrl.searchParams.get("includeHealth") !== "false";
const rawTimeout = parseInt(request.nextUrl.searchParams.get("timeout") || "3000", 10);
const timeout = Number.isFinite(rawTimeout) && rawTimeout > 0 ? Math.min(Math.max(rawTimeout, 1000), 30000) : 3000;

const warnings: string[] = [];
const docsRoot = await findDocsRoot();
if (!docsRoot) {
return NextResponse.json(
{
generatedAt: new Date().toISOString(),
warnings: ["docs root not found; expected pmoves/docs in working tree"],
},
{ status: 500 },
);
}

const dashboardPath = path.join(docsRoot, "PRODUCTION_AUDIT_DASHBOARD.md");
const dashboardMarkdown = await readText(dashboardPath);
if (!dashboardMarkdown) {
warnings.push("missing PRODUCTION_AUDIT_DASHBOARD.md");
}

const releaseGatePath = await latestReleaseGatePath(docsRoot);
const releaseGateMarkdown = releaseGatePath ? await readText(releaseGatePath) : null;
if (!releaseGateMarkdown) {
warnings.push("missing release gate check markdown");
}

const roadmapPath = path.join(docsRoot, "PMOVES.AI PLANS", "ROADMAP.md");
const roadmapMarkdown = await readText(roadmapPath);
const nextStepsPath = path.join(docsRoot, "NEXT_STEPS.md");
const nextStepsMarkdown = await readText(nextStepsPath);

Comment on lines +319 to +323

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Emit warnings when planning files are missing.

ROADMAP/NEXT_STEPS reads currently fail silently, so warnings[] under-reports missing artifacts.

Proposed fix
   const roadmapPath = path.join(docsRoot, "PMOVES.AI PLANS", "ROADMAP.md");
   const roadmapMarkdown = await readText(roadmapPath);
   const nextStepsPath = path.join(docsRoot, "NEXT_STEPS.md");
   const nextStepsMarkdown = await readText(nextStepsPath);
+  if (!roadmapMarkdown) warnings.push("missing ROADMAP.md");
+  if (!nextStepsMarkdown) warnings.push("missing NEXT_STEPS.md");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/audit/summary/route.ts` around lines 319 - 323, The code
currently reads ROADMAP.md and NEXT_STEPS.md silently failing; wrap the reads
for roadmapPath and nextStepsPath in existence checks or try/catch and when a
read fails push a descriptive warning into the existing warnings array (include
the path variable like roadmapPath/nextStepsPath and context such as "ROADMAP.md
missing" or the caught error message). Update the logic around roadmapMarkdown
and nextStepsMarkdown to only use their values if the read succeeded. Ensure you
reference the variables roadmapPath, nextStepsPath, roadmapMarkdown,
nextStepsMarkdown and the warnings array so missing files are reported rather
than ignored.

const prMonitor = await readPrMonitorSummary(docsRoot);
if (!prMonitor.available) {
warnings.push("pr monitor artifact not found");
}
const graphiti = await readGraphitiSummary(docsRoot);
if (!graphiti.available) {
warnings.push("graphiti artifact not found");
}

let runtimeHealth: RuntimeHealthSummary | null = null;
if (includeHealth) {
try {
const health = await checkAllServices(timeout);
runtimeHealth = {
total: health.total,
healthy: health.healthy,
unhealthy: health.unhealthy,
unknown: health.unknown,
percentage: getHealthPercentage(health),
timestamp: health.timestamp,
};
} catch (error) {
warnings.push(`runtime health check failed: ${error instanceof Error ? error.message : String(error)}`);
}
}

return NextResponse.json(
{
generatedAt: new Date().toISOString(),
docsRoot,
warnings,
productionAudit: {
source: dashboardMarkdown ? dashboardPath : null,
lastUpdated: dashboardMarkdown ? parseLabelValue(dashboardMarkdown, "Last Updated") : null,
branch: dashboardMarkdown ? parseLabelValue(dashboardMarkdown, "Branch") : null,
commit: dashboardMarkdown ? parseLabelValue(dashboardMarkdown, "Commit") : null,
executiveMetrics: dashboardMarkdown ? parseMetricTable(dashboardMarkdown) : {},
activeBlockers: dashboardMarkdown ? parseActiveBlockers(dashboardMarkdown) : [],
},
releaseGates: {
source: releaseGateMarkdown ? releaseGatePath : null,
items: releaseGateMarkdown ? parseReleaseGateRows(releaseGateMarkdown) : [],
},
planning: {
roadmap: {
source: roadmapMarkdown ? roadmapPath : null,
lastUpdated: roadmapMarkdown ? parsePlanLastUpdated(roadmapMarkdown) : null,
},
nextSteps: {
source: nextStepsMarkdown ? nextStepsPath : null,
lastUpdated: nextStepsMarkdown ? parsePlanLastUpdated(nextStepsMarkdown) : null,
},
},
prMonitor,
graphiti,
runtimeHealth,
Comment on lines +353 to +379

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not return absolute filesystem paths in API payload.

docsRoot and several source fields can expose host path structure to clients. Return docs-relative identifiers instead.

Proposed fix
+function toPublicPath(docsRoot: string, absolutePath: string | null): string | null {
+  if (!absolutePath) return null;
+  const rel = path.relative(docsRoot, absolutePath).replace(/\\/g, "/");
+  return rel.startsWith("..") ? null : `docs/${rel}`;
+}
+
 export async function GET(request: NextRequest) {
@@
   return NextResponse.json(
     {
       generatedAt: new Date().toISOString(),
-      docsRoot,
+      docsRoot: "docs",
       warnings,
       productionAudit: {
-        source: dashboardMarkdown ? dashboardPath : null,
+        source: toPublicPath(docsRoot, dashboardMarkdown ? dashboardPath : null),
@@
       releaseGates: {
-        source: releaseGateMarkdown ? releaseGatePath : null,
+        source: toPublicPath(docsRoot, releaseGateMarkdown ? releaseGatePath : null),
@@
         roadmap: {
-          source: roadmapMarkdown ? roadmapPath : null,
+          source: toPublicPath(docsRoot, roadmapMarkdown ? roadmapPath : null),
@@
         nextSteps: {
-          source: nextStepsMarkdown ? nextStepsPath : null,
+          source: toPublicPath(docsRoot, nextStepsMarkdown ? nextStepsPath : null),
@@
-      prMonitor,
-      graphiti,
+      prMonitor: { ...prMonitor, source: toPublicPath(docsRoot, prMonitor.source) },
+      graphiti: { ...graphiti, source: toPublicPath(docsRoot, graphiti.source) },
       runtimeHealth,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pmoves/ui/app/api/audit/summary/route.ts` around lines 353 - 379, Remove
absolute filesystem exposure by not returning docsRoot and by converting every
source path to a docs-relative identifier: compute a relative path using
path.relative(docsRoot, dashboardPath|releaseGatePath|roadmapPath|nextStepsPath)
(or null if the markdown var is falsy or the path is outside docsRoot), and set
productionAudit.source, releaseGates.source, planning.roadmap.source and
planning.nextSteps.source to that relative identifier instead of the raw
absolute path; ensure docsRoot itself is omitted from the returned payload.

},
{
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate",
"Content-Type": "application/json",
},
},
);
}
Loading