Files
fusion/packages/engine/src/self-healing-path-utils.ts
gsxdsm 06a5fd813d refactor: package code organization waves 6–7 (#2166)
## Summary

Waves 6–7 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`),
after #2148.

### Wave 6
| New module | Parent |
|---|---|
| `merger-autostash-labels.ts` | `merger.ts` |
| `types/agent-state.ts` | `types.ts` |
| `app/api/tasks-lifecycle.ts` | `legacy.ts` |
| `task-store/task-row-mappers.ts` | `remaining-ops-3.ts` (rename) |

### Wave 7
| New module | Parent |
|---|---|
| `self-healing-optional-step-revision.ts` | `self-healing.ts` |
| `self-healing-path-utils.ts` | `self-healing.ts` |
| `merger-git-parse` (+ `quoteArg`, `getBranchChangedFiles`) |
`merger.ts` |
| `app/api/settings.ts` | `legacy.ts` |

Public import paths stay stable via re-exports.

## Test plan
- [x] engine + dashboard typecheck (incl. app)
- [x] eslint on touched modules
- [x] merger-autostash / parse-porcelain / getBranchChanged / api-tasks
- [ ] CI merge gate

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a dedicated task lifecycle API client (task promotion, merge,
retry/reset/duplicate, pause/unpause, archive/unarchive, revert, plan
approve/reject) plus branch-group and planner oversight actions.
* Added a settings/config API service (effective task settings, update
check/refresh/install).
* Introduced standardized agent lifecycle states with identity/ephemeral
detection helpers.
* **Bug Fixes**
* Improved autostash label compatibility and NUL-delimited changed-file
detection for branch diffs.
* **Refactor**
* Modularized merger labeling/parsing, self-healing helpers, and
lifecycle/type wiring while keeping behavior consistent.
* **Tests**
  * Updated merger verification tests for `git diff -z` output handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 15:42:22 -07:00

78 lines
3.1 KiB
TypeScript

/**
* FNXC:CodeOrganization 2026-07-16-14:00:
* Pure path/error/scope helpers peeled from self-healing.ts.
*/
import type { Task } from "@fusion/core";
export function extractTaskIdFromTempMergeDir(dirname: string): string | null {
const match = /^fusion-ai-merge-(fn-\d+)-[a-z0-9]+$/i.exec(dirname);
return match?.[1]?.toUpperCase() ?? null;
}
export function getErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
export function isTaskNotFoundError(err: unknown): boolean {
return /\btask\s+fn-\d+\s+not found\b/i.test(getErrorMessage(err));
}
export function buildResumeLimboStepSignature(task: Task): string {
return JSON.stringify({
currentStep: task.currentStep ?? null,
steps: Array.isArray(task.steps) ? task.steps.map((step) => step.status) : [],
});
}
export function formatRecoveryTimestamp(date = new Date()): string {
const pad = (value: number) => String(value).padStart(2, "0");
return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;
}
export function matchGlob(path: string, pattern: string): boolean {
if (pattern.includes("**")) {
const regexPattern = pattern
.replace(/\./g, "\\.")
.replace(/\*\*/g, "<<<DOUBLESTAR>>>")
.replace(/\*/g, "[^/]*")
.replace(/<<<DOUBLESTAR>>>/g, ".*");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
const lastSlash = pattern.lastIndexOf("/");
if (lastSlash !== -1) {
const patternDir = pattern.slice(0, lastSlash);
const patternFile = pattern.slice(lastSlash + 1);
const pathDir = path.lastIndexOf("/") !== -1 ? path.slice(0, path.lastIndexOf("/")) : "";
const pathFile = path.lastIndexOf("/") !== -1 ? path.slice(path.lastIndexOf("/")) : path;
if (patternDir.includes("*")) {
const dirRegex = new RegExp(`^${patternDir.replace(/\./g, "\\.").replace(/\*/g, "[^/]*")}$`);
if (!dirRegex.test(pathDir)) return false;
} else if (!pathDir.endsWith(patternDir) && patternDir !== pathDir) {
return false;
}
return matchGlob(pathFile, patternFile);
}
const fileName = path.lastIndexOf("/") !== -1 ? path.slice(path.lastIndexOf("/") + 1) : path;
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, "[^/]*");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(fileName) || regex.test(path);
}
export function matchesScope(filePath: string, scopePatterns: string[]): boolean {
for (const pattern of scopePatterns) {
if (matchGlob(filePath, pattern)) return true;
const dirPattern = pattern.replace(/\/\*+$/, "");
if (dirPattern !== pattern && filePath.startsWith(dirPattern + "/")) return true;
if (pattern.endsWith("/") && filePath.startsWith(pattern)) return true;
const patternDir = pattern.lastIndexOf("/") >= 0 ? pattern.slice(0, pattern.lastIndexOf("/")) : "";
const fileDir = filePath.lastIndexOf("/") >= 0 ? filePath.slice(0, filePath.lastIndexOf("/")) : "";
if (patternDir && fileDir === patternDir) return true;
}
return false;
}