Files
fusion/packages/dashboard/app/utils/taskTiming.ts
gsxdsm dc50425e98 docs: correct 104 future-dated FNXC timestamps across 61 files (#2680)
## What

The FNXC convention exists so a reader can place a note against the
change that motivated it. A stamp dated *after* the edit landed defeats
exactly that.

This is program-wide drift, not one author's slip — I contributed to it
in my own commits this week, which is how I noticed it.

## Measured, on this tree

**104 stamps across 61 files** dated later than the day they were
written, from one day ahead to **2026-10-19 (81 days)**:

| count | date | count | date | count | date |
|---|---|---|---|---|---|
| 50 | 2026-07-31 | 6 | 2026-08-05 | 3 | 2026-08-13 |
| 17 | 2026-08-01 | 1 | 2026-08-07 | 1 | 2026-08-19 |
| 7 | 2026-08-02 | 1 | 2026-08-12 | 2 | 2026-08-26 |
| 11 | 2026-08-03 | | | 3 | 2026-10-19 |

An earlier number I circulated was ~70. That came from a narrower
pathspec and was wrong; **104** is the measurement.

## How

Each stamp is rewritten to the date of the commit that introduced **that
line**, via per-line `git blame` — deliberately *not* stamped uniformly
with today's date. A uniform stamp swaps a wrong date for a different
wrong date and flattens the ordering that makes these comments
navigable; blame preserves it. Times of day are untouched, and a blame
date in the future is clamped rather than trusted.

## Why the verification is listed

A docs sweep across 61 files is precisely where a stray edit hides, so
the safety claims are mechanical rather than asserted:

- every changed line begins with a comment marker — **no code touched**;
- **no test asserts an FNXC date later than today**, so no `toContain`
assertion on embedded source text can be silently invalidated (several
such assertions do exist);
- CSS files, which carry several of those assertions, are outside the
pathspec.

## Verified

lint clean · merge gate green (487 + 158 + 10 + 71) · `census --strict`
exit 0 · tsc clean for core, engine, and dashboard
(`tsconfig.app.json`).

**No behavior change.** Comment text only.

## Not done here

A guard preventing recurrence. A check that rejects an FNXC stamp dated
after the commit would stop this returning, but it needs a decision
about where it runs (lint rule vs. gate) and it is a behavior change to
CI — it does not belong riding inside the sweep it would police.
2026-07-30 02:38:58 -07:00

143 lines
4.8 KiB
TypeScript

import type { Task, TaskLogEntry, WorkflowStepResult } from "@fusion/core";
export interface TimingEvent {
timestamp: string;
durationMs?: number;
summary: string;
}
function summarizeTimingLabel(entry: TaskLogEntry): string {
const timingText = entry.action || entry.outcome || "";
const stripped = timingText
.replace(/^\[timing\]\s*/i, "")
.replace(/^\[[^\]]+\]\s*/i, "")
.replace(/\s+in\s+\d+(?:\.\d+)?ms\b/i, "")
.replace(/\s+after\s+\d+(?:\.\d+)?ms\b/i, "")
.trim();
return stripped || "Timing event";
}
export function extractTimingEvents(logEntries: TaskLogEntry[]): TimingEvent[] {
return logEntries
.filter((entry) => {
const actionText = typeof entry.action === "string" ? entry.action : "";
const outcomeText = typeof entry.outcome === "string" ? entry.outcome : "";
return actionText.includes("[timing]") || outcomeText.includes("[timing]");
})
.map((entry) => {
const haystack = `${entry.action ?? ""}\n${entry.outcome ?? ""}`;
const durationMatch = haystack.match(/(\d+(?:\.\d+)?)ms\b/i);
const durationMs = durationMatch ? Number(durationMatch[1]) : undefined;
return {
timestamp: entry.timestamp,
durationMs: Number.isFinite(durationMs) ? durationMs : undefined,
summary: summarizeTimingLabel(entry),
};
});
}
export function getTimedDurationMs(logEntries: TaskLogEntry[] | undefined): number | null {
if (!logEntries || logEntries.length === 0) return null;
let total = 0;
let counted = 0;
for (const event of extractTimingEvents(logEntries)) {
if (typeof event.durationMs !== "number") continue;
total += event.durationMs;
counted += 1;
}
return counted > 0 ? total : null;
}
export function parseTimestampToMs(value?: string): number | null {
if (!value) return null;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
export function getWorkflowRuntimeMs(results: WorkflowStepResult[] | undefined, nowMs: number): number | null {
if (!results || results.length === 0) return null;
let total = 0;
let counted = 0;
for (const step of results) {
if (!step.startedAt) continue;
const startedMs = parseTimestampToMs(step.startedAt);
if (startedMs == null) continue;
let endMs: number;
if (step.completedAt) {
const completedMs = parseTimestampToMs(step.completedAt);
if (completedMs == null || completedMs < startedMs) continue;
endMs = completedMs;
} else {
endMs = Math.max(startedMs, nowMs);
}
total += endMs - startedMs;
counted += 1;
}
return counted > 0 ? total : null;
}
export function getEndToEndDurationMs(
executionStartedAt: string | undefined,
executionCompletedAt: string | undefined,
nowMs: number,
): number | null {
const startedMs = parseTimestampToMs(executionStartedAt);
if (startedMs == null) return null;
const completedMs = parseTimestampToMs(executionCompletedAt);
const endMs = completedMs != null && completedMs >= startedMs ? completedMs : nowMs;
return Math.max(0, endMs - startedMs);
}
export function getActiveRuntimeMs(
task: Pick<Task, "column" | "cumulativeActiveMs" | "executionStartedAt" | "columnMovedAt">,
nowMs: number,
): number | null {
const persisted = task.cumulativeActiveMs;
const base = persisted ?? 0;
if (task.column === "in-progress") {
const startedMs = parseTimestampToMs(task.executionStartedAt);
if (startedMs != null) {
return base + Math.max(0, nowMs - startedMs);
}
}
if (persisted != null) {
return Math.max(0, persisted);
}
return null;
}
/** FNXC:TaskTiming 2026-07-20-10:00: rendered task totals include planning AI
* segments while getActiveRuntimeMs intentionally remains execution-only. */
export function getTotalAgentActiveMs(
task: Pick<Task, "column" | "cumulativeActiveMs" | "executionStartedAt" | "cumulativePlanningMs" | "planningStartedAt">,
nowMs: number,
): number | null {
const execution = getActiveRuntimeMs(task, nowMs) ?? 0;
const planningStart = parseTimestampToMs(task.planningStartedAt);
const planning = Math.max(0, task.cumulativePlanningMs ?? 0) + (planningStart != null ? Math.max(0, nowMs - planningStart) : 0);
return task.cumulativeActiveMs != null || task.cumulativePlanningMs != null || (task.column === "in-progress" && parseTimestampToMs(task.executionStartedAt) != null) || planningStart != null
? execution + planning
: null;
}
export function getWallClockSinceFirstExecutionMs(
firstExecutionAt: string | undefined,
executionCompletedAt: string | undefined,
nowMs: number,
): number | null {
const firstMs = parseTimestampToMs(firstExecutionAt);
if (firstMs == null) return null;
const completedMs = parseTimestampToMs(executionCompletedAt);
const endMs = completedMs != null ? completedMs : nowMs;
return Math.max(0, endMs - firstMs);
}