mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
679658b287
* fix(reference): report entries beyond the path limit as I/O failures Reference-source traversal joins each child onto the root path. A valid tree whose nested entries exceed the platform's absolute path limit (PATH_MAX 1024 on macOS) makes lstat fail with ENAMETOOLONG. The reader's filesystem error allow-list lacked that code, so filesystemFailureDetail returned undefined and the native rejection escaped both readReferenceSource and importReferenceSource instead of becoming the existing failed `io` entry. Add ENAMETOOLONG to the allow-list, matching the JavaScript application reader's list. Readable siblings stay in the graph and the unreachable entry is recorded with its OS message. Fixes #2652 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(reference): assert the imported graph keeps readable and failed entries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(process): report no remaining time after a provider deadline fires Node timers can fire while performance.now() is still a fraction of a millisecond short of the monotonic deadline. ProviderStartupDeadline then reported interruption "timeout" and an aborted signal while remainingMs() returned ceil(0.x) = 1, so polling loops guarded by remainingMs() > 0 (Hopper and Ghidra startup) could run past an elapsed deadline, and the real-timer test "aborts a bounded wait when its real monotonic deadline expires" failed intermittently in CI (expected 1 to be 0). Once the deadline's own timer has classified a timeout, report zero remaining time. A deterministic regression test fires the timer with the monotonic clock 0.4 ms short of the deadline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(reference): preserve path limit entry uncertainty * fix(reference): preserve unknown entry failures and identity * fix(reference): type unknown-kind exclusions and keep them out of symlink counts The metadata-failure path offers "unknown" to exclusion predicates, but the predicate type only admitted concrete kinds, which broke the build. Directory-only secret and ignore rules now also match an unknown-kind entry (it may be a directory), and the summary counts unknown entries separately instead of as symlinks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Codex <codex@users.noreply.github.com>
230 lines
7.8 KiB
TypeScript
230 lines
7.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { posix } from "node:path";
|
|
|
|
import { logicalPathEscapesRoot } from "../domain/artifactIdentity.js";
|
|
import {
|
|
createReferenceSourcePathLookup,
|
|
isPortableAbsoluteReferenceTarget,
|
|
type ReferenceSourcePathLookup,
|
|
} from "../domain/referenceSourcePathIdentity.js";
|
|
import {
|
|
classifyReferenceSourcePath,
|
|
detectReferenceSourceLanguage,
|
|
} from "../domain/referenceSourceClassification.js";
|
|
import type { HistoricalSourceGraphInput } from "../domain/referenceSourceGraph.js";
|
|
import {
|
|
parseReferenceSourceImports,
|
|
PARSEABLE_REFERENCE_SOURCE_LANGUAGES,
|
|
} from "../domain/referenceSourceImportParsing.js";
|
|
import type {
|
|
ReferenceSourceEntry,
|
|
ReferenceSourceRead,
|
|
} from "../reference/ReferenceSourceReaderTypes.js";
|
|
import { typescriptEmittedSourceCandidates } from "./TypeScriptEmittedSourceCandidates.js";
|
|
|
|
export interface ParsedReferenceSourceEntries {
|
|
readonly entries: HistoricalSourceGraphInput["entries"];
|
|
readonly relationships: HistoricalSourceGraphInput["relationships"];
|
|
readonly parseFailures: HistoricalSourceGraphInput["parse_failures"];
|
|
readonly limitations: string[];
|
|
}
|
|
|
|
/** Preserve the observed reader failure and add entry-specific recovery guidance. */
|
|
export const projectReferenceSourceEntryFailure = (
|
|
entry: Extract<ReferenceSourceEntry, { status: "failed" }>,
|
|
): string => `${entry.message} ${entryRecoveryGuidance(entry)}`;
|
|
|
|
const entryRecoveryGuidance = (
|
|
entry: Extract<ReferenceSourceEntry, { status: "failed" }>,
|
|
): string => {
|
|
if (entry.code === "cancelled")
|
|
return "This entry was not read because the import was cancelled. Start the import again when ready.";
|
|
if (entry.code === "unsupported")
|
|
return "Exclude this entry or replace it with a regular file.";
|
|
if (entry.kind === "directory")
|
|
return "This directory could not be read. Check its permissions, then try again.";
|
|
if (entry.kind === "symlink")
|
|
return "This symbolic link could not be read safely. Check the link and its permissions, then try again.";
|
|
if (entry.kind === "unknown")
|
|
return "This entry's type could not be determined. Check the parent directory and try again.";
|
|
return "This file could not be read. Check its permissions, then try again.";
|
|
};
|
|
|
|
const hashBytes = (bytes: Uint8Array): string =>
|
|
createHash("sha256").update(bytes).digest("hex");
|
|
|
|
const failedEntry = (
|
|
entry: Extract<ReferenceSourceEntry, { status: "failed" }>,
|
|
): HistoricalSourceGraphInput["entries"][number] => {
|
|
const classifications = classifyReferenceSourcePath(entry.path);
|
|
const limitation = projectReferenceSourceEntryFailure(entry);
|
|
if (entry.kind === "directory")
|
|
return {
|
|
path: entry.path,
|
|
kind: "directory",
|
|
classifications,
|
|
tree_state: "unreadable",
|
|
limitations: [limitation],
|
|
};
|
|
if (entry.kind === "symlink")
|
|
return {
|
|
path: entry.path,
|
|
kind: "symlink",
|
|
target: null,
|
|
target_state: "unreadable",
|
|
classifications,
|
|
limitations: [limitation],
|
|
};
|
|
if (entry.kind === "unknown")
|
|
return {
|
|
path: entry.path,
|
|
kind: "unknown",
|
|
classifications: ["unknown"],
|
|
limitations: [limitation],
|
|
};
|
|
return {
|
|
path: entry.path,
|
|
kind: "file",
|
|
sha256: null,
|
|
size: entry.size ?? null,
|
|
language: null,
|
|
classifications: entry.kind === "file" ? classifications : ["unknown"],
|
|
content_state: "unreadable",
|
|
limitations: [limitation],
|
|
};
|
|
};
|
|
|
|
const resolveInternalSpecifier = (
|
|
fromPath: string,
|
|
specifier: string,
|
|
lookupPath: ReferenceSourcePathLookup,
|
|
): { to: string; resolution: "internal" | "unresolved" } => {
|
|
if (posix.isAbsolute(specifier))
|
|
return { to: specifier, resolution: "unresolved" };
|
|
const normalized = posix.normalize(
|
|
posix.join(posix.dirname(fromPath), specifier),
|
|
);
|
|
if (logicalPathEscapesRoot(normalized))
|
|
return { to: normalized, resolution: "unresolved" };
|
|
// Preserve directory intent before normalization removes terminal dot segments.
|
|
const directoryReference =
|
|
specifier.endsWith("/") || [".", ".."].includes(posix.basename(specifier));
|
|
const suffixes = ["", ".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx"];
|
|
const candidates = suffixes.flatMap((suffix) => [
|
|
...(directoryReference ? [] : [`${normalized}${suffix}`]),
|
|
posix.join(normalized, `index${suffix}`),
|
|
]);
|
|
// Retain present runtime files first; substitute only a missing counterpart.
|
|
// Counterparts cover explicit filenames, not directory/index resolution.
|
|
if (!directoryReference)
|
|
candidates.push(
|
|
...typescriptEmittedSourceCandidates(fromPath, normalized).map(
|
|
({ path }) => path,
|
|
),
|
|
);
|
|
const match = lookupPath.resolveCandidates(candidates);
|
|
return match === undefined
|
|
? { to: normalized, resolution: "unresolved" }
|
|
: { to: match, resolution: "internal" };
|
|
};
|
|
|
|
type ReadFile = Extract<ReferenceSourceEntry, { status: "read"; kind: "file" }>;
|
|
|
|
const appendFile = (
|
|
entry: ReadFile,
|
|
lookupPath: ReferenceSourcePathLookup,
|
|
output: ParsedReferenceSourceEntries,
|
|
): void => {
|
|
const language = detectReferenceSourceLanguage(entry.path);
|
|
const parsed =
|
|
language === null || !PARSEABLE_REFERENCE_SOURCE_LANGUAGES.has(language)
|
|
? undefined
|
|
: parseReferenceSourceImports(entry.path, entry.bytes, language);
|
|
output.entries.push({
|
|
path: entry.path,
|
|
kind: "file",
|
|
sha256: hashBytes(entry.bytes),
|
|
size: entry.size,
|
|
language,
|
|
classifications: classifyReferenceSourcePath(entry.path),
|
|
content_state: "hashed",
|
|
limitations: [...(parsed?.limitations ?? [])],
|
|
});
|
|
if (parsed === undefined) return;
|
|
for (const relationship of parsed.relationships) {
|
|
if (relationship.resolution !== "internal") {
|
|
output.relationships.push(relationship);
|
|
continue;
|
|
}
|
|
const resolved = resolveInternalSpecifier(
|
|
relationship.from_path,
|
|
relationship.to,
|
|
lookupPath,
|
|
);
|
|
output.relationships.push({
|
|
...relationship,
|
|
to: resolved.to,
|
|
resolution: resolved.resolution,
|
|
});
|
|
}
|
|
output.parseFailures.push(...parsed.parse_failures);
|
|
};
|
|
|
|
const appendReadEntry = (
|
|
entry: Extract<ReferenceSourceEntry, { status: "read" }>,
|
|
lookupPath: ReferenceSourcePathLookup,
|
|
output: ParsedReferenceSourceEntries,
|
|
): void => {
|
|
if (entry.kind === "file") {
|
|
appendFile(entry, lookupPath, output);
|
|
return;
|
|
}
|
|
const classifications = classifyReferenceSourcePath(entry.path);
|
|
if (entry.kind === "directory") {
|
|
output.entries.push({
|
|
path: entry.path,
|
|
kind: "directory",
|
|
classifications,
|
|
tree_state: "enumerated",
|
|
limitations: [],
|
|
});
|
|
return;
|
|
}
|
|
output.entries.push({
|
|
path: entry.path,
|
|
kind: "symlink",
|
|
// The reader resolves inventory identities from its root; the persisted
|
|
// graph represents relative symlink targets from the link's directory.
|
|
target:
|
|
entry.targetState === "internal" ||
|
|
(entry.targetState === "missing" &&
|
|
!isPortableAbsoluteReferenceTarget(entry.target))
|
|
? posix.relative(posix.dirname(entry.path), entry.target) || "."
|
|
: entry.target,
|
|
target_state: entry.targetState,
|
|
classifications,
|
|
limitations: [],
|
|
});
|
|
};
|
|
|
|
/** Convert bounded reader observations into graph entries and parsed edges. */
|
|
export const parseReferenceSourceEntries = (
|
|
read: ReferenceSourceRead,
|
|
filePaths: ReadonlySet<string>,
|
|
signal?: AbortSignal,
|
|
): ParsedReferenceSourceEntries => {
|
|
const output: ParsedReferenceSourceEntries = {
|
|
entries: [],
|
|
relationships: [],
|
|
parseFailures: [],
|
|
limitations: [...read.limitations],
|
|
};
|
|
const lookupPath = createReferenceSourcePathLookup(filePaths);
|
|
for (const entry of read.entries) {
|
|
if (signal?.aborted === true) break;
|
|
if (entry.status === "failed") output.entries.push(failedEntry(entry));
|
|
else appendReadEntry(entry, lookupPath, output);
|
|
}
|
|
return output;
|
|
};
|