* fix(reference): resolve emitted extensions to TypeScript source Signed-off-by: Rudy Celekli <rudy@gradiahq.com> * test(reference): retain legitimate declaration import counterparts Signed-off-by: Rudy Celekli <rudy@gradiahq.com> * fix(mcp): recognize implicit form elicitation capability Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> * fix(ghidra): reject contradictory NativeAOT reports * fix(native): preserve thin architectures across echoed filename lines Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(native): preserve app bundle executable filename whitespace Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(doctor): validate Node-wrapped client policy Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> * fix(reference): preserve observed entry failure reasons Signed-off-by: Rudy Celekli <rudy@gradiahq.com> * fix(artifacts): verify opaque bytes before reusing inventory identities Signed-off-by: Rudy Celekli <rudy@gradiahq.com> * fix(browser): resolve DOM destinations against document base URL Signed-off-by: Rudy Celekli <rudy@gradiahq.com> * fix: recover serialized native NIB view hierarchy Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> * ci: exercise native compiled NIB hierarchy regressions Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> * fix(deps): remediate reachable archive-parsing advisories Two high-severity advisories are reachable from untrusted target content: - @xmldom/xmldom 0.9.10 (via plist) parses XML plist bytes from analyzed app bundles in the keyed-archive reader. Refreshed to 0.9.12. - brace-expansion 5.0.7 (via @electron/asar -> minimatch) is reached while analyzing untrusted ASAR archives. Refreshed to 5.0.12. Both are in-range patch updates (plist declares ^0.9.10, minimatch declares ^5.0.5), so no source or API change is required and no direct dependency declaration moves. Lockfile-only change. The remaining smol-toml advisory is not included: it parses operator-owned client configuration rather than untrusted target content, and its fix moves outside the declared range and changes leading-BOM handling. * fix(semantics): link wrapped arrow Promise returns * fix(reconstruction): preserve unknown graph limitations * fix(native): preserve exact code signing identifiers Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(native): recognize only whole otool command headers Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(native): retain nm-proven literal export names Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(javascript): project decoded file URL paths Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix: preserve explicit source-relative URL observation spelling Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(native): project Mach-O evidence into container file offsets Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * fix(browser): normalize referrer policy fallback lists Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --------- Signed-off-by: Rudy Celekli <rudy@gradiahq.com> Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Co-authored-by: Rudy Celekli <rudy@gradiahq.com> Co-authored-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> Co-authored-by: Kaoru0822 <aisakamimaki@gmail.com> Co-authored-by: DeryFerd <deryferdikao125@gmail.com>
205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { posix } from "node:path";
|
|
|
|
import {
|
|
classifyReferenceSourcePath,
|
|
detectReferenceSourceLanguage,
|
|
} from "../domain/referenceSourceClassification.js";
|
|
import type { HistoricalSourceGraphInput } from "../domain/referenceSourceGraph.js";
|
|
import { parseReferenceSourceImports } from "../domain/referenceSourceImportParsing.js";
|
|
import type {
|
|
ReferenceSourceEntry,
|
|
ReferenceSourceRead,
|
|
} from "../reference/ReferenceSourceReader.js";
|
|
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.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.";
|
|
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: "<unreadable>",
|
|
target_state: "unreadable",
|
|
classifications,
|
|
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,
|
|
filePaths: ReadonlySet<string>,
|
|
): { 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 (normalized.startsWith("../") || normalized === "..")
|
|
return { to: normalized, resolution: "unresolved" };
|
|
const suffixes = ["", ".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx"];
|
|
const candidates = suffixes.flatMap((suffix) => [
|
|
`${normalized}${suffix}`,
|
|
`${normalized}/index${suffix}`,
|
|
]);
|
|
// TypeScript source commonly names the extension emitted for NodeNext.
|
|
// Retain present runtime files first; substitute only a missing counterpart.
|
|
// Counterparts cover explicit filenames, not directory/index resolution.
|
|
if (/\.(?:ts|tsx|mts|cts)$/u.test(fromPath)) {
|
|
const sourceSuffixes = normalized.endsWith(".js")
|
|
? [".ts", ".tsx", ".d.ts"]
|
|
: normalized.endsWith(".mjs")
|
|
? [".mts", ".d.mts"]
|
|
: normalized.endsWith(".cjs")
|
|
? [".cts", ".d.cts"]
|
|
: normalized.endsWith(".jsx")
|
|
? [".tsx", ".d.ts"]
|
|
: [];
|
|
const stem = normalized.slice(0, -posix.extname(normalized).length);
|
|
candidates.push(...sourceSuffixes.map((suffix) => `${stem}${suffix}`));
|
|
}
|
|
const match = candidates.find((candidate) => filePaths.has(candidate));
|
|
return match === undefined
|
|
? { to: normalized, resolution: "unresolved" }
|
|
: { to: match, resolution: "internal" };
|
|
};
|
|
|
|
const appendFile = (
|
|
entry: Extract<ReferenceSourceEntry, { status: "read"; kind: "file" }>,
|
|
filePaths: ReadonlySet<string>,
|
|
output: ParsedReferenceSourceEntries,
|
|
): void => {
|
|
const language = detectReferenceSourceLanguage(entry.path);
|
|
output.entries.push({
|
|
path: entry.path,
|
|
kind: "file",
|
|
sha256: hashBytes(entry.bytes),
|
|
size: entry.size,
|
|
language,
|
|
classifications: classifyReferenceSourcePath(entry.path),
|
|
content_state: "hashed",
|
|
limitations: [],
|
|
});
|
|
if (language === null || !PARSEABLE_REFERENCE_SOURCE_LANGUAGES.has(language))
|
|
return;
|
|
const parsed = parseReferenceSourceImports(entry.path, entry.bytes, language);
|
|
for (const relationship of parsed.relationships) {
|
|
if (relationship.resolution !== "internal") {
|
|
output.relationships.push(relationship);
|
|
continue;
|
|
}
|
|
const resolved = resolveInternalSpecifier(
|
|
relationship.from_path,
|
|
relationship.to,
|
|
filePaths,
|
|
);
|
|
output.relationships.push({
|
|
...relationship,
|
|
to: resolved.to,
|
|
resolution: resolved.resolution,
|
|
});
|
|
}
|
|
output.parseFailures.push(...parsed.parse_failures);
|
|
};
|
|
|
|
const appendReadEntry = (
|
|
entry: Extract<ReferenceSourceEntry, { status: "read" }>,
|
|
filePaths: ReadonlySet<string>,
|
|
output: ParsedReferenceSourceEntries,
|
|
): void => {
|
|
if (entry.kind === "file") {
|
|
appendFile(entry, filePaths, 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",
|
|
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],
|
|
};
|
|
for (const entry of read.entries) {
|
|
if (signal?.aborted === true) break;
|
|
if (entry.status === "failed") output.entries.push(failedEntry(entry));
|
|
else appendReadEntry(entry, filePaths, output);
|
|
}
|
|
return output;
|
|
};
|