## Summary Waves 3–5 of package code organization (plan: `docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`). Behavior-preserving peels after #2139 and #2143. ### Wave 3 — Merger + heartbeat recovery - **`merger-errors.ts`** — verification/abort error classes - **`merger-owned-landed.ts`** — ownership classification + `Fusion-Task-Id` trailer - **`merger-conflict-resolution.ts`** — conflict classify/auto-resolve - **`agent-heartbeat-error-recovery.ts`** — durable error-recovery budget helpers ### Wave 4 — Self-healing + dashboard API - **`self-healing-constants.ts`** — public timing/budget constants - **`self-healing-branch.ts`** — `isBranchAheadOfBase` - **`app/api/client.ts`** — `api` / `ApiRequestError` / `buildApiUrl` / `proxyApi` - **`app/api/health.ts`** — health, engine status, updates + `withProjectId` ### Wave 5 — Types tracking + merger parse + task CRUD - **`types/task-tracking.ts`** — PR/issue/GitHub/GitLab tracking contracts - **`merger-git-parse.ts`** — `parseFailingFilesFromOutput`, `parsePorcelainZ`, `parseShortstatSummary` - **`app/api/tasks.ts`** — task list/detail/create/update/move client surface - Line-count baselines ratcheted down for `merger.ts`, `types.ts`, `legacy.ts` Public import paths stay on parent modules / `legacy.ts` / package barrels via re-exports. ## Test plan - [x] core/engine/dashboard typecheck (including `tsconfig.app.json`) - [x] eslint on touched modules - [x] `parse-porcelain-z` + merger parseFailing/getBranchChanged tests - [x] dashboard `api-tasks` + legacy-prinfo/pr-types (69) - [ ] CI merge gate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added dashboard API support for task listing/detail, archiving, creation, review updates, duplicate detection, bulk model updates, moving tasks, and overlap repair. - Added health/engine status and refresh/start controls, plus update checking. - **Bug Fixes** - Improved dashboard API handling for non-JSON/HTML responses with clearer errors, better URL routing for remote nodes, and project-scoped queries. - Strengthened automated recovery for heartbeat error/model-unavailable scenarios and safer merge-conflict classification/auto-resolution. - **Tests** - Updated merge-conflict resolution and lifecycle test mocks to match the updated git command behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
/**
|
||
* FNXC:CodeOrganization 2026-07-16-00:30:
|
||
* Pure git/test-output parse helpers peeled from merger.ts (string parsing only).
|
||
*/
|
||
|
||
export function parseFailingFilesFromOutput(output: string): string[] {
|
||
const paths = new Set<string>();
|
||
for (const line of output.split("\n")) {
|
||
// jest/vitest: "FAIL packages/engine/src/__tests__/foo.test.ts"
|
||
const failMatch = line.match(/^FAIL\s+(\S+)/);
|
||
if (failMatch && failMatch[1]) {
|
||
paths.add(failMatch[1]);
|
||
continue;
|
||
}
|
||
// vitest summary: " ❯ packages/engine/src/__tests__/foo.test.ts (2 tests | 1 failed)"
|
||
const vitestSummaryMatch = line.match(/^\s*[❯>]\s+(\S+\.(?:test|spec)\.[jt]sx?)\s/);
|
||
if (vitestSummaryMatch && vitestSummaryMatch[1]) {
|
||
paths.add(vitestSummaryMatch[1]);
|
||
continue;
|
||
}
|
||
// vitest: " × src/__tests__/foo.test.ts > some test name"
|
||
const crossMatch = line.match(/^\s*[×✕✗]\s+(\S+\.(?:test|spec)\.[jt]sx?)\s/);
|
||
if (crossMatch && crossMatch[1]) {
|
||
paths.add(crossMatch[1]);
|
||
}
|
||
}
|
||
return Array.from(paths);
|
||
}
|
||
|
||
/** Parse `git status -z --porcelain` into a Set of paths.
|
||
*
|
||
* Format per entry: `XY <space> <path>\0` where X = staged status, Y =
|
||
* unstaged status. Renames and copies are special: they emit TWO
|
||
* NUL-separated entries, `R <new>\0<old>\0` (or `C <new>\0<old>\0`).
|
||
* We must consume the trailing `<old>` entry without treating it as a
|
||
* separate path, otherwise observability code over-reports "cleared
|
||
* paths" with the historical names of renames. */
|
||
export function parsePorcelainZ(raw: string): Set<string> {
|
||
const paths = new Set<string>();
|
||
const entries = raw.split("\0");
|
||
for (let i = 0; i < entries.length; i++) {
|
||
const entry = entries[i];
|
||
if (!entry) continue;
|
||
if (entry.length < 4) continue;
|
||
const status = entry.slice(0, 2);
|
||
const path = entry.slice(3);
|
||
if (!path) continue;
|
||
paths.add(path);
|
||
// Rename/copy: the very next entry is the old path — skip it so it
|
||
// isn't mistaken for an independent dirty path.
|
||
if (status.charAt(0) === "R" || status.charAt(0) === "C") {
|
||
i++;
|
||
}
|
||
}
|
||
return paths;
|
||
}
|
||
|
||
export function parseShortstatSummary(statsOutput: string): { filesChanged: number; insertions: number; deletions: number } {
|
||
const normalized = statsOutput.trim().replace(/\n/g, " ");
|
||
const filesMatch = normalized.match(/(\d+) files? changed/);
|
||
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
|
||
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
|
||
return {
|
||
filesChanged: filesMatch ? Number.parseInt(filesMatch[1], 10) : 0,
|
||
insertions: insertionsMatch ? Number.parseInt(insertionsMatch[1], 10) : 0,
|
||
deletions: deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0,
|
||
};
|
||
}
|