mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
Keep expected readlink and follow-up lstat failures as typed entry omissions,
including the observed path and errno. Retain directory metadata and sibling
observations, distinguish known identity replacements, and keep cancellation
and unexpected I/O failures terminal. Reuse admitted root metadata and test
cancellation by descriptor state rather than a count of named lookups.
Adapt the symlink race handling proposed in #4587 at fae4e25539.
Closes #4575
BREAKING CHANGE: Filesystem enumeration omission reasons now also include
entry_unavailable and identity_changed. Clients validating the old literal
reason must accept the expanded capture schema.
Co-authored-by: rudrakshagarwal4200-ai <rudrakshagarwal4200@gmail.com>
141 lines
5.2 KiB
TypeScript
141 lines
5.2 KiB
TypeScript
import { z } from "zod";
|
|
import type { UnverifiedProcessCapture } from "./processCapture.js";
|
|
import type { ProcessObservationSource } from "./processObservation.js";
|
|
|
|
const byteRetentionSchema = z.strictObject({
|
|
budget_bytes: z.number().int().nonnegative(),
|
|
observed_bytes: z.number().int().nonnegative(),
|
|
retained_bytes: z.number().int().nonnegative(),
|
|
observed_frames: z.number().int().nonnegative(),
|
|
retained_frames: z.number().int().nonnegative(),
|
|
unknown_size_frames: z.number().int().nonnegative().optional(),
|
|
});
|
|
|
|
const enumerationOmissionReasonSchema = z.enum([
|
|
"identity_changed",
|
|
"directory_unavailable",
|
|
"entry_unavailable",
|
|
]);
|
|
|
|
/** Filesystem enumeration and whole-file digest coverage for one checkpoint. */
|
|
export const filesystemCoverageSchema = z.strictObject({
|
|
files_limit: z.number().int().nonnegative(),
|
|
depth_limit: z.number().int().nonnegative(),
|
|
enumeration_truncated: z.boolean(),
|
|
enumeration_reasons: z.array(
|
|
z.enum([
|
|
"files_limit",
|
|
"depth_limit",
|
|
...enumerationOmissionReasonSchema.options,
|
|
]),
|
|
),
|
|
enumeration_omissions: z.array(
|
|
z.strictObject({
|
|
path: z.string(),
|
|
reason: enumerationOmissionReasonSchema,
|
|
system_code: z.string().nullable(),
|
|
}),
|
|
),
|
|
hash_budget_bytes: z.number().int().nonnegative(),
|
|
hashed_bytes: z.number().int().nonnegative(),
|
|
hash_omissions: z.array(
|
|
z.strictObject({
|
|
path: z.string(),
|
|
size_bytes: z.number().int().nonnegative(),
|
|
remaining_budget_bytes: z.number().int().nonnegative(),
|
|
reason: z.enum([
|
|
"file_bytes_budget",
|
|
"file_changed_or_short_read",
|
|
"file_unavailable",
|
|
]),
|
|
system_code: z.string().nullable(),
|
|
}),
|
|
),
|
|
});
|
|
export type FilesystemCoverage = z.infer<typeof filesystemCoverageSchema>;
|
|
|
|
/** Actual budget accounting; rendered bytes count state and visible lines, not JSON. */
|
|
export const processCaptureTruncationDetailsSchema = z.strictObject({
|
|
raw_terminal: byteRetentionSchema.describe(
|
|
"UTF-8 bytes of original PTY chunks; whole chunks are retained or omitted.",
|
|
),
|
|
rendered_terminal: byteRetentionSchema.describe(
|
|
"Cumulative UTF-8 bytes of serialized state plus visible lines, evaluated from retained PTY input; whole frames are retained or omitted. When normalization exceeds a bound before the frame size is known, observed_bytes contains the exact total for size-known frames and unknown_size_frames counts the other omissions.",
|
|
),
|
|
normalized_terminal: z
|
|
.strictObject({
|
|
budget_bytes: z.number().int().nonnegative(),
|
|
retained_bytes: z.number().int().nonnegative(),
|
|
omitted_frames: z.number().int().nonnegative(),
|
|
})
|
|
.optional()
|
|
.describe(
|
|
"UTF-8 bytes of normalized raw-chunk text retained under output_bytes; omitted frames can have unknown final size when normalization exceeded an intermediate bound.",
|
|
),
|
|
filesystem_before: filesystemCoverageSchema,
|
|
filesystem_after: filesystemCoverageSchema,
|
|
process: z.strictObject({
|
|
sample_limit: z.number().int().nonnegative(),
|
|
sampling_partial: z.boolean(),
|
|
retained_samples: z.number().int().nonnegative(),
|
|
sample_limit_reached: z.boolean(),
|
|
sampling_failures: z.number().int().nonnegative(),
|
|
first_sampling_failure: z.string().nullable(),
|
|
coverage: z.literal("sampled"),
|
|
}),
|
|
});
|
|
export type ProcessCaptureTruncationDetails = z.infer<
|
|
typeof processCaptureTruncationDetailsSchema
|
|
>;
|
|
export type TerminalRetention = ProcessCaptureTruncationDetails["raw_terminal"];
|
|
export type NormalizedTerminalRetention = NonNullable<
|
|
ProcessCaptureTruncationDetails["normalized_terminal"]
|
|
>;
|
|
export type ProcessSamplingCoverage =
|
|
ProcessCaptureTruncationDetails["process"];
|
|
|
|
const omitted = (retention: TerminalRetention): boolean =>
|
|
retention.observed_frames > retention.retained_frames;
|
|
const filesystemTruncated = (coverage: FilesystemCoverage): boolean =>
|
|
coverage.enumeration_truncated ||
|
|
coverage.enumeration_omissions.length > 0 ||
|
|
coverage.hash_omissions.length > 0;
|
|
|
|
/** Derive the aggregate truncation flag from independent producer observations. */
|
|
export const hasCaptureTruncation = (
|
|
details: ProcessCaptureTruncationDetails,
|
|
): boolean =>
|
|
omitted(details.raw_terminal) ||
|
|
omitted(details.rendered_terminal) ||
|
|
(details.normalized_terminal?.omitted_frames ?? 0) > 0 ||
|
|
(details.rendered_terminal.unknown_size_frames ?? 0) > 0 ||
|
|
filesystemTruncated(details.filesystem_before) ||
|
|
filesystemTruncated(details.filesystem_after) ||
|
|
details.process.sampling_partial;
|
|
|
|
/** Whether producer accounting reports a gap in the selected observation source. */
|
|
export const processSourceTruncated = (
|
|
capture: UnverifiedProcessCapture,
|
|
source: ProcessObservationSource,
|
|
): boolean => {
|
|
const details = capture.truncation_details;
|
|
switch (source) {
|
|
case "terminal_raw":
|
|
return omitted(details.raw_terminal);
|
|
case "terminal_rendered":
|
|
return (
|
|
omitted(details.raw_terminal) || omitted(details.rendered_terminal)
|
|
);
|
|
case "filesystem":
|
|
return (
|
|
filesystemTruncated(details.filesystem_before) ||
|
|
filesystemTruncated(details.filesystem_after)
|
|
);
|
|
case "process":
|
|
return details.process.sampling_partial;
|
|
case "interaction":
|
|
case "lifecycle":
|
|
return false;
|
|
}
|
|
};
|