chore(native): integrate archive case controls and current catalogs

This commit is contained in:
N0zoM1z0
2026-10-10 15:40:35 +08:00
57 changed files with 2298 additions and 244 deletions
+7 -2
View File
@@ -126,8 +126,13 @@ also stops target hashing before provider admission.
These operations do not execute the APK or provide split APK/AAB handling,
signature validation, full resource-table semantics, native-library analysis or
Android runtime capture. Existing artifact inventory/extraction tools can supply
archive evidence. Project that Evidence with the execution-free inventory tool:
Android runtime capture. An `.aab` suffix is a ZIP archive, not an APK, so
Android tools and the application graph do not treat it as a standalone APK.
Existing artifact inventory/extraction tools can supply archive evidence.
Inventory keeps ZIP names that differ only in case and records that a
case-insensitive destination cannot store both spellings. Extraction fails
only when that destination directory cannot keep both names. Project that
Evidence with the execution-free inventory tool:
```sh
rea project-android-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
+11 -4
View File
@@ -135,10 +135,17 @@ listed after the table because its connector is not one of these files:
| Antigravity | `antigravity` |
| GitHub Copilot CLI | `copilot_cli` |
| Command Code | `commandcode` |
| Qwen Code | `qwen_code` |
| VS Code | `vscode` |
| Grok Build | `grok_build` |
| OMP | `omp` |
For Qwen Code, setup registers `rea` in the `mcpServers` table of the
user-level `~/.qwen/settings.json`, or `$QWEN_HOME/settings.json` when configured.
Setup and doctor follow Qwen Code's tilde and working-directory-relative home
overrides. Qwen Code also discovers personal skills in `~/.agents/skills`,
where setup installs the bundled workflow.
For OMP, setup writes a `type: "stdio"` entry to the user-level
`~/.omp/agent/mcp.json`. It follows `PI_CONFIG_DIR`, an absolute
`PI_CODING_AGENT_DIR`, and the profile selected by `OMP_PROFILE` or
@@ -194,10 +201,10 @@ save verified paths for an existing Ghidra installation.
The bundled skill is installed where each selected client discovers personal
skills: Claude Code uses `~/.claude/skills` (or
`$CLAUDE_CONFIG_DIR/skills` when configured), while other supported clients
use the shared `~/.agents/skills` directory. A mixed selection plans both
paths. Selecting the skill without a client uses the shared directory. Setup
leaves existing skill copies in other locations untouched.
`$CLAUDE_CONFIG_DIR/skills` when configured), while other supported clients,
including Qwen Code, use the shared `~/.agents/skills` directory. A mixed selection
plans the relevant paths. Selecting the skill without a client uses the shared
directory. Setup leaves existing skill copies in other locations untouched.
`doctor --skill --json` verifies the selected copies against the bundled
instructions and references. Consumers should use `identity.skill.state`,
+8 -5
View File
@@ -110,11 +110,14 @@ while comparisons and trace assertions use the normalized text. Retaining the
original adds at most `limits.output_bytes` of text, because it comes from the
same admitted PTY chunks rather than a second unbounded stream.
Rendered frame `lines` hold the visible rows after normalization, without
trailing spaces; pad a line to the frame's `columns` to restore the fixed-width
row. `serialized_state` keeps the exact terminal state. Captures commit this
line format in their comparison contract, so a capture whose lines were padded
to full width does not compare as the same contract.
Rendered frame `lines` hold the visible rows after normalization, with trailing
U+0020 spaces removed. Blank rows and internal spaces remain. `columns` records
terminal cell width; normalization and Unicode can make string lengths differ
from that width. `serialized_state` holds terminal serialization after the
selected normalization. Original admitted PTY chunks are available through
`raw_data ?? data`, as described above. Captures commit this line format in their
comparison contract, so a capture whose lines were padded to full width does not
compare as the same contract.
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
+1
View File
@@ -85,6 +85,7 @@ export async function verifyPackageEnvironment({
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
QWEN_HOME: undefined,
OPENCODE_CONFIG: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
@@ -91,6 +91,7 @@ export const readClientRegistrationStatuses = async (
PI_CODING_AGENT_DIR: options.environment.PI_CODING_AGENT_DIR,
PI_CONFIG_DIR: options.environment.PI_CONFIG_DIR,
PI_PROFILE: options.environment.PI_PROFILE,
QWEN_HOME: options.environment.QWEN_HOME,
SAND_DATA_ROOT: options.environment.SAND_DATA_ROOT,
XDG_CONFIG_HOME: options.environment.XDG_CONFIG_HOME,
},
+30 -1
View File
@@ -1,4 +1,4 @@
import { isAbsolute, join } from "node:path";
import { isAbsolute, join, resolve } from "node:path";
import { lstatSync } from "node:fs";
/** One supported client configuration location. */
@@ -35,6 +35,7 @@ interface ClientPathContext {
readonly PI_CODING_AGENT_DIR?: string | undefined;
readonly PI_CONFIG_DIR?: string | undefined;
readonly PI_PROFILE?: string | undefined;
readonly QWEN_HOME?: string | undefined;
readonly SAND_DATA_ROOT?: string | undefined;
readonly XDG_CONFIG_HOME?: string | undefined;
};
@@ -85,6 +86,25 @@ const claudeCodeMarkerDirectory = ({ home, env }: ClientPathContext): string =>
const codexDirectory = ({ home, env }: ClientPathContext): string =>
env.CODEX_HOME ?? join(home, ".codex");
/** Match Qwen Code's home override, including tilde and cwd-relative paths. */
const qwenCodeDirectory = ({ home, env }: ClientPathContext): string => {
const override = env.QWEN_HOME;
if (!override) return join(home, ".qwen");
const expanded =
override === "~"
? home
: override.startsWith("~/") || override.startsWith("~\\")
? join(
home,
...override
.slice(2)
.split(/[/\\]+/)
.filter(Boolean),
)
: override;
return isAbsolute(expanded) ? expanded : resolve(expanded);
};
const grokDirectory = ({ home, env }: ClientPathContext): string =>
env.GROK_HOME ?? join(home, ".grok");
@@ -272,6 +292,14 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: commandCodeDirectory,
format: "commandcode",
},
{
name: "qwen_code",
displayName: "Qwen Code",
configPath: (context: ClientPathContext) =>
join(qwenCodeDirectory(context), "settings.json"),
markerPath: qwenCodeDirectory,
format: "json",
},
{
name: "vscode",
displayName: "VS Code",
@@ -323,6 +351,7 @@ export const supportedClients = (
PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR,
PI_CONFIG_DIR: process.env.PI_CONFIG_DIR,
PI_PROFILE: process.env.PI_PROFILE,
QWEN_HOME: process.env.QWEN_HOME,
SAND_DATA_ROOT: process.env.SAND_DATA_ROOT,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
},
+157 -11
View File
@@ -1,9 +1,24 @@
import { posix } from "node:path";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
const DRIVE_OR_UNC = /^(?:[A-Za-z]:|\\|\/\/)/u;
/**
* English Unicode case folding used only to describe logical names a
* case-insensitive destination might collapse. It is not a filesystem case table.
*/
const ENGLISH_UNICODE_CASE_NOTE =
"This comparison is not a filesystem observation.";
/** Inventory limitation when logical names collide under English Unicode case folding. */
export const ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION = `Logical archive names collide under English (en-US) Unicode case folding; a case-insensitive destination cannot store both spellings. ${ENGLISH_UNICODE_CASE_NOTE}`;
/** User-facing prefix for a destination that cannot keep two logical spellings. */
export const DESTINATION_CASE_COLLISION_PREFIX =
"Destination filesystem cannot store both ";
/** Normalize an untrusted logical archive path without touching filesystem. */
export const normalizeArtifactPath = (input: string): string => {
if (
@@ -30,7 +45,7 @@ export const normalizeArtifactPath = (input: string): string => {
return normalized;
};
/** Reject exact, Unicode-normalized, case-folded, and prefix collisions. */
/** Reject exact and same-spelling prefix collisions. Case-distinct names stay distinct. */
export class ArtifactPathRegistry {
readonly #root: PathTrieNode = { kind: undefined, children: new Map() };
@@ -43,16 +58,10 @@ export class ArtifactPathRegistry {
"path",
`Artifact prefix conflict: ${path}`,
);
const foldedPart = part.toLocaleLowerCase("en-US");
let child = node.children.get(foldedPart);
if (child !== undefined && child.spelling !== part)
throw new ArtifactReaderFailure(
"path",
`Artifact path collision: ${path} differs only in case from ${[...parts.slice(0, index), child.spelling].join("/")}`,
);
let child = node.children.get(part);
if (child === undefined) {
child = { spelling: part, kind: undefined, children: new Map() };
node.children.set(foldedPart, child);
child = { kind: undefined, children: new Map() };
node.children.set(part, child);
}
node = child;
if (index === parts.length - 1) {
@@ -73,7 +82,144 @@ export class ArtifactPathRegistry {
}
interface PathTrieNode {
readonly spelling?: string;
kind: "file" | "directory" | "symlink" | "slice" | undefined;
readonly children: Map<string, PathTrieNode>;
}
interface CaseNode {
readonly bySpelling: Map<string, CaseNode>;
readonly spellingsByFold: Map<string, string[]>;
readonly terminals: string[];
}
const emptyCaseNode = (): CaseNode => ({
bySpelling: new Map(),
spellingsByFold: new Map(),
terminals: [],
});
const englishCaseFold = (part: string): string =>
part.toLocaleLowerCase("en-US");
/**
* Record English (en-US) Unicode case collisions on each affected occurrence.
* Returns the inventory limitation when any logical names collide.
*/
export const annotateEnglishUnicodeCaseCollisions = (
occurrences: readonly {
readonly logical_path: string;
limitations: string[];
}[],
): string | undefined => {
const collisions = englishUnicodeCaseCollisions(
occurrences.map(({ logical_path }) => logical_path),
);
if (collisions.size === 0) return undefined;
for (const occurrence of occurrences) {
const others = collisions.get(occurrence.logical_path);
if (others === undefined || others.length === 0) continue;
occurrence.limitations.push(
`Logical path ${occurrence.logical_path} collides under English (en-US) Unicode case folding with ${others.join(", ")}; a case-insensitive destination cannot store both. ${ENGLISH_UNICODE_CASE_NOTE}`,
);
}
return ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION;
};
/**
* Name the directory-listing spelling that differs from the requested segment
* only by English (en-US) Unicode case. Exact matches are the same logical name.
*/
export const destinationCaseCollisionMessage = (
requestedPath: string,
existingNames: readonly string[],
): string | undefined => {
const slash = requestedPath.lastIndexOf("/");
const segment = slash < 0 ? requestedPath : requestedPath.slice(slash + 1);
if (existingNames.includes(segment)) return undefined;
const folded = englishCaseFold(segment);
const existing = existingNames
.filter((name) => englishCaseFold(name) === folded)
.sort(compareUnicodeCodePoints)[0];
if (existing === undefined) return undefined;
const parent = slash < 0 ? "" : requestedPath.slice(0, slash + 1);
return `${DESTINATION_CASE_COLLISION_PREFIX}${requestedPath} and ${parent}${existing}; inventory retains both logical names.`;
};
const englishUnicodeCaseCollisions = (
paths: readonly string[],
): ReadonlyMap<string, readonly string[]> => {
const root = emptyCaseNode();
for (const path of paths) {
if (path.length === 0 || path === ".") continue;
let node = root;
for (const part of path.split("/")) {
const folded = englishCaseFold(part);
const spellings = node.spellingsByFold.get(folded);
if (spellings === undefined) node.spellingsByFold.set(folded, [part]);
else if (!spellings.includes(part)) spellings.push(part);
let child = node.bySpelling.get(part);
if (child === undefined) {
child = emptyCaseNode();
node.bySpelling.set(part, child);
}
node = child;
}
node.terminals.push(path);
}
const collisions = new Map<string, Set<string>>();
recordCaseCollisions(root, collisions);
return new Map(
[...collisions.entries()].map(([path, others]) => [
path,
[...others].sort(compareUnicodeCodePoints),
]),
);
};
const recordCaseCollisions = (
node: CaseNode,
collisions: Map<string, Set<string>>,
): void => {
for (const spellings of node.spellingsByFold.values())
if (spellings.length > 1) noteFoldedSpellings(node, spellings, collisions);
for (const child of node.bySpelling.values())
recordCaseCollisions(child, collisions);
};
const noteFoldedSpellings = (
node: CaseNode,
spellings: readonly string[],
collisions: Map<string, Set<string>>,
): void => {
const groups = spellings.map((spelling) => {
const child = node.bySpelling.get(spelling);
return child === undefined ? [] : terminalsUnder(child);
});
for (const [index, group] of groups.entries())
noteCaseGroup(
group,
groups.flatMap((candidate, candidateIndex) =>
candidateIndex === index ? [] : candidate,
),
collisions,
);
};
const noteCaseGroup = (
group: readonly string[],
others: readonly string[],
collisions: Map<string, Set<string>>,
): void => {
for (const path of group) {
const set = collisions.get(path) ?? new Set<string>();
for (const other of others) if (other !== path) set.add(other);
collisions.set(path, set);
}
};
const terminalsUnder = (node: CaseNode): readonly string[] => {
const paths = [...node.terminals];
for (const child of node.bySpelling.values())
paths.push(...terminalsUnder(child));
return paths;
};
+33 -2
View File
@@ -4,6 +4,7 @@ import {
lstat,
mkdir,
open,
readdir,
realpath,
rm,
type FileHandle,
@@ -14,6 +15,7 @@ import { streamChunkToBuffer } from "./StreamBytes.js";
import {
ArtifactPathRegistry,
destinationCaseCollisionMessage,
normalizeArtifactPath,
} from "./ArtifactPaths.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
@@ -134,7 +136,8 @@ export class SafeOutputTree {
constants.O_WRONLY |
constants.O_NOFOLLOW,
0o600,
).catch((cause: unknown) => {
).catch(async (cause: unknown) => {
await throwIfDestinationCaseCollision(dirname(destination), path, cause);
throw new ArtifactReaderFailure(
"path",
`Could not exclusively create extraction path: ${path}`,
@@ -245,17 +248,28 @@ export class SafeOutputTree {
if (fileName === undefined)
throw new ArtifactReaderFailure("path", "Invalid extraction path");
let current = this.#outputRoot;
let logicalParent = "";
for (const part of parts) {
const parentDirectory = current;
const logicalPath =
logicalParent.length === 0 ? part : `${logicalParent}/${part}`;
current = join(current, part);
let created = true;
await mkdir(current, { mode: 0o700 }).catch((cause: unknown) => {
if (!isAlreadyExists(cause)) throw cause;
created = false;
});
if (!created)
await throwIfDestinationCaseCollision(parentDirectory, logicalPath);
const metadata = await lstat(current);
if (!metadata.isDirectory() || metadata.isSymbolicLink())
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
await throwIfDestinationCaseCollision(parentDirectory, logicalPath);
throw new ArtifactReaderFailure(
"path",
`Unsafe extraction parent: ${relativePath}`,
);
}
logicalParent = logicalPath;
}
return join(current, fileName);
}
@@ -330,6 +344,23 @@ const isNotFound = (cause: unknown): boolean =>
const isAlreadyExists = (cause: unknown): boolean =>
cause instanceof Error && "code" in cause && cause.code === "EEXIST";
/** Fail when this directory already holds another spelling of the requested segment. */
const throwIfDestinationCaseCollision = async (
parentDirectory: string,
logicalPath: string,
cause?: unknown,
): Promise<void> => {
const names = await readdir(parentDirectory).catch(() => undefined);
if (names === undefined) return;
const message = destinationCaseCollisionMessage(logicalPath, names);
if (message === undefined) return;
throw new ArtifactReaderFailure(
"path",
message,
cause === undefined ? undefined : { cause },
);
};
const abortIfNeeded = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new ArtifactReaderFailure(
+16 -7
View File
@@ -73,12 +73,12 @@ export const scanCanonicalArtifactInventory = async (
try {
if (reader instanceof AsarArtifactReader)
await reader.prepareContainer(rootDigest?.sha256, options.signal);
const { nodes, occurrences, pendingContradictions } = await scanReader(
reader,
ownedReaders,
options.signal,
integrity,
);
const {
nodes,
occurrences,
pendingContradictions,
caseCollisionLimitation,
} = await scanReader(reader, ownedReaders, options.signal, integrity);
outcome = {
kind: "completed",
snapshot: await buildInventorySnapshot({
@@ -91,6 +91,7 @@ export const scanCanonicalArtifactInventory = async (
nodes,
occurrences,
pendingContradictions,
caseCollisionLimitation,
}),
};
} catch (cause: unknown) {
@@ -130,6 +131,7 @@ interface SnapshotBuildInput {
readonly nodes: Map<string, ArtifactNode>;
readonly occurrences: MutableOccurrence[];
readonly pendingContradictions: PendingIntegrityContradiction[];
readonly caseCollisionLimitation: string | undefined;
}
const buildInventorySnapshot = async (
@@ -202,7 +204,12 @@ const buildInventorySnapshot = async (
edges: orderedEdges,
provenance: reader?.provenance() ?? [],
integrity_contradictions: integrityContradictions,
limitations: buildLimitations(rootFormat, reader, integrityContradictions),
limitations: buildLimitations(
rootFormat,
reader,
integrityContradictions,
input.caseCollisionLimitation,
),
};
};
@@ -310,8 +317,10 @@ const buildLimitations = (
rootFormat: ArtifactOccurrence["artifact_format"],
reader: ArtifactReader | undefined,
integrityContradictions: readonly IntegrityContradiction[],
caseCollisionLimitation: string | undefined,
): string[] => [
...inventoryLimitations(rootFormat, reader),
...(caseCollisionLimitation === undefined ? [] : [caseCollisionLimitation]),
...(integrityContradictions.length === 0
? []
: [
+14 -2
View File
@@ -1,6 +1,7 @@
import { isAbsolute } from "node:path";
import {
annotateEnglishUnicodeCaseCollisions,
ArtifactPathRegistry,
normalizeArtifactPath,
} from "../ArtifactPaths.js";
@@ -64,10 +65,16 @@ export const scanReader = async (
readonly nodes: Map<string, ArtifactNode>;
readonly occurrences: MutableOccurrence[];
readonly pendingContradictions: PendingIntegrityContradiction[];
readonly caseCollisionLimitation: string | undefined;
}> => {
const { nodes, occurrences, pendingContradictions } = emptyScan();
if (reader === undefined)
return { nodes, occurrences, pendingContradictions };
return {
nodes,
occurrences,
pendingContradictions,
caseCollisionLimitation: undefined,
};
const context: ScanContext = {
reader,
signal,
@@ -90,7 +97,12 @@ export const scanReader = async (
context.occurrenceByPath,
context.expandedContainerIds,
)?.occurrence_id ?? null;
return { nodes, occurrences, pendingContradictions };
return {
nodes,
occurrences,
pendingContradictions,
caseCollisionLimitation: annotateEnglishUnicodeCaseCollisions(occurrences),
};
};
const visitArtifactEntries = async (
+1 -1
View File
@@ -63,7 +63,7 @@ const artifact = <
export const ARTIFACT_TOOL_CONTRACTS = [
artifact(
"inspect_artifact",
"Inspect an active archive or application package in one call. Returns the complete content-addressed artifact graph, source Evidence, observations, relationships, integrity contradictions, hypotheses, unexplored branches, limitations, and next probes inline. Read-only DMG mounting is used automatically on supported macOS hosts. Integrity mismatches fail by default; record-and-continue keeps mismatches explicitly untrusted. It does not extract files; use extract_artifact to materialize its regular contents.",
"Inspect an active archive or application package in one call. Returns the complete content-addressed artifact graph once, as the nested inventory Evidence, with root and integrity observations, integrity contradictions, hypotheses, unexplored branches, limitations, and next probes inline. Read-only DMG mounting is used automatically on supported macOS hosts. Integrity mismatches fail by default; record-and-continue keeps mismatches explicitly untrusted. It does not extract files; use extract_artifact to materialize its regular contents.",
artifactInventoryInputSchema,
),
artifact(
+10 -10
View File
@@ -59,7 +59,7 @@ const referencedDescription = (
const describeSchema = (
schema: unknown,
root: Readonly<Record<string, unknown>>,
describeProperty: (property: string) => string,
describeProperty: (property: string) => string | undefined,
): unknown => {
if (!isObject(schema)) return schema;
return Object.fromEntries(
@@ -70,16 +70,16 @@ const describeSchema = (
Object.fromEntries(
Object.entries(value).map(([name, child]) => {
const described = describeSchema(child, root, describeProperty);
if (key !== "properties" || !isObject(described))
return [name, described];
const description =
referencedDescription(root, described) ??
describeProperty(name);
return [
name,
key === "properties" && isObject(described)
? {
...described,
description:
referencedDescription(root, described) ??
describeProperty(name),
}
: described,
description === undefined
? described
: { ...described, description },
];
}),
),
@@ -223,7 +223,7 @@ const flattenRootObjectUnion = (
/** Describe canonical input fields and present the root as one object. */
export const presentInputJsonSchema = (
root: Record<string, unknown>,
describeProperty: (property: string) => string,
describeProperty: (property: string) => string | undefined,
): Record<string, unknown> => {
const flattened = flattenRootObjectUnion(root);
const described = describeSchema(flattened, flattened, describeProperty);
+6 -1
View File
@@ -97,9 +97,14 @@ describe("advertised input references", () => {
before: { description: "Exact byte range in the selected artifact." },
after: { description: "Exact byte range in the selected artifact." },
metadata: { default: literal, examples: [literal] },
constructor: { description: "Value for constructor." },
},
});
// Inherited object keys never supply a description.
expect(inline).not.toHaveProperty([
"properties",
"constructor",
"description",
]);
const ajv =
target === "draft-2020-12"
? new Ajv2020({ strict: false })
+3 -2
View File
@@ -164,7 +164,7 @@ const memoizeByTarget = (
};
};
const fallbackPropertyDescription = (property: string): string => {
const fallbackPropertyDescription = (property: string): string | undefined => {
const explicit = Object.hasOwn(PROPERTY_DESCRIPTIONS, property)
? PROPERTY_DESCRIPTIONS[property]
: undefined;
@@ -192,5 +192,6 @@ const fallbackPropertyDescription = (property: string): string => {
return `Local filesystem ${words} selected for this operation.`;
if (property.startsWith("is_") || property.startsWith("has_"))
return `Whether ${words}.`;
return `Value for ${words}.`;
// A generic sentence would only restate the property name in every schema.
return undefined;
};
+20 -3
View File
@@ -191,7 +191,7 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
const standardMessage = standardErrorMessage(error._tag);
if (standardMessage !== undefined) return standardMessage;
if (error instanceof ArtifactOperationError)
return artifactMessage(error.operation, error.reason);
return artifactMessage(error.operation, error.reason, error.detail);
if (error instanceof EvidenceReferenceError)
return error.reason === "missing"
? `Evidence ${error.evidenceId} is not retained in this session. Supply complete inline Evidence, re-run its producer, or import its bundle before using this reference.`
@@ -329,13 +329,13 @@ const START_FAILURE_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
const artifactMessage = (
operation: ArtifactOperationError["operation"],
reason: ArtifactOperationError["reason"],
detail?: string,
): string => {
if (reason === "cancelled")
return "Artifact operation was cancelled. Start it again when ready.";
if (reason === "limit")
return "Artifact is too large to process safely. Narrow the requested path or use a smaller artifact.";
if (reason === "path")
return "Artifact contains an unsafe or conflicting internal path. Inspect the reported path and correct the artifact before retrying.";
if (reason === "path") return artifactPathMessage(detail);
if (reason === "unavailable")
return "Artifact processing is unavailable for the current target or host. Check artifact support and required tools.";
if (reason === "integrity" && operation === "analyze_javascript_application")
@@ -345,6 +345,23 @@ const artifactMessage = (
return "Artifact could not be read or written. Check file access and try again.";
};
const artifactPathMessage = (detail: string | undefined): string => {
// Kept in step with DESTINATION_CASE_COLLISION_PREFIX. Domain does not import
// the artifact adapter that produces the detail.
if (
detail !== undefined &&
detail.startsWith("Destination filesystem cannot store both ")
)
return detail;
if (
detail !== undefined &&
(detail.startsWith("Artifact path is absolute or unsafe:") ||
detail.startsWith("Artifact path is not normalized:"))
)
return "Artifact contains an unsafe internal path. Inspect the reported path and correct the artifact before retrying.";
return "Artifact contains a conflicting internal path. Inspect the reported path before retrying.";
};
const evidenceFileMessage = ({
operation,
reason,
+9 -28
View File
@@ -125,6 +125,8 @@ export const projectAndroidApplication = (
"Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.",
"Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid DEX or JVM class bytes.",
"Bridge candidates are path-based hypotheses, not decoded JNI declarations or observed runtime calls.",
"A bridge basis is inferred from the native path and is repeated for every managed component.",
"ZIP inventory cannot see the APK Signing Block, so an empty signing array does not mean the APK is unsigned.",
...(bridgeProjection.coverage.status === "partial"
? [
`Bridge candidate pairs exceeded the projection safety budget; ${bridgeProjection.coverage.omitted_candidates} hypotheses are omitted. Component arrays still include every component from the supplied inventory pages.`,
@@ -157,23 +159,12 @@ export const projectAndroidApplication = (
};
const classify = (all: readonly Component[]) => ({
manifests: all.filter(
({ path, format }) =>
format === "android-manifest" ||
/(?:^|\/)AndroidManifest\.xml$/u.test(path),
),
resources: all.filter(
({ path, format }) =>
format === "android-resources" || /(?:^|\/)resources\.arsc$/u.test(path),
),
dex: all.filter(
({ path, format }) =>
format === "dex" || /(?:^|\/)[^/]+\.dex$/iu.test(path),
),
jvm_classes: all.filter(
({ path, format }) =>
format === "jvm-class" || /(?:^|\/)[^/]+\.class$/iu.test(path),
manifests: all.filter(({ path }) =>
/(?:^|\/)AndroidManifest\.xml$/u.test(path),
),
resources: all.filter(({ path }) => /(?:^|\/)resources\.arsc$/u.test(path)),
dex: all.filter(({ path }) => /(?:^|\/)[^/]+\.dex$/iu.test(path)),
jvm_classes: all.filter(({ path }) => /(?:^|\/)[^/]+\.class$/iu.test(path)),
native_libraries: all.filter(
({ path, format }) =>
format === "elf" || /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/iu.test(path),
@@ -189,19 +180,9 @@ const runtimeFamilies = (all: readonly Component[]) => {
const families = new Set<
AndroidApplicationProjectionResult["runtime_families"][number]
>();
if (
all.some(
({ path, format }) =>
format === "dex" || path.toLowerCase().endsWith(".dex"),
)
)
if (all.some(({ path }) => path.toLowerCase().endsWith(".dex")))
families.add("dalvik-art");
if (
all.some(
({ path, format }) =>
format === "jvm-class" || path.toLowerCase().endsWith(".class"),
)
)
if (all.some(({ path }) => path.toLowerCase().endsWith(".class")))
families.add("java-kotlin");
if (all.some(({ format }) => format === "elf")) families.add("native");
if (all.some(({ format }) => format === "javascript-bundle"))
@@ -552,3 +552,32 @@ it("names the archive once per evidence record", () => {
evidence.filter(({ description }) => description.includes(relativePath)),
).toEqual([]);
});
it("states each object's class, label, and ID once", () => {
const { objects } = parseInterfaceBuilderRecords({
"com.apple.ibtool.document.objects": {
"7": {
customClass: "NSButton",
label: "Start",
objectID: "7",
nibValues: { NSContents: "Start" },
},
button: { class: "UIButton", title: "Build", objectID: "authored" },
},
});
expect(objects).toEqual([
expect.objectContaining({
id: "7",
class_name: "NSButton",
name: "Start",
attributes: { nibValues: { NSContents: "Start" } },
}),
expect.objectContaining({
id: "button",
class_name: "UIButton",
name: "Build",
// Values that differ from the canonical fields stay as authored.
attributes: { class: "UIButton", title: "Build", objectID: "authored" },
}),
]);
});
@@ -71,13 +71,23 @@ export const parseInterfaceBuilderRecords = (
attributes["name"],
className,
);
// class_name, name, and id already carry these values; repeating them in
// attributes doubled them on every graph node.
const carried = new Set<string>();
if (attributes["customClass"] === className) carried.add("customClass");
if (attributes["label"] === label) carried.add("label");
if (attributes["objectID"] === id) carried.add("objectID");
objects.push(
objectNode.parse({
id,
kind: classifyObject(className),
class_name: className,
name: label ?? id,
attributes: jsonSafeRecord(attributes),
attributes: jsonSafeRecord(
Object.fromEntries(
Object.entries(attributes).filter(([key]) => !carried.has(key)),
),
),
}),
);
}
+3 -56
View File
@@ -16,30 +16,12 @@ const artifactIdSchema = prefixedDigestSchema("art");
const inspectionObservationSchema = z.strictObject({
observation_id: prefixedDigestSchema("aio"),
kind: z.enum(["root-manifest", "artifact", "occurrence", "integrity"]),
kind: z.enum(["root-manifest", "integrity"]),
subject: textSchema,
value: jsonValueSchema,
evidence_id: evidenceIdSchema,
});
const inspectionRelationshipSchema = z.strictObject({
relationship_id: prefixedDigestSchema("air"),
relation: z.enum([
"contains",
"extracts",
"slice-of",
"embeds",
"loads",
"maps-source",
"derived-from",
]),
source_artifact_id: artifactIdSchema,
target_artifact_id: artifactIdSchema,
occurrence_id: prefixedDigestSchema("occ"),
logical_path: textSchema.nullable(),
evidence_id: evidenceIdSchema,
});
const inspectionHypothesisSchema = z.strictObject({
hypothesis_id: prefixedDigestSchema("aih"),
statement: textSchema,
@@ -97,7 +79,6 @@ export const artifactInspectionResultSchema = z.strictObject({
)
.min(1),
observations: z.array(inspectionObservationSchema),
derived_relationships: z.array(inspectionRelationshipSchema),
hypotheses: z.array(inspectionHypothesisSchema),
contradictions: z.array(inspectionContradictionSchema),
unexplored_branches: z.array(unexploredBranchSchema),
@@ -107,8 +88,6 @@ export const artifactInspectionResultSchema = z.strictObject({
substeps_completed: z.literal(1),
observations_retained: z.number().int().min(0),
observations_omitted: z.number().int().min(0),
relationships_retained: z.number().int().min(0),
relationships_omitted: z.number().int().min(0),
hypotheses_retained: z.number().int().min(0),
hypotheses_omitted: z.number().int().min(0),
unexplored_branches_retained: z.number().int().min(0),
@@ -136,10 +115,6 @@ export const createArtifactInspection = (
inventory,
inventoryEvidence.evidence_id,
);
const relationships = allRelationships(
inventory,
inventoryEvidence.evidence_id,
);
const hypotheses = allHypotheses(inventory, inventoryEvidence.evidence_id);
const nextProbes = probesFor(inventory);
const branches = allUnexploredBranches(
@@ -149,14 +124,12 @@ export const createArtifactInspection = (
);
const retained = {
observations,
relationships,
hypotheses,
branches,
probes: nextProbes,
};
const omissions = {
observations: 0,
relationships: 0,
hypotheses: 0,
branches: 0,
probes: 0,
@@ -184,7 +157,6 @@ export const createArtifactInspection = (
},
],
observations: retained.observations,
derived_relationships: retained.relationships,
hypotheses: retained.hypotheses,
contradictions: contradictions(inventory, inventoryEvidence.evidence_id),
unexplored_branches: retained.branches,
@@ -194,8 +166,6 @@ export const createArtifactInspection = (
substeps_completed: 1 as const,
observations_retained: retained.observations.length,
observations_omitted: omissions.observations,
relationships_retained: retained.relationships.length,
relationships_omitted: omissions.relationships,
hypotheses_retained: retained.hypotheses.length,
hypotheses_omitted: omissions.hypotheses,
unexplored_branches_retained: retained.branches.length,
@@ -224,6 +194,8 @@ const parseInventoryEvidence = (input: unknown): Evidence => {
return evidence;
};
// Artifact, occurrence, and edge records stay in the nested inventory Evidence;
// restating each one here would repeat the complete graph in every result.
const allObservations = (
inventory: ArtifactInventoryResult,
evidenceId: string,
@@ -234,12 +206,6 @@ const allObservations = (
inventory.manifest,
evidenceId,
),
...inventory.nodes.map((node) =>
observation("artifact", node.artifact_id, node, evidenceId),
),
...inventory.occurrences.map((occurrence) =>
observation("occurrence", occurrence.occurrence_id, occurrence, evidenceId),
),
...inventory.integrity_contradictions.map((value) =>
observation("integrity", value.contradiction_id, value, evidenceId),
),
@@ -263,25 +229,6 @@ const observation = (
};
};
const allRelationships = (
inventory: ArtifactInventoryResult,
evidenceId: string,
): ArtifactInspectionResult["derived_relationships"] =>
inventory.edges.map((edge) => {
const semantic = {
relation: edge.relation,
source_artifact_id: edge.parent_artifact_id,
target_artifact_id: edge.child_artifact_id,
occurrence_id: edge.occurrence_id,
logical_path: edge.logical_path,
evidence_id: evidenceId,
};
return {
relationship_id: `air_${digestCanonicalValue(semantic, "Artifact inspection")}`,
...semantic,
};
});
const allHypotheses = (
inventory: ArtifactInventoryResult,
evidenceId: string,
@@ -547,6 +547,21 @@ describe("JavaScript Application Graph: source maps and observations", () => {
/Edge identifier/u,
);
});
it("creates exactly the graph the public schema accepts", () => {
const graph = buildSyntheticJavaScriptApplicationGraph();
expect(JSON.stringify(graph)).toBe(
JSON.stringify(
javascriptApplicationGraphSchema.parse(structuredClone(graph)),
),
);
const { graph_id: _graphId, ...semantic } = structuredClone(graph);
firstOf(semantic.edges, "edge").edge_id = `jag_edge_${"f".repeat(64)}`;
expect(() => createJavaScriptApplicationGraph(semantic)).toThrow(
/Edge identifier/u,
);
});
});
describe("JavaScript Application Graph: duplicate and orphan rejection", () => {
@@ -304,8 +304,8 @@ const checkNode = (
type GraphRecord = z.infer<typeof javascriptApplicationGraphRecordSchema>;
const checkGraphInvariants = (
graph: GraphRecord,
const checkGraphContent = (
graph: JavaScriptApplicationGraphInput,
context: z.RefinementCtx,
): void => {
sortedUniqueIssue(
@@ -394,7 +394,13 @@ const checkGraphInvariants = (
context,
);
}
};
const checkGraphInvariants = (
graph: GraphRecord,
context: z.RefinementCtx,
): void => {
checkGraphContent(graph, context);
const { graph_id: identifier, ...semantic } = graph;
if (
identifier !==
@@ -411,6 +417,9 @@ const checkGraphInvariants = (
export const javascriptApplicationGraphSchema =
javascriptApplicationGraphRecordSchema.superRefine(checkGraphInvariants);
const javascriptApplicationGraphContentSchema =
javascriptApplicationGraphInputSchema.superRefine(checkGraphContent);
/** Fully validated JavaScript Application Graph. */
export type JavaScriptApplicationGraph = z.infer<
typeof javascriptApplicationGraphSchema
@@ -482,10 +491,11 @@ export const createJavaScriptApplicationGraph = (
},
limitations: uniqueSorted(parsed.limitations),
};
return javascriptApplicationGraphSchema.parse({
...semantic,
graph_id: `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`,
});
const content = javascriptApplicationGraphContentSchema.parse(semantic);
return {
...content,
graph_id: `jag_${digestCanonicalValue(content, "JavaScript Application Graph")}`,
};
};
const validatedImmutableApplicationGraphs = new WeakSet<object>();
@@ -127,10 +127,6 @@ export const createJavaScriptSemanticGraphNode = (
...parsed,
application_node_ids: uniqueSorted(parsed.application_node_ids),
evidence: evidenceContexts.intern(parsed.evidence),
identifier_strategy: {
strategy: "semantic-content-sha256" as const,
stability: "artifact-version" as const,
},
};
return javaScriptSemanticNodeSchema.parse({
...semantic,
@@ -147,10 +143,6 @@ export const createJavaScriptSemanticGraphRelation = (
const semantic = {
...parsed,
evidence: evidenceContexts.intern(parsed.evidence),
identifier_strategy: {
strategy: "semantic-content-sha256" as const,
stability: "relationship-exact" as const,
},
};
return javaScriptSemanticRelationSchema.parse({
...semantic,
@@ -233,11 +233,9 @@ export const javaScriptSemanticNodeInputSchema = z.strictObject({
export const javaScriptSemanticNodeSchema =
javaScriptSemanticNodeInputSchema.extend({
evidence: javaScriptSemanticEvidenceReferenceSchema,
node_id: semanticNodeIdSchema,
identifier_strategy: z.strictObject({
strategy: z.literal("semantic-content-sha256"),
stability: z.literal("artifact-version"),
}),
node_id: semanticNodeIdSchema.describe(
"SHA-256 of the node kind and identity; stable for one artifact version.",
),
});
/** One directed semantic relationship before its ID is derived. */
@@ -254,11 +252,9 @@ export const javaScriptSemanticRelationInputSchema = z.strictObject({
export const javaScriptSemanticRelationSchema =
javaScriptSemanticRelationInputSchema.extend({
evidence: javaScriptSemanticEvidenceReferenceSchema,
relation_id: semanticRelationIdSchema,
identifier_strategy: z.strictObject({
strategy: z.literal("semantic-content-sha256"),
stability: z.literal("relationship-exact"),
}),
relation_id: semanticRelationIdSchema.describe(
"SHA-256 of the exact relation content, including its evidence reference.",
),
});
/** Reasons semantic analysis can leave an explicit frontier. */
+2 -1
View File
@@ -22,7 +22,8 @@ export const zipPackageFormatForPath = (
const lower = path.toLowerCase();
if (lower.endsWith(".zip")) return "zip";
if (lower.endsWith(".ipa")) return "ipa";
if (lower.endsWith(".apk") || lower.endsWith(".aab")) return "apk";
// An Android App Bundle is a ZIP archive, not a standalone APK.
if (lower.endsWith(".apk")) return "apk";
if (lower.endsWith(".msix") || lower.endsWith(".msixbundle")) return "msix";
if (lower.endsWith(".appx") || lower.endsWith(".appxbundle")) return "appx";
return undefined;
@@ -100,7 +100,6 @@ describe("entry-order validation and reader controls", () => {
it.each([
["pkg/data.txt", "pkg/data.txt/"],
["pkg/data.txt/", "pkg/data.txt"],
["pkg/data.txt", "PKG/other.txt"],
["pkg/data.txt", "pkg"],
["../escape.txt"],
])("keeps invalid/colliding paths rejected for %j", async (...entries) => {
@@ -112,6 +111,25 @@ describe("entry-order validation and reader controls", () => {
});
});
it("keeps case-distinct directory names separate in either entry order", async () => {
const root = await createTestTempDirectory("rea-entry-order-case-");
const archive = join(root, "case-distinct.zip");
const entries = ["pkg/", "pkg/data.txt", "PKG/", "PKG/other.txt"];
const parents = {
".": null,
pkg: ".",
"pkg/data.txt": "pkg",
PKG: ".",
"PKG/other.txt": "PKG",
};
for (const ordered of [entries, entries.toReversed()]) {
await writeOrderedZip(archive, ordered);
expect(artifactParentPaths(await inventoryArtifact(archive))).toEqual(
parents,
);
}
});
it("preserves ordinary directory and ASAR structure", async () => {
const root = await createTestTempDirectory("rea-entry-order-readers-");
const directory = join(root, "source");
@@ -59,6 +59,7 @@ describe("client configuration filesystem lifecycle", () => {
"antigravity",
"copilot_cli",
"commandcode",
"qwen_code",
"vscode",
"grok_build",
"omp",
@@ -0,0 +1,183 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { parse as parseJsonc } from "jsonc-parser";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
import { systemSetupHost } from "../../../src/application/SetupHost.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
import { systemUninstallHost } from "../../../src/application/Uninstall.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
clearClientLocationEnvironment,
NPX_REGISTRATION_COMMAND,
} from "../../fixtures/clientEnvironment.js";
const command = NPX_REGISTRATION_COMMAND;
beforeEach(clearClientLocationEnvironment);
afterEach(() => vi.unstubAllEnvs());
const qwenPath = (
home: string,
platform: NodeJS.Platform,
env: Parameters<typeof supportedClients>[2] = {},
) =>
supportedClients(home, platform, env).find(
({ name }) => name === "qwen_code",
);
describe("Qwen Code configuration paths", () => {
it("resolves the user settings file and marker directory", () => {
expect(qwenPath("/home/a", "linux")).toEqual({
name: "qwen_code",
displayName: "Qwen Code",
configPath: join("/home/a", ".qwen", "settings.json"),
markerPath: join("/home/a", ".qwen"),
format: "json",
});
expect(qwenPath("/Users/a", "darwin")?.configPath).toBe(
join("/Users/a", ".qwen", "settings.json"),
);
});
it.each([
["", join("/home/a", ".qwen")],
["~", "/home/a"],
["~/profiles/work", join("/home/a", "profiles", "work")],
["~\\profiles\\work", join("/home/a", "profiles", "work")],
["profiles/work", resolve("profiles/work")],
[" profile ", resolve(" profile ")],
["$PROFILE", resolve("$PROFILE")],
])("matches Qwen's QWEN_HOME resolution for %j", (override, directory) => {
vi.stubEnv("QWEN_HOME", override);
const client = qwenPath("/home/a", process.platform, {
QWEN_HOME: override,
});
expect(
supportedClients("/home/a").find(({ name }) => name === "qwen_code"),
).toEqual(client);
expect(client).toMatchObject({
configPath: join(directory, "settings.json"),
markerPath: directory,
});
});
});
describe("Qwen Code client lifecycle", () => {
const qwenClient = async () => {
const home = await createTestTempDirectory("rea-qwen-");
const client = qwenPath(home, process.platform, {});
if (client?.markerPath === undefined)
throw new Error("missing Qwen Code client");
await mkdir(client.markerPath, { recursive: true });
return { home, client };
};
it("registers rea in mcpServers while preserving unrelated settings and comments", async () => {
const { home, client } = await qwenClient();
await writeFile(
client.configPath,
`{
// Keep this note.
"model": { "name": "qwen3-coder-plus" },
"mcpServers": { "other": { "command": "other-server" } }
}
`,
);
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const text = await readFile(client.configPath, "utf8");
expect(text).toContain("// Keep this note.");
expect(parseJsonc(text)).toEqual({
model: { name: "qwen3-coder-plus" },
mcpServers: {
other: { command: "other-server" },
rea: { command: "npx", args: command.slice(1) },
},
});
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect((await systemUninstallHost(home).removeClient(client)).status).toBe(
"removed",
);
const removedText = await readFile(client.configPath, "utf8");
const removed = parseJsonc(removedText);
expect(removed).toEqual({
model: { name: "qwen3-coder-plus" },
mcpServers: { other: { command: "other-server" } },
});
expect(removedText).toContain("// Keep this note.");
});
it("reports a package-runner registration as aligned", async () => {
const { home, client } = await qwenClient();
expect(
(await configureClientConfiguration(client, {}, command)).status,
).toBe("configured");
const entry = resolve("scripts/rea.mjs");
const status = (
await readClientRegistrationStatuses(home, entry, { environment: {} })
).find(({ client: name }) => name === "qwen_code");
expect(status).toMatchObject({ state: "aligned" });
});
});
describe("Qwen Code profile setup", () => {
it.each([false, true])(
"installs and removes the shared skill with a custom home: %s",
async (customHome) => {
const home = await createTestTempDirectory("rea-qwen-profile-");
const profile = join(home, customHome ? "qwen-profile" : ".qwen");
const environment = {
HOME: home,
USERPROFILE: home,
...(customHome ? { QWEN_HOME: profile } : {}),
};
await mkdir(profile);
const setup = systemSetupHost(undefined, environment);
const client = (await setup.detectedClients()).find(
({ name }) => name === "qwen_code",
);
if (client === undefined) throw new Error("missing Qwen Code client");
expect(client.configPath).toBe(join(profile, "settings.json"));
expect(await setup.configureClient(client, {}, command)).toEqual({
status: "configured",
});
expect(await setup.installSkill([client.name])).toBe("installed");
const skill = join(home, ".agents", "skills", PRODUCT_IDENTITY.skillName);
expect(await readFile(join(skill, "SKILL.md"), "utf8")).toContain(
"reverse-engineer-anything",
);
expect(await setup.clientNeedsConfigure(client, {}, command)).toBe(false);
expect(await setup.skillNeedsInstall([client.name])).toBe(false);
expect(await setup.installSkill([client.name])).toBe("unchanged");
expect(
(
await readClientRegistrationStatuses(
home,
resolve("scripts/rea.mjs"),
{ environment },
)
).find(({ client: name }) => name === "qwen_code"),
).toMatchObject({ state: "aligned" });
const uninstall = systemUninstallHost(home, undefined, environment);
const registered = (await uninstall.clients()).find(
({ name }) => name === "qwen_code",
);
expect(registered).toEqual(client);
expect((await uninstall.removeClient(client)).status).toBe("removed");
expect((await uninstall.removeSkill()).status).toBe("removed");
await expect(access(skill)).rejects.toMatchObject({ code: "ENOENT" });
if (customHome)
await expect(access(join(home, ".qwen"))).rejects.toMatchObject({
code: "ENOENT",
});
},
);
});
+12 -9
View File
@@ -117,7 +117,7 @@ function expectKnownAuthorityHints(tools: readonly ToolSchemas[]): void {
expect(advertised.get(name), name).toMatchObject({ annotations });
}
function expectRecursivePropertyDescriptions(
function expectMeaningfulPropertyDescriptions(
schema: unknown,
path: string,
root: unknown = schema,
@@ -125,7 +125,7 @@ function expectRecursivePropertyDescriptions(
): void {
if (!isRecord(schema)) return;
if (typeof schema.$ref === "string" && !active.has(schema.$ref))
expectRecursivePropertyDescriptions(
expectMeaningfulPropertyDescriptions(
resolveReference(root, schema.$ref),
`${path}.${schema.$ref}`,
root,
@@ -133,10 +133,13 @@ function expectRecursivePropertyDescriptions(
);
if (isRecord(schema.properties)) {
for (const [property, child] of Object.entries(schema.properties)) {
expect(child, `${path}.${property}`).toMatchObject({
description: expect.any(String),
});
expectRecursivePropertyDescriptions(
// Omit a description rather than restate the property name.
const description = isRecord(child) ? child.description : undefined;
if (description !== undefined)
expect(description, `${path}.${property}`).toEqual(
expect.not.stringMatching(/^(?:Value for .*)?$/u),
);
expectMeaningfulPropertyDescriptions(
child,
`${path}.${property}`,
root,
@@ -147,12 +150,12 @@ function expectRecursivePropertyDescriptions(
for (const key of ["items", "additionalProperties"])
if (schema[key] !== undefined)
expectRecursivePropertyDescriptions(schema[key], path, root, active);
expectMeaningfulPropertyDescriptions(schema[key], path, root, active);
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
const children = schema[key];
if (Array.isArray(children))
children.forEach((child: unknown, index: number) =>
expectRecursivePropertyDescriptions(
expectMeaningfulPropertyDescriptions(
child,
`${path}.${key}[${index}]`,
root,
@@ -358,7 +361,7 @@ describe("MCP JSON Schema validity", () => {
contract.inputSchema.safeParse(example.input).success,
`${contract.name}: ${example.title}`,
).toBe(true);
expectRecursivePropertyDescriptions(tool?.inputSchema, contract.name);
expectMeaningfulPropertyDescriptions(tool?.inputSchema, contract.name);
}
} finally {
await Promise.allSettled([client.close(), server.close()]);
@@ -60,6 +60,15 @@ describe("artifact archive inventory", () => {
expect(inspectionEvidence.evidence_links).toEqual(
inspection.evidence_links,
);
// The graph is delivered once, in the nested inventory Evidence.
expect(
artifactInventoryResultSchema.parse(
inspection.substeps[0]?.evidence.normalized_result,
).occurrences,
).toHaveLength(3);
expect(inspection.observations.map(({ kind }) => kind)).toEqual([
"root-manifest",
]);
const cancellation = new AbortController();
cancellation.abort();
await expect(
@@ -1,4 +1,4 @@
import { readFile, writeFile } from "node:fs/promises";
import { lstat, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
@@ -9,6 +9,8 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js
import { createDirectAnalysis } from "../../../../src/composition/directAnalysis.js";
import {
ArtifactPathRegistry,
destinationCaseCollisionMessage,
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
normalizeArtifactPath,
} from "../../../../src/artifacts/ArtifactPaths.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
@@ -18,6 +20,7 @@ import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGr
import type { BinaryTarget } from "../../../../src/domain/binaryTargetTypes.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
const { runProviderAnalysis } = createDirectAnalysis({});
@@ -95,17 +98,8 @@ describe("artifact archive safety", () => {
});
const registry = new ArtifactPathRegistry();
registry.add("A.js", "file");
let collisionError: unknown;
try {
registry.add("a.js", "file");
} catch (error: unknown) {
collisionError = error;
}
expect(collisionError).toBeInstanceOf(ArtifactReaderFailure);
expect(collisionError).toMatchObject({
reason: "path",
message: "Artifact path collision: a.js differs only in case from A.js",
});
expect(() => registry.add("a.js", "file")).not.toThrow();
expect(() => registry.add("A.js", "file")).toThrow(ArtifactReaderFailure);
for (const [filePath, childPath] of [
["Foo", "foo/bar"],
@@ -113,11 +107,15 @@ describe("artifact archive safety", () => {
] as const) {
const casePrefixRegistry = new ArtifactPathRegistry();
casePrefixRegistry.add(filePath, "file");
expect(() => casePrefixRegistry.add(childPath, "file")).toThrow(
ArtifactReaderFailure,
);
expect(() => casePrefixRegistry.add(childPath, "file")).not.toThrow();
}
const exactPrefixRegistry = new ArtifactPathRegistry();
exactPrefixRegistry.add("Foo", "file");
expect(() => exactPrefixRegistry.add("Foo/bar", "file")).toThrow(
ArtifactReaderFailure,
);
const sameDirectoryRegistry = new ArtifactPathRegistry();
sameDirectoryRegistry.add("Foo/one.js", "file");
expect(() => sameDirectoryRegistry.add("Foo/two.js", "file")).not.toThrow();
@@ -126,8 +124,64 @@ describe("artifact archive safety", () => {
caseVariantDirectoryRegistry.add("Foo/one.js", "file");
expect(() =>
caseVariantDirectoryRegistry.add("foo/two.js", "file"),
).toThrow(ArtifactReaderFailure);
).not.toThrow();
});
it("names a destination case collision from the directory listing", () => {
expect(
destinationCaseCollisionMessage("res/2f.xml", ["2F.xml", "other.xml"]),
).toBe(
"Destination filesystem cannot store both res/2f.xml and res/2F.xml; inventory retains both logical names.",
);
expect(
destinationCaseCollisionMessage("res/2f.xml", ["2f.xml", "2F.xml"]),
).toBeUndefined();
});
});
it("extracts case-distinct names only when the destination can preserve them", async () => {
const root = await createTestTempDirectory("rea-case-distinct-zip-");
await writeFile(join(root, "case-probe"), "lower");
await writeFile(join(root, "CASE-PROBE"), "upper");
const caseSensitive =
(await readFile(join(root, "case-probe"), "utf8")) === "lower";
const path = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("Main.js", new TextReader("upper"));
await writer.add("main.js", new TextReader("lower"));
await writeFile(path, await writer.close());
const parsed = await parseBinaryTarget(path);
if (!parsed.ok) throw parsed.error;
const result = await inventory(parsed.value);
expect(result.limitations).toContain(
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
);
expect(
result.occurrences
.filter(({ logical_path }) => logical_path !== ".")
.map(({ logical_path }) => logical_path)
.sort(),
).toEqual(["Main.js", "main.js"]);
const output = join(root, "output");
const extracted = await new ArtifactProvider(process.env)
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionExecutionSchema.parse({ output_root: output }),
);
if (!caseSensitive) {
if (extracted.ok) throw new Error("expected a destination case collision");
expect(projectAnalysisError(extracted.error)).toMatchObject({
message:
"Destination filesystem cannot store both main.js and Main.js; inventory retains both logical names.",
details: { operation: "extract_artifact", reason: "path" },
});
await expect(lstat(output)).rejects.toMatchObject({ code: "ENOENT" });
return;
}
if (!extracted.ok) throw extracted.error;
expect(await readFile(join(output, "Main.js"), "utf8")).toBe("upper");
expect(await readFile(join(output, "main.js"), "utf8")).toBe("lower");
});
const inventory = async (targetValue: BinaryTarget) => {
const result = await new ArtifactProvider(process.env)
@@ -6,30 +6,73 @@ import { writeOrderedZip } from "../../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import {
destinationCaseCollisionMessage,
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
} from "../../../../src/artifacts/ArtifactPaths.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import { ArtifactOperationError } from "../../../../src/domain/artifactOperationError.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
it.each([
[
["Main.js", "main.js"],
"Artifact path collision: main.js differs only in case from Main.js",
],
[["a\\b.js"], 'Artifact path is absolute or unsafe: "a\\\\b.js"'],
] as const)(
"reports the failed path constraint for %j",
async (entries, detail) => {
const root = await createTestTempDirectory("rea-artifact-detail-");
const path = join(root, "fixture.zip");
await writeOrderedZip(path, entries);
const target = await parseBinaryTarget(path);
if (!target.ok) throw new Error("expected a ZIP target");
const result = await new ArtifactProvider(process.env)
.createClient(target.value)
.execute("inventory_artifact", {});
if (result.ok) throw new Error("expected the path constraint to fail");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "artifact_operation_failed",
details: { operation: "inventory_artifact", reason: "path", detail },
});
},
);
it("keeps logical names that differ only in case and records the limitation", async () => {
const root = await createTestTempDirectory("rea-artifact-detail-");
const path = join(root, "fixture.zip");
await writeOrderedZip(path, ["Main.js", "main.js"]);
const target = await parseBinaryTarget(path);
if (!target.ok) throw new Error("expected a ZIP target");
const result = await new ArtifactProvider(process.env)
.createClient(target.value)
.execute("inventory_artifact", {});
if (!result.ok) throw result.error;
const inventory = artifactInventoryResultSchema.parse(result.value.result);
expect(inventory.occurrences.map(({ logical_path }) => logical_path)).toEqual(
expect.arrayContaining(["Main.js", "main.js"]),
);
expect(inventory.limitations).toContain(
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
);
for (const logicalPath of ["Main.js", "main.js"]) {
expect(
inventory.occurrences
.find((occurrence) => occurrence.logical_path === logicalPath)
?.limitations.join("\n"),
).toContain("English (en-US) Unicode case folding");
}
});
it("reports an unsafe path as a hostile artifact path", async () => {
const root = await createTestTempDirectory("rea-artifact-detail-");
const path = join(root, "fixture.zip");
await writeOrderedZip(path, ["a\\b.js"]);
const target = await parseBinaryTarget(path);
if (!target.ok) throw new Error("expected a ZIP target");
const result = await new ArtifactProvider(process.env)
.createClient(target.value)
.execute("inventory_artifact", {});
if (result.ok) throw new Error("expected the path constraint to fail");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "artifact_operation_failed",
message:
"Artifact contains an unsafe internal path. Inspect the reported path and correct the artifact before retrying.",
details: {
operation: "inventory_artifact",
reason: "path",
detail: 'Artifact path is absolute or unsafe: "a\\\\b.js"',
},
});
});
it("returns a destination case collision as the user-facing path message", () => {
const detail = destinationCaseCollisionMessage("res/2f.xml", ["2F.xml"]);
expect(detail).toEqual(expect.any(String));
if (detail === undefined) return;
expect(
projectAnalysisError(
new ArtifactOperationError("extract_artifact", "path", undefined, detail),
),
).toMatchObject({
message: detail,
details: { reason: "path", detail },
});
});
@@ -86,6 +86,12 @@ describe("Android application projection", () => {
expect(left.bridge_candidates).toEqual([
expect.objectContaining({ basis: "react-native-convention" }),
]);
expect(left.limitations).toEqual(
expect.arrayContaining([
"A bridge basis is inferred from the native path and is repeated for every managed component.",
"ZIP inventory cannot see the APK Signing Block, so an empty signing array does not mean the APK is unsigned.",
]),
);
expect(JSON.stringify(left)).not.toContain("opaque signing");
});
@@ -146,6 +152,45 @@ describe("Android application projection", () => {
});
});
describe("case-distinct APK resource names", () => {
it("inventories and projects an APK whose resource names differ only in case", async () => {
const root = await createTestTempDirectory("rea-android-case-");
const path = join(root, "Shrunk.apk");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(
"AndroidManifest.xml",
new Uint8ArrayReader(Uint8Array.from([3, 0, 8, 0])),
);
await writer.add(
"classes.dex",
new Uint8ArrayReader(
Uint8Array.from([0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0]),
),
);
await writer.add("res/2F.xml", new TextReader("upper"));
await writer.add("res/2f.xml", new TextReader("lower"));
await writeFile(path, await writer.close());
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
const result = projectAndroidApplicationEvidence({
inventory_evidence: [inventory],
});
const projection = androidApplicationProjectionResultSchema.parse(
requireSuccessfulProjection(result).normalized_result,
);
expect(projection.root_format).toBe("apk");
expect(projection.components.manifests).toHaveLength(1);
expect(projection.components.dex).toHaveLength(1);
expect(JSON.stringify(inventory)).toContain("res/2F.xml");
expect(JSON.stringify(inventory)).toContain("res/2f.xml");
expect(JSON.stringify(inventory)).toContain(
"English (en-US) Unicode case folding",
);
});
});
describe("Android bridge candidate coverage", () => {
it("returns every component and bridge candidate from the inventory", async () => {
const root = await createTestTempDirectory("rea-android-complete-");
+5 -4
View File
@@ -74,10 +74,11 @@ export const createJadxProtocolFixture = async (
ownsProcessLifetime: true,
cleanup: () => cleanupOwnedProcessGroup(spawned.ownership),
});
const stopped = await supervisor.stop();
expect(stopped, JSON.stringify(stopped)).not.toMatchObject({
status: "incomplete",
});
// The SDK can close the child transport before process exit settles.
// Retry retained ownership without bypassing the supervisor's proof.
await expect
.poll(() => supervisor.stop(), { timeout: 5_000 })
.not.toMatchObject({ status: "incomplete" });
if (options.cwd !== undefined)
await rm(options.cwd, { recursive: true, force: true });
});
+1
View File
@@ -22,6 +22,7 @@ export const CLIENT_LOCATION_ENVIRONMENT = [
"PI_CODING_AGENT_DIR",
"PI_CONFIG_DIR",
"PI_PROFILE",
"QWEN_HOME",
"SAND_DATA_ROOT",
"XDG_CONFIG_HOME",
] as const;
+5
View File
@@ -14,6 +14,11 @@ for asset notes and the source of case-study claims.
case study focused on one question, with further examples in native details.
- Show what REA returns and how the agent uses it. Preserve source attribution,
target identity and verification scope.
- Blog articles teach methods, decisions and workflows through concrete cases.
Lead with the takeaway. Essays can develop a personal argument through
experience; figures are optional and should add an explanation the prose
needs. Use engine details only when they explain a decision. Label working
outlines and mark them `noindex`.
- Keep personal paths, account data, credentials and raw captures outside the
public assets. Use generic inputs and label shortened display paths.
- Follow the same clear writing standard in search titles and descriptions.
+20 -2
View File
@@ -42,6 +42,8 @@ HTML files. No URL list needs to be maintained.
- `public/examples/dino-lab/index.html`: adjustable-speed mini-game, recovered rule, original-game check and browser analysis steps.
- `public/first-investigation/index.html`: a guided Notes export investigation, from setup to a checked CSV prediction.
- `public/showcase/index.html`: the case-study index.
- `public/blog/index.html`: articles about reconstruction methods, ports, mods and reverse engineering.
- `public/blog/touhou-reconstruction/index.html`: a personal essay about agent-driven reconstruction, knowledge that compounds across TH08 and TH095, and the workflow's applicability to PC-98 TH04.
- `public/showcase/aegis/index.html`: Aegis's Android login-code calculation, with an adjustable clock and reference checks.
- `public/showcase/dx-ball/index.html`: sound-pan investigation and project status.
- `public/showcase/notion/index.html`: Notion's Electron clipboard bridge and rich clipboard format.
@@ -121,6 +123,19 @@ and Cloudflare's [managed robots behavior](https://developers.cloudflare.com/bot
## Content
Blog articles explain methods, decisions and ideas through project experience.
Lead with a takeaway, then develop the argument through concrete examples.
An essay can explore a change in perspective without becoming a step-by-step
guide. Add a figure when it contributes a useful explanation; an opening flow
is optional. Case studies remain focused on one inspected behavior. Keep engine
details in supporting references unless they explain a relevant decision.
The Touhou article is an English essay. Its provenance,
quoted README passage and milestone counting rules are in
[evidence/touhou-reconstruction-method.md](evidence/touhou-reconstruction-method.md).
The completed text has normal search metadata and is included in the generated
sitemap.
Keep the copy direct and specific. Explain the task and the result before listing
tool names. Setup commands and runtime requirements should match the released
package. Keep the core learning path inside the site. Source, issue and evidence
@@ -177,11 +192,14 @@ The homepage starts with a short installation prompt and the ordinary setup
command, then explains what reverse engineering is and why someone would use
it. The manual/agent comparison introduces REA’s role before the examples.
Its opening links directly to Showcases and lets experienced readers skip to
the analysis guides. Case-study previews live on the Showcases page.
the analysis guides. A Blog link below that shortcut leads to reconstruction
methods and project notes. Case-study previews live on the Showcases page.
The closing section offers copyable project prompts, from cloning `rea.tools`
to reconstructing a game from its executable. The experienced-reader shortcut
lands directly on the guide links below these prompts.
The page ends with FAQ, Discord and issue-report links under “Any questions?”.
“Any questions?” offers FAQ, Discord and issue-report links. The page ends
with “Join the community”, which links Discord and REA's X account.
Every content page's header and footer link GitHub, Discord and X.
A right-side table of contents stays visible at widths of 1440px and above.
On narrower screens it becomes a sticky, native disclosure; selecting a link
closes the menu and focuses the destination. The homepage script follows
@@ -0,0 +1,175 @@
# Touhou reconstruction method: evidence notes
Checked on 10 October 2026. This document supports the full English essay
at `public/blog/touhou-reconstruction/index.html`.
## Method sources
The Factory checkpoint is
[`038b6bed`](https://github.com/N0zoM1z0/touhou-reconstruction-factory/tree/038b6bed51d273c8b3680553b6f43e1b97d19bd1).
Read its [agent autonomy](https://github.com/N0zoM1z0/touhou-reconstruction-factory/blob/038b6bed51d273c8b3680553b6f43e1b97d19bd1/docs/agent-autonomy.md),
[semantic workflow](https://github.com/N0zoM1z0/touhou-reconstruction-factory/blob/038b6bed51d273c8b3680553b6f43e1b97d19bd1/docs/semantic-reconstruction.md),
and [cross-game knowledge](https://github.com/N0zoM1z0/touhou-reconstruction-factory/blob/038b6bed51d273c8b3680553b6f43e1b97d19bd1/docs/knowledge-base.md)
documents for the underlying method: autonomous investigation, bounded changes,
empirical feedback, durable checkpoints and reusable lessons.
The industrial-age analogy explains that method. The Factory's
software architecture also uses the Abstract Factory pattern; the article's
analogy should focus on the working process rather than that implementation.
REA supplies analysis evidence to an agent. The historical projects also used
project-specific analysis bridges and tooling. The article should attribute
historical work to those projects and explain where REA fits in the general
workflow, without relabeling every historical tool result as REA output.
## Collection and counting
Public history was retrieved with `gh api`, including all pages of each
repository's default-branch commits, repository metadata and release records.
Pinned progress documents were read from the corresponding Git objects in the
local project checkouts. Every milestone SHA below appears in the public
GitHub history. No local-only revision or uncommitted change supplies a claim.
Elapsed days are the difference between public commit **committer timestamps**,
in UTC, divided by 86,400 and rounded to two decimal places. The article uses
rounded calendar intervals. They measure recorded milestone spacing, not
compute time, person-hours or a controlled speedup benchmark.
TH08's start is the independent continuation at `001bf3e`, not its inherited
2025 initial commit or its hosting creation date. Its imported GensokyoClub
history ends at `7ad3792`; preserve that attribution and license. TH095 starts
with zero confirmed authored functions, while using methods and infrastructure
from earlier projects. TH04 uses ReC98 as a credited source of existing work.
The denominators change as candidates are classified and boundaries reviewed.
Read each pinned progress document's definitions. Avoid a cross-game function
rate or an executable-wide completion percentage.
## Public history snapshot
This is the initial 10 October research snapshot. The later TH04 phase update
below uses its own pinned checkpoint; it does not change these earlier counts.
| Project | Default-branch commits | Commits whose subject begins `gpt-web:` | Public head |
| ------- | ---------------------: | --------------------------------------: | ------------------------------------------ |
| TH08 | 1,117 | 400 | `d2a00f4acb4a13916e07a00bd13f714961714628` |
| TH095 | 713 | 450 | `6084b36cfa90ac1720f0cba36caec3ee56fc5e1f` |
| TH04 | 957 | 623 | `33b80522f03565022dbfe4d5c8a88bdc57f566ae` |
TH08 has 696 reachable continuation commits after the imported boundary;
its total includes the upstream history. Subject prefixes identify a recorded
workflow convention, not the number of sessions, experiments or verified
functions. Commit counts remain supporting research rather than article
headlines.
## Milestone records
### TH08
Start: [001bf3e9](https://github.com/N0zoM1z0/th08/commit/001bf3e9c91cc35b79c7a0e36b3565b86f494362) at 2026-08-13T02:23:48Z.
| Checkpoint | UTC timestamp | Elapsed days | Recorded milestone |
| -------------------------------------------------------------------------------------------- | -------------------- | -----------: | ---------------------------------------------------------------- |
| [15caba73](https://github.com/N0zoM1z0/th08/commit/15caba73df6735fc44eac4783bc61b8d098ea830) | 2026-08-19T12:19:33Z | 6.41 | Restore clean authored build baseline |
| [da07b21c](https://github.com/N0zoM1z0/th08/commit/da07b21c8cb63230142b0a66228d6aac66555f3f) | 2026-08-24T10:43:33Z | 11.35 | Add playable Linux reconstruction port |
| [529eb1a3](https://github.com/N0zoM1z0/th08/commit/529eb1a3afa05ae36467731eaccb12e3e50e132f) | 2026-08-26T05:12:23Z | 13.12 | Link the playable TH08 Web edition |
| [5d314e21](https://github.com/N0zoM1z0/th08/commit/5d314e21ec725a0de7a38b7c735f683ad358e79f) | 2026-08-30T10:30:16Z | 17.34 | docs: publish native Linux 64-bit release |
| [bd1bae44](https://github.com/N0zoM1z0/th08/commit/bd1bae444803adfa1fc865bfe64415e9951945e6) | 2026-08-26T15:59:04Z | 13.57 | semantic: recover GameManager state and exact title registration |
### TH095
Start: [8c31e6f8](https://github.com/N0zoM1z0/th095/commit/8c31e6f892d945423b7f7b439747a2bd915f82d8) at 2026-08-29T07:49:20Z.
| Checkpoint | UTC timestamp | Elapsed days | Recorded milestone |
| --------------------------------------------------------------------------------------------- | -------------------- | -----------: | ---------------------------------------- |
| [0fa857fb](https://github.com/N0zoM1z0/th095/commit/0fa857fb2b2ec6237732e4d6be8d0a28069c9330) | 2026-09-07T05:23:52Z | 8.90 | gpt-web: match enemy movement exactly |
| [74baddfc](https://github.com/N0zoM1z0/th095/commit/74baddfccb712d1311a4d6e7487a63cd1100452e) | 2026-09-08T09:19:45Z | 10.06 | Match final ANM x87 draw functions |
| [33d46a0e](https://github.com/N0zoM1z0/th095/commit/33d46a0ee48f37060d973125a1a7632a40f8d998) | 2026-09-09T11:15:00Z | 11.14 | build: complete whole-program linkage |
| [3442dcf2](https://github.com/N0zoM1z0/th095/commit/3442dcf29d9e0b5bef384a49bd0c9ad32a711826) | 2026-09-10T07:07:28Z | 11.97 | Mark the Windows reconstruction playable |
### TH04
Start: [f402cb30](https://github.com/N0zoM1z0/th04/commit/f402cb30b7e795ea4328b6de794e9eefc52c97b3) at 2026-09-06T09:54:05Z.
| Checkpoint | UTC timestamp | Elapsed days | Recorded milestone |
| -------------------------------------------------------------------------------------------- | -------------------- | -----------: | --------------------------------------------------------------- |
| [1cd1a257](https://github.com/N0zoM1z0/th04/commit/1cd1a2577a3dbf4c17ac7a939faed5eeb81a6ebf) | 2026-09-26T21:16:40Z | 20.47 | gpt-web: accept MAIN SND_LOAD |
| [4b6a57f8](https://github.com/N0zoM1z0/th04/commit/4b6a57f8c0625da07c38258b78f5f205aae92805) | 2026-10-02T07:44:53Z | 25.91 | gpt-6.1-sol: build four native products and restore DOS handoff |
## What the milestones establish
- **TH08:** the 19 August progress ledger records 1,107 / 1,107 authored
functions with source. The 24 August change introduces a playable Linux
reconstruction port; the 30 August GitHub release publishes the 64-bit Linux
product. The 26 August exact ledger records 1,106 / 1,107 accepted functions.
- **TH095:** the initial ledger records zero confirmed/source-present authored
functions. The 7 September ledger records 697 source-present mappings;
8 September records 696 exact functions out of 697 confirmed authored
functions. The 10 September checkpoint marks the Windows i386 reconstruction
playable and documents the exercised startup, gameplay and transition paths.
The remaining `Controller::GetInput` comparison is separate from playability.
- **TH04:** the 26 September ledger records 661 exact functions out of 663
reviewed authored candidates across OP, MAIN, MAINE and ZUN. MAIN's byte
denominator covers its reviewed authored extents; other artifacts use decoded
comparisons. The 2 October checkpoint builds four standalone DOS products.
Runtime work and a modern x64 product have their own milestones; a four-product
build is not a whole-game runtime result.
TH08's playable ports preceded later historical-platform runtime audits. The
Factory's current recommended order reflects lessons learned across those
projects; do not present that later recommendation as a process applied
perfectly from the beginning.
## Editorial scope
The article follows the author's change in perspective: previous manual RE,
starting agent-driven work in August 2026, TH08's continuation, TH095's reuse of
experience, and TH04's application of the method to another architecture. The
manual-RE background and August starting point are author-provided experience.
The industrial-era conclusion and the interpretation of the two contribution
models are the author's argument, supported by the described project record.
They are not a measured agent-versus-human performance ratio.
Engine APIs, compiler flags, addresses and ledger schemas remain in references.
The full essay replaces the outline and removes its `noindex` directive; the
generated sitemap includes the article. The author approved publication on
10 October 2026, and the review labels were removed from the article and Blog hub.
## GensokyoClub quotation and policy
The public README was read with `gh api` on 10 October 2026. Its latest
README-changing commit is
[`e874b98e`](https://github.com/GensokyoClub/th08/blob/e874b98e210b3be1ed21c5e03d54b883eac5069c/README.md#important-notice),
dated 6 September. The notice announces a decision effective 5 September and
describes concerns about AI decompilations, ports, attribution and their effect
on the maintainers. Its stated plan is private development until completion.
The article quotes 23 words from the passage's opening clause, followed by an
ellipsis; it paraphrases the remaining point about the psychological toll.
The excerpt was checked against the retrieved original, with whitespace
normalized for display. The linked notice supplies its context.
The current public head is
[`f345c7de`](https://github.com/GensokyoClub/th08/tree/f345c7dee07b849f2ff93a0029ab5ede126bf12c).
Its [contribution policy](https://github.com/GensokyoClub/th08/blob/f345c7dee07b849f2ff93a0029ab5ede126bf12c/CODE_OF_CONDUCT.md)
excludes PRs created primarily using AI. The article paraphrases this policy
and discusses the different contribution models. It preserves GensokyoClub's
credit, describes the author's own manual and agent workflows, and does not
assign motives beyond what the public notice states.
## TH04's current DOS and 64-bit phases
The latest public main checkpoint checked for this revision is
[`bb9faca5`](https://github.com/N0zoM1z0/th04/blob/bb9faca54e2eb3c0d6c757c75d59ac29d2cd5e93/docs/RE_HANDOFF.md),
dated 10 October 2026. Its handoff records the standalone DOS products,
repaired normal and invincible variants, and the user's manual Windows
observations of complete Normal routes, endings and saves. It identifies
resumed x64 work as the current phase on the separate `port/modern-64` branch.
That branch was publicly present at `1ffc53a61e1e82409adb40c7658d4ee2f3a21916`
when inspected with `gh`.
The essay's first-person DOS testing statement describes those maintainer
observations, also confirmed by the author in this revision request. It is
not attributed to a new automated website test or a fresh runtime replay.
The 64-bit port is described as in development. The earlier four-product build
and function-count milestones retain their historical scopes.
+8
View File
@@ -94,6 +94,14 @@ APK's selected Java and an explanatory summary.
APK, methods, source attribution and reconstruction checks. Keep the block
formula, default settings, slider outputs and reference cases consistent.
## Touhou reconstruction essay
The essay opens with a takeaway and develops its argument in prose. It has no
opening flow figure. Project dates and the attributed README excerpt support
the narrative; detailed milestone references are available in a native
disclosure. Their provenance is documented in
[evidence/touhou-reconstruction-method.md](evidence/touhou-reconstruction-method.md).
## Original image generation prompts
## Prompt 1
+37 -5
View File
@@ -163,10 +163,12 @@ section[id] {
white-space: nowrap;
}
.navigation a:hover,
.navigation a[aria-current="page"] {
.navigation a[aria-current="page"],
.navigation a[aria-current="true"] {
color: var(--ink);
}
.navigation a[aria-current="page"] {
.navigation a[aria-current="page"],
.navigation a[aria-current="true"] {
text-decoration: underline;
}
.navigation .nav-start {
@@ -178,7 +180,8 @@ section[id] {
gap: 8px;
}
.nav-github,
.nav-discord {
.nav-discord,
.nav-x {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -192,12 +195,14 @@ section[id] {
text-decoration: none;
}
.nav-github:hover,
.nav-discord:hover {
.nav-discord:hover,
.nav-x:hover {
background: var(--soft);
color: var(--ink);
}
.nav-github svg,
.nav-discord svg {
.nav-discord svg,
.nav-x svg {
display: block;
flex-shrink: 0;
}
@@ -675,6 +680,27 @@ pre code {
.article .stats dd {
color: var(--muted);
}
.blog-takeaway {
max-width: 730px;
margin-top: 1.8rem;
padding-left: 1.25rem;
border-left: 2px solid var(--accent);
}
.blog-takeaway .eyebrow {
color: var(--accent);
}
.blog-takeaway p:last-child {
margin-bottom: 0;
}
.blog-article blockquote {
margin: 1.6rem 0;
padding-left: 1.25rem;
border-left: 2px solid var(--line);
font-size: 1.05rem;
}
.blog-article blockquote p {
color: var(--ink);
}
.caption {
font-size: 0.875rem;
color: var(--muted);
@@ -1372,6 +1398,9 @@ pre code {
.home-expert-shortcut a {
white-space: nowrap;
}
.home-expert-shortcut + .home-expert-shortcut {
margin-top: 0.5rem;
}
@media (min-width: 1440px) {
.home-page-nav {
position: fixed;
@@ -2117,6 +2146,9 @@ pre code {
right: calc(1rem + env(safe-area-inset-right, 0px));
bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
}
.site-footer {
padding-bottom: calc(2rem + 44px + env(safe-area-inset-bottom, 0px));
}
.container {
width: calc(100% - 2.5rem);
}
+174
View File
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>REA Blog — Thoughts on Reverse Engineering</title>
<meta
name="description"
content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents."
/>
<link rel="canonical" href="https://rea.tools/blog/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="REA Blog — Thoughts on Reverse Engineering"
/>
<meta
property="og:description"
content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents."
/>
<meta property="og:url" content="https://rea.tools/blog/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="REA Blog — Thoughts on Reverse Engineering"
/>
<meta
name="twitter:description"
content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
<script src="../assets/main.js" defer></script>
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
><span class="brand-name">REA</span
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="./" aria-current="page">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
class="nav-github"
href="https://github.com/morluto/rea"
aria-label="REA on GitHub"
title="REA on GitHub"
>
<svg
width="22"
height="22"
viewBox="0 0 16 16"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
/>
</svg>
<span class="nav-github-count" hidden aria-hidden="true"></span>
</a>
<a
class="nav-discord"
href="https://discord.gg/GkcryMnJDM"
aria-label="Join REA on Discord"
title="Join REA on Discord"
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Blog</p>
<h1>Blog.</h1>
<p class="lede">Thoughts on reverse engineering.</p>
</div>
<div class="guide-index">
<section class="guide-entry">
<p class="eyebrow">Game reconstruction<br />Essay</p>
<div>
<h2>The industrial revolution in reverse engineering</h2>
<p>
What manual RE and reconstructing TH08, TH095 and TH04 taught me
about agents, verification and knowledge that compounds across
projects.
</p>
<a class="text-link" href="touhou-reconstruction/"
>Read the article</a
>
<p class="page-meta">By N0zoM1z0 · 10 October 2026</p>
</div>
</section>
</div>
</main>
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
<div class="footer-links">
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
</body>
</html>
@@ -0,0 +1,669 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reverse Engineering’s Industrial Revolution — REA</title>
<meta
name="description"
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
/>
<link
rel="canonical"
href="https://rea.tools/blog/touhou-reconstruction/"
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Reverse Engineering’s Industrial Revolution — REA"
/>
<meta
property="og:description"
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
/>
<meta
property="og:url"
content="https://rea.tools/blog/touhou-reconstruction/"
/>
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Reverse Engineering’s Industrial Revolution — REA"
/>
<meta
name="twitter:description"
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
<script src="../../assets/main.js" defer></script>
</head>
<body id="top" class="blog-article">
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
><span class="brand-name">REA</span
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a>
<a href="../" aria-current="true">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
class="nav-github"
href="https://github.com/morluto/rea"
aria-label="REA on GitHub"
title="REA on GitHub"
>
<svg
width="22"
height="22"
viewBox="0 0 16 16"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
/>
</svg>
<span class="nav-github-count" hidden aria-hidden="true"></span>
</a>
<a
class="nav-discord"
href="https://discord.gg/GkcryMnJDM"
aria-label="Join REA on Discord"
title="Join REA on Discord"
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
<a href="../../">Home</a> / <a href="../">Blog</a> / Touhou
reconstruction
</p>
<p class="eyebrow">Essay · Touhou reconstruction</p>
<h1>The industrial revolution in reverse engineering.</h1>
<p class="page-meta">By N0zoM1z0 · 10 October 2026</p>
<div class="blog-takeaway">
<p class="eyebrow">Takeaway</p>
<p>
<strong
>Agent-driven reverse engineering changes how the work can
accumulate.</strong
>
An investigation can become a checked result, a failed experiment
can become a reusable test, and one project's experience can become
the next project's starting point.
</p>
<p>
Reconstructing TH08, TH095 and TH04 convinced me that RE is entering
its industrial age.
</p>
</div>
</div>
<div class="article-layout">
<nav class="contents" aria-label="On this page">
<p>In this article</p>
<ul>
<li><a href="#august">August 2026</a></li>
<li><a href="#th08">TH08: the continuation</a></li>
<li><a href="#different-approaches">Two approaches</a></li>
<li><a href="#th095">TH095: compounding</a></li>
<li><a href="#th04">TH04: across architectures</a></li>
<li><a href="#industrial-shift">The industrial shift</a></li>
<li><a href="#possibilities">What becomes possible</a></li>
</ul>
</nav>
<article class="article" aria-label="Touhou reconstruction essay">
<section class="article-section" id="august">
<h2>What changed in August 2026</h2>
<p>
Before this reconstruction work, I had done a lot of manual
reverse engineering. The familiar loop was to inspect
instructions, follow references, form an explanation, test it, and
revise it. Progress depended on how much of that loop I could
carry out myself, and how well I could keep the growing picture in
my head.
</p>
<p>
In August 2026, I began exploring agent-driven RE seriously. I
wanted to see how far an agent could go when it had access to the
program, ordinary development tools, and feedback from real
checks. Touhou reconstruction became the place where I could
pursue that question through a substantial project.
</p>
<p>
The early progress was startling. Tasks that I would have budgeted
around my own time and attention could keep moving while the agent
investigated, tried source formulations, compiled them, and
examined the result. My work increasingly involved choosing the
direction, improving the tools and checks, and deciding what the
accumulated evidence meant.
</p>
<p>
That changed which projects seemed reasonable to attempt. A large
executable began to look like a collection of investigations we
could organize and keep advancing. Recovering source, making it
readable, and eventually using it for a port became parts of an
achievable project.
</p>
<p>
The numbers first got my attention. The more interesting question
was what made that progress possible—and whether the process could
improve as it worked.
</p>
</section>
<section class="article-section" id="th08">
<h2>TH08: building on human work</h2>
<p>
<a href="https://github.com/N0zoM1z0/th08">TH08</a>, Imperishable
Night, began for me as a continuation of
<a href="https://github.com/GensokyoClub/th08"
>GensokyoClub's reconstruction</a
>. Their public work already contained source, names, build
knowledge and a contribution history. It gave the continuation a
foundation. That foundation matters when discussing everything
that followed.
</p>
<p>
I preserved the imported history through the public 10 August
checkpoint. The independent continuation began on 13 August. By 19
August, the project's ledger recorded source for all 1,107
identified game functions. A playable Linux reconstruction port
was committed on 24 August, roughly eleven days after the
continuation began. A Web edition followed, then a native Linux
64-bit release on 30 August.
</p>
<p>
Those dates made the change in pace tangible. What interested me
most was that the work extended beyond producing plausible C++. It
included comparisons, build repairs, recovery of meaningful names
and types, and the work needed to turn reconstructed pieces into a
program people could run.
</p>
<p>
This also exposed the importance of feedback. A function can look
correct and compile correctly while its place in the whole program
is wrong. Two functions can accidentally use separate copies of
state that should be shared. A port can hide a problem that
appears when the historical build runs. Each failure adds
something useful if we can reproduce it and keep the check.
</p>
<p>
Over time, more of the project lived outside any one conversation:
source, evidence, comparison scripts, regression tests, and
focused notes about what to do next. A fresh agent could inspect
that state and continue.
<strong
>The repository was becoming the project's working
memory.</strong
>
</p>
<p>
<strong
>A successful batch gave us both recovered code and a better
environment for the next batch.</strong
>
</p>
</section>
<section class="article-section" id="different-approaches">
<h2>Two different ideas of reconstruction</h2>
<p>
There is a real disagreement around this work, and GensokyoClub's
public README expresses it directly. Its notice announces that
further development will happen privately until completion. One
passage reads:
</p>
<blockquote>
<p>
The rise of grifters (AI decompilations and ports) in this space
taking from our work paints a bad image for future decompilation
efforts…
</p>
</blockquote>
<p>
The notice also describes the psychological toll on the
maintainers. Their contribution policy excludes pull requests
produced primarily with AI. These are people putting their free
time into difficult work, and their published work helped make my
continuation possible. That effort deserves respect. The
disagreement I want to discuss concerns how reconstruction is done
and how contributions are evaluated.
</p>
<p>
In the manual RE workflow I knew, expertise and execution were
closely connected. The person developing an understanding of a
function also carried out much of the investigation and
reconstruction. Personal knowledge, careful authorship and trust
in contributors were central to the project.
</p>
<p>
Manual projects already preserve knowledge in code, build tools
and tests. What changed for me was having an agent use that
accumulated knowledge to investigate, implement and test the next
change.
</p>
<p>
My continuation takes an agent-led approach. Agents carry out the
new engineering; the human sets direction and release decisions.
Understanding has to be retained in source and evidence, while
comparisons, builds and runtime experiments test the results. A
contribution can be useful even when an agent performed most of
the work, provided its reasoning and result can be examined.
</p>
<p>
This creates a difficult transition for communities built around
the earlier model. Open source makes existing work available for
continuation, and agents can change the pace of that continuation
dramatically. Years of careful work may become the starting point
for something that moves much faster. Recognition, provenance and
the standards for accepting new work become more important in that
situation.
</p>
<p>
The industrial analogy helps me make sense of the disagreement. A
craft contains skill, judgment and accumulated knowledge. When
machinery changes how execution happens, those skills find new
roles in designing the process, detecting defects and deciding
what should be produced. Different communities can reasonably
choose different ways to organize that work.
</p>
<p>
My choice is to develop this continuation openly, preserve its
sources and history, and make the new work reviewable. The
technical question I want to pursue is how far agent-driven
reconstruction can go with strong evidence and a process that
keeps improving.
</p>
<p class="caption">
Source: GensokyoClub's
<a
href="https://github.com/GensokyoClub/th08/blob/e874b98e210b3be1ed21c5e03d54b883eac5069c/README.md#important-notice"
>README notice</a
>, checked on 10 October 2026, and its
<a
href="https://github.com/GensokyoClub/th08/blob/f345c7dee07b849f2ff93a0029ab5ede126bf12c/CODE_OF_CONDUCT.md"
>contribution policy</a
>. The quotation is a shortened excerpt. TH08's
<a
href="https://github.com/N0zoM1z0/th08/blob/d2a00f4acb4a13916e07a00bd13f714961714628/README.md#credits-and-provenance"
>credits and provenance</a
>
record the continuation boundary.
</p>
</section>
<section class="article-section" id="th095">
<h2>TH095: experience starts compounding</h2>
<p>
<a href="https://github.com/N0zoM1z0/th095">TH095</a>, Shoot the
Bullet, made the accumulated experience much easier to see. Its
new repository began on 29 August with zero confirmed game
functions in the reconstruction ledger. It already had a way to
work: identify the target, bring up analysis and comparison tools,
keep tasks manageable, preserve evidence, and leave checkpoints
another session could use.
</p>
<p>
By 7 September, all 697 identified game functions had source. By 8
September, 696 of them were accepted as exact comparisons. The
whole program linked on 9 September, and the Windows i386
reconstruction was marked playable on 10 September, about twelve
days after initialization.
</p>
<p>
I found this more exciting than the first project's speed. TH095
began with the process and experience developed during TH08 and
the other reconstruction work. We could approach a new target with
better questions, better checks and fewer assumptions left
implicit.
</p>
<p>
For example, matching individual functions had taught us to pay
attention to the assembled program early. Does the source build
together? Do different parts of the game use the same state? Does
the reconstructed executable survive the transitions we need to
exercise? Those questions became part of how the next project was
organized.
</p>
<p>
This is what I mean by knowledge compounding. A lesson that stays
in my head helps while I am present. A lesson retained as a test,
a script or a precise evidence note can help every later session.
A failure becomes valuable when the next agent can recognize it
without repeating the whole investigation.
</p>
<p>
<strong
>The method becomes part of the starting material for the next
game.</strong
>
As that material improves, we can spend more attention on what is
new about the target.
</p>
<p>
This compounds across people as well as projects. Someone joining
later can rerun a check, inspect the reason for a source choice,
or continue from a recorded unknown. The project can make use of
experience without requiring everyone to reconstruct its entire
history from scratch.
</p>
</section>
<section class="article-section" id="th04">
<h2>TH04: the workflow survives a different architecture</h2>
<p>
<a href="https://github.com/N0zoM1z0/th04">TH04</a>, Lotus Land
Story, took the work into the PC-98 DOS era. We moved from 32-bit
Windows games to a 16-bit environment with four cooperating
programs and hardware behavior that had to be understood on its
own terms. Existing
<a href="https://github.com/nmlgc/ReC98">ReC98 work</a> provided
valuable knowledge and source material here too.
</p>
<p>
The DOS reconstruction is now working, including complete Normal
routes, endings and saves from my manual testing. The current work
is a native 64-bit port. These are distinct phases: first
establish a working reconstruction on the original platform, then
carry its behavior into a new environment.
</p>
<p>
TH04's architecture changed the evidence we needed and the tools
used to collect it. It did not require inventing a different way
to organize the work. We could still isolate a question, inspect
the original behavior, reconstruct it, compare the result and
retain the lesson.
</p>
<p>
The difficult parts became concrete experiments. Which part of
startup establishes a piece of state? What happens when gameplay
hands control to an ending? Which hardware effect must the
reconstructed code preserve? With observations and a useful check,
an agent could work through these questions in the same iterative
way as it did on the Windows titles.
</p>
<p>
That is why TH04 matters to the larger argument. The process
generalized across a substantial change in platform. Architecture
determined the subject of the investigation; it did not become a
fundamental barrier to the workflow.
</p>
<p>
The 64-bit port now gives that work another use. We can revisit
the recovered behavior, separate it from old platform services and
test new implementations against what we learned. The
reconstruction supplies both source and a growing body of
knowledge about how the game should behave.
</p>
<p class="caption">
Project state as of 10 October 2026:
<a
href="https://github.com/N0zoM1z0/th04/blob/bb9faca54e2eb3c0d6c757c75d59ac29d2cd5e93/docs/RE_HANDOFF.md"
>DOS reconstruction and manual testing</a
>
·
<a href="https://github.com/N0zoM1z0/th04/tree/port/modern-64"
>64-bit port</a
>. The port remains in development.
</p>
</section>
<section class="article-section" id="industrial-shift">
<h2>What makes this an industrial shift</h2>
<p>
These projects changed my view of the bottleneck. As more
investigation and implementation can be carried out by agents, the
working environment becomes increasingly important. The agent
needs access to the target, tools it can compose, feedback it can
act on, and memory that survives its current session.
</p>
<p>
REA fits into that picture by making reverse-engineering tools
available through interfaces an agent can use. It can inspect
code, follow relationships and return evidence for the next
question. Reconstruction projects add their own compilers,
comparisons and runtime checks. Together, these give an agent a
way to move from an explanation to an experiment with an
observable result.
</p>
<p>
Fast execution makes good feedback more valuable. A bad assumption
can produce a large amount of plausible source. A useful check can
reject that assumption early and point to the next investigation.
Improving a check improves many later decisions, which is why
tooling and verification deserve as much attention as the
recovered code.
</p>
<p>
The work also needs continuity. A conversation can end while the
campaign continues. Its result should leave the repository in a
state another agent can understand: what changed, what was tested,
what remains uncertain, and where to continue. This lets a large
reconstruction proceed through many small experiments without
relying on one uninterrupted session.
</p>
<p>
The
<a
href="https://github.com/N0zoM1z0/touhou-reconstruction-factory"
>Touhou Reconstruction Factory</a
>
grew out of these experiences. It gives the projects shared ways
to retain evidence, run checks and carry useful lessons between
games. The point is to make the next investigation easier to
execute, easier to judge and easier to resume.
</p>
<p>
This is the industrial shift I see: expertise increasingly lives
in a working process that can be reused and improved. Human
judgment shapes the process. Agents supply much of the execution.
Verified results and accumulated lessons make the process more
capable over time.
</p>
</section>
<section class="article-section" id="possibilities">
<h2>The projects we can now consider</h2>
<p>
The pace matters because it changes the cost of beginning. When I
imagined doing every investigation manually, I also imagined
committing my own attention to each one. A sufficiently large
project could be interesting and still remain something I would
never realistically attempt.
</p>
<p>
An agent with access, feedback and project memory changes that
calculation. Recovering a game, making its source readable,
building a port, or exploring a mod can become a sustained
engineering effort with a practical way to keep going. The work
still involves difficult questions, but there is now a process for
giving those questions sustained attention.
</p>
<p>
TH08 showed me what an agent-led continuation could achieve on top
of human work. TH095 showed how the experience could carry
forward. TH04 showed that the method could cross into a very
different platform. Together, they made the industrial analogy
feel concrete.
</p>
<p>
I now look at an unfamiliar program and ask:
<strong
>what access, feedback and accumulated knowledge would let an
agent work on this reliably?</strong
>
</p>
<p>
That question opens up projects I would previously have left
alone. Each one can add code, checks and experience that make the
next more approachable. That is the future of reverse engineering
I want to keep exploring.
</p>
</section>
<section class="article-section" id="sources">
<details class="native-details">
<summary>Project milestones and sources</summary>
<p>
The dates describe recorded project checkpoints, checked against
public GitHub history on 10 October 2026. Elapsed time is
calendar time between commits. Source presence, exact
comparisons, a build and a runtime result each name a different
milestone.
</p>
<ul>
<li>
<strong>TH08:</strong>
<a
href="https://github.com/N0zoM1z0/th08/commit/001bf3e9c91cc35b79c7a0e36b3565b86f494362"
>13 August continuation</a
>,
<a
href="https://github.com/N0zoM1z0/th08/blob/15caba73df6735fc44eac4783bc61b8d098ea830/docs/PROGRESS.md"
>19 August source ledger</a
>,
<a
href="https://github.com/N0zoM1z0/th08/commit/da07b21c8cb63230142b0a66228d6aac66555f3f"
>24 August Linux port</a
>,
<a
href="https://github.com/N0zoM1z0/th08/commit/529eb1a3afa05ae36467731eaccb12e3e50e132f"
>26 August Web edition</a
>, and
<a
href="https://github.com/N0zoM1z0/th08/releases/tag/v0.2.0-linux-64bit"
>30 August 64-bit Linux release</a
>.
</li>
<li>
<strong>TH095:</strong>
<a
href="https://github.com/N0zoM1z0/th095/blob/8c31e6f892d945423b7f7b439747a2bd915f82d8/docs/PROGRESS.md"
>29 August initial ledger</a
>,
<a
href="https://github.com/N0zoM1z0/th095/blob/0fa857fb2b2ec6237732e4d6be8d0a28069c9330/docs/PROGRESS.md"
>7 September source ledger</a
>,
<a
href="https://github.com/N0zoM1z0/th095/blob/74baddfccb712d1311a4d6e7487a63cd1100452e/docs/PROGRESS.md"
>8 September comparisons</a
>,
<a
href="https://github.com/N0zoM1z0/th095/commit/33d46a0ee48f37060d973125a1a7632a40f8d998"
>9 September linkage</a
>, and
<a
href="https://github.com/N0zoM1z0/th095/blob/3442dcf29d9e0b5bef384a49bd0c9ad32a711826/docs/WHOLE_BUILD_TODO.md"
>10 September playable-build record</a
>.
</li>
<li>
<strong>TH04:</strong>
<a
href="https://github.com/N0zoM1z0/th04/blob/bb9faca54e2eb3c0d6c757c75d59ac29d2cd5e93/docs/RE_HANDOFF.md"
>10 October handoff</a
>
records the working DOS reconstruction, the maintainer's
complete Normal-route testing and the current 64-bit phase.
</li>
<li>
<strong>Method:</strong> the Factory's
<a
href="https://github.com/N0zoM1z0/touhou-reconstruction-factory/blob/038b6bed51d273c8b3680553b6f43e1b97d19bd1/docs/agent-autonomy.md"
>agent autonomy</a
>
and
<a
href="https://github.com/N0zoM1z0/touhou-reconstruction-factory/blob/038b6bed51d273c8b3680553b6f43e1b97d19bd1/docs/knowledge-base.md"
>cross-game knowledge</a
>
documents retain the working principles and lessons.
</li>
</ul>
</details>
<p>
<a class="text-link" href="../">More articles</a> ·
<a class="text-link" href="../../showcase/th04/"
>Explore a TH04 investigation</a
>
</p>
</section>
</article>
</div>
</main>
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
<div class="footer-links">
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
</body>
</html>
+23 -2
View File
@@ -63,7 +63,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../../guides/">Guides</a><a href="../../faq/">FAQ</a>
</nav>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -383,7 +403,8 @@
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -63,6 +63,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../guides/">Guides</a>
<a href="./" aria-current="page">FAQ</a>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -482,7 +502,8 @@
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+23 -2
View File
@@ -62,7 +62,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a
><a class="nav-start" href="../get-started/">Get started</a
><a href="../guides/">Guides</a><a href="../faq/">FAQ</a>
</nav>
@@ -106,6 +107,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -420,7 +440,8 @@ JS</code></pre>
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+23 -2
View File
@@ -62,7 +62,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a
><a class="nav-start" href="./" aria-current="page">Get started</a
><a href="../guides/">Guides</a><a href="../faq/">FAQ</a>
</nav>
@@ -106,6 +107,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -479,7 +499,8 @@ npx rea-agents@latest setup</code></pre>
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+23 -2
View File
@@ -56,7 +56,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
</nav>
@@ -100,6 +101,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -403,7 +423,8 @@ link.click();</code></pre>
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+23 -2
View File
@@ -56,7 +56,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a
><a class="nav-start" href="../get-started/">Get started</a
><a href="./" aria-current="page">Guides</a><a href="../faq/">FAQ</a>
</nav>
@@ -100,6 +101,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -186,7 +206,8 @@
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+23 -2
View File
@@ -62,7 +62,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
</nav>
@@ -106,6 +107,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -405,7 +425,8 @@
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+23 -2
View File
@@ -56,7 +56,8 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
</nav>
@@ -100,6 +101,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -335,7 +355,8 @@ return (DxBallInt)pan;</code></pre>
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+44 -1
View File
@@ -65,6 +65,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="showcase/">Showcases</a>
<a href="blog/">Blog</a>
<a class="nav-start" href="get-started/">Get started</a>
<a href="guides/">Guides</a>
<a href="faq/">FAQ</a>
@@ -109,6 +110,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -134,6 +154,7 @@
<li><a href="#try-rea">Try REA</a></li>
<li><a href="#analyze">Go deeper</a></li>
<li><a href="#questions">Help &amp; feedback</a></li>
<li><a href="#community">Community</a></li>
</ul>
</div>
</nav>
@@ -157,6 +178,10 @@
Already know RE?
<a href="#analysis-guides">Skip to analysis guides →</a>
</p>
<p class="home-expert-shortcut">
Want to go deeper?
<a href="blog/">Read the Blog →</a>
</p>
</div>
<div class="home-setup">
<h2>Set up REA</h2>
@@ -1052,6 +1077,23 @@ SPEED: 6</code></pre>
</p>
</div>
</section>
<section class="section" id="community">
<div class="container">
<h2>Join the community</h2>
<p>
Share investigations and get help from other REA users on Discord.
Follow REA on X for project updates.
</p>
<div class="actions">
<a class="button" href="https://discord.gg/GkcryMnJDM"
>Join the community</a
>
<a class="button secondary" href="https://x.com/reatools"
>Follow @reatools on X</a
>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-inner">
@@ -1060,7 +1102,8 @@ SPEED: 6</code></pre>
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -64,6 +64,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
@@ -108,6 +109,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -718,7 +738,8 @@ npx rea-agents@latest inspect-android-package \
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -63,6 +63,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -688,7 +708,8 @@ chmod +x ms_flag_checker
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -63,6 +63,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -696,7 +716,8 @@
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -63,6 +63,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="./" aria-current="page">Showcases</a>
<a href="../blog/">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -228,7 +248,8 @@
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -63,6 +63,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -534,7 +554,8 @@ mimeTypes:
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+22 -1
View File
@@ -63,6 +63,7 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
@@ -107,6 +108,25 @@
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
@@ -711,7 +731,8 @@ no_aim:
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Community</a>
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
+30
View File
@@ -144,6 +144,36 @@ Keep the explanation on the website. Repository links provide source,
reproduction instructions and further evidence. Use specific links for those
purposes, and immutable commit links for checkpoint-dependent facts.
### Method articles in the Blog
Use longer articles to explain methods, choices and ideas through project
experience. Open with a clear takeaway. A personal essay can follow how the
author's understanding changed, using concrete episodes to develop a deeper
argument. Keep the reasoning easy to follow; a Blog article does not need to
become a tutorial. Figures are optional. Add one when it explains something
useful rather than repeating a familiar opening flow.
Choose cases that teach different decisions. For example, TH08 illustrates
continuing existing work, TH095 illustrates reusing an established method in a
new project, and TH04 illustrates adapting checks to an older platform. Close
each case with the lesson the reader can apply. Put dates and progress figures
beside the milestone they measure, with pinned evidence links in supporting
details. Calendar time, source presence, exact comparison, build success and
runtime checks each describe a different result.
Make an analogy concrete. The industrial analogy can connect agent execution,
quality checks, durable knowledge and a change in how work is organized. Explain
what changes and support the argument with experience. Keep necessary terms
plain and leave engine inventories in references. A method guide can finish with
a first task; an essay can finish with the implications of its argument.
When discussing another project's approach, preserve its credit and contribution
history. Attribute quotations, link their context and distinguish the author's
interpretation from the other project's stated position. Describe the technical
or contribution-model disagreement without assigning motives to its authors.
Working outlines should be visibly labeled and marked `noindex` until they
become finished articles.
## Make figures explain a relationship
Each figure should answer a question that a reader can name. Choose the visual