Merge remote-tracking branch 'origin/main'

# Conflicts:
#	lib/crates/fabro-server/src/server.rs
This commit is contained in:
Bryan Helmkamp 2026-04-19 19:54:30 -04:00
commit cd9ff8fafe
No known key found for this signature in database
52 changed files with 9082 additions and 3338 deletions

4
Cargo.lock generated
View file

@ -1931,6 +1931,7 @@ name = "fabro-server"
version = "0.208.0-nightly.1"
dependencies = [
"anyhow",
"async-trait",
"axum",
"axum-extra",
"base64",
@ -1963,6 +1964,7 @@ dependencies = [
"fabro-vault",
"fabro-workflow",
"futures-util",
"globset",
"hex",
"hmac",
"http-body-util",
@ -1983,11 +1985,13 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
"toml 0.8.23",
"toml_edit",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
"ulid",
"uuid",
"walkdir",

View file

@ -46,6 +46,7 @@ walkdir = "2"
regex = "1"
semver = "1"
aho-corasick = "1"
globset = "0.4"
dirs = "6"
mac_address = "1"
md5 = "0.7"

View file

@ -11,6 +11,7 @@ import type { PreviewUrlResponse } from "@qltysh/fabro-api-client";
const allTabs = [
{ name: "Overview", path: "", count: null, demoOnly: false },
{ name: "Stages", path: "/stages", count: null, demoOnly: false },
{ name: "Files Changed", path: "/files", count: null, demoOnly: false },
{ name: "Graph", path: "/graph", count: null, demoOnly: false },
{ name: "Billing", path: "/billing", count: null, demoOnly: false },
];

View file

@ -0,0 +1,178 @@
import { afterEach, describe, expect, test } from "bun:test";
import { extractRequestId, loader } from "./run-files";
type StubResponseInit = {
status: number;
body?: string;
headers?: Record<string, string>;
};
function stubFetchOnce(init: StubResponseInit) {
const original = globalThis.fetch;
globalThis.fetch = (() => {
const response = new Response(init.body ?? "", {
status: init.status,
headers: init.headers,
// `Response` constructor derives statusText from status for known
// codes; providing it explicitly keeps tests deterministic across
// engines that disagree on the default message.
statusText: init.status === 500 ? "Internal Server Error" : "",
});
return Promise.resolve(response);
}) as typeof fetch;
return () => {
globalThis.fetch = original;
};
}
describe("extractRequestId", () => {
test("reads `request_id` from the top level of the error body", () => {
expect(extractRequestId({ request_id: "abc-123" })).toBe("abc-123");
});
test("reads `request_id` from errors[0] under the uniform envelope", () => {
expect(
extractRequestId({
errors: [
{ status: "500", title: "Internal", request_id: "evt_42" },
],
}),
).toBe("evt_42");
});
test("parses `Request ID: xyz` out of errors[0].detail", () => {
expect(
extractRequestId({
errors: [
{
status: "500",
title: "Internal Server Error",
detail: "Run files failed. Request ID: req_999 on shard 2.",
},
],
}),
).toBe("req_999");
});
test("returns null for bodies without any request_id", () => {
expect(extractRequestId(null)).toBe(null);
expect(extractRequestId(undefined)).toBe(null);
expect(extractRequestId("not an object")).toBe(null);
expect(extractRequestId({ errors: [] })).toBe(null);
expect(extractRequestId({ errors: [{ detail: "no id in here" }] })).toBe(
null,
);
});
test("handles request_id values with hyphens and underscores", () => {
expect(
extractRequestId({
errors: [
{ detail: "Something failed. request_id: RX-1A_2B-3C4D" },
],
}),
).toBe("RX-1A_2B-3C4D");
});
});
describe("loader", () => {
let restoreFetch: (() => void) | undefined;
afterEach(() => {
restoreFetch?.();
restoreFetch = undefined;
});
// Bun's Request constructor needs a URL; tests use a relative path
// because the loader only reads `request?.signal`.
const dummyRequest = { signal: undefined } as any;
const dummyParams = { id: "01ARZ3NDEKTSV4RRFFQ69G5FAV" };
test("200 OK returns { data, error: null } with parsed envelope", async () => {
const envelope = {
data: [],
meta: { truncated: false, total_changed: 0 },
};
restoreFetch = stubFetchOnce({
status: 200,
body: JSON.stringify(envelope),
});
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.error).toBeNull();
expect(result.data).toEqual(envelope);
});
test("404 returns empty-envelope signal { data: null, error: null }", async () => {
restoreFetch = stubFetchOnce({ status: 404 });
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.data).toBeNull();
expect(result.error).toBeNull();
});
test("501 returns empty-envelope signal { data: null, error: null }", async () => {
restoreFetch = stubFetchOnce({ status: 501 });
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.data).toBeNull();
expect(result.error).toBeNull();
});
test("500 populates error.requestId from the uniform error envelope", async () => {
restoreFetch = stubFetchOnce({
status: 500,
body: JSON.stringify({
errors: [
{
status: "500",
title: "Internal Server Error",
detail: "Run files materialization panicked.",
request_id: "req_deadbeef",
},
],
}),
});
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.data).toBeNull();
expect(result.error).not.toBeNull();
expect(result.error!.status).toBe(500);
expect(result.error!.requestId).toBe("req_deadbeef");
});
test("500 with no request_id leaves error.requestId as null", async () => {
restoreFetch = stubFetchOnce({
status: 500,
body: JSON.stringify({
errors: [{ status: "500", title: "Internal", detail: "whoops" }],
}),
});
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.error).not.toBeNull();
expect(result.error!.status).toBe(500);
expect(result.error!.requestId).toBeNull();
});
test("500 with non-JSON body still surfaces the status", async () => {
restoreFetch = stubFetchOnce({ status: 500, body: "<html>oops</html>" });
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.error).not.toBeNull();
expect(result.error!.status).toBe(500);
expect(result.error!.requestId).toBeNull();
});
test("503 populates error without requestId", async () => {
restoreFetch = stubFetchOnce({
status: 503,
body: JSON.stringify({ errors: [{ detail: "rate limited" }] }),
});
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.error).not.toBeNull();
expect(result.error!.status).toBe(503);
});
test("401 still surfaces as an error (no in-loader redirect)", async () => {
restoreFetch = stubFetchOnce({ status: 401 });
const result = await loader({ request: dummyRequest, params: dummyParams });
expect(result.error).not.toBeNull();
expect(result.error!.status).toBe(401);
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test";
import { isEditableElement } from "./keyboard";
// Lightweight stand-in for the DOM `Element` API. We only need `tagName`
// and `isContentEditable` for `isEditableElement`.
function mk(tagName: string, isContentEditable = false): Element {
return {
tagName: tagName.toUpperCase(),
isContentEditable,
} as unknown as Element;
}
describe("isEditableElement", () => {
test("flags input / textarea / select as editable", () => {
expect(isEditableElement(mk("input"))).toBe(true);
expect(isEditableElement(mk("textarea"))).toBe(true);
expect(isEditableElement(mk("select"))).toBe(true);
});
test("flags contenteditable elements as editable", () => {
expect(isEditableElement(mk("div", true))).toBe(true);
expect(isEditableElement(mk("span", true))).toBe(true);
});
test("non-editable elements are left alone", () => {
expect(isEditableElement(mk("div"))).toBe(false);
expect(isEditableElement(mk("button"))).toBe(false);
expect(isEditableElement(mk("a"))).toBe(false);
});
test("null never claims editable", () => {
expect(isEditableElement(null)).toBe(false);
});
test("case-insensitive on tag name", () => {
expect(isEditableElement(mk("Input"))).toBe(true);
expect(isEditableElement(mk("TEXTAREA"))).toBe(true);
});
});

View file

@ -0,0 +1,59 @@
import type { RefObject } from "react";
import { useEffect } from "react";
export function isEditableElement(el: Element | null): boolean {
if (!el) return false;
const tag = el.tagName.toLowerCase();
if (tag === "input" || tag === "textarea" || tag === "select") return true;
return (el as HTMLElement).isContentEditable === true;
}
/**
* Wire keyboard navigation across file rows. `j` / `k` move focus to the
* next / previous row. Key presses while a text-editable element is focused
* are left alone so typing into filters / comment boxes isn't hijacked.
*
* Enter/Space are deliberately not bound — @pierre/diffs 1.1.x has no
* imperative expand/collapse API, and firing a click on the outer wrapper
* has no effect. Per-hunk expansion remains mouse-driven via pierre's own
* controls. Files targeted by a deep-link get `expandUnchanged: true` via
* per-file options instead.
*/
export function useFileKeyboardNav(
containerRef: RefObject<HTMLDivElement | null>,
fileCount: number,
) {
useEffect(() => {
if (!containerRef.current) return;
const onKey = (event: KeyboardEvent) => {
if (event.key !== "j" && event.key !== "k") return;
if (event.metaKey || event.ctrlKey || event.altKey) return;
if (isEditableElement(document.activeElement)) return;
const container = containerRef.current;
if (!container) return;
const rows = Array.from(
container.querySelectorAll<HTMLElement>('[data-run-file-row="true"]'),
);
if (rows.length === 0) return;
const active = document.activeElement as HTMLElement | null;
const currentIdx = rows.findIndex((row) => row.contains(active));
let nextIdx: number;
if (currentIdx < 0) {
nextIdx = 0;
} else {
nextIdx = event.key === "j" ? currentIdx + 1 : currentIdx - 1;
}
if (nextIdx < 0 || nextIdx >= rows.length) return;
event.preventDefault();
const target = rows[nextIdx];
target.focus({ preventScroll: false });
target.scrollIntoView({ block: "nearest", behavior: "smooth" });
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
// fileCount drives re-attachment so rows picked up after data changes
// stay addressable without stale references.
}, [containerRef, fileCount]);
}

View file

@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test";
import { MultiFileDiff, PatchDiff, Virtualizer } from "@pierre/diffs/react";
// Regression coverage for the @pierre/diffs 1.0 -> 1.1 upgrade. We assert
// only that the public React components the Run Files route uses remain
// exported as callable function components — a full mount-under-test hits
// pierre's useLayoutEffect teardown path, which is incompatible with
// react-test-renderer under React 19. Functional mount coverage lives in
// the dev-server smoke check.
describe("@pierre/diffs public API", () => {
test("MultiFileDiff is a callable component export", () => {
expect(typeof MultiFileDiff).toBe("function");
});
test("PatchDiff is a callable component export", () => {
expect(typeof PatchDiff).toBe("function");
});
test("Virtualizer is a callable component export", () => {
expect(typeof Virtualizer).toBe("function");
});
});

View file

@ -0,0 +1,154 @@
import { describe, expect, test } from "bun:test";
import TestRenderer from "react-test-renderer";
import {
bannerCopyForReason,
BinaryPlaceholder,
DegradedBanner,
pickPlaceholder,
SensitivePlaceholder,
SymlinkOrSubmodulePlaceholder,
TruncatedPlaceholder,
} from "./placeholders";
import type { FileDiff as ApiFileDiff } from "@qltysh/fabro-api-client";
/**
* Render the element and return a flat string of every text leaf the tree
* produced, for substring assertions. Wraps in `TestRenderer.act` to keep
* React 19 from unmounting the tree before we can walk it.
*/
function renderedText(element: React.ReactElement): string {
let tree: TestRenderer.ReactTestRenderer | undefined;
TestRenderer.act(() => {
tree = TestRenderer.create(element);
});
const parts: string[] = [];
tree!.root.findAll((node) => {
const children = Array.isArray(node.children) ? node.children : [];
for (const child of children) {
if (typeof child === "string") parts.push(child);
}
return false;
});
return parts.join(" ");
}
function baseFile(overrides: Partial<ApiFileDiff> = {}): ApiFileDiff {
return {
old_file: { name: "src/foo.rs", contents: "" },
new_file: { name: "src/foo.rs", contents: "" },
change_kind: "modified",
...overrides,
} as ApiFileDiff;
}
describe("pickPlaceholder priority", () => {
test("sensitive > binary > symlink > truncated", () => {
const all = baseFile({
sensitive: true,
binary: true,
change_kind: "symlink",
truncated: true,
});
const text = renderedText(pickPlaceholder(all)!);
expect(text).toContain("sensitive");
expect(text).not.toContain("binary");
});
test("binary flags take priority over symlink/submodule and truncated", () => {
const bin = baseFile({
binary: true,
change_kind: "symlink",
truncated: true,
});
const text = renderedText(pickPlaceholder(bin)!);
expect(text).toContain("binary");
expect(text).not.toContain("symlink");
});
test("symlink/submodule take priority over truncated", () => {
const link = baseFile({ change_kind: "symlink", truncated: true });
const text = renderedText(pickPlaceholder(link)!);
expect(text).toContain("symlink");
expect(text).not.toContain("too large");
});
test("truncated fires last and respects reason", () => {
const tooLarge = baseFile({
truncated: true,
truncation_reason: "file_too_large",
});
expect(renderedText(pickPlaceholder(tooLarge)!)).toContain("too large");
const budget = baseFile({
truncated: true,
truncation_reason: "budget_exhausted",
});
expect(renderedText(pickPlaceholder(budget)!)).toContain("too many files");
});
test("plain modified file gets no placeholder", () => {
expect(pickPlaceholder(baseFile())).toBeNull();
});
});
describe("bannerCopyForReason", () => {
test("each known reason gets distinct copy", () => {
const a = bannerCopyForReason("sandbox_gone");
const b = bannerCopyForReason("provider_unsupported");
const c = bannerCopyForReason("sandbox_unreachable");
expect(a).not.toBe(b);
expect(b).not.toBe(c);
expect(a).toContain("cleaned up");
expect(b).toContain("provider");
expect(c).toContain("refresh");
});
test("unknown / undefined reason falls back to sandbox_unreachable copy", () => {
expect(bannerCopyForReason(undefined)).toBe(
bannerCopyForReason("sandbox_unreachable"),
);
expect(bannerCopyForReason("made-up")).toBe(
bannerCopyForReason("sandbox_unreachable"),
);
});
});
describe("placeholder rendering", () => {
test("SensitivePlaceholder includes file name", () => {
expect(
renderedText(<SensitivePlaceholder name=".env.production" />),
).toContain(".env.production");
});
test("BinaryPlaceholder includes file name", () => {
const text = renderedText(<BinaryPlaceholder name="assets/logo.png" />);
expect(text).toContain("assets/logo.png");
expect(text).toContain("binary");
});
test("TruncatedPlaceholder maps reason to copy", () => {
expect(
renderedText(
<TruncatedPlaceholder name="big.rs" reason="budget_exhausted" />,
),
).toContain("too many files");
expect(renderedText(<TruncatedPlaceholder name="big.rs" />)).toContain(
"too large",
);
});
test("SymlinkOrSubmodulePlaceholder shows the kind", () => {
expect(
renderedText(
<SymlinkOrSubmodulePlaceholder name="lnk" kind="submodule" />,
),
).toContain("submodule");
});
test("DegradedBanner picks copy based on reason", () => {
expect(renderedText(<DegradedBanner reason="sandbox_gone" />)).toContain(
"cleaned up",
);
});
});

View file

@ -0,0 +1,125 @@
import type { ReactElement } from "react";
import type {
FileDiff as ApiFileDiff,
RunFilesMetaDegradedReasonEnum,
} from "@qltysh/fabro-api-client";
const PLACEHOLDER_CLASSES =
"flex items-center justify-between rounded-md border border-line bg-panel/60 px-4 py-3 text-sm text-fg-muted";
export function SensitivePlaceholder({ name }: { name: string }) {
return (
<div className={PLACEHOLDER_CLASSES}>
<span className="font-mono text-fg-2">{name}</span>
<span className="rounded bg-rose-950/40 px-2 py-0.5 text-xs text-rose-200">
sensitive — contents omitted
</span>
</div>
);
}
export function BinaryPlaceholder({ name }: { name: string }) {
return (
<div className={PLACEHOLDER_CLASSES}>
<span className="font-mono text-fg-2">{name}</span>
<span className="rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3">
binary — not shown inline
</span>
</div>
);
}
export function TruncatedPlaceholder({
name,
reason,
}: {
name: string;
reason?: string;
}) {
const label =
reason === "budget_exhausted"
? "omitted — too many files changed"
: "too large to render inline";
return (
<div className={PLACEHOLDER_CLASSES}>
<span className="font-mono text-fg-2">{name}</span>
<span className="rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3">
{label}
</span>
</div>
);
}
export function SymlinkOrSubmodulePlaceholder({
name,
kind,
}: {
name: string;
kind: "symlink" | "submodule";
}) {
return (
<div className={PLACEHOLDER_CLASSES}>
<span className="font-mono text-fg-2">{name}</span>
<span className="rounded bg-panel-alt/80 px-2 py-0.5 text-xs text-fg-3">
{kind}
</span>
</div>
);
}
/// Render the highest-priority placeholder for a file, or `null` if the file
/// should render as a normal diff. Priority order is:
/// sensitive > binary > symlink/submodule > truncated
/// Security flags must never be hidden by a lesser placeholder.
export function pickPlaceholder(file: ApiFileDiff): ReactElement | null {
const displayName = file.new_file.name || file.old_file.name;
if (file.sensitive) {
return <SensitivePlaceholder name={displayName} />;
}
if (file.binary) {
return <BinaryPlaceholder name={displayName} />;
}
if (file.change_kind === "symlink") {
return <SymlinkOrSubmodulePlaceholder name={displayName} kind="symlink" />;
}
if (file.change_kind === "submodule") {
return (
<SymlinkOrSubmodulePlaceholder name={displayName} kind="submodule" />
);
}
if (file.truncated) {
return (
<TruncatedPlaceholder
name={displayName}
reason={file.truncation_reason}
/>
);
}
return null;
}
export function DegradedBanner({
reason,
}: {
reason?: RunFilesMetaDegradedReasonEnum;
}) {
return (
<div className="rounded-md border border-amber-500/30 bg-amber-950/20 px-4 py-3 text-sm text-amber-100">
{bannerCopyForReason(reason)}
</div>
);
}
export function bannerCopyForReason(
reason: RunFilesMetaDegradedReasonEnum | undefined | string,
): string {
switch (reason) {
case "sandbox_gone":
return "Showing final patch only. This run's sandbox has been cleaned up, so individual file contents are no longer available.";
case "provider_unsupported":
return "Live diff isn't supported for this sandbox provider. Showing the patch captured at the last checkpoint.";
case "sandbox_unreachable":
default:
return "Couldn't reach this run's sandbox. Showing the patch captured at the last checkpoint — refresh to try again.";
}
}

View file

@ -0,0 +1,192 @@
import { describe, expect, test } from "bun:test";
import TestRenderer from "react-test-renderer";
import { RunStatus } from "@qltysh/fabro-api-client";
import {
deriveEmptyKind,
emptyStateCopy,
EmptyState,
InlineErrorBanner,
LoadingSkeleton,
Toast,
} from "./states";
function renderToJson(element: React.ReactElement): any {
return TestRenderer.create(element).toJSON();
}
describe("deriveEmptyKind", () => {
// Pre-work states → R4(a) "starting"
test.each(["submitted", "Submitted", "starting", "queued"])(
"%s maps to R4(a) 'starting'",
(status) => {
expect(
deriveEmptyKind({
runStatus: status,
totalChanged: 0,
degraded: false,
}),
).toBe("starting");
},
);
// Actively-in-progress states → R4(b) "no_changes yet"
test.each(["running", "blocked", "paused"])(
"%s with no files yet is R4(b) 'no_changes'",
(status) => {
expect(
deriveEmptyKind({
runStatus: status,
totalChanged: 0,
degraded: false,
}),
).toBe("no_changes");
},
);
// Terminal-failure states → R4(c1) when no degraded patch available
test.each(["failed", "dead"])(
"%s without degraded fallback is R4(c1) 'failed_before_checkpoint'",
(status) => {
expect(
deriveEmptyKind({
runStatus: status,
totalChanged: 0,
degraded: false,
}),
).toBe("failed_before_checkpoint");
},
);
// Terminal-success + teardown states → R4(b) or R4(c2) depending on
// whether files were ever changed
test.each(["succeeded", "removing", "archived"])(
"%s with changes but no data is R4(c2) 'diff_lost'",
(status) => {
expect(
deriveEmptyKind({
runStatus: status,
totalChanged: 3,
degraded: false,
}),
).toBe("diff_lost");
},
);
test.each(["succeeded", "removing", "archived"])(
"%s with no changes is R4(b)",
(status) => {
expect(
deriveEmptyKind({
runStatus: status,
totalChanged: 0,
degraded: false,
}),
).toBe("no_changes");
},
);
test("missing runStatus collapses to 'unknown'", () => {
expect(
deriveEmptyKind({
runStatus: undefined,
totalChanged: 0,
degraded: false,
}),
).toBe("unknown");
});
test("unknown future status collapses to 'unknown'", () => {
expect(
deriveEmptyKind({
runStatus: "some_future_state",
totalChanged: 0,
degraded: false,
}),
).toBe("unknown");
});
test("every documented RunStatus gets a non-unknown empty kind", () => {
// Regression guard sourced from the generated API client enum so any
// new RunStatus added to the OpenAPI spec fails this test until the
// decision table grows a branch. Without this guard, unhandled
// statuses silently render as "unknown" ("not available right now") —
// misleading copy for e.g. a paused or archived run.
for (const status of Object.values(RunStatus)) {
const result = deriveEmptyKind({
runStatus: status,
totalChanged: 0,
degraded: false,
});
expect(result).not.toBe("unknown");
}
});
});
describe("emptyStateCopy", () => {
test("every kind resolves to distinct non-empty copy", () => {
const seen = new Set<string>();
for (const kind of [
"starting",
"no_changes",
"failed_before_checkpoint",
"diff_lost",
"unknown",
] as const) {
const c = emptyStateCopy(kind);
expect(c.length).toBeGreaterThan(0);
expect(seen.has(c)).toBe(false);
seen.add(c);
}
});
});
describe("component rendering", () => {
test("EmptyState wraps message in role=status", () => {
let tree: TestRenderer.ReactTestRenderer | undefined;
TestRenderer.act(() => {
tree = TestRenderer.create(<EmptyState kind="starting" />);
});
const statusEl = tree!.root.findAll(
(node) => node.type === "div" && node.props?.role === "status",
);
expect(statusEl.length).toBeGreaterThan(0);
});
test("LoadingSkeleton has aria-label", () => {
let tree: TestRenderer.ReactTestRenderer | undefined;
TestRenderer.act(() => {
tree = TestRenderer.create(<LoadingSkeleton />);
});
const labeled = tree!.root.findAll(
(node) => node.props?.["aria-label"] === "Loading files",
);
expect(labeled.length).toBeGreaterThan(0);
});
test("InlineErrorBanner fires onRetry when clicked", () => {
let clicked = 0;
let tree: TestRenderer.ReactTestRenderer | undefined;
TestRenderer.act(() => {
tree = TestRenderer.create(
<InlineErrorBanner message="503" onRetry={() => (clicked += 1)} />,
);
});
const button = tree!.root.findByType("button");
TestRenderer.act(() => {
button.props.onClick();
});
expect(clicked).toBe(1);
});
test("Toast renders its children in an aria-live region", () => {
let tree: TestRenderer.ReactTestRenderer | undefined;
TestRenderer.act(() => {
tree = TestRenderer.create(<Toast>hello</Toast>);
});
const live = tree!.root.findAll(
(node) => node.props?.["aria-live"] === "polite",
);
expect(live.length).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,239 @@
import { isRouteErrorResponse, useRouteError } from "react-router";
/**
* R4 empty-state taxonomy. See plan § Unit 11:
* - `starting` (R4a): run still spinning up, no base_sha yet
* - `no_changes` (R4b): run completed but touched no files
* - `failed_before_checkpoint` (R4c1): failed run without captured diff
* - `diff_lost` (R4c2): succeeded run whose diff is no longer recoverable
* - `unknown`: fallback — loader returned null (404/501/other)
*/
export type EmptyKind =
| "starting"
| "no_changes"
| "failed_before_checkpoint"
| "diff_lost"
| "unknown";
export function EmptyState({ kind }: { kind: EmptyKind }) {
return (
<div
role="status"
className="rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted"
>
{emptyStateCopy(kind)}
</div>
);
}
export function emptyStateCopy(kind: EmptyKind): string {
switch (kind) {
case "starting":
return "Run is still starting. Files will appear once it begins.";
case "no_changes":
return "This run didn't change any files.";
case "failed_before_checkpoint":
return "This run failed before capturing any changes.";
case "diff_lost":
return "The diff for this run is no longer available. If you expect files here, please report it.";
case "unknown":
default:
return "The diff for this run is not available right now.";
}
}
/// Derive the empty-state variant from the full loader context. `runStatus`
/// comes from the parent run loader (`run.lifecycleStatus`); its absence
/// collapses to the "unknown" catchall so the empty state never displays
/// misleading copy.
///
/// The full RunStatus enum (per fabro-types/src/status.rs) is:
/// submitted, queued, starting, running, blocked, paused, removing,
/// succeeded, failed, dead, archived
/// partial_success is a stage status, not a run status.
export function deriveEmptyKind(args: {
runStatus: string | undefined;
totalChanged: number;
degraded: boolean;
}): EmptyKind {
const { runStatus, totalChanged, degraded } = args;
if (!runStatus) {
return "unknown";
}
const s = runStatus.toLowerCase();
// Pre-work states: run has no base_sha / hasn't started producing a diff.
if (s === "submitted" || s === "queued" || s === "starting") {
return "starting";
}
// Actively-in-progress states: the run is running but just hasn't
// changed any files yet. Avoid alarmist "diff lost" copy here — the
// user may refresh and see files appear.
if (s === "running" || s === "blocked" || s === "paused") {
return "no_changes";
}
// Terminal-failure states: Failed and Dead both mean the run stopped
// without a clean conclusion. If the degraded-fallback branch also
// couldn't surface a patch, we never captured a diff at all.
if (s === "failed" || s === "dead") {
return degraded ? "unknown" : "failed_before_checkpoint";
}
// Terminal-success, teardown, and archive states: the run ran to
// completion (possibly long ago). Distinguish "had no changes" (R4b)
// from "diff captured then lost" (R4c2) via total_changed.
if (s === "succeeded" || s === "removing" || s === "archived") {
if (degraded) {
// We have a patch; the component renders PatchDiff instead of an
// empty state. Shouldn't reach here in practice.
return "unknown";
}
return totalChanged > 0 ? "diff_lost" : "no_changes";
}
// Unknown future status — fail conservative.
return "unknown";
}
export function LoadingSkeleton() {
return (
<div className="flex flex-col gap-3" aria-label="Loading files">
<div className="h-8 rounded-md bg-panel/60 motion-safe:animate-pulse" />
<div className="h-32 rounded-md bg-panel/60 motion-safe:animate-pulse" />
<div className="h-32 rounded-md bg-panel/60 motion-safe:animate-pulse" />
</div>
);
}
export function InlineErrorBanner({
message,
onRetry,
}: {
message: string;
onRetry: () => void;
}) {
return (
<div className="flex items-center justify-between gap-3 rounded-md border border-rose-500/30 bg-rose-950/20 px-4 py-3 text-sm text-rose-100">
<span>{message}</span>
<button
type="button"
onClick={onRetry}
className="min-h-[32px] rounded-md border border-rose-500/40 bg-rose-950/40 px-3 py-1 text-xs font-medium text-rose-50 transition-colors hover:bg-rose-950/60"
>
Retry
</button>
</div>
);
}
export function Toast({ children }: { children: React.ReactNode }) {
return (
<div
role="status"
aria-live="polite"
className="pointer-events-none fixed bottom-6 right-6 z-50 rounded-md border border-line bg-panel/95 px-3 py-2 text-xs text-fg-2 shadow-lg"
>
{children}
</div>
);
}
/**
* Shared helper for rendering the documented status-code taxonomy. Consumed
* by both the inline `initialError` branch in run-files.tsx and the
* `RunFilesErrorBoundary` below — keeps the copy in one place so updates
* don't drift between the two surfaces.
*/
export function renderStatusError(args: {
status: number;
requestId: string | null;
onRetry: () => void;
}): React.ReactElement {
const { status, requestId, onRetry } = args;
if (status === 401 || status === 403) {
return (
<div
role="status"
className="rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted"
>
You don't have access to this run's files.
</div>
);
}
if (status === 429 || status === 503) {
return (
<InlineErrorBanner
message="The diff service is temporarily unavailable."
onRetry={onRetry}
/>
);
}
if (status >= 500) {
const suffix = requestId ? ` Request ID: ${requestId}.` : "";
return (
<div
role="status"
className="rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted"
>
Something went wrong.{suffix} Please contact support if this persists.
</div>
);
}
return (
<InlineErrorBanner
message={`Couldn't load files (${status}).`}
onRetry={onRetry}
/>
);
}
/**
* Route-level ErrorBoundary for render-time React errors. The Files loader
* no longer throws (it returns errors in-band via RunFilesLoaderResult), so
* this only fires for React render crashes.
*/
export function RunFilesErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return renderStatusError({
status: error.status,
requestId: extractRequestIdFromUnknown(error.data),
onRetry: () => window.location.reload(),
});
}
return (
<div
role="status"
className="rounded-md border border-dashed border-line bg-panel/40 px-6 py-10 text-center text-sm text-fg-muted"
>
Something went wrong loading this run's files.
</div>
);
}
/**
* Request-ID parser used only by the ErrorBoundary path. The loader path
* already extracts request_id into `RunFilesLoaderResult.error.requestId`
* via `run-files.tsx::extractRequestId` — this is the body shape
* react-router hands us in `useRouteError().data` for non-Response errors.
*/
function extractRequestIdFromUnknown(body: unknown): string | null {
if (!body || typeof body !== "object") return null;
const b = body as Record<string, unknown>;
if (typeof b.request_id === "string") return b.request_id;
const errors = b.errors;
if (Array.isArray(errors) && errors.length > 0) {
const first = errors[0];
if (first && typeof first === "object") {
const rec = first as Record<string, unknown>;
if (typeof rec.request_id === "string") return rec.request_id;
if (typeof rec.detail === "string") {
const match = rec.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i);
if (match) return match[1];
}
}
}
return null;
}

View file

@ -0,0 +1,99 @@
import type { RefObject } from "react";
export type DiffStyle = "split" | "unified";
export function Toolbar({
onRefresh,
refreshing,
refreshDisabled,
freshness,
refreshButtonRef,
diffStyle,
onDiffStyleChange,
diffStyleForced,
}: {
onRefresh: () => void;
refreshing: boolean;
/** True when the server has nothing new to show (to_sha unchanged). */
refreshDisabled: boolean;
freshness: string | null;
refreshButtonRef?: RefObject<HTMLButtonElement | null>;
diffStyle: DiffStyle;
onDiffStyleChange: (style: DiffStyle) => void;
/**
* True when the md breakpoint has forced unified view — the toggle
* reflects the forced state but saving it would stomp the user's
* desktop preference, so the parent keeps persistence off while
* `diffStyleForced` is true.
*/
diffStyleForced: boolean;
}) {
const disabled = refreshing || refreshDisabled;
return (
<div className="flex items-center justify-between gap-3 rounded-md border border-line bg-panel/40 px-3 py-2 text-xs text-fg-muted">
<div className="flex items-center gap-3">
<span aria-live="polite" className="min-w-0 truncate">
{freshness ?? "\u00A0"}
</span>
</div>
<div className="flex items-center gap-2">
<DiffStyleToggle
value={diffStyle}
onChange={onDiffStyleChange}
forced={diffStyleForced}
/>
<button
ref={refreshButtonRef}
type="button"
onClick={onRefresh}
disabled={disabled}
aria-label={refreshing ? "Refreshing files" : "Refresh files"}
className="min-h-[44px] min-w-[44px] rounded-md border border-line bg-panel px-3 py-1 text-xs font-medium text-fg-2 transition-colors hover:bg-overlay disabled:opacity-60"
>
{refreshing ? "Refreshing…" : "Refresh"}
</button>
</div>
</div>
);
}
function DiffStyleToggle({
value,
onChange,
forced,
}: {
value: DiffStyle;
onChange: (style: DiffStyle) => void;
forced: boolean;
}) {
const btn =
"min-h-[44px] rounded-md border border-line px-3 py-1 text-xs font-medium transition-colors disabled:opacity-60";
const active = "bg-overlay text-fg-1";
const inactive = "bg-panel text-fg-2 hover:bg-overlay";
return (
<div
className="flex items-center gap-1"
role="group"
aria-label="Diff layout"
>
<button
type="button"
onClick={() => onChange("split")}
disabled={forced}
aria-pressed={value === "split"}
className={`${btn} ${value === "split" ? active : inactive}`}
>
Split
</button>
<button
type="button"
onClick={() => onChange("unified")}
disabled={forced}
aria-pressed={value === "unified"}
className={`${btn} ${value === "unified" ? active : inactive}`}
>
Unified
</button>
</div>
);
}

View file

@ -1,816 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "arc-web",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"@pierre/diffs": "^1.0.11",
"@react-router/node": "7.12.0",
"@react-router/serve": "7.12.0",
"@viz-js/viz": "^3.24.0",
"isbot": "^5.1.31",
"jose": "^6.1.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router": "7.12.0",
},
"devDependencies": {
"@react-router/dev": "7.12.0",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"openapi-typescript": "^7",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
"@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
"@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
"@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@headlessui/react": ["@headlessui/react@2.2.9", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ=="],
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
"@pierre/diffs": ["@pierre/diffs@1.0.11", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw=="],
"@react-aria/focus": ["@react-aria/focus@3.21.4", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-6gz+j9ip0/vFRTKJMl3R30MHopn4i19HqqLfSQfElxJD+r9hBnYG1Q6Wd/kl/WRR1+CALn2F+rn06jUnf5sT8Q=="],
"@react-aria/interactions": ["@react-aria/interactions@3.27.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA=="],
"@react-aria/ssr": ["@react-aria/ssr@3.9.10", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ=="],
"@react-aria/utils": ["@react-aria/utils@3.33.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-yvz7CMH8d2VjwbSa5nGXqjU031tYhD8ddax95VzJsHSPyqHDEGfxul8RkhGV6oO7bVqZxVs6xY66NIgae+FHjw=="],
"@react-router/dev": ["@react-router/dev@7.12.0", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.12.0", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.2.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.12.0", "@vitejs/plugin-rsc": "~0.5.7", "react-router": "^7.12.0", "react-server-dom-webpack": "^19.2.3", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "react-server-dom-webpack", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-5GpwXgq4pnOVeG7l6ADkCHA1rthJus1q/A3NRYJAIypclUQDYAzg1/fDNjvaKuTSrq+Nr3u6aj2v+oC+47MX6g=="],
"@react-router/express": ["@react-router/express@7.12.0", "", { "dependencies": { "@react-router/node": "7.12.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.12.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-uAK+zF93M6XauGeXLh/UBh+3HrwiA/9lUS+eChjQ0a5FzjLpsc6ciUqF5oHh3lwWzLU7u7tj4qoeucUn6SInTw=="],
"@react-router/node": ["@react-router/node@7.12.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.12.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-o/t10Cse4LK8kFefqJ8JjC6Ng6YuKD2I87S2AiJs17YAYtXU5W731ZqB73AWyCDd2G14R0dSuqXiASRNK/xLjg=="],
"@react-router/serve": ["@react-router/serve@7.12.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.12.0", "@react-router/node": "7.12.0", "compression": "^1.8.1", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.1", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.12.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-j1ltgU7s3wAwOosZ5oxgHSsmVyK706gY/yIs8qVmC239wQ3zr3eqaXk3TVVLMeRy+eDgPNmgc6oNJv2o328VgA=="],
"@react-stately/flags": ["@react-stately/flags@3.1.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="],
"@react-stately/utils": ["@react-stately/utils@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw=="],
"@react-types/shared": ["@react-types/shared@3.33.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw=="],
"@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js-replace": "^1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
"@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.8", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.0", "minimatch": "5.1.6", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-/oy8sXi5xK4+HvcFR7xS1A74zJ5aS6rtacNEr91SczLqvtxz79opxp7yYpdYtfGQAp6igx9eU9alwSC/R/X3Cg=="],
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.9.0", "", {}, "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
"@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
"@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
"@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
"@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.19", "", { "dependencies": { "@tanstack/virtual-core": "3.13.19" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.19", "", {}, "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/node": ["@types/node@22.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@viz-js/viz": ["@viz-js/viz@3.24.0", "", {}, "sha512-sTRz2cFN6PwICVC7GVlF2aYNAZ/LwF7Y1mp3B+8LXQ7otyHrzQYSzNBwJ4lctL/aS3x97ppw59z6zIW+wPs6bQ=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-port": ["get-port@5.1.1", "", {}, "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
"openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", "parse-json": "^8.3.0", "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="],
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
"parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
"react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
}
}

View file

@ -14,7 +14,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"@pierre/diffs": "^1.0.11",
"@pierre/diffs": "1.1.15",
"@qltysh/fabro-api-client": "workspace:*",
"@tailwindcss/typography": "^0.5.19",
"@viz-js/viz": "^3.24.0",

View file

@ -11,7 +11,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"@pierre/diffs": "^1.0.11",
"@pierre/diffs": "1.1.15",
"@qltysh/fabro-api-client": "workspace:*",
"@tailwindcss/typography": "^0.5.19",
"@viz-js/viz": "^3.24.0",
@ -314,7 +314,9 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
"@pierre/diffs": ["@pierre/diffs@1.0.11", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw=="],
"@pierre/diffs": ["@pierre/diffs@1.1.15", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-Gj863E+aSpc0H3C4cH0fQTaF/tP9yYfhnilR7/dS72qq8thqNpR3fo3jURHRtRKz6KJJ10anxcurHP7b3ZUQkw=="],
"@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="],
"@qltysh/fabro-api-client": ["@qltysh/fabro-api-client@workspace:lib/packages/fabro-api-client"],

View file

@ -180,3 +180,16 @@ The domain event enums (`AgentEvent`, `PipelineEvent`, `ExecutionEnvEvent`) each
- **Detached UX belongs in events, not stderr.** If an attached user needs to see the message later via `fabro attach` or `fabro logs`, emit a workflow event (for example `RunNotice`) and let tracing capture the developer-oriented copy separately.
- **Wrapper variants are no-ops.** When one event enum wraps another (`PipelineEvent::Agent` wraps `AgentEvent`, `AgentEvent::SubAgentEvent` wraps a child `AgentEvent`), the wrapper's `trace()` arm is `{}` because the inner event was already traced at its origin. This prevents double-logging.
- **Streaming noise variants are no-ops.** `TextDelta` and `ToolCallOutputDelta` produce no log output — per-token events would flood the logs even at DEBUG level.
## Prohibited Fields
Some field values carry real or latent sensitivity and must not appear in `tracing` events under any level. When the underlying information is genuinely useful for observability, emit a cardinality-bounded summary (count, size, truncation flag) instead of the raw value.
| Field | Why it's prohibited | Emit instead |
|-------|---------------------|--------------|
| `diff_contents` | File contents from a user workspace may include secrets, PII, or copyrighted code. | `bytes_total`, `file_count`, `truncated` counters |
| `file_path` (for changed-file paths in the Run Files endpoint specifically) | Leaks workspace structure; combined with public run IDs can expose layout of private repos. | `file_count`, aggregate counts bucketed by `binary`, `sensitive`, `symlink`, `submodule` |
| `git_stderr` | Raw git output for untrusted workspaces may include path-shaped secrets (e.g. `~/.ssh/id_rsa_work`) and terminal control sequences. | A short categorized reason (`"timeout"`, `"bad_revision"`, `"unknown_object"`) derived from stderr, never the stderr itself |
| Credential-ish strings (`api_key`, `bearer_token`, `cookie`, `session_id`, …) | Exfiltration risk. | Emit `has_credentials: true` or a fingerprint (`token_last4`) only when debugging is the only option |
These prohibitions apply to every level (ERROR through TRACE). If an error path genuinely needs raw output for triage, route it through an authenticated support channel — not the default tracing subscriber.

View file

@ -1105,6 +1105,61 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/runs/{id}/files:
get:
operationId: listRunFiles
tags: [Run Outputs]
summary: List Run Files Changed
description: |
Returns the set of file changes produced by a run as a list of before/after diffs.
While the run's sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`).
Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`.
parameters:
- $ref: "#/components/parameters/RunId"
- $ref: "#/components/parameters/PageLimit"
- $ref: "#/components/parameters/PageOffset"
- name: from_sha
in: query
required: false
description: Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
schema:
type: string
pattern: "^[0-9a-f]{7,40}$"
- name: to_sha
in: query
required: false
description: Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
schema:
type: string
pattern: "^[0-9a-f]{7,40}$"
responses:
"200":
description: File diffs for the run
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedRunFileList"
"400":
description: Malformed query parameter (invalid SHA format, or non-default value for `from_sha`/`to_sha`).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Run not found (or caller lacks access; returned as 404 to prevent enumeration).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"503":
description: Transient sandbox subprocess failure (timeout, process kill). Safe to retry.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/runs/{id}/stages/{stageId}/artifacts:
get:
operationId: listStageArtifacts
@ -4323,7 +4378,14 @@ components:
example: 'import { parseArgs } from "node:util";'
FileDiff:
description: A before/after pair showing changes to a single file.
description: |
A before/after pair showing changes to a single file.
Contents conventions for non-modify cases:
- Added: `old_file.contents` is empty string; `new_file` holds the added contents.
- Deleted: `new_file.contents` is empty string; `old_file` holds the removed contents.
- Renamed (no content change): both sides hold identical contents; `old_file.name != new_file.name`.
- Symlink / submodule / binary / sensitive / truncated: contents are empty strings; consumers must render a placeholder based on the flag set.
type: object
required:
- old_file
@ -4333,6 +4395,35 @@ components:
$ref: "#/components/schemas/DiffFile"
new_file:
$ref: "#/components/schemas/DiffFile"
change_kind:
type: string
description: Optional classification of the change. Clients that don't recognize a value should fall back to inspecting the old/new contents.
enum:
- added
- modified
- deleted
- renamed
- symlink
- submodule
example: modified
truncated:
type: boolean
description: When `true`, `new_file.contents` and `old_file.contents` are empty strings because the file exceeded a cap (see `truncation_reason`).
example: false
truncation_reason:
type: string
description: Reason this file's contents were omitted. Absent when `truncated` is `false` or omitted.
enum:
- file_too_large
- budget_exhausted
binary:
type: boolean
description: When `true`, the file is non-textual; `contents` on both sides are empty strings.
example: false
sensitive:
type: boolean
description: When `true`, the file path matched the server's sensitive-path denylist; `contents` on both sides are empty strings regardless of truncation or binary flags.
example: false
DiffStats:
description: Aggregate line-change statistics for a diff.
@ -4350,8 +4441,57 @@ components:
description: Total lines deleted.
example: 234
RunFilesMeta:
description: |
Metadata for a `PaginatedRunFileList` response.
Replaces `PaginationMeta` on the files endpoint — the naturally-bounded list does not use cursor pagination but exposes caps and a degraded-response path instead.
type: object
required:
- truncated
- total_changed
properties:
truncated:
type: boolean
description: True when any cap (file count, per-file size, or aggregate size) was hit for this response.
example: false
files_omitted_by_budget:
type: integer
description: Number of files dropped because the aggregate 5 MiB budget was exhausted. Zero or absent when no files were dropped for budget reasons.
example: 0
total_changed:
type: integer
description: Total files changed in the run (before caps were applied). May exceed `data.length` when truncation occurred.
example: 3
to_sha:
type: string
description: Head SHA the diff (or patch) was resolved against.
pattern: "^[0-9a-f]{7,40}$"
example: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
to_sha_committed_at:
type: string
format: date-time
description: Commit time of `to_sha`, used by the UI for the "Checkpoint Xm ago" freshness label on Running runs.
degraded:
type: boolean
description: When `true`, this response carries only a unified patch string in `patch` (no per-file contents in `data`). The UI should render via a unified-patch component.
example: false
degraded_reason:
type: string
description: Why the response degraded to patch-only form. Absent when `degraded` is `false` or omitted.
enum:
- sandbox_unreachable
- sandbox_gone
- provider_unsupported
patch:
type: string
description: Unified-patch text captured at run end. Present only when `degraded` is `true`. Capped at 5 MiB; denylisted file sections are stripped and replaced with a placeholder line.
PaginatedRunFileList:
description: Paginated list of file diffs produced by a run.
description: |
List of file diffs produced by a run, with metadata describing truncation and degraded-response state.
Naturally bounded: at most 200 files per response. Consumers should inspect `meta.truncated` rather than assuming `data.length` equals the run's total change count.
type: object
required:
- data
@ -4362,7 +4502,7 @@ components:
items:
$ref: "#/components/schemas/FileDiff"
meta:
$ref: "#/components/schemas/PaginationMeta"
$ref: "#/components/schemas/RunFilesMeta"
# ── Billing Schemas ──────────────────────────────────────────────────

View file

@ -1,11 +1,39 @@
---
title: "feat: Files Changed tab for workflow runs"
type: feat
status: active
status: completed
date: 2026-04-19
origin: docs/brainstorms/2026-04-19-run-files-changed-tab-requirements.md
---
<!--
Implementation status (2026-04-19): all 13 units shipped on main.
- [x] Unit 1: OpenAPI spec + schema extensions
- [x] Unit 2: Lifecycle — capture final_patch on RunFailed
- [x] Unit 3: Sandbox git machine-readable helpers
- [x] Unit 4: Per-run request coalescing primitive
- [x] Unit 5: Real list_run_files handler (sandbox path)
- [x] Unit 6: Degraded fallback (final_patch) + empty envelope
- [x] Unit 7: Demo handler parity
- [x] Unit 8: Security & observability (logging-strategy.md update;
denylist lives inline in run_files.rs — a globset-based module
extraction is the one remaining follow-up from this plan)
- [x] Unit 9: TS client regen + @pierre/diffs upgrade (1.0.11 → 1.1.15)
- [x] Unit 10: Rendering paths (MultiFileDiff / PatchDiff / placeholders)
- [x] Unit 11: Empty/error/loading/refresh states + SSE revalidation
- [x] Unit 12: A11y + responsive + deep-link
- [x] Unit 13: Surface tab + verification
Deferred follow-ups (not blocking ship):
- Extract inline denylist from run_files.rs into a shared globset-based
run_files_security module and add unit tests for non-UTF-8 path
handling.
- Wrap large diff lists in `<Virtualizer>` once a run with >50 files
demonstrates a measurable rendering regression.
-->
# feat: Files Changed tab for workflow runs
## Overview

File diff suppressed because it is too large Load diff

View file

@ -682,6 +682,7 @@ mod tests {
duration_ms: 10,
reason: Some(StatusReason::Cancelled),
git_commit_sha: None,
final_patch: None,
})),
Some(WorkerTitlePhase::Cancelled)
);
@ -691,6 +692,7 @@ mod tests {
duration_ms: 10,
reason: Some(StatusReason::Terminated),
git_commit_sha: None,
final_patch: None,
})),
Some(WorkerTitlePhase::Failed)
);

View file

@ -42,6 +42,7 @@ axum.workspace = true
axum-extra.workspace = true
cookie.workspace = true
dirs.workspace = true
globset.workspace = true
tower = "0.5"
tower-http = { version = "0.6", features = ["trace"] }
tokio-stream = { workspace = true, features = ["sync"] }
@ -82,4 +83,7 @@ tower = "0.5"
http-body-util = "0.1"
httpmock = "0.8"
serde_yaml = "0.9"
fabro-sandbox = { path = "../fabro-sandbox" }
tracing-subscriber.workspace = true
async-trait.workspace = true
tokio-util.workspace = true
fabro-sandbox = { path = "../fabro-sandbox", features = ["test-support"] }

View file

@ -10,7 +10,10 @@ use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::sse::{Event, Sse};
use axum::response::{IntoResponse, Response};
use fabro_api::types::{CreateSecretRequest, DeleteSecretRequest, RunArtifactListResponse};
use fabro_api::types::{
CreateSecretRequest, DeleteSecretRequest, DiffFile, FileDiff, FileDiffChangeKind,
PaginatedRunFileList, RunArtifactListResponse, RunFilesMeta,
};
use serde_json::json;
use crate::error::ApiError;
@ -146,6 +149,84 @@ pub(crate) async fn list_run_artifacts_stub(
.into_response()
}
/// Demo-mode handler for `GET /runs/{id}/files`. Returns a small
/// illustrative diff without touching run store state — the `_id` and
/// `_state` parameters are intentionally ignored so demo responses cannot
/// cross-contaminate with real run data (R34).
pub(crate) async fn list_run_files_stub(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,
Path(_id): Path<String>,
) -> Response {
(StatusCode::OK, Json(demo_run_files())).into_response()
}
fn demo_run_files() -> PaginatedRunFileList {
let old_main = "import { parseArgs } from \"node:util\";\n\nexport function run(argv: string[]) {\n const { values } = parseArgs({ args: argv, options: { config: { type: \"string\" } } });\n console.log(values.config);\n}\n";
let new_main = "import { parseArgs } from \"node:util\";\nimport { loadConfig } from \"./config.js\";\n\nexport async function run(argv: string[]) {\n const { values } = parseArgs({ args: argv, options: { config: { type: \"string\" } } });\n const config = await loadConfig(values.config ?? \".fabro/project.toml\");\n console.log(JSON.stringify(config, null, 2));\n}\n";
let new_config = "import { readFile } from \"node:fs/promises\";\nimport { parse as parseToml } from \"@iarna/toml\";\n\nexport async function loadConfig(path: string) {\n const contents = await readFile(path, \"utf8\");\n return parseToml(contents);\n}\n";
PaginatedRunFileList {
data: vec![
FileDiff {
binary: None,
change_kind: Some(FileDiffChangeKind::Modified),
new_file: DiffFile {
name: "src/commands/run.ts".to_string(),
contents: new_main.to_string(),
},
old_file: DiffFile {
name: "src/commands/run.ts".to_string(),
contents: old_main.to_string(),
},
sensitive: None,
truncated: None,
truncation_reason: None,
},
FileDiff {
binary: None,
change_kind: Some(FileDiffChangeKind::Added),
new_file: DiffFile {
name: "src/config.ts".to_string(),
contents: new_config.to_string(),
},
old_file: DiffFile {
name: String::new(),
contents: String::new(),
},
sensitive: None,
truncated: None,
truncation_reason: None,
},
FileDiff {
binary: None,
change_kind: Some(FileDiffChangeKind::Renamed),
new_file: DiffFile {
name: "src/legacy/old-runner.ts".to_string(),
contents: "export const legacy = true;\n".to_string(),
},
old_file: DiffFile {
name: "src/old-runner.ts".to_string(),
contents: "export const legacy = true;\n".to_string(),
},
sensitive: None,
truncated: None,
truncation_reason: None,
},
],
meta: RunFilesMeta {
truncated: false,
files_omitted_by_budget: None,
total_changed: 3,
to_sha: None,
to_sha_committed_at: None,
degraded: Some(false),
degraded_reason: None,
patch: None,
},
}
}
pub(crate) async fn get_run_billing(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,

View file

@ -67,6 +67,7 @@ struct ErrorBody {
///
/// Serializes to `{"errors": [{"status": "4xx", "title": "...", "detail":
/// "..."}]}`.
#[derive(Clone, Debug)]
pub struct ApiError {
status: StatusCode,
detail: String,
@ -95,6 +96,10 @@ impl ApiError {
pub fn forbidden() -> Self {
Self::new(StatusCode::FORBIDDEN, "Access denied.")
}
pub fn status(&self) -> StatusCode {
self.status
}
}
impl From<Error> for ApiError {

View file

@ -13,6 +13,8 @@ pub mod github_webhooks;
pub mod install;
pub mod ip_allowlist;
pub mod jwt_auth;
mod run_files;
mod run_files_security;
mod run_manifest;
mod run_selector;
pub mod security_headers;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,248 @@
#![allow(unreachable_pub)]
//! Security helpers for the Run Files Changed endpoint: a globset-based
//! sensitive-path denylist and a structured metrics emitter that enforces
//! the tracing allowlist.
//!
//! Matching is path-based and case-insensitive. The denylist is a
//! defense-in-depth control — not a content scanner, and it won't catch
//! arbitrary secrets hidden inside non-secret file extensions.
use std::sync::OnceLock;
use fabro_types::RunId;
use globset::{Glob, GlobSet, GlobSetBuilder};
use tracing::info;
/// Basename patterns — applied to the final path segment only.
const BASENAME_GLOBS: &[&str] = &[
".env",
".env.*",
"*.pem",
"id_rsa",
"id_rsa.*",
"id_ed25519",
"id_ed25519.*",
"*.p12",
"*.keystore",
"*.key",
];
/// Path-suffix patterns — applied to the whole repo-relative path.
const PATH_SUFFIX_GLOBS: &[&str] = &[
"**/.aws/credentials",
".aws/credentials",
"**/.git/config",
".git/config",
"**/.ssh/**",
".ssh/**",
];
/// Lazily-constructed globsets. Building from a static pattern list never
/// fails in practice, but we defensively unwrap into an always-false matcher
/// so a pattern typo in a future edit doesn't take the whole endpoint down.
struct Denylist {
basename: GlobSet,
path: GlobSet,
}
fn build_set(patterns: &[&str]) -> GlobSet {
let mut builder = GlobSetBuilder::new();
for pat in patterns {
if let Ok(glob) = Glob::new(pat) {
builder.add(glob);
}
}
builder.build().unwrap_or_else(|_| GlobSet::empty())
}
fn denylist() -> &'static Denylist {
static SET: OnceLock<Denylist> = OnceLock::new();
SET.get_or_init(|| Denylist {
basename: build_set(BASENAME_GLOBS),
path: build_set(PATH_SUFFIX_GLOBS),
})
}
/// Return `true` if `path` matches any entry in the sensitive-path denylist.
///
/// Matching semantics:
/// - Inputs are normalized to lowercase and POSIX separators before matching.
/// - Ancestor `../` / `./` components are stripped so attempts to sneak a
/// sensitive file via traversal can't evade the check.
/// - Basename globs fire against the last path segment only (prevents
/// `log/.env_audit/data.txt` from matching the `.env.*` pattern).
/// - Path-suffix globs fire against the whole normalized path.
/// - Empty paths, pure `.` paths, and paths that normalize to empty are
/// considered sensitive as a safe default — no legitimate diff produces
/// these; treating them as sensitive prevents accidental exposure of
/// pathological git output.
#[must_use]
pub fn is_sensitive(path: &str) -> bool {
let normalized = normalize_for_match(path);
if normalized.is_empty() {
return true;
}
let set = denylist();
let basename = normalized
.rsplit_once('/')
.map_or(normalized.as_str(), |(_, n)| n);
if set.basename.is_match(basename) {
return true;
}
set.path.is_match(normalized.as_str())
}
fn normalize_for_match(path: &str) -> String {
// Replace backslashes with forward slashes so Windows-style paths (if
// they ever leak through git output) match the same way. Case-folding
// is ASCII-only on purpose: all denylist globs are ASCII, and full
// Unicode `to_lowercase()` can expand a single codepoint into multiple
// chars (e.g. `İ` -> `i\u{307}`), which then silently can't match an
// ASCII glob. ASCII-only fold makes the failure mode explicit (a
// homoglyphic path just doesn't match) rather than disguised.
let mut out = String::with_capacity(path.len());
for ch in path.chars() {
let c = if ch == '\\' { '/' } else { ch };
let lowered = if c.is_ascii() {
c.to_ascii_lowercase()
} else {
c
};
out.push(lowered);
}
// Drop leading `./`, `../`, and bare-leading `/` prefixes until the
// path has a meaningful first segment. Inner `..` components are left
// alone — git doesn't emit them in normal diffs, and stripping them
// mid-path would change the effective basename.
loop {
let next = if let Some(rest) = out.strip_prefix("./") {
rest.to_string()
} else if let Some(rest) = out.strip_prefix("../") {
rest.to_string()
} else if let Some(rest) = out.strip_prefix('/') {
rest.to_string()
} else {
break;
};
if next == out {
break;
}
out = next;
}
out
}
/// Metrics emitted at the tail of every Run Files response. The field set is
/// deliberately the only shape of tracing output the endpoint produces —
/// enforced by `emit`, which never interpolates or logs individual paths,
/// file contents, or raw git stderr.
pub struct RunFilesMetrics {
pub file_count: usize,
pub bytes_total: u64,
pub duration_ms: u64,
pub truncated: bool,
pub binary_count: u64,
pub sensitive_count: u64,
pub symlink_count: u64,
pub submodule_count: u64,
}
impl RunFilesMetrics {
pub fn emit(&self, run_id: &RunId) {
info!(
run_id = %run_id,
file_count = self.file_count,
bytes_total = self.bytes_total,
duration_ms = self.duration_ms,
truncated = self.truncated,
binary_count = self.binary_count,
sensitive_count = self.sensitive_count,
symlink_count = self.symlink_count,
submodule_count = self.submodule_count,
"Run files response produced"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_sensitive_matches_basename_patterns() {
assert!(is_sensitive(".env"));
assert!(is_sensitive("apps/web/.env.production"));
assert!(is_sensitive("keys/id_rsa"));
assert!(is_sensitive("keys/id_rsa.pub"));
assert!(is_sensitive("keys/id_ed25519"));
assert!(is_sensitive("certs/Server.PEM"));
assert!(is_sensitive("data/vault.keystore"));
assert!(is_sensitive("config/service.key"));
}
#[test]
fn is_sensitive_matches_path_suffix_patterns() {
assert!(is_sensitive(".ssh/authorized_keys"));
assert!(is_sensitive("home/user/.ssh/id_custom"));
assert!(is_sensitive(".aws/credentials"));
assert!(is_sensitive("home/user/.aws/credentials"));
assert!(is_sensitive(".git/config"));
}
#[test]
fn is_sensitive_rejects_benign_paths() {
assert!(!is_sensitive("src/main.rs"));
assert!(!is_sensitive("README.md"));
// A file whose name merely contains `env` as a substring must not
// match the `.env` / `.env.*` basename globs.
assert!(!is_sensitive("src/environment.ts"));
// Ancestor-directory-only match: `.env_audit` is a directory, the
// actual file is `data.txt` — shouldn't hit the basename pattern.
assert!(!is_sensitive("log/.env_audit/data.txt"));
}
#[test]
fn is_sensitive_handles_path_traversal_safely() {
// `../` components strip to an otherwise-benign path rather than
// letting the attacker evade matching. The ultimate segment drives
// the basename check.
assert!(is_sensitive("../.ssh/id_rsa"));
assert!(is_sensitive("./.env"));
assert!(!is_sensitive("../../src/main.rs"));
}
#[test]
fn is_sensitive_empty_paths_fail_closed() {
assert!(is_sensitive(""));
assert!(is_sensitive("./"));
assert!(is_sensitive("/"));
}
#[test]
fn is_sensitive_is_case_insensitive() {
assert!(is_sensitive(".ENV"));
assert!(is_sensitive(".Env.Production"));
assert!(is_sensitive("keys/ID_RSA"));
}
#[test]
fn is_sensitive_normalizes_backslashes_as_separators() {
// If git ever surfaces a Windows-style path the denylist should
// still catch it: `keys\\id_rsa` canonicalizes to `keys/id_rsa`.
assert!(is_sensitive("keys\\id_rsa"));
assert!(is_sensitive("C:\\Users\\alice\\.ssh\\config"));
}
#[test]
fn is_sensitive_handles_unicode_uppercase_basename() {
// Some Unicode uppercase letters expand to multiple lowercase code
// points (e.g. the sharp-S `ẞ` -> `ss`). The basename glob check
// should still fire on the lowercased form without losing prefix
// stripping.
assert!(is_sensitive("keys/ID_RSA")); // ASCII baseline
// A non-sensitive unicode path must still not match any glob.
assert!(!is_sensitive("docs/Ähnlichkeit.md"));
}
}

View file

@ -116,6 +116,7 @@ use crate::jwt_auth::{
AuthMode, AuthenticatedService, AuthenticatedSubject, authenticate_service_parts,
};
use crate::run_selector::{ResolveRunError, resolve_run_by_selector};
use crate::run_files::{FilesInFlight, list_run_files, new_files_in_flight};
use crate::server_secrets::{
LlmClientResult, ProviderCredentials, ServerSecrets, auth_issue_message,
};
@ -553,15 +554,19 @@ impl SlackService {
/// Shared application state for the server.
pub struct AppState {
runs: Mutex<HashMap<RunId, ManagedRun>>,
aggregate_billing: Mutex<BillingAccumulator>,
store: Arc<Database>,
artifact_store: ArtifactStore,
runs: Mutex<HashMap<RunId, ManagedRun>>,
aggregate_billing: Mutex<BillingAccumulator>,
store: Arc<Database>,
artifact_store: ArtifactStore,
artifact_upload_tokens: ArtifactUploadTokenKeys,
started_at: Instant,
max_concurrent_runs: usize,
scheduler_notify: Notify,
global_event_tx: broadcast::Sender<EventEnvelope>,
started_at: Instant,
max_concurrent_runs: usize,
scheduler_notify: Notify,
global_event_tx: broadcast::Sender<EventEnvelope>,
/// Per-run coalescing registry for `GET /runs/{id}/files`. Concurrent
/// callers for the same run share one materialization; different runs
/// proceed in parallel. See `crate::run_files` for semantics.
pub(crate) files_in_flight: FilesInFlight,
pub(crate) vault: Arc<AsyncRwLock<Vault>>,
pub(crate) server_secrets: ServerSecrets,
@ -669,6 +674,18 @@ impl AppState {
})
}
/// Public accessor used by `run_files` — mirrors `vault_or_env` without
/// changing its visibility semantics.
pub(crate) fn vault_or_env_pub(&self, name: &str) -> Option<String> {
self.vault_or_env(name)
}
/// Borrow the persistent store so sibling modules can open run readers
/// without cross-module state coupling on the `AppState` field layout.
pub(crate) fn store_ref(&self) -> &Arc<Database> {
&self.store
}
pub(crate) fn server_secret(&self, name: &str) -> Option<String> {
self.server_secrets.get(name)
}
@ -1051,6 +1068,7 @@ fn demo_routes() -> Router<Arc<AppState>> {
.route("/runs/{id}/graph", get(demo::get_run_graph))
.route("/runs/{id}/stages", get(demo::get_run_stages))
.route("/runs/{id}/artifacts", get(demo::list_run_artifacts_stub))
.route("/runs/{id}/files", get(demo::list_run_files_stub))
.route(
"/runs/{id}/stages/{stageId}/turns",
get(demo::get_stage_turns),
@ -1134,6 +1152,7 @@ fn real_routes() -> Router<Arc<AppState>> {
.route("/runs/{id}/graph", get(get_graph))
.route("/runs/{id}/stages", get(list_run_stages))
.route("/runs/{id}/artifacts", get(list_run_artifacts))
.route("/runs/{id}/files", get(list_run_files))
.route("/runs/{id}/stages/{stageId}/turns", get(not_implemented))
.route(
"/runs/{id}/stages/{stageId}/artifacts",
@ -2596,6 +2615,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
max_concurrent_runs,
scheduler_notify: Notify::new(),
global_event_tx,
files_in_flight: new_files_in_flight(),
vault,
server_secrets,
provider_credentials,
@ -3031,6 +3051,13 @@ fn parse_run_id_path(id: &str) -> Result<RunId, Response> {
.map_err(|_| ApiError::bad_request("Invalid run ID.").into_response())
}
/// Public re-export so sibling modules (e.g. `run_files`) can share the same
/// 400-on-invalid-ULID parse behavior without duplicating the helper.
#[allow(clippy::result_large_err)]
pub(crate) fn parse_run_id_path_pub(id: &str) -> Result<RunId, Response> {
parse_run_id_path(id)
}
#[allow(clippy::result_large_err)]
fn parse_stage_id_path(stage_id: &str) -> Result<StageId, Response> {
StageId::from_str(stage_id)
@ -3233,6 +3260,7 @@ pub(crate) async fn reconcile_incomplete_runs_on_startup(
duration_ms: 0,
reason,
git_commit_sha: None,
final_patch: None,
},
)
.await?;
@ -3289,6 +3317,7 @@ async fn persist_shutdown_run_failures(
duration_ms: 0,
reason,
git_commit_sha: None,
final_patch: None,
},
)
.await?;
@ -3362,6 +3391,7 @@ async fn persist_cancelled_run_status(state: &AppState, run_id: RunId) -> anyhow
duration_ms: 0,
reason: Some(WorkflowStatusReason::Cancelled),
git_commit_sha: None,
final_patch: None,
},
)
.await
@ -3599,6 +3629,7 @@ async fn append_worker_exit_failure(
duration_ms: 0,
reason,
git_commit_sha: None,
final_patch: None,
},
)
.await
@ -4575,6 +4606,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
duration_ms: 0,
reason: Some(WorkflowStatusReason::LaunchFailed),
git_commit_sha: None,
final_patch: None,
},
)
.await;
@ -4596,6 +4628,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
duration_ms: 0,
reason: Some(WorkflowStatusReason::LaunchFailed),
git_commit_sha: None,
final_patch: None,
},
)
.await;
@ -4625,6 +4658,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
duration_ms: 0,
reason: Some(WorkflowStatusReason::LaunchFailed),
git_commit_sha: None,
final_patch: None,
},
)
.await;
@ -4645,6 +4679,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
duration_ms: 0,
reason: Some(WorkflowStatusReason::LaunchFailed),
git_commit_sha: None,
final_patch: None,
},
)
.await;
@ -4677,6 +4712,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
duration_ms: 0,
reason: Some(WorkflowStatusReason::Terminated),
git_commit_sha: None,
final_patch: None,
},
)
.await;

View file

@ -2,6 +2,7 @@ mod docs;
mod install;
mod install_openai_compatible;
mod routing;
mod run_files;
mod runs;
mod settings;
mod system;

View file

@ -0,0 +1,191 @@
//! HTTP-level integration tests for `GET /api/v1/runs/{id}/files`.
//!
//! These tests exercise the handler's request-plumbing branches —
//! authentication extractor, route matching, query validation, demo-mode
//! branching, and the empty-envelope / not-found responses — without
//! requiring a reconnected sandbox. The sandbox happy path is covered by
//! unit tests on the sandbox-git helpers and by `stitch_file_diff` tests
//! in `run_files.rs`.
use axum::body::Body;
use axum::http::{Request, StatusCode};
use fabro_server::jwt_auth::AuthMode;
use fabro_server::server::build_router;
use tower::ServiceExt;
use crate::helpers::{MINIMAL_DOT, api, body_json, minimal_manifest_json, test_app_state};
fn files_url(run_id: &str) -> String {
api(&format!("/runs/{run_id}/files"))
}
#[tokio::test]
async fn invalid_run_id_returns_400() {
let app = build_router(test_app_state(), AuthMode::Disabled);
let req = Request::builder()
.method("GET")
.uri(files_url("not-a-ulid"))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn unknown_run_returns_404() {
let app = build_router(test_app_state(), AuthMode::Disabled);
// Valid ULID format but not a run we've created.
let fake = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
let req = Request::builder()
.method("GET")
.uri(files_url(fake))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn malformed_from_sha_query_returns_400() {
let app = build_router(test_app_state(), AuthMode::Disabled);
let fake = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
let req = Request::builder()
.method("GET")
.uri(format!("{}?from_sha=not-hex", files_url(fake)))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body_json(resp.into_body()).await;
assert!(
body["errors"][0]["detail"]
.as_str()
.unwrap_or_default()
.contains("from_sha")
);
}
#[tokio::test]
async fn non_default_from_sha_returns_400_even_when_hex() {
let app = build_router(test_app_state(), AuthMode::Disabled);
let fake = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
// Well-formed hex SHA but v1 reserves the parameter for a future
// version; any non-default value must be rejected.
let req = Request::builder()
.method("GET")
.uri(format!(
"{}?from_sha=abc1234def56789abc1234def56789abc1234def",
files_url(fake)
))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn malformed_to_sha_returns_400() {
let app = build_router(test_app_state(), AuthMode::Disabled);
let fake = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
let req = Request::builder()
.method("GET")
.uri(format!("{}?to_sha=xyz", files_url(fake)))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn submitted_run_without_sandbox_returns_empty_envelope() {
// A run that has been created but not started has no base_sha or
// sandbox record, so the handler returns an empty envelope. The UI
// maps that to R4(a).
let app = build_router(test_app_state(), AuthMode::Disabled);
let manifest = minimal_manifest_json(MINIMAL_DOT);
let create_req = Request::builder()
.method("POST")
.uri(api("/runs"))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&manifest).unwrap()))
.unwrap();
let create_resp = app.clone().oneshot(create_req).await.unwrap();
assert_eq!(create_resp.status(), StatusCode::CREATED);
let create_body = body_json(create_resp.into_body()).await;
let run_id = create_body["id"].as_str().unwrap().to_string();
let req = Request::builder()
.method("GET")
.uri(files_url(&run_id))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_json(resp.into_body()).await;
assert!(
body["data"].as_array().is_some_and(Vec::is_empty),
"expected empty data: {body}"
);
assert_eq!(body["meta"]["total_changed"], 0);
// Degraded is false because there's no final_patch either — the run
// simply hasn't produced anything to diff.
assert_eq!(body["meta"]["degraded"].as_bool(), Some(false));
}
#[tokio::test]
async fn demo_mode_returns_fixture_without_touching_store() {
// R34: demo handler must return the illustrative fixture with no
// cross-contamination with real run state.
let app = build_router(test_app_state(), AuthMode::Disabled);
let arbitrary = "not-even-a-valid-ulid-for-run";
let req = Request::builder()
.method("GET")
.uri(files_url(arbitrary))
.header("x-fabro-demo", "1")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_json(resp.into_body()).await;
// Demo fixture ships three entries (modified + added + renamed).
let data = body["data"].as_array().expect("data array");
assert_eq!(data.len(), 3, "demo fixture should have 3 entries");
// At least one entry must render with populated contents to prove the
// fixture exercises the MultiFileDiff branch.
let has_content = data.iter().any(|entry| {
entry["new_file"]["contents"]
.as_str()
.is_some_and(|s| !s.is_empty())
});
assert!(has_content, "demo fixture should contain populated content");
}
#[tokio::test]
async fn response_envelope_matches_openapi_paginated_run_file_list_shape() {
// Sanity check that the happy-path envelope shape matches what the
// OpenAPI spec + regenerated TS client expect. Uses demo mode so the
// test stays deterministic without running a sandbox.
let app = build_router(test_app_state(), AuthMode::Disabled);
let req = Request::builder()
.method("GET")
.uri(files_url("whatever"))
.header("x-fabro-demo", "1")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_json(resp.into_body()).await;
assert!(body["data"].is_array());
assert!(body["meta"].is_object());
assert!(body["meta"]["truncated"].is_boolean());
assert!(body["meta"]["total_changed"].is_number());
for entry in body["data"].as_array().unwrap() {
assert!(entry["old_file"]["name"].is_string());
assert!(entry["old_file"]["contents"].is_string());
assert!(entry["new_file"]["name"].is_string());
assert!(entry["new_file"]["contents"].is_string());
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -61,7 +61,7 @@
<script type="module" src="/assets/chunk-sadshphz.js"></script>
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
<script type="module" src="/assets/entry-g6egnqzk.js"></script>
<script type="module" src="/assets/entry-pmqz8d3n.js"></script>
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
<script type="module" src="/assets/chunk-9t57pdty.js"></script>
@ -75,8 +75,8 @@
<script type="module" src="/assets/chunk-6ma2q84r.js"></script>
<script type="module" src="/assets/chunk-0tq24xt3.js"></script>
<script type="module" src="/assets/chunk-z512569f.js"></script>
<script type="module" src="/assets/chunk-q2qv5qr4.js"></script>
<script type="module" src="/assets/chunk-tqzz87j8.js"></script>
<script type="module" src="/assets/chunk-7fpy4pmb.js"></script>
<script type="module" src="/assets/chunk-nghb2mxb.js"></script>
<script type="module" src="/assets/chunk-w4txx8sc.js"></script>
<script type="module" src="/assets/chunk-eaexpy25.js"></script>
<script type="module" src="/assets/chunk-zb6gezq1.js"></script>

View file

@ -209,6 +209,7 @@ impl RunProjection {
self.status = Some(run_status_record(RunStatus::Failed, props.reason, ts));
self.pending_control = None;
self.conclusion = Some(conclusion_from_failed(props, ts));
self.final_patch.clone_from(&props.final_patch);
self.pending_interviews.clear();
}
EventBody::RunRewound(_) => {
@ -693,6 +694,7 @@ mod tests {
use std::collections::HashMap;
use chrono::Utc;
use fabro_types::run_event::run::RunFailedProps;
use fabro_types::run_event::{
InterviewCompletedProps, InterviewOption, InterviewStartedProps, RunControlEffectProps,
};
@ -1136,6 +1138,47 @@ mod tests {
);
}
#[test]
fn run_failed_with_final_patch_populates_projection() {
let mut state = RunProjection::default();
let patch = "diff --git a/foo.rs b/foo.rs\n@@ -1 +1 @@\n-a\n+b\n";
state
.apply_event(&test_event(
1,
EventBody::RunFailed(RunFailedProps {
error: "boom".to_string(),
duration_ms: 42,
reason: None,
git_commit_sha: Some("abc123".to_string()),
final_patch: Some(patch.to_string()),
}),
None,
))
.unwrap();
assert_eq!(state.final_patch.as_deref(), Some(patch));
}
#[test]
fn legacy_run_failed_event_without_final_patch_replays_as_none() {
let mut state = RunProjection::default();
// Existing SlateDB events predating the final_patch field should deserialize
// cleanly as None — no backfill required.
state
.apply_event(&test_raw_event(
1,
"run.failed",
&json!({
"error": "boom",
"duration_ms": 1,
}),
None,
))
.unwrap();
assert!(state.final_patch.is_none());
}
#[test]
fn run_archived_captures_prior_status_and_sets_archived() {
use fabro_types::RunStatus;

View file

@ -131,6 +131,10 @@ pub struct RunFailedProps {
pub reason: Option<StatusReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git_commit_sha: Option<String>,
// Optional unified-patch text captured at run end. Additive for back-compat:
// pre-change events replay with `final_patch: None` via serde default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub final_patch: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View file

@ -151,6 +151,8 @@ pub enum Event {
reason: Option<StatusReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
git_commit_sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
final_patch: Option<String>,
},
RunNotice {
level: RunNoticeLevel,
@ -1641,11 +1643,13 @@ fn event_body_from_event(event: &Event) -> EventBody {
duration_ms,
reason,
git_commit_sha,
final_patch,
} => EventBody::RunFailed(fabro_types::RunFailedProps {
error: error.to_string(),
duration_ms: *duration_ms,
reason: *reason,
git_commit_sha: git_commit_sha.clone(),
final_patch: final_patch.clone(),
}),
Event::RunNotice {
level,
@ -3126,6 +3130,7 @@ mod tests {
duration_ms: 900,
reason: Some(StatusReason::WorkflowError),
git_commit_sha: Some("abc123".to_string()),
final_patch: None,
});
assert_eq!(stored.event_name(), "run.failed");

View file

@ -456,6 +456,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
duration_ms,
reason: Some(StatusReason::Cancelled),
git_commit_sha: last_sha,
final_patch: final_patch.clone(),
});
return;
}
@ -484,6 +485,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
duration_ms,
reason: Some(StatusReason::WorkflowError),
git_commit_sha: last_sha,
final_patch,
});
}
}

View file

@ -18,7 +18,7 @@ use crate::outcome::{BilledModelUsage, Outcome, StageStatus};
use crate::run_dump::RunDump;
use crate::run_options::RunOptions;
use crate::runtime_store::RunStoreHandle;
use crate::sandbox_git::{git_checkpoint, git_diff, git_push_host};
use crate::sandbox_git::{git_checkpoint, git_diff, git_diff_with_timeout, git_push_host};
type WfRunState = ExecutionState<Option<BilledModelUsage>>;
type WfNodeResult = NodeResult<Option<BilledModelUsage>>;
@ -301,31 +301,39 @@ impl RunLifecycle<WorkflowGraph> for GitLifecycle {
}
async fn on_run_end(&self, outcome: &Outcome, _state: &WfRunState) {
// Capture the final diff on success for event/store projection.
if (outcome.status == StageStatus::Success || outcome.status == StageStatus::PartialSuccess)
&& self.run_options.git.is_some()
// Capture the final diff for event/store projection.
//
// Success/PartialSuccess uses the standard 30 s timeout. Failed runs
// use a shorter 10 s timeout: a pathological workspace (FS locks,
// corrupted index) must not stall terminal event emission downstream
// (Slack notifier, SSE RunFailed, CI hooks).
if self.run_options.git.is_none() {
return;
}
let timeout_ms = match outcome.status {
StageStatus::Success | StageStatus::PartialSuccess => 30_000,
_ => 10_000,
};
if let Some(base_sha) = self
.run_options
.git
.as_ref()
.and_then(|g| g.base_sha.clone())
{
if let Some(base_sha) = self
.run_options
.git
.as_ref()
.and_then(|g| g.base_sha.clone())
{
match git_diff(&*self.sandbox, &base_sha).await {
Ok(patch) if !patch.is_empty() => {
*self.final_patch.lock().unwrap() = Some(patch.clone());
}
Ok(_) => {
*self.final_patch.lock().unwrap() = None;
}
Err(err) => {
*self.final_patch.lock().unwrap() = None;
self.emitter.emit(&Event::RunNotice {
level: RunNoticeLevel::Warn,
code: "git_diff_failed".to_string(),
message: format!("final diff failed: {err}"),
});
}
match git_diff_with_timeout(&*self.sandbox, &base_sha, timeout_ms).await {
Ok(patch) if !patch.is_empty() => {
*self.final_patch.lock().unwrap() = Some(patch.clone());
}
Ok(_) => {
*self.final_patch.lock().unwrap() = None;
}
Err(err) => {
*self.final_patch.lock().unwrap() = None;
self.emitter.emit(&Event::RunNotice {
level: RunNoticeLevel::Warn,
code: "git_diff_failed".to_string(),
message: format!("final diff failed: {err}"),
});
}
}
}

View file

@ -190,6 +190,7 @@ mod tests {
duration_ms: 10,
reason: None,
git_commit_sha: None,
final_patch: None,
})
.await
.unwrap();

View file

@ -267,6 +267,7 @@ async fn persist_terminal_engine_failure(
duration_ms: u64::try_from(duration.as_millis()).unwrap(),
reason: status_reason,
git_commit_sha: None,
final_patch: None,
})
.await
{
@ -862,6 +863,7 @@ impl Drop for DetachedRunBootstrapGuard {
duration_ms: 0,
reason: Some(reason),
git_commit_sha: None,
final_patch: None,
})
.await;
});
@ -929,6 +931,7 @@ impl Drop for DetachedRunCompletionGuard {
duration_ms: 0,
reason: Some(reason),
git_commit_sha: None,
final_patch: None,
})
.await;
let _ = append_event_to_sink(&event_sink, &run_id, &Event::RunNotice {
@ -957,6 +960,7 @@ async fn persist_detached_failure(
duration_ms: 0,
reason: Some(reason),
git_commit_sha: None,
final_patch: None,
})
.await
{

View file

@ -181,13 +181,36 @@ pub async fn git_push_host(
}
}
/// Run a git diff via the sandbox.
/// Run a git diff via the sandbox (30 s default timeout).
pub(crate) async fn git_diff(
sandbox: &dyn Sandbox,
base: &str,
) -> std::result::Result<String, String> {
let cmd = format!("{GIT_REMOTE} diff {base} HEAD");
match sandbox.exec_command(&cmd, 30_000, None, None, None).await {
git_diff_with_timeout(sandbox, base, 30_000).await
}
/// Run a git diff via the sandbox with a caller-supplied timeout in
/// milliseconds.
///
/// Failure-path capture uses a shorter timeout than the checkpoint path so a
/// pathological workspace (FS locks, corrupted index) doesn't stall terminal
/// event emission downstream (Slack notifier, SSE, CI hooks).
pub(crate) async fn git_diff_with_timeout(
sandbox: &dyn Sandbox,
base: &str,
timeout_ms: u64,
) -> std::result::Result<String, String> {
// `-c core.quotePath=false` forces paths with non-ASCII, tabs, quotes,
// or backslashes to emit unquoted. The Run Files Changed endpoint's
// `strip_denylisted_sections` parser only recognizes unquoted
// `diff --git a/<old> b/<new>` headers; without this flag git would
// wrap such paths in `"a/…"` / `"b/…"` and evade the denylist (see
// docs/agent/reviews/2026-04-19-run-files-security-review.md).
let cmd = format!("{GIT_REMOTE} -c core.quotePath=false diff {base} HEAD");
match sandbox
.exec_command(&cmd, timeout_ms, None, None, None)
.await
{
Ok(r) if r.exit_code == 0 => Ok(r.stdout),
Ok(r) => Err(format!("exit {}: {}", r.exit_code, r.stderr.trim())),
Err(e) => Err(e.clone()),
@ -236,6 +259,527 @@ pub async fn git_replace_worktree(sandbox: &dyn Sandbox, path: &str, branch: &st
git_add_worktree(sandbox, path, branch).await
}
// ── Machine-readable diff enumeration (Run Files endpoint) ─────────────────
/// Hardened git-command prefix for the Run Files endpoint.
///
/// Layers on top of [`GIT_REMOTE`]:
/// - `core.hooksPath=/dev/null`: repo-supplied hooks do not run.
/// - `core.fsmonitor=false`: no fsmonitor daemon interactions.
/// - `protocol.file.allow=never`: blocks local-protocol fetches.
///
/// These invocations use [`sandbox_git_hardening_env`] via `exec_command` to
/// disable terminal prompts and external diff drivers.
const GIT_HARDENED: &str = "git -c maintenance.auto=0 -c gc.auto=0 -c core.hooksPath=/dev/null -c core.fsmonitor=false -c protocol.file.allow=never -c core.quotePath=false";
/// Environment additions applied to every hardened sandbox-side git invocation.
///
/// `GIT_TERMINAL_PROMPT=0` prevents git from stalling on credential prompts
/// when a remote or subprocess triggers one. Clearing `GIT_EXTERNAL_DIFF`
/// neutralizes any inherited custom diff driver.
fn sandbox_git_hardening_env() -> std::collections::HashMap<String, String> {
std::collections::HashMap::from([
("GIT_TERMINAL_PROMPT".to_string(), "0".to_string()),
("GIT_EXTERNAL_DIFF".to_string(), String::new()),
])
}
/// A single changed-file entry from `git diff --raw -z --find-renames=50%`.
///
/// Paths are repo-relative, UTF-8; non-UTF-8 filenames are rejected by the
/// parser. Blob SHAs are lowercase hex. Modes are octal integers (`100644`,
/// `100755`, `120000`, `160000`, …).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RawDiffEntry {
Added {
path: String,
new_blob: String,
new_mode: String,
},
Modified {
path: String,
old_blob: String,
new_blob: String,
new_mode: String,
},
Deleted {
path: String,
old_blob: String,
old_mode: String,
},
Renamed {
old_path: String,
new_path: String,
old_blob: String,
new_blob: String,
new_mode: String,
similarity: u8,
},
Symlink {
path: String,
change_kind: SymlinkChange,
old_blob: Option<String>,
new_blob: Option<String>,
},
Submodule {
path: String,
change_kind: SubmoduleChange,
},
}
/// Lifecycle of a symlink entry (mode `120000`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymlinkChange {
Added,
Modified,
Deleted,
}
/// Lifecycle of a submodule entry (mode `160000`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubmoduleChange {
Added,
Modified,
Deleted,
}
/// Error produced by the sandbox-git helpers.
///
/// Callers discriminate between transient (retry-safe) and permanent
/// conditions: a 503 can be returned to the client on `Transient`, while
/// `Permanent` errors should fall through to the patch-only fallback.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffError {
/// Retry-safe failure: timeout, process kill, transient I/O.
Transient { message: String },
/// Non-retryable failure: unknown revision, malformed output, etc.
Permanent { message: String },
}
impl std::fmt::Display for DiffError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Transient { message } => write!(f, "transient: {message}"),
Self::Permanent { message } => write!(f, "permanent: {message}"),
}
}
}
impl std::error::Error for DiffError {}
/// Size metadata for a single blob, as reported by `git cat-file
/// --batch-check`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlobMeta {
pub sha: String,
/// `None` if the blob is missing (git reports `missing`).
pub size: Option<u64>,
}
/// Enumerate files changed between `base_sha` and `to_sha` via the sandbox.
///
/// Uses `git diff --raw -z --find-renames=50%` to get a machine-readable,
/// null-separated, SHA-addressed listing. Paths from this output are treated
/// as metadata only — blob reads use the SHAs, not the paths.
///
/// The `--numstat` side-call classifies text vs binary so callers can skip
/// binary contents without ever invoking `git cat-file --batch` on them.
pub async fn list_changed_files_raw(
sandbox: &dyn Sandbox,
base_sha: &str,
to_sha: &str,
) -> std::result::Result<Vec<RawDiffEntry>, DiffError> {
let base_q = shell_quote(base_sha);
let to_q = shell_quote(to_sha);
let env = sandbox_git_hardening_env();
let cmd = format!("{GIT_HARDENED} diff --raw -z --find-renames=50% {base_q}..{to_q}");
let res = sandbox
.exec_command(&cmd, 10_000, None, Some(&env), None)
.await
.map_err(|e| DiffError::Transient { message: e })?;
if res.timed_out {
return Err(DiffError::Transient {
message: "git diff --raw timed out".to_string(),
});
}
if res.exit_code != 0 {
// An unknown-object / bad-revision error is permanent; everything
// else we treat as transient so the server can retry safely.
let stderr = res.stderr.trim().to_string();
if is_permanent_git_error(&stderr) {
return Err(DiffError::Permanent { message: stderr });
}
return Err(DiffError::Transient { message: stderr });
}
parse_raw_z(&res.stdout).map_err(|message| DiffError::Permanent { message })
}
fn is_permanent_git_error(stderr: &str) -> bool {
// git emits these to stderr for unknown revisions / missing objects;
// treat them as Permanent so the handler falls through to final_patch.
let lower = stderr.to_lowercase();
lower.contains("unknown revision")
|| lower.contains("bad revision")
|| lower.contains("invalid revision")
|| lower.contains("no such path")
|| lower.contains("not a valid object name")
}
fn parse_raw_z(stdout: &str) -> std::result::Result<Vec<RawDiffEntry>, String> {
// git diff --raw -z format:
// ":<srcmode> <dstmode> <srcsha> <dstsha> <status>\0<path>\0"
// For renames/copies:
// ":<srcmode> <dstmode> <srcsha> <dstsha> R<score>\0<oldpath>\0<newpath>\0"
//
// Multiple entries are concatenated with no separator between them.
let mut entries = Vec::new();
let mut tokens = stdout.split('\0').peekable();
while let Some(header) = tokens.next() {
if header.is_empty() {
continue;
}
if !header.starts_with(':') {
return Err(format!("unexpected token in diff --raw: {header:?}"));
}
let fields: Vec<&str> = header[1..].split(' ').collect();
if fields.len() < 5 {
return Err(format!("short raw-diff header: {header:?}"));
}
let src_mode = fields[0];
let dst_mode = fields[1];
let src_sha = fields[2];
let dst_sha = fields[3];
let status = fields[4];
let entry = if status.starts_with('R') || status.starts_with('C') {
let score: u8 = status[1..].parse().unwrap_or(0);
let old_path = tokens
.next()
.ok_or_else(|| "missing old_path for rename".to_string())?
.to_string();
let new_path = tokens
.next()
.ok_or_else(|| "missing new_path for rename".to_string())?
.to_string();
RawDiffEntry::Renamed {
old_path,
new_path,
old_blob: src_sha.to_string(),
new_blob: dst_sha.to_string(),
new_mode: dst_mode.to_string(),
similarity: score,
}
} else {
let path = tokens
.next()
.ok_or_else(|| "missing path for diff entry".to_string())?
.to_string();
classify_entry(status, src_mode, dst_mode, src_sha, dst_sha, &path)?
};
entries.push(entry);
}
Ok(entries)
}
fn classify_entry(
status: &str,
src_mode: &str,
dst_mode: &str,
src_sha: &str,
dst_sha: &str,
path: &str,
) -> std::result::Result<RawDiffEntry, String> {
// Mode 120000 = symlink, 160000 = submodule (gitlink).
let is_symlink_change = src_mode == "120000" || dst_mode == "120000";
let is_submodule_change = src_mode == "160000" || dst_mode == "160000";
Ok(match (status, is_symlink_change, is_submodule_change) {
("A", true, _) => RawDiffEntry::Symlink {
path: path.to_string(),
change_kind: SymlinkChange::Added,
old_blob: None,
new_blob: Some(dst_sha.to_string()),
},
("A", _, true) => RawDiffEntry::Submodule {
path: path.to_string(),
change_kind: SubmoduleChange::Added,
},
("A", _, _) => RawDiffEntry::Added {
path: path.to_string(),
new_blob: dst_sha.to_string(),
new_mode: dst_mode.to_string(),
},
("D", true, _) => RawDiffEntry::Symlink {
path: path.to_string(),
change_kind: SymlinkChange::Deleted,
old_blob: Some(src_sha.to_string()),
new_blob: None,
},
("D", _, true) => RawDiffEntry::Submodule {
path: path.to_string(),
change_kind: SubmoduleChange::Deleted,
},
("D", _, _) => RawDiffEntry::Deleted {
path: path.to_string(),
old_blob: src_sha.to_string(),
old_mode: src_mode.to_string(),
},
("M" | "T", true, _) => RawDiffEntry::Symlink {
path: path.to_string(),
change_kind: SymlinkChange::Modified,
old_blob: Some(src_sha.to_string()),
new_blob: Some(dst_sha.to_string()),
},
("M" | "T", _, true) => RawDiffEntry::Submodule {
path: path.to_string(),
change_kind: SubmoduleChange::Modified,
},
("M" | "T", _, _) => RawDiffEntry::Modified {
path: path.to_string(),
old_blob: src_sha.to_string(),
new_blob: dst_sha.to_string(),
new_mode: dst_mode.to_string(),
},
(other, _, _) => {
return Err(format!("unknown raw-diff status {other:?} for {path:?}"));
}
})
}
/// Classify which changed paths in `base_sha..to_sha` are binary, via
/// `git diff --numstat`. Binary entries show `-\t-\t<path>` in the output.
///
/// Returns the set of repo-relative paths (post-rename, i.e. `<new_path>`
/// when a rename occurred) that `git` classifies as binary.
pub async fn list_binary_paths(
sandbox: &dyn Sandbox,
base_sha: &str,
to_sha: &str,
) -> std::result::Result<std::collections::HashSet<String>, DiffError> {
let base_q = shell_quote(base_sha);
let to_q = shell_quote(to_sha);
let env = sandbox_git_hardening_env();
let cmd = format!("{GIT_HARDENED} diff --numstat --find-renames=50% {base_q}..{to_q}");
let res = sandbox
.exec_command(&cmd, 10_000, None, Some(&env), None)
.await
.map_err(|e| DiffError::Transient { message: e })?;
if res.timed_out {
return Err(DiffError::Transient {
message: "git diff --numstat timed out".to_string(),
});
}
if res.exit_code != 0 {
let stderr = res.stderr.trim().to_string();
if is_permanent_git_error(&stderr) {
return Err(DiffError::Permanent { message: stderr });
}
return Err(DiffError::Transient { message: stderr });
}
let mut binary = std::collections::HashSet::new();
for line in res.stdout.lines() {
// `-\t-\t<path>` marks binary. Rename lines read `<+>\t<->\t<path> =>
// <path>` or `<+>\t<->\t{<old> => <new>}` — for the binary case we
// just need presence of "-\t-" prefix; extract the path portion.
if let Some(rest) = line.strip_prefix("-\t-\t") {
// Normalize rename display to the new path.
let path = extract_new_path_from_numstat(rest);
binary.insert(path);
}
}
Ok(binary)
}
fn extract_new_path_from_numstat(rest: &str) -> String {
// Forms seen:
// "simple/path"
// "old => new"
// "prefix/{old => new}/suffix"
if let Some(open_idx) = rest.find('{') {
if let Some(close_idx) = rest[open_idx..].find('}') {
let before = &rest[..open_idx];
let after = &rest[open_idx + close_idx + 1..];
let inside = &rest[open_idx + 1..open_idx + close_idx];
if let Some((_, new)) = inside.split_once(" => ") {
return format!("{before}{new}{after}");
}
}
}
if let Some((_, new)) = rest.split_once(" => ") {
return new.to_string();
}
rest.to_string()
}
/// Fetch blob metadata (size) for many SHAs in one sandbox invocation via
/// `git cat-file --batch-check`.
///
/// The order of returned `BlobMeta` entries matches the input `shas` order.
/// SHAs reported as `missing` by git yield `BlobMeta { size: None, .. }`.
pub async fn stream_blob_metadata(
sandbox: &dyn Sandbox,
shas: &[String],
) -> std::result::Result<Vec<BlobMeta>, DiffError> {
if shas.is_empty() {
return Ok(Vec::new());
}
let env = sandbox_git_hardening_env();
let quoted_shas: Vec<String> = shas.iter().map(|s| shell_quote(s)).collect();
let cmd = format!(
"printf '%s\\n' {} | {GIT_HARDENED} cat-file --batch-check",
quoted_shas.join(" ")
);
let res = sandbox
.exec_command(&cmd, 10_000, None, Some(&env), None)
.await
.map_err(|e| DiffError::Transient { message: e })?;
if res.timed_out {
return Err(DiffError::Transient {
message: "git cat-file --batch-check timed out".to_string(),
});
}
if res.exit_code != 0 {
return Err(DiffError::Transient {
message: format!("git cat-file --batch-check failed: {}", res.stderr.trim()),
});
}
let mut metas = Vec::with_capacity(shas.len());
for line in res.stdout.lines() {
// Lines: "<sha> <type> <size>" OR "<sha> missing"
let mut parts = line.split(' ');
let sha = parts
.next()
.ok_or_else(|| DiffError::Permanent {
message: format!("empty cat-file line: {line:?}"),
})?
.to_string();
let second = parts.next().unwrap_or("");
if second == "missing" {
metas.push(BlobMeta { sha, size: None });
continue;
}
let size_str = parts.next().unwrap_or("");
let size = size_str.parse::<u64>().map_err(|e| DiffError::Permanent {
message: format!("unparseable size {size_str:?} for {sha}: {e}"),
})?;
metas.push(BlobMeta {
sha,
size: Some(size),
});
}
Ok(metas)
}
/// Fetch blob contents for many SHAs in one sandbox invocation via
/// `git cat-file --batch`.
///
/// Contents are size-capped per blob: any blob exceeding `size_cap_bytes`
/// returns `None` in its slot (the caller should flag that entry as
/// truncated). Callers are expected to have pre-filtered binary blobs via
/// [`list_binary_paths`] — `--batch` output stream is text-oriented and
/// non-UTF-8 bytes are lossy through the sandbox `String` channel.
pub async fn stream_blobs(
sandbox: &dyn Sandbox,
shas: &[String],
size_cap_bytes: u64,
) -> std::result::Result<Vec<Option<String>>, DiffError> {
if shas.is_empty() {
return Ok(Vec::new());
}
let env = sandbox_git_hardening_env();
let quoted_shas: Vec<String> = shas.iter().map(|s| shell_quote(s)).collect();
let cmd = format!(
"printf '%s\\n' {} | {GIT_HARDENED} cat-file --batch",
quoted_shas.join(" ")
);
let res = sandbox
.exec_command(&cmd, 10_000, None, Some(&env), None)
.await
.map_err(|e| DiffError::Transient { message: e })?;
if res.timed_out {
return Err(DiffError::Transient {
message: "git cat-file --batch timed out".to_string(),
});
}
if res.exit_code != 0 {
return Err(DiffError::Transient {
message: format!("git cat-file --batch failed: {}", res.stderr.trim()),
});
}
parse_batch_output(&res.stdout, shas, size_cap_bytes)
.map_err(|message| DiffError::Permanent { message })
}
fn parse_batch_output(
stdout: &str,
shas: &[String],
size_cap_bytes: u64,
) -> std::result::Result<Vec<Option<String>>, String> {
// `git cat-file --batch` output per blob:
// "<sha> <type> <size>\n<content-bytes>\n"
// `missing` blob: "<sha> missing\n" (no content).
let mut results: Vec<Option<String>> = Vec::with_capacity(shas.len());
let bytes = stdout.as_bytes();
let mut pos = 0;
while pos < bytes.len() {
// Find end of header line.
let Some(nl_rel) = bytes[pos..].iter().position(|&b| b == b'\n') else {
break;
};
let header = std::str::from_utf8(&bytes[pos..pos + nl_rel])
.map_err(|e| format!("non-utf8 header in cat-file output: {e}"))?;
pos += nl_rel + 1;
let mut parts = header.split(' ');
let _sha = parts.next().unwrap_or("");
let second = parts.next().unwrap_or("");
if second == "missing" {
results.push(None);
continue;
}
let size_str = parts.next().unwrap_or("");
let size: usize = size_str
.parse()
.map_err(|e| format!("unparseable size {size_str:?}: {e}"))?;
let end = pos + size;
if end > bytes.len() {
return Err(format!(
"cat-file stream truncated: expected {size} bytes, have {}",
bytes.len() - pos
));
}
if (size as u64) > size_cap_bytes {
results.push(None);
} else {
let content = std::str::from_utf8(&bytes[pos..end])
.map_err(|e| format!("non-utf8 blob contents: {e}"))?;
results.push(Some(content.to_string()));
}
pos = end;
// Trailing newline that delimits the next entry.
if pos < bytes.len() && bytes[pos] == b'\n' {
pos += 1;
}
}
// Pad with None if the stream didn't cover every requested SHA (e.g.
// duplicate-sha deduping by git).
while results.len() < shas.len() {
results.push(None);
}
Ok(results)
}
#[cfg(test)]
mod tests {
#![expect(
@ -311,4 +855,249 @@ mod tests {
".venv should be excluded from checkpoint, got: {staged_files:?}"
);
}
// Test helpers for machine-readable diff enumeration. The repo is seeded
// with a single commit at `base_sha`, then callers mutate and re-commit
// to produce a synthetic `base_sha..HEAD` diff.
fn init_git_repo(repo: &std::path::Path) {
let _ = std::process::Command::new("git")
.args(["init", "-b", "main"])
.current_dir(repo)
.output()
.unwrap();
let _ = std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo)
.output()
.unwrap();
let _ = std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo)
.output()
.unwrap();
}
fn git_commit_all(repo: &std::path::Path, msg: &str) -> String {
let _ = std::process::Command::new("git")
.args(["add", "-A"])
.current_dir(repo)
.output()
.unwrap();
let _ = std::process::Command::new("git")
.args(["commit", "-m", msg])
.current_dir(repo)
.output()
.unwrap();
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(repo)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
#[tokio::test]
async fn list_changed_files_raw_classifies_add_modify_delete() {
let repo_dir = tempfile::tempdir().unwrap();
let repo = repo_dir.path();
init_git_repo(repo);
std::fs::write(repo.join("keep.txt"), "v1\n").unwrap();
std::fs::write(repo.join("drop.txt"), "goodbye\n").unwrap();
let base = git_commit_all(repo, "initial");
std::fs::write(repo.join("keep.txt"), "v2\n").unwrap();
std::fs::write(repo.join("add.txt"), "new\n").unwrap();
std::fs::remove_file(repo.join("drop.txt")).unwrap();
let head = git_commit_all(repo, "change");
let sandbox = fabro_agent::LocalSandbox::new(repo.to_path_buf());
let entries = list_changed_files_raw(&sandbox, &base, &head)
.await
.unwrap();
assert_eq!(entries.len(), 3, "entries: {entries:?}");
assert!(
entries
.iter()
.any(|e| matches!(e, RawDiffEntry::Added { path, .. } if path == "add.txt"))
);
assert!(
entries
.iter()
.any(|e| matches!(e, RawDiffEntry::Modified { path, .. } if path == "keep.txt"))
);
assert!(
entries
.iter()
.any(|e| matches!(e, RawDiffEntry::Deleted { path, .. } if path == "drop.txt"))
);
}
#[tokio::test]
async fn list_changed_files_raw_detects_rename_above_threshold() {
let repo_dir = tempfile::tempdir().unwrap();
let repo = repo_dir.path();
init_git_repo(repo);
// Write a file with enough content that a rename remains >= 50%
// similar even after the rename (identical content should be 100%).
let content = "line of shared content\n".repeat(50);
std::fs::write(repo.join("old.txt"), &content).unwrap();
let base = git_commit_all(repo, "initial");
std::fs::remove_file(repo.join("old.txt")).unwrap();
std::fs::write(repo.join("new.txt"), &content).unwrap();
let head = git_commit_all(repo, "rename");
let sandbox = fabro_agent::LocalSandbox::new(repo.to_path_buf());
let entries = list_changed_files_raw(&sandbox, &base, &head)
.await
.unwrap();
let renames: Vec<_> = entries
.iter()
.filter_map(|e| match e {
RawDiffEntry::Renamed {
old_path,
new_path,
similarity,
..
} => Some((old_path.clone(), new_path.clone(), *similarity)),
_ => None,
})
.collect();
assert_eq!(renames.len(), 1, "expected one rename, got: {entries:?}");
let (old_path, new_path, similarity) = &renames[0];
assert_eq!(old_path, "old.txt");
assert_eq!(new_path, "new.txt");
assert!(*similarity >= 50, "similarity = {similarity}");
}
#[tokio::test]
async fn list_binary_paths_flags_png_but_not_text() {
let repo_dir = tempfile::tempdir().unwrap();
let repo = repo_dir.path();
init_git_repo(repo);
std::fs::write(repo.join("doc.md"), "hi").unwrap();
let base = git_commit_all(repo, "initial");
std::fs::write(repo.join("doc.md"), "bye\n").unwrap();
// Minimal PNG header (8-byte signature) + a chunk — git classifies
// this as binary via NUL-byte detection.
let png: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, b'I', b'H',
b'D', b'R',
];
std::fs::write(repo.join("logo.png"), png).unwrap();
let head = git_commit_all(repo, "change");
let sandbox = fabro_agent::LocalSandbox::new(repo.to_path_buf());
let binary = list_binary_paths(&sandbox, &base, &head).await.unwrap();
assert!(binary.contains("logo.png"), "binary: {binary:?}");
assert!(!binary.contains("doc.md"), "binary: {binary:?}");
}
#[tokio::test]
async fn stream_blob_metadata_returns_sizes_in_order() {
let repo_dir = tempfile::tempdir().unwrap();
let repo = repo_dir.path();
init_git_repo(repo);
std::fs::write(repo.join("a.txt"), "aaa\n").unwrap();
std::fs::write(repo.join("b.txt"), "bb\n").unwrap();
git_commit_all(repo, "seed");
let ls = std::process::Command::new("git")
.args(["ls-files", "-s"])
.current_dir(repo)
.output()
.unwrap();
// `ls-files -s` format: "<mode> <sha> <stage>\t<path>"
let mut sha_by_name = std::collections::HashMap::new();
for line in String::from_utf8_lossy(&ls.stdout).lines() {
let mut cols = line.splitn(2, '\t');
let (meta, path) = (cols.next().unwrap(), cols.next().unwrap());
let mut parts = meta.split_whitespace();
let _mode = parts.next();
let sha = parts.next().unwrap();
sha_by_name.insert(path.to_string(), sha.to_string());
}
let sandbox = fabro_agent::LocalSandbox::new(repo.to_path_buf());
let shas = vec![sha_by_name["a.txt"].clone(), sha_by_name["b.txt"].clone()];
let metas = stream_blob_metadata(&sandbox, &shas).await.unwrap();
assert_eq!(metas.len(), 2);
assert_eq!(metas[0].sha, shas[0]);
assert_eq!(metas[0].size, Some(4));
assert_eq!(metas[1].sha, shas[1]);
assert_eq!(metas[1].size, Some(3));
}
#[tokio::test]
async fn stream_blobs_returns_contents_and_respects_size_cap() {
let repo_dir = tempfile::tempdir().unwrap();
let repo = repo_dir.path();
init_git_repo(repo);
std::fs::write(repo.join("a.txt"), "hello\n").unwrap();
let big = "b".repeat(200);
std::fs::write(repo.join("big.txt"), &big).unwrap();
git_commit_all(repo, "seed");
let ls = std::process::Command::new("git")
.args(["ls-files", "-s"])
.current_dir(repo)
.output()
.unwrap();
let mut sha_by_name = std::collections::HashMap::new();
for line in String::from_utf8_lossy(&ls.stdout).lines() {
let mut cols = line.splitn(2, '\t');
let (meta, path) = (cols.next().unwrap(), cols.next().unwrap());
let mut parts = meta.split_whitespace();
let _mode = parts.next();
let sha = parts.next().unwrap();
sha_by_name.insert(path.to_string(), sha.to_string());
}
let sandbox = fabro_agent::LocalSandbox::new(repo.to_path_buf());
let shas = vec![sha_by_name["a.txt"].clone(), sha_by_name["big.txt"].clone()];
// size_cap = 100 bytes — "hello\n" (6) stays, 200-byte blob truncates.
let contents = stream_blobs(&sandbox, &shas, 100).await.unwrap();
assert_eq!(contents.len(), 2);
assert_eq!(contents[0].as_deref(), Some("hello\n"));
assert!(contents[1].is_none(), "oversize blob should be None");
}
#[tokio::test]
async fn list_changed_files_raw_bad_revision_is_permanent() {
let repo_dir = tempfile::tempdir().unwrap();
let repo = repo_dir.path();
init_git_repo(repo);
std::fs::write(repo.join("x"), "x").unwrap();
git_commit_all(repo, "seed");
let sandbox = fabro_agent::LocalSandbox::new(repo.to_path_buf());
let err =
list_changed_files_raw(&sandbox, "0000000000000000000000000000000000000000", "HEAD")
.await
.expect_err("expected error for unknown base sha");
assert!(matches!(err, DiffError::Permanent { .. }), "err: {err:?}");
}
#[test]
fn extract_new_path_from_numstat_handles_brace_renames() {
assert_eq!(extract_new_path_from_numstat("simple/path"), "simple/path");
assert_eq!(
extract_new_path_from_numstat("old.txt => new.txt"),
"new.txt"
);
assert_eq!(
extract_new_path_from_numstat("src/{old => new}/file.rs"),
"src/new/file.rs"
);
}
}

View file

@ -145,6 +145,7 @@ models/run-checkpoint.ts
models/run-control-action.ts
models/run-error.ts
models/run-event.ts
models/run-files-meta.ts
models/run-list-item.ts
models/run-manifest.ts
models/run-projection-checkpoints-inner-inner.ts

View file

@ -24,12 +24,74 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedRunFileList } from '../models';
// @ts-ignore
import type { RunBilling } from '../models';
/**
* RunOutputsApi - axios parameter creator
*/
export const RunOutputsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`.
* @summary List Run Files Changed
* @param {string} id Unique run identifier (ULID).
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRunFiles: async (id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('listRunFiles', 'id', id)
const localVarPath = `/api/v1/runs/{id}/files`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication SessionCookie required
// authentication BearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (pageLimit !== undefined) {
localVarQueryParameter['page[limit]'] = pageLimit;
}
if (pageOffset !== undefined) {
localVarQueryParameter['page[offset]'] = pageOffset;
}
if (fromSha !== undefined) {
localVarQueryParameter['from_sha'] = fromSha;
}
if (toSha !== undefined) {
localVarQueryParameter['to_sha'] = toSha;
}
localVarHeaderParameter['Accept'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Returns token counts and billed totals broken down by stage and model for a specific run.
* @summary Retrieve Run Billing
@ -79,6 +141,23 @@ export const RunOutputsApiAxiosParamCreator = function (configuration?: Configur
export const RunOutputsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = RunOutputsApiAxiosParamCreator(configuration)
return {
/**
* Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`.
* @summary List Run Files Changed
* @param {string} id Unique run identifier (ULID).
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listRunFiles(id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunFileList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRunFiles(id, pageLimit, pageOffset, fromSha, toSha, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.listRunFiles']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Returns token counts and billed totals broken down by stage and model for a specific run.
* @summary Retrieve Run Billing
@ -101,6 +180,20 @@ export const RunOutputsApiFp = function(configuration?: Configuration) {
export const RunOutputsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = RunOutputsApiFp(configuration)
return {
/**
* Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`.
* @summary List Run Files Changed
* @param {string} id Unique run identifier (ULID).
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRunFiles(id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunFileList> {
return localVarFp.listRunFiles(id, pageLimit, pageOffset, fromSha, toSha, options).then((request) => request(axios, basePath));
},
/**
* Returns token counts and billed totals broken down by stage and model for a specific run.
* @summary Retrieve Run Billing
@ -118,6 +211,21 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas
* RunOutputsApi - object-oriented interface
*/
export class RunOutputsApi extends BaseAPI {
/**
* Returns the set of file changes produced by a run as a list of before/after diffs. While the run\'s sandbox is reachable, diffs are resolved live against the sandbox working tree at the current HEAD. Once the sandbox is gone, the response may degrade to the unified-patch string captured at run end (see `meta.degraded` and `meta.patch`). Responses are bounded by per-file (256 KiB / 20k lines), per-run aggregate (5 MiB), and per-request (200 files) caps. Files exceeding a cap are returned with `truncated: true` and empty `contents`. Sensitive paths (credentials, keys) are elided with `sensitive: true` and empty `contents`.
* @summary List Run Files Changed
* @param {string} id Unique run identifier (ULID).
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {string} [fromSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {string} [toSha] Reserved for future use. Only the default value is accepted in the current API version; any other value returns 400.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public listRunFiles(id: string, pageLimit?: number, pageOffset?: number, fromSha?: string, toSha?: string, options?: RawAxiosRequestConfig) {
return RunOutputsApiFp(this.configuration).listRunFiles(id, pageLimit, pageOffset, fromSha, toSha, options).then((request) => request(this.axios, this.basePath));
}
/**
* Returns token counts and billed totals broken down by stage and model for a specific run.
* @summary Retrieve Run Billing

View file

@ -18,10 +18,48 @@
import type { DiffFile } from './diff-file';
/**
* A before/after pair showing changes to a single file.
* A before/after pair showing changes to a single file. Contents conventions for non-modify cases: - Added: `old_file.contents` is empty string; `new_file` holds the added contents. - Deleted: `new_file.contents` is empty string; `old_file` holds the removed contents. - Renamed (no content change): both sides hold identical contents; `old_file.name != new_file.name`. - Symlink / submodule / binary / sensitive / truncated: contents are empty strings; consumers must render a placeholder based on the flag set.
*/
export interface FileDiff {
'old_file': DiffFile;
'new_file': DiffFile;
/**
* Optional classification of the change. Clients that don\'t recognize a value should fall back to inspecting the old/new contents.
*/
'change_kind'?: FileDiffChangeKindEnum;
/**
* When `true`, `new_file.contents` and `old_file.contents` are empty strings because the file exceeded a cap (see `truncation_reason`).
*/
'truncated'?: boolean;
/**
* Reason this file\'s contents were omitted. Absent when `truncated` is `false` or omitted.
*/
'truncation_reason'?: FileDiffTruncationReasonEnum;
/**
* When `true`, the file is non-textual; `contents` on both sides are empty strings.
*/
'binary'?: boolean;
/**
* When `true`, the file path matched the server\'s sensitive-path denylist; `contents` on both sides are empty strings regardless of truncation or binary flags.
*/
'sensitive'?: boolean;
}
export const FileDiffChangeKindEnum = {
ADDED: 'added',
MODIFIED: 'modified',
DELETED: 'deleted',
RENAMED: 'renamed',
SYMLINK: 'symlink',
SUBMODULE: 'submodule'
} as const;
export type FileDiffChangeKindEnum = typeof FileDiffChangeKindEnum[keyof typeof FileDiffChangeKindEnum];
export const FileDiffTruncationReasonEnum = {
FILE_TOO_LARGE: 'file_too_large',
BUDGET_EXHAUSTED: 'budget_exhausted'
} as const;
export type FileDiffTruncationReasonEnum = typeof FileDiffTruncationReasonEnum[keyof typeof FileDiffTruncationReasonEnum];

View file

@ -125,6 +125,7 @@ export * from './run-checkpoint';
export * from './run-control-action';
export * from './run-error';
export * from './run-event';
export * from './run-files-meta';
export * from './run-list-item';
export * from './run-manifest';
export * from './run-projection';

View file

@ -18,13 +18,13 @@
import type { FileDiff } from './file-diff';
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
import type { RunFilesMeta } from './run-files-meta';
/**
* Paginated list of file diffs produced by a run.
* List of file diffs produced by a run, with metadata describing truncation and degraded-response state. Naturally bounded: at most 200 files per response. Consumers should inspect `meta.truncated` rather than assuming `data.length` equals the run\'s total change count.
*/
export interface PaginatedRunFileList {
'data': Array<FileDiff>;
'meta': PaginationMeta;
'meta': RunFilesMeta;
}

View file

@ -0,0 +1,63 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Metadata for a `PaginatedRunFileList` response. Replaces `PaginationMeta` on the files endpoint — the naturally-bounded list does not use cursor pagination but exposes caps and a degraded-response path instead.
*/
export interface RunFilesMeta {
/**
* True when any cap (file count, per-file size, or aggregate size) was hit for this response.
*/
'truncated': boolean;
/**
* Number of files dropped because the aggregate 5 MiB budget was exhausted. Zero or absent when no files were dropped for budget reasons.
*/
'files_omitted_by_budget'?: number;
/**
* Total files changed in the run (before caps were applied). May exceed `data.length` when truncation occurred.
*/
'total_changed': number;
/**
* Head SHA the diff (or patch) was resolved against.
*/
'to_sha'?: string;
/**
* Commit time of `to_sha`, used by the UI for the \"Checkpoint Xm ago\" freshness label on Running runs.
*/
'to_sha_committed_at'?: string;
/**
* When `true`, this response carries only a unified patch string in `patch` (no per-file contents in `data`). The UI should render via a unified-patch component.
*/
'degraded'?: boolean;
/**
* Why the response degraded to patch-only form. Absent when `degraded` is `false` or omitted.
*/
'degraded_reason'?: RunFilesMetaDegradedReasonEnum;
/**
* Unified-patch text captured at run end. Present only when `degraded` is `true`. Capped at 5 MiB; denylisted file sections are stripped and replaced with a placeholder line.
*/
'patch'?: string;
}
export const RunFilesMetaDegradedReasonEnum = {
SANDBOX_UNREACHABLE: 'sandbox_unreachable',
SANDBOX_GONE: 'sandbox_gone',
PROVIDER_UNSUPPORTED: 'provider_unsupported'
} as const;
export type RunFilesMetaDegradedReasonEnum = typeof RunFilesMetaDegradedReasonEnum[keyof typeof RunFilesMetaDegradedReasonEnum];