Compare commits

..
Author SHA1 Message Date
N0zoM1z0 591b910e31 chore(review): integrate selected-archive traversal fix 2026-10-10 20:58:39 +08:00
N0zoM1z0 9ffc6210a4 test(javascript): remove duplicate escaped-receiver cases 2026-10-10 20:53:47 +08:00
GeekwolfandYsopwnable b973b1b1ec test(javascript): cover returned object scaling 2026-10-10 20:53:47 +08:00
GeekwolfandYsopwnable 47947eb01c perf(javascript): index returned object targets 2026-10-10 20:53:47 +08:00
YsopwnableandGitHub 2d99e1e3b2 test(ghidra): preserve Node heap options in adapter verification (#1550) 2026-10-10 20:27:45 +08:00
5d6eeb22fb fix(javascript): yield during the largest AST traversals (#1545)
After #1542 the longest event-loop blocks on large bundles were single AST
traversals inside one analyzer: member-mutation collection with its
fixpoint, static source analysis, and function fingerprints, each about
one second on Obsidian's 3.7 MB app.js.

- traverseJavaScriptAstSteps yields every 1,024 visited nodes with the
  same visit order; traverseJavaScriptAst drains it, so every existing
  caller is unchanged.
- Member mutations, static analysis and fingerprints run as steps. The
  member-mutation fixpoint yields every 64 references, and fingerprints
  yield between callables.
- The now-unused synchronous member-mutation and fingerprint wrappers are
  removed.

Graph IDs and complete result digests match main on three inputs,
including obsidian.asar. Over two runs there, the longest block falls
from about 920 ms to 740 ms, the second from 860 to 595 ms, and blocks
over 250 ms from 27 to 18, with total time within 1.5%.

Refs #1462

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:27:41 +08:00
N0zoM1z0 e964a06b83 fix(website): preserve template-owned HTML formatting 2026-10-10 20:27:01 +08:00
N0zoM1z0 5aa30b8b86 chore(website): simplify maintenance and detect documentation drift 2026-10-10 20:27:01 +08:00
YsopwnableandGitHub 8c7569aca7 test(clients): verify native Claude Code integration (#1549) 2026-10-10 20:16:41 +08:00
YsopwnableandGitHub 539a0f95b5 test(clients): verify native OpenCode integration (#1546) 2026-10-10 19:51:30 +08:00
Rudy Celekli 2309cebb7b fix(apple): scope keyed archive traversal to the selection
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-09 19:15:24 -04:00
49 changed files with 2732 additions and 683 deletions
+2 -6
View File
@@ -35,9 +35,5 @@ jobs:
sudo apt-get update
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
python3 -m pip install -r website/requirements.txt
- name: Prepare website assets
run: python3 scripts/prepare-website.py
- name: Check website links, metadata and publishing configuration
run: python3 scripts/verify-website.py
- name: Check sitemap discovery and metadata regressions
run: python3 scripts/test-website.py
- name: Check shared HTML, links, assets and contributor workflows
run: python3 scripts/website.py check
+2 -4
View File
@@ -59,10 +59,8 @@ jobs:
sudo apt-get update
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
python3 -m pip install -r website/requirements.txt
- name: Prepare website assets
run: python3 scripts/prepare-website.py
- name: Check website links, metadata and publishing configuration
run: python3 scripts/verify-website.py
- name: Check shared HTML, links, assets and contributor workflows
run: python3 scripts/website.py check
- name: Record the published commit
run: |
python3 - <<'PY'
+28
View File
@@ -118,6 +118,34 @@ The local fixture exercises the real Gemini API adapter, including its
Google API and native Windows compatibility remain unverified. The lane disables
the client's memory-based relaunch to preserve the caller's Node heap budget.
`verify:opencode-client` requires an installed OpenCode (verified with
`opencode-ai@1.18.35`); select it with `REA_VERIFY_OPENCODE_COMMAND`.
The optional POSIX lane configures isolated XDG roots and `OPENCODE_CONFIG_DIR`,
checks setup plans, backups, preserved JSONC comments and idempotence, activates
the installed skill, validates all forwarded input schemas and verifies complete
JavaScript Evidence for a Unicode path. JSONC is the default fixture; set
`REA_VERIFY_OPENCODE_CONFIG_FORMAT=json` for JSON. Use `-- chat` for ordinary
chat or `REA_VERIFY_RUNTIME_ROOT` for a production-only installed package.
The native core runs with external plugins disabled through `OPENCODE_PURE`;
the local OpenAI-compatible fixture has a caller-declared one-million-token
context and synthetic usage. Skills use an explicit isolated `skills.paths`
directory, so default OS-home discovery remains unverified. Live model APIs and
native Windows are also unverified.
`verify:claude-client` requires installed Claude Code (verified with
`@anthropic-ai/claude-code@2.1.296`); select it with `REA_VERIFY_CLAUDE_COMMAND`.
The optional POSIX lane uses native `CLAUDE_CONFIG_DIR` discovery, an independent
Git workspace, user settings and default built-in tools. It preserves caller
preferences and backups, checks idempotent setup, waits through the native
`WaitForMcpServers` tool when discovery is pending, and validates the complete
catalog and forwarded input schemas. Call mode loads the personal skill through
native `Skill`, checks its full body and verifies named-schema JavaScript Evidence
for a Unicode path. Use `-- chat` for ordinary chat with REA enabled, or
`REA_VERIFY_RUNTIME_ROOT` for a production-only installed package. The local
Anthropic Messages/SSE fixture preserves native resource-hint envelopes in its
request artifacts. Usage is synthetic; live Anthropic API, native Windows and
bare-mode personal-skill activation remain unverified by this lane.
`verify:deepseek-client` requires an installed DeepSeek Harness (`dsh`;
verified with `@deepseek-ai/dsh@0.2.0-rc.2`) and Git. Set
`REA_VERIFY_DEEPSEEK_COMMAND` to select its executable. This optional POSIX lane
+2
View File
@@ -181,6 +181,8 @@
"verify:package:windows": "npm run build:cached && node scripts/verify-windows-package.mjs",
"verify:qwen-client": "npm run build:cached && node scripts/verify-qwen-client.mjs",
"verify:gemini-client": "npm run build:cached && node scripts/verify-gemini-client.mjs",
"verify:opencode-client": "npm run build:cached && node scripts/verify-opencode-client.mjs",
"verify:claude-client": "npm run build:cached && node scripts/verify-claude-client.mjs",
"verify:pi-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs pi",
"verify:hermes-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs hermes",
"verify:deepseek-client": "npm run build:cached && node scripts/verify-deepseek-client.mjs",
+135
View File
@@ -0,0 +1,135 @@
"""Optionally audit external website links; network failures stay distinct from missing targets."""
import argparse
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from html.parser import HTMLParser
import json
from pathlib import Path
import re
import subprocess
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import quote, unquote, urlsplit
from urllib.request import Request, urlopen
from website import authored_markdown
@lru_cache(maxsize=None)
def github_api(endpoint):
"""Read GitHub evidence through the caller's gh installation and authentication."""
result = subprocess.run(["gh", "api", endpoint], capture_output=True, text=True, timeout=20)
if result.returncode:
raise ValueError(result.stderr.strip())
return json.loads(result.stdout)
def github_target(url):
"""Resolve repository URLs, including branch names containing slashes."""
parts = [unquote(part) for part in url.path.strip("/").split("/")]
if len(parts) < 2:
return None
repo = f"repos/{parts[0]}/{parts[1]}"
if url.hostname == "raw.githubusercontent.com" and len(parts) >= 4:
return f"{repo}/contents/{'/'.join(parts[3:])}?ref={quote(parts[2], safe='')}"
if len(parts) == 2:
return repo
kind = parts[2]
if kind in ("blob", "tree") and len(parts) >= 4:
ref = parts[3]
# GitHub uses the longest matching branch name before the source path.
if not re.fullmatch(r"[0-9a-f]{40}", ref):
references = github_api(f"{repo}/git/matching-refs/heads/{quote(ref, safe='')}")
references += github_api(f"{repo}/git/matching-refs/tags/{quote(ref, safe='')}")
suffix = "/".join(parts[3:])
matches = [item["ref"].split("/", 2)[2] for item in references]
matches = [name for name in matches if suffix == name or suffix.startswith(name + "/")]
if matches:
ref = max(matches, key=len)
path = "/".join(parts[3:])[len(ref):].lstrip("/")
return f"{repo}/contents/{quote(path, safe='/')}?ref={quote(ref, safe='')}"
if kind in ("issues", "pull") and len(parts) == 4 and parts[3].isdigit():
return f"{repo}/{'pulls' if kind == 'pull' else 'issues'}/{parts[3]}"
if kind == "commit" and len(parts) == 4:
return f"{repo}/commits/{parts[3]}"
if kind == "releases" and len(parts) >= 5 and parts[3] in ("tag", "download"):
return f"{repo}/releases/tags/{quote(parts[4], safe='')}"
return None
def audit_url(item):
"""Classify confirmed missing links separately from blocked or unsupported probes."""
value, sources = item
result = {"url": value, "sources": sorted(set(sources))}
url = urlsplit(value)
try:
if url.hostname in ("github.com", "raw.githubusercontent.com"):
endpoint = github_target(url)
if endpoint is None:
return dict(result, status="unknown", reason="GitHub UI route; check in a browser")
data = github_api(endpoint)
parts = url.path.strip("/").split("/")
if len(parts) >= 6 and parts[2:4] == ["releases", "download"]:
if unquote(parts[5]) not in [asset["name"] for asset in data.get("assets", [])]:
return dict(result, status="broken", reason="Release asset is missing")
return dict(result, status="ok", via="gh api", endpoint=endpoint)
request = Request(value.split("#", 1)[0], headers={
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
})
with urlopen(request, timeout=15) as response:
return dict(result, status="ok", http_status=response.status, final_url=response.url)
except HTTPError as error:
return dict(result, status="broken" if error.code in (404, 410) else "unknown", reason=str(error))
except ValueError as error:
return dict(result, status="broken" if "HTTP 404" in str(error) else "unknown", reason=str(error))
except (OSError, URLError, TimeoutError, subprocess.TimeoutExpired) as error:
return dict(result, status="unknown", reason=str(error))
def external_links(root):
"""Discover authored external links, excluding locally checked site and preview URLs."""
references = {}
def add(value, source):
url = urlsplit(value)
if url.scheme in ("http", "https") and url.hostname not in ("rea.tools", "localhost", "127.0.0.1", "::1"):
references.setdefault(value, []).append(str(source.relative_to(root)))
class Links(HTMLParser):
def handle_starttag(self, tag, attrs):
for key, value in attrs:
if key in ("href", "src") and value:
add(value, path)
for path in sorted((root / "website/public").rglob("*.html")):
Links().feed(path.read_text(encoding="utf-8"))
for path in authored_markdown(root):
for value in re.findall(r"(?:\]\(|<)(https?://[^\s<>\)]+)", path.read_text(encoding="utf-8")):
add(value, path)
return references
def main():
"""Run an explicit network audit without making PR checks depend on third-party uptime."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--report", type=Path, help="Write the complete JSON report outside public assets")
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(audit_url, external_links(root).items()))
if args.report:
if args.report.resolve().is_relative_to(root / "website/public"):
parser.error("Keep audit reports outside website/public.")
args.report.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
counts = {status: sum(item["status"] == status for item in results) for status in ("ok", "broken", "unknown")}
print(f"External links: {counts['ok']} reachable, {counts['broken']} missing, {counts['unknown']} need manual verification.")
print("GitHub checks establish target existence; external fragment IDs are not checked.")
for item in results:
if item["status"] != "ok":
print(f"{item['status']}: {item['url']} — {item['reason']}")
return 1 if counts["broken"] else 0
if __name__ == "__main__":
sys.exit(main())
+3
View File
@@ -16,6 +16,9 @@ import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
...(process.env.NODE_OPTIONS === undefined
? {}
: { NODE_OPTIONS: process.env.NODE_OPTIONS }),
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
+4 -33
View File
@@ -1,16 +1,14 @@
"""Prepare downloads, the sitemap and the sharing image for the static website."""
from html.parser import HTMLParser
from io import BytesIO
from pathlib import Path
import re
import sys
from urllib.parse import quote
import xml.etree.ElementTree as ET
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from website import PageMetadata, page_url
SITE_ORIGIN = "https://rea.tools"
EXAMPLE_FILES = (
"package.json",
"main.js",
@@ -21,47 +19,20 @@ EXAMPLE_FILES = (
)
class IndexPolicy(HTMLParser):
"""Read indexing directives from the authored HTML head."""
def __init__(self):
super().__init__()
self.in_head = False
self.noindex = False
def handle_starttag(self, tag, attrs):
if tag == "head":
self.in_head = True
if tag == "meta" and self.in_head:
attributes = dict(attrs)
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
directives = re.split(
r"[\s,]+", (attributes.get("content") or "").lower()
)
self.noindex |= bool(set(directives) & {"noindex", "none"})
def handle_endtag(self, tag):
if tag == "head":
self.in_head = False
def prepare_sitemap(site):
"""Discover HTML routes automatically, excluding pages marked noindex."""
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
ET.register_namespace("", namespace)
sitemap = ET.Element(f"{{{namespace}}}urlset")
for path in sorted(site.rglob("*.html")):
policy = IndexPolicy()
policy = PageMetadata()
policy.feed(path.read_text(encoding="utf-8"))
if policy.noindex:
continue
route = path.relative_to(site).as_posix()
if path.name == "index.html":
route = route.removesuffix("index.html")
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
ET.SubElement(
entry, f"{{{namespace}}}loc"
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
).text = page_url(site, path)
ET.indent(sitemap, space=" ")
ET.ElementTree(sitemap).write(
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
+116
View File
@@ -6,7 +6,11 @@ import io
from pathlib import Path
import shutil
import tempfile
from threading import Thread
import unittest
from http.server import ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.request import urlopen
import xml.etree.ElementTree as ET
@@ -23,6 +27,7 @@ def load_script(name):
PREPARE = load_script("prepare-website")
VERIFY = load_script("verify-website")
WEBSITE = load_script("website")
class WebsiteSearchChecks(unittest.TestCase):
@@ -126,5 +131,116 @@ class WebsiteSearchChecks(unittest.TestCase):
)
class WebsiteMaintenanceChecks(unittest.TestCase):
"""Exercise contributor edits and the static preview through real consumers."""
def test_shared_edits_preserve_body_and_catch_drift_before_rewriting(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("public", "templates"):
shutil.copytree(ROOT / "website" / name, root / "website" / name)
page = root / "website/public/blog/touhou-reconstruction/index.html"
original = page.read_text()
body = original.split('<main id="main"', 1)[1].split("</main>", 1)[0]
template = root / "website/templates/header.html.tmpl"
template.write_text(template.read_text().replace("Reverse Engineer Anything", "REA test navigation"))
with self.assertRaisesRegex(ValueError, "Shared HTML is stale"):
WEBSITE.sync_pages(root, check=True)
self.assertEqual(page.read_text(), original)
WEBSITE.sync_pages(root)
updated = page.read_text()
self.assertIn("REA test navigation", updated)
self.assertEqual(updated.split('<main id="main"', 1)[1].split("</main>", 1)[0], body)
self.assertEqual(WEBSITE.sync_pages(root), [])
page.write_text(updated.replace(' <!-- website:header:end -->', '', 1))
with self.assertRaisesRegex(ValueError, "needs one header region"):
WEBSITE.sync_pages(root)
def test_new_page_escapes_metadata_stays_a_draft_and_joins_sitemap_when_ready(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("public", "templates"):
shutil.copytree(ROOT / "website" / name, root / "website" / name)
site = root / "website/public"
page = WEBSITE.new_page(root, "blog/new-example", 'A <test> & "result"', 'Check "quotes" & <markup>.')
source = page.read_text()
self.assertIn('href="../../blog/" aria-current="page"', source)
self.assertIn('href="../../assets/styles.css"', source)
self.assertIn("A &lt;test&gt; &amp; &quot;result&quot;", source)
with self.assertRaisesRegex(ValueError, "Page already exists"):
WEBSITE.new_page(root, "blog/new-example", "Another page", "Description")
with self.assertRaises(ValueError):
WEBSITE.new_page(root, "../outside", "Title", "Description")
WEBSITE.sync_pages(root, check=True)
PREPARE.prepare_sitemap(site)
self.assertNotIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
page.write_text(source.replace(' <meta name="robots" content="noindex" />\n', ''))
PREPARE.prepare_sitemap(site)
self.assertIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
self.assertEqual(VERIFY.check_site(site)[3], [])
def test_links_in_prompts_fragments_and_copy_controls_are_verified(self):
with tempfile.TemporaryDirectory() as directory:
site = Path(directory) / "public"
shutil.copytree(ROOT / "website/public", site)
page = site / "faq/index.html"
original = page.read_text()
cases = (
('<a href="https://rea.tools/deleted/">Broken</a>', "missing local target"),
('<code>https://rea.tools/guides/native/#deleted</code>', "missing HTML fragment"),
('<code>https://morluto.github.io/rea/examples/notes-web/</code>', "obsolete website URL"),
('<button data-copy="deleted">Copy</button>', "missing copy target"),
('<a href="https://[">Malformed</a>', "invalid URL"),
)
for inserted, message in cases:
with self.subTest(message=message):
page.write_text(original.replace("</main>", inserted + "</main>", 1))
self.assertTrue(any(message in error for error in VERIFY.check_site(site)[3]))
def test_repository_links_reject_missing_files_and_old_markdown_anchors(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "website/public").mkdir(parents=True)
(root / "docs").mkdir()
(root / "docs/guide.md").write_text("# Guide\n## Set up REA\n")
page = root / "website/README.md"
page.write_text("[Guide](../docs/guide.md#set-up-rea)\n")
(root / "website/.venv").mkdir()
(root / "website/.venv/vendor.md").write_text("[File](missing.md)")
self.assertEqual(VERIFY.check_repository_links(root), [])
page.write_text("[Guide](../docs/guide.md#removed)\n[File](../docs/removed.md)\n")
errors = VERIFY.check_repository_links(root)
self.assertTrue(any("missing Markdown fragment" in error for error in errors))
self.assertTrue(any("missing repository target" in error for error in errors))
def test_preview_preserves_pages_prefix_for_assets_and_directory_redirects(self):
with tempfile.TemporaryDirectory() as directory:
site = Path(directory)
(site / "guide").mkdir()
(site / "guide/index.html").write_text("<h1>Guide</h1>")
(site / "asset.css").write_text("body { color: black; }")
for prefix in ("/", "/rea/"):
with self.subTest(prefix=prefix), ThreadingHTTPServer(("127.0.0.1", 0), WEBSITE.preview_handler(site, prefix)) as server:
worker = Thread(target=server.serve_forever, daemon=True)
worker.start()
try:
base = f"http://127.0.0.1:{server.server_port}"
with urlopen(base + prefix + "guide?test=1") as response:
self.assertEqual(response.url, base + prefix + "guide/?test=1")
self.assertEqual(response.read(), b"<h1>Guide</h1>")
with urlopen(base + prefix + "asset.css") as response:
self.assertIn(b"color: black", response.read())
with self.assertRaises(HTTPError) as error:
urlopen(base + prefix + "missing")
self.assertEqual(error.exception.code, 404)
if prefix != "/":
with self.assertRaises(HTTPError) as error:
urlopen(base + "/guide/")
self.assertEqual(error.exception.code, 404)
finally:
server.shutdown()
worker.join()
if __name__ == "__main__":
unittest.main()
+368
View File
@@ -0,0 +1,368 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv2020 from "ajv/dist/2020.js";
import {
TOOL_CONTRACTS,
toolContract,
} from "../dist/contracts/toolContracts.js";
import { exec } from "./lib/verify-package-core.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createAnthropicModelFixture } from "./verify/clients/anthropic-model-fixture.mjs";
assert(
process.platform !== "win32",
"Native Claude Code verifier requires POSIX; Windows is unverified",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const command = process.env.REA_VERIFY_CLAUDE_COMMAND ?? "claude";
const mode = process.argv[2] ?? "call";
assert(
["chat", "call"].includes(mode),
"Usage: verify-claude-client.mjs [chat|call]",
);
const verifier = createVerifierRun();
const lab = await mkdtemp(join(tmpdir(), "rea-claude-client-"));
const profile = join(lab, "selected profile");
const workspace = join(lab, "workspace");
const configPath = join(profile, ".claude.json");
const skillDirectory = join(profile, "skills", "reverse-engineer-anything");
await Promise.all([mkdir(profile), mkdir(workspace)]);
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
const environment = {
...process.env,
CLAUDE_CONFIG_DIR: profile,
DISABLE_AUTOUPDATER: "1",
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
REA_PROCESS_RUN_ID: verifier.run_id,
};
let clientVersion;
try {
clientVersion = (
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
).stdout.trim();
} catch (cause) {
throw new Error(
`Claude Code preflight failed for ${command}; install Claude Code or set REA_VERIFY_CLAUDE_COMMAND. ${String(cause)}`,
);
}
const target = join(lab, "fixture α测试");
await mkdir(target);
await writeFile(
join(target, "app.js"),
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
);
let skillRequested = false;
let skillLoaded = false;
let expectedSkillBody;
let evidenceSeen = false;
let finalSeen = false;
let catalogSize = 0;
let schemasChecked = 0;
let pendingServerWaits = 0;
let artifactDigest;
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
const containsText = (value, text) => {
if (typeof value === "string") return value.includes(text);
if (value && typeof value === "object")
return Object.values(value).some((nested) => containsText(nested, text));
return false;
};
const extractEvidence = (value) => {
if (typeof value === "string") {
try {
// Native Claude appends this resource hint to tool results. Original bodies remain intact in request artifacts.
const json = value.replace(
/\n\n<system-reminder>\n<total_tokens>\d+ tokens left<\/total_tokens>\n<\/system-reminder>$/u,
"",
);
return extractEvidence(JSON.parse(json));
} catch {
return undefined;
}
}
if (value && typeof value === "object") {
if (
typeof value.evidence_id === "string" &&
value.normalized_result?.graph?.nodes?.length > 0
)
return value;
for (const nested of Object.values(value)) {
const evidence = extractEvidence(nested);
if (evidence) return evidence;
}
}
return undefined;
};
const fixture = await createAnthropicModelFixture({
directory: lab,
onRequest: async ({ body, tools, requestIndex }) => {
if (tools.length === 0)
return [{ type: "text", text: "REA fixture title" }];
assert(requestIndex <= 8, "Local model workflow exceeded its turn budget");
const call = (name, input) => [
{ type: "tool_use", id: `toolu_rea_${requestIndex}`, name, input },
];
const analysis = tools.find((t) =>
t.name.endsWith("analyze_javascript_application"),
);
if (!analysis) {
// Native automatic discovery can begin the conversation while MCP is still connecting.
const wait = tools.find((t) => t.name === "WaitForMcpServers");
assert(
wait,
"Claude must expose REA tools or its native pending-server wait",
);
pendingServerWaits++;
return call(wait.name, { servers: ["rea"] });
}
const prefix = analysis.name.slice(
0,
-"analyze_javascript_application".length,
);
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
const reaTools = tools.filter((t) => t.name.startsWith(prefix));
assert.deepEqual(
reaTools.map((t) => t.name).sort(),
expected,
"Claude must forward the complete REA catalog after connection",
);
catalogSize = expected.length;
if (schemasChecked === 0) {
for (const tool of reaTools) {
assert(
tool.input_schema,
`${tool.name}: missing advertised input schema`,
);
assert(
schemaValidator.validateSchema(tool.input_schema),
`${tool.name}: invalid input schema: ${JSON.stringify(schemaValidator.errors)}`,
);
schemasChecked++;
}
}
skillLoaded ||=
expectedSkillBody !== undefined &&
containsText(body.messages, expectedSkillBody) &&
containsText(body.messages, skillDirectory);
const observed = extractEvidence(body.messages);
if (observed) {
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(observed);
assert.equal(evidence.subject.local_path, target);
const shape = evidence.normalized_result.graph.nodes
.flatMap((n) => n.observations)
.map((o) => o.properties)
.find(
(p) =>
p.semantic_role === "export-return-shapes" &&
p.exported_name === "result",
);
assert(shape?.return_shape_coverage?.projection_complete);
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
assert(
fields.some(
(f) =>
f.path === "/clientCompatibility" &&
f.state === "literal" &&
f.value === "REA_CLIENT_FIXTURE",
),
);
assert(
fields.some(
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
),
);
const bytes = JSON.stringify(evidence);
artifactDigest = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(lab, "claude-evidence.json"), bytes);
evidenceSeen = true;
}
if (mode === "chat" || evidenceSeen) {
finalSeen = true;
return [{ type: "text", text: "REA_CLIENT_COMPATIBILITY_OK" }];
}
if (!skillRequested) {
const skill = tools.find((t) => t.name === "Skill");
assert(skill, "Claude must expose its native Skill tool");
skillRequested = true;
return call(skill.name, { skill: "reverse-engineer-anything" });
}
assert(
skillLoaded,
"Claude must load the complete installed personal REA skill",
);
return call(analysis.name, { input_path: target, format: "directory" });
},
});
const original = JSON.stringify({ theme: "light", mcpServers: {} }, null, 2);
await writeFile(configPath, original);
let failure;
try {
for (const args of [
["setup", "--client", "claude_code", "--dry-run"],
["setup", "--client", "claude_code", "--yes"],
["doctor", "--client", "claude_code", "--skill"],
]) {
const result = await exec(
process.execPath,
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
{ env: environment, cwd: workspace, timeout: 60_000 },
);
await writeFile(
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
result.stdout,
);
const parsed = JSON.parse(result.stdout);
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
else {
assert.equal(
parsed.status,
args.includes("--dry-run") ? "planned" : "ready",
);
if (args.includes("--dry-run")) {
assert(parsed.plannedActions.some((a) => a.target === configPath));
assert(parsed.plannedActions.some((a) => a.target === skillDirectory));
assert(
parsed.plannedActions.every(
(a) =>
a.target === configPath || a.target.startsWith(skillDirectory),
),
);
}
}
}
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const configured = await readFile(configPath, "utf8");
assert.equal(JSON.parse(configured).theme, "light");
const repeated = await exec(
process.execPath,
[
join(runtimeRoot, "scripts/rea.mjs"),
"setup",
"--client",
"claude_code",
"--yes",
"--json",
],
{ env: environment, cwd: workspace, timeout: 60_000 },
);
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
assert.equal(await readFile(configPath, "utf8"), configured);
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const installedSkill = await readFile(
join(skillDirectory, "SKILL.md"),
"utf8",
);
assert(installedSkill.includes("name: reverse-engineer-anything"));
const lines = installedSkill.split("\n");
const headerEnd = lines.indexOf("---", 1);
assert(
lines[0] === "---" && headerEnd > 0,
"Installed skill must have its authored frontmatter",
);
expectedSkillBody = lines
.slice(headerEnd + 1)
.join("\n")
.trim();
assert(expectedSkillBody.length > 0);
const execution = exec(
command,
[
"--setting-sources",
"user",
"--print",
"--output-format",
"json",
"--no-session-persistence",
"--model",
"claude-sonnet-4-6",
"--tools",
"default",
"--permission-mode",
"dontAsk",
"--settings",
JSON.stringify({
permissions: {
allow: [
"Skill",
"WaitForMcpServers",
"mcp__rea__analyze_javascript_application",
],
},
}),
mode === "chat"
? "Say hello briefly."
: `Use the REA skill and tools to analyze the JavaScript directory ${target}. Report the result.`,
],
{
env: {
...environment,
ANTHROPIC_API_KEY: "local-fixture-key",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_BASE_URL: fixture.baseUrl,
},
cwd: workspace,
timeout: 120_000,
maxBuffer: 10 * 1024 * 1024,
},
);
execution.child.stdin.end();
const response = await execution;
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
assert.equal(fixture.failure, undefined);
const parsed = JSON.parse(response.stdout);
assert.equal(parsed.is_error, false);
assert.equal(parsed.subtype, "success");
assert.equal(parsed.result, "REA_CLIENT_COMPATIBILITY_OK");
assert.deepEqual(parsed.permission_denials, []);
assert(finalSeen);
if (mode === "call") assert(evidenceSeen && skillLoaded);
} catch (cause) {
failure = cause;
if (typeof cause?.stdout === "string")
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
if (typeof cause?.stderr === "string")
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
} finally {
await fixture.close();
const receipt = {
client: "Claude Code",
clientVersion,
mode,
passed: failure === undefined,
artifacts: lab,
runtimeRoot,
host: {
platform: process.platform,
architecture: process.arch,
node: process.versions.node,
},
modelFixture:
"loopback Anthropic Messages/SSE; synthetic usage and native resource hints; no live API claim",
discovery:
"native CLAUDE_CONFIG_DIR user configuration and skills; separate Git root, user settings only, default built-in tools",
bareMode:
"not used; native bare print mode did not load the personal skill",
catalogSize,
schemasChecked,
pendingServerWaits,
evidenceSeen,
skillLoaded,
artifactDigest,
finalSeen,
requests: fixture.requests,
probes: fixture.probes,
fixtureFailure: fixture.failure,
verifier: await completeVerifierRun(verifier),
};
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
console.log(JSON.stringify(receipt));
}
if (failure !== undefined) throw failure;
+377
View File
@@ -0,0 +1,377 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv2020 from "ajv/dist/2020.js";
import {
TOOL_CONTRACTS,
toolContract,
} from "../dist/contracts/toolContracts.js";
import { exec } from "./lib/verify-package-core.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
assert(
process.platform !== "win32",
"Native OpenCode verifier requires POSIX; Windows is unverified",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const command = process.env.REA_VERIFY_OPENCODE_COMMAND ?? "opencode";
const mode = process.argv[2] ?? "call";
const configFormat = process.env.REA_VERIFY_OPENCODE_CONFIG_FORMAT ?? "jsonc";
assert(
["chat", "call"].includes(mode),
"Usage: verify-opencode-client.mjs [chat|call]",
);
assert(
["json", "jsonc"].includes(configFormat),
"REA_VERIFY_OPENCODE_CONFIG_FORMAT must be json or jsonc",
);
const verifier = createVerifierRun();
const lab = await mkdtemp(join(tmpdir(), "rea-opencode-client-"));
const account = join(lab, "account");
const profile = join(lab, "selected profile");
const workspace = join(lab, "workspace");
const configPath = join(profile, `opencode.${configFormat}`);
const skillDirectory = join(
account,
".agents",
"skills",
"reverse-engineer-anything",
);
await Promise.all([mkdir(account), mkdir(profile), mkdir(workspace)]);
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
const environment = {
USERPROFILE: account,
OPENCODE_CONFIG_DIR: profile,
XDG_CONFIG_HOME: join(lab, "config"),
XDG_CACHE_HOME: join(lab, "cache"),
XDG_DATA_HOME: join(lab, "data"),
XDG_STATE_HOME: join(lab, "state"),
OPENCODE_DISABLE_AUTOUPDATE: "true",
OPENCODE_DISABLE_MODELS_FETCH: "true",
// Use the native core without external account plugins.
OPENCODE_PURE: "true",
REA_PROCESS_RUN_ID: verifier.run_id,
};
let clientVersion;
try {
clientVersion = (
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
).stdout.trim();
} catch (cause) {
throw new Error(
`OpenCode preflight failed for ${command}; install OpenCode or set REA_VERIFY_OPENCODE_COMMAND. ${String(cause)}`,
);
}
const target = join(lab, "fixture α测试");
await mkdir(target);
await writeFile(
join(target, "app.js"),
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
);
let skillRequested = false;
let skillLoaded = false;
let expectedSkillBody;
let evidenceSeen = false;
let finalSeen = false;
let catalogSize = 0;
let schemasChecked = 0;
let artifactDigest;
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
const extractEvidence = (value) => {
if (typeof value === "string") {
try {
return extractEvidence(JSON.parse(value));
} catch {
return undefined;
}
}
if (value && typeof value === "object") {
if (
typeof value.evidence_id === "string" &&
value.normalized_result?.graph?.nodes?.length > 0
)
return value;
for (const nested of Object.values(value)) {
const evidence = extractEvidence(nested);
if (evidence) return evidence;
}
}
return undefined;
};
const fixture = await createOpenAiModelFixture({
directory: lab,
prefix: "opencode",
onRequest: async ({ body, tools, requestIndex }) => {
// Native session-title requests do not execute the analyst's workflow.
if (tools.length === 0)
return { role: "assistant", content: "REA fixture title" };
const analysis = tools.find((t) =>
t.name.endsWith("analyze_javascript_application"),
);
assert(analysis, "OpenCode must expose the REA analysis tool");
const prefix = analysis.name.slice(
0,
-"analyze_javascript_application".length,
);
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
const reaTools = tools.filter((t) => expected.includes(t.name));
assert.deepEqual(
reaTools.map((t) => t.name).sort(),
expected,
"OpenCode must forward the complete REA catalog",
);
catalogSize = expected.length;
if (schemasChecked === 0) {
for (const tool of reaTools) {
assert(
tool.parameters,
`${tool.name}: missing advertised input schema`,
);
assert(
schemaValidator.validateSchema(tool.parameters),
`${tool.name}: invalid input schema: ${JSON.stringify(schemaValidator.errors)}`,
);
schemasChecked++;
}
}
const replies = body.messages.filter((m) => m.role === "tool");
skillLoaded ||= replies.some(
(m) =>
typeof m.content === "string" &&
expectedSkillBody !== undefined &&
m.content.includes(skillDirectory) &&
m.content.includes(expectedSkillBody),
);
const observed = extractEvidence(replies);
if (observed) {
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(observed);
assert.equal(evidence.subject.local_path, target);
const shape = evidence.normalized_result.graph.nodes
.flatMap((n) => n.observations)
.map((o) => o.properties)
.find(
(p) =>
p.semantic_role === "export-return-shapes" &&
p.exported_name === "result",
);
assert(shape?.return_shape_coverage?.projection_complete);
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
assert(
fields.some(
(f) =>
f.path === "/clientCompatibility" &&
f.state === "literal" &&
f.value === "REA_CLIENT_FIXTURE",
),
);
assert(
fields.some(
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
),
);
const bytes = JSON.stringify(evidence);
artifactDigest = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(lab, "opencode-evidence.json"), bytes);
evidenceSeen = true;
}
if (mode === "chat" || evidenceSeen) {
finalSeen = true;
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
}
assert(requestIndex <= 8, "Local model workflow exceeded its turn budget");
const call = (name, args) => ({
role: "assistant",
content: null,
tool_calls: [
{
id: `call_rea_fixture_${requestIndex}`,
type: "function",
function: { name, arguments: JSON.stringify(args) },
},
],
});
if (!skillRequested) {
const skill = tools.find((t) => t.name === "skill");
assert(skill, "OpenCode must expose its native skill tool");
skillRequested = true;
return call(skill.name, { name: "reverse-engineer-anything" });
}
assert(skillLoaded, "OpenCode must load the installed personal REA skill");
return call(analysis.name, { input_path: target, format: "directory" });
},
});
const original =
(configFormat === "jsonc" ? "// Keep caller configuration.\n" : "") +
JSON.stringify(
{
autoupdate: false,
skills: { paths: [join(account, ".agents", "skills")] },
permission: { "*": "allow" },
provider: {
"rea-fixture": {
npm: "@ai-sdk/openai-compatible",
name: "REA local model fixture",
options: { baseURL: fixture.baseUrl, apiKey: "local-fixture-key" },
models: {
"rea-client-fixture": {
name: "REA fixture",
tool_call: true,
limit: { context: 1_000_000, output: 4096 },
},
},
},
},
mcp: {
other: { type: "local", command: ["unrelated-server"], enabled: false },
},
},
null,
2,
);
await writeFile(configPath, original);
let failure;
try {
for (const args of [
["setup", "--client", "opencode", "--dry-run"],
["setup", "--client", "opencode", "--yes"],
["doctor", "--client", "opencode", "--skill"],
]) {
const result = await exec(
process.execPath,
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
{ env: environment, cwd: workspace, timeout: 60_000 },
);
await writeFile(
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
result.stdout,
);
const parsed = JSON.parse(result.stdout);
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
else {
assert.equal(
parsed.status,
args.includes("--dry-run") ? "planned" : "ready",
);
if (args.includes("--dry-run")) {
assert(parsed.plannedActions.some((a) => a.target === configPath));
assert(parsed.plannedActions.some((a) => a.target === skillDirectory));
assert(
parsed.plannedActions.every(
(a) =>
a.target === configPath || a.target.startsWith(skillDirectory),
),
);
}
}
}
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const configured = await readFile(configPath, "utf8");
assert(configured.includes("unrelated-server"));
if (configFormat === "jsonc")
assert(configured.includes("// Keep caller configuration."));
const repeated = await exec(
process.execPath,
[
join(runtimeRoot, "scripts/rea.mjs"),
"setup",
"--client",
"opencode",
"--yes",
"--json",
],
{ env: environment, cwd: workspace, timeout: 60_000 },
);
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
assert.equal(await readFile(configPath, "utf8"), configured);
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const installedSkill = await readFile(
join(skillDirectory, "SKILL.md"),
"utf8",
);
assert(installedSkill.includes("name: reverse-engineer-anything"));
const skillLines = installedSkill.split("\n");
const headerEnd = skillLines.indexOf("---", 1);
assert(
skillLines[0] === "---" && headerEnd > 0,
"Installed skill must have its authored frontmatter",
);
expectedSkillBody = skillLines
.slice(headerEnd + 1)
.join("\n")
.trim();
assert(expectedSkillBody.length > 0);
const execution = exec(
command,
[
"run",
"--format",
"json",
"--model",
"rea-fixture/rea-client-fixture",
mode === "chat"
? "Say hello briefly."
: `Use REA to analyze the JavaScript directory ${target}. Report the result.`,
],
{
env: environment,
cwd: workspace,
timeout: 120_000,
maxBuffer: 10 * 1024 * 1024,
},
);
execution.child.stdin.end();
const response = await execution;
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
assert.equal(fixture.failure, undefined);
assert(finalSeen && response.stdout.includes("REA_CLIENT_COMPATIBILITY_OK"));
if (mode === "call") assert(evidenceSeen && skillLoaded);
} catch (cause) {
failure = cause;
if (typeof cause?.stdout === "string")
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
if (typeof cause?.stderr === "string")
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
} finally {
await fixture.close();
const lineage = await completeVerifierRun(verifier);
const receipt = {
client: "OpenCode",
clientVersion,
mode,
configFormat,
passed: failure === undefined,
artifacts: lab,
runtimeRoot,
host: {
platform: process.platform,
architecture: process.arch,
node: process.versions.node,
},
modelFixture:
"loopback custom OpenAI-compatible provider; one-million-token fixture capacity and synthetic usage; no live provider claim",
skillDiscovery:
"caller-configured isolated shared directory; default OS home discovery unverified",
externalPlugins: "disabled through native OPENCODE_PURE",
catalogSize,
schemasChecked,
evidenceSeen,
skillLoaded,
artifactDigest,
finalSeen,
requests: fixture.requests,
probes: fixture.probes,
fixtureFailure: fixture.failure,
verifier: lineage,
};
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
console.log(JSON.stringify(receipt));
}
if (failure !== undefined) throw failure;
+103 -18
View File
@@ -5,13 +5,79 @@ from pathlib import Path
import re
import struct
import sys
from urllib.parse import quote, unquote, urlsplit
import unicodedata
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
from website import NOTES_FIXTURES, SITE_ORIGIN, authored_markdown, page_url, sync_pages
SITE_ORIGIN = "https://rea.tools"
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
LEGACY_ORIGIN = "morluto.github.io"
SITE_TEXT_URL = re.compile(
r"https?://(?:rea\.tools|morluto\.github\.io)/[^\s<>\"'`]+", re.IGNORECASE
)
def markdown_ids(path):
"""Collect GitHub heading anchors and explicit HTML IDs in local Markdown."""
source = path.read_text(encoding="utf-8")
parser = HtmlReferences()
parser.feed(source)
identifiers = set(parser.ids)
counts = {}
fenced = False
for line in source.splitlines():
if line.lstrip().startswith(("```", "~~~")):
fenced = not fenced
if fenced:
continue
heading = re.match(r"^ {0,3}#{1,6}\s+(.+?)(?:\s+#+)?\s*$", line)
if not heading:
continue
text = re.sub(r"<[^>]*>", "", heading[1]).lower()
slug = "".join(c for c in text if c in "-_ " or unicodedata.category(c)[0] in "LN").replace(" ", "-")
count = counts.get(slug, 0)
identifiers.add(f"{slug}-{count}" if count else slug)
counts[slug] = count + 1
return identifiers
def check_repository_links(root):
"""Check local links in website Markdown and current-repository source links."""
errors = []
sources = {}
for path in authored_markdown(root):
text = re.sub(r"(?ms)^\s*(```|~~~).*?^\s*\1[^\n]*$", "", path.read_text(encoding="utf-8"))
text = re.sub(r"`[^`]*`", "", text)
sources[path] = re.findall(r"\[[^\]\n]*\]\(([^\s)]+)\)", text)
for path in sorted((root / "website/public").rglob("*.html")):
parser = HtmlReferences()
parser.feed(path.read_text(encoding="utf-8"))
sources[path] = parser.references
for path, references in sources.items():
for reference in references:
try:
url = urlsplit(reference)
except ValueError as error:
errors.append(f"{path.relative_to(root)}: invalid URL {reference!r}: {error}")
continue
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
errors.append(f"{path.relative_to(root)}: obsolete website URL: {reference}")
continue
if url.hostname == "github.com":
match = re.match(r"^/morluto/rea/(?:blob|tree)/main/(.+)$", url.path)
if not match:
continue
target = (root / unquote(match[1])).resolve()
elif url.scheme or url.netloc or path.suffix == ".html":
continue
else:
target = (path.parent / unquote(url.path)).resolve() if url.path else path
if not target.is_relative_to(root) or not target.exists():
errors.append(f"{path.relative_to(root)}: missing repository target: {reference}")
elif target.suffix == ".md" and url.fragment and unquote(url.fragment) not in markdown_ids(target):
errors.append(f"{path.relative_to(root)}: missing Markdown fragment: {reference}")
return errors
class HtmlReferences(HTMLParser):
@@ -30,6 +96,8 @@ class HtmlReferences(HTMLParser):
self.canonicals = []
self.lang = None
self.h1_count = 0
self.text = []
self.copy_targets = []
def handle_starttag(self, tag, attrs):
attributes = dict(attrs)
@@ -67,6 +135,8 @@ class HtmlReferences(HTMLParser):
reference = attributes.get(attribute)
if reference is not None:
self.references.append(reference)
if attributes.get("data-copy"):
self.copy_targets.append(attributes["data-copy"])
def handle_endtag(self, tag):
if tag == "head":
@@ -75,6 +145,7 @@ class HtmlReferences(HTMLParser):
self.in_title = False
def handle_data(self, data):
self.text.append(data)
if self.in_title:
self.titles[-1] += data
@@ -104,15 +175,7 @@ def check_search_metadata(site, pages):
)
if page.noindex():
continue
route = (
path.relative_to(site).parent.as_posix()
if path.name == "index.html"
else label
)
route = "" if route == "." else route
url = f"{SITE_ORIGIN}/{quote(route, safe='/')}" + (
"/" if route and path.name == "index.html" else ""
)
url = page_url(site, path)
indexable.add(url)
if page.canonicals != [url]:
errors.append(f"{label}: needs exactly one head canonical for {url}")
@@ -233,15 +296,32 @@ def check_site(site):
label = path.relative_to(site)
for identifier in page.duplicate_ids:
errors.append(f"{label}: duplicate ID {identifier!r}")
for reference in page.references:
url = urlsplit(reference)
if url.scheme or url.netloc:
for identifier in page.copy_targets:
if identifier not in page.ids:
errors.append(f"{label}: missing copy target: {identifier}")
text_urls = [match.group().rstrip(".,;)") for match in SITE_TEXT_URL.finditer(" ".join(page.text))]
for reference in page.references + text_urls:
try:
url = urlsplit(reference)
except ValueError as error:
errors.append(f"{label}: invalid URL {reference!r}: {error}")
continue
references += 1
if url.path.startswith("/"):
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
errors.append(f"{label}: obsolete website URL: {reference}; use rea.tools")
continue
if url.hostname == "rea.tools":
if url.scheme != "https" or url.netloc != "rea.tools":
errors.append(f"{label}: website URL must use {SITE_ORIGIN}: {reference}")
continue
target = (site / unquote(url.path).lstrip("/")).resolve()
elif url.scheme or url.netloc:
continue
elif url.path.startswith("/"):
errors.append(f"{label}: use a relative site reference: {reference}")
continue
target = (path.parent / unquote(url.path)).resolve() if url.path else path
else:
target = (path.parent / unquote(url.path)).resolve() if url.path else path
references += 1
if not target.is_relative_to(site):
errors.append(f"{label}: reference leaves the website: {reference}")
continue
@@ -323,6 +403,11 @@ def main():
pages, references, indexable, errors = check_site(root / "website/public")
errors.extend(check_example_archive(root / "website/public"))
errors.extend(check_publisher(root))
errors.extend(check_repository_links(root))
try:
sync_pages(root, check=True)
except (OSError, ValueError, KeyError) as error:
errors.append(str(error))
if errors:
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
return 1
@@ -0,0 +1,158 @@
import assert from "node:assert/strict";
import { createServer } from "node:http";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
/** Exercise native Anthropic adapters locally, without live-provider claims. */
export const createAnthropicModelFixture = async ({ directory, onRequest }) => {
const requests = [];
const probes = [];
let incoming = 0;
let failure;
const server = createServer(async (request, response) => {
try {
const chunks = [];
let bytes = 0;
for await (const chunk of request) {
bytes += chunk.length;
assert(
bytes <= 10 * 1024 * 1024,
"Model fixture request exceeds 10 MiB budget",
);
chunks.push(chunk);
}
const raw = Buffer.concat(chunks).toString("utf8");
const body = raw.length === 0 ? null : JSON.parse(raw);
const path = new URL(request.url, "http://localhost").pathname;
// Retain producer bodies and URLs, without transport authentication headers.
await writeFile(
join(directory, `claude-incoming-${++incoming}.json`),
JSON.stringify({ method: request.method, path: request.url, body }),
);
if (path === "/v1/messages/count_tokens") {
probes.push({ path, kind: "synthetic-token-count" });
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify({ input_tokens: 100 }));
return;
}
if (path !== "/v1/messages") {
probes.push({ path, status: 404 });
response.statusCode = 404;
response.end(
JSON.stringify({
error: { message: "Unsupported fixture endpoint" },
}),
);
return;
}
assert.equal(request.method, "POST");
assert(body && typeof body === "object" && !Array.isArray(body));
assert(Array.isArray(body.messages));
const tools = body.tools ?? [];
requests.push({
path: request.url,
stream: body.stream,
tools: tools.map((t) => t.name),
messageRoles: body.messages.map((m) => m.role),
});
const content = await onRequest({
body,
tools,
requestIndex: requests.length,
});
const stopReason = content.some((c) => c.type === "tool_use")
? "tool_use"
: "end_turn";
const usage = {
input_tokens: 100,
output_tokens: 20,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
};
const result = {
id: `msg_rea_${requests.length}`,
type: "message",
role: "assistant",
model: body.model,
content,
stop_reason: stopReason,
stop_sequence: null,
usage,
};
if (body.stream) {
response.setHeader("Content-Type", "text/event-stream");
const emit = (event, data) =>
response.write(
`event: ${event}\ndata: ${JSON.stringify({ type: event, ...data })}\n\n`,
);
emit("message_start", {
message: {
...result,
content: [],
stop_reason: null,
usage: { ...usage, output_tokens: 0 },
},
});
for (const [index, block] of content.entries()) {
emit("content_block_start", {
index,
content_block:
block.type === "tool_use"
? { ...block, input: {} }
: { type: "text", text: "" },
});
if (block.type === "tool_use") {
const json = JSON.stringify(block.input);
const split = Math.floor(json.length / 2);
for (const partialJson of [
json.slice(0, split),
json.slice(split),
]) {
emit("content_block_delta", {
index,
delta: { type: "input_json_delta", partial_json: partialJson },
});
}
} else {
emit("content_block_delta", {
index,
delta: { type: "text_delta", text: block.text },
});
}
emit("content_block_stop", { index });
}
emit("message_delta", {
delta: { stop_reason: stopReason, stop_sequence: null },
usage: { output_tokens: 20 },
});
emit("message_stop", {});
response.end();
} else {
response.setHeader("Content-Type", "application/json");
response.end(JSON.stringify(result));
}
} catch (cause) {
failure = String(cause);
response.statusCode = 400;
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
type: "error",
error: { type: "invalid_request_error", message: failure },
}),
);
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
assert(address !== null && typeof address !== "string");
return {
baseUrl: `http://127.0.0.1:${address.port}`,
requests,
probes,
get failure() {
return failure;
},
close: () => new Promise((resolve) => server.close(resolve)),
};
};
+246
View File
@@ -0,0 +1,246 @@
"""Maintain shared HTML and run the static website's contributor commands."""
import argparse
from functools import partial
from html import escape
from html.parser import HTMLParser
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path, PurePosixPath
from string import Template
import subprocess
import sys
from textwrap import indent
from urllib.parse import quote, urlsplit
SITE_ORIGIN = "https://rea.tools"
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
NAVIGATION = (
("Showcases", "showcase/"),
("Blog", "blog/"),
("Get started", "get-started/"),
("Guides", "guides/"),
("FAQ", "faq/"),
)
def authored_markdown(root):
"""Discover website documentation while excluding local environments and vendors."""
site = root / "website"
return [
path for path in sorted(site.rglob("*.md"))
if not any(part.startswith(".") or part in ("node_modules", "__pycache__")
for part in path.relative_to(site).parts)
]
class PageMetadata(HTMLParser):
"""Read the authored title, description and indexing policy from the head."""
def __init__(self):
super().__init__()
self.in_head = False
self.in_title = False
self.titles = []
self.descriptions = []
self.noindex = False
def handle_starttag(self, tag, attrs):
if tag == "head":
self.in_head = True
if not self.in_head:
return
if tag == "title":
self.in_title = True
self.titles.append("")
if tag == "meta":
attributes = dict(attrs)
name = (attributes.get("name") or "").lower()
content = attributes.get("content") or ""
if name == "description":
self.descriptions.append(content)
if name in ("robots", "googlebot"):
self.noindex |= bool(
set(content.lower().replace(",", " ").split()) & {"noindex", "none"}
)
def handle_endtag(self, tag):
if tag == "head":
self.in_head = False
if tag == "title":
self.in_title = False
def handle_data(self, data):
if self.in_title:
self.titles[-1] += data
def page_route(site, path):
"""Map an HTML file to its public route without a separate page registry."""
route = path.relative_to(site).as_posix()
return route.removesuffix("index.html") if path.name == "index.html" else route
def page_url(site, path):
"""Return the canonical public URL of an authored HTML file."""
return f"{SITE_ORIGIN}/{quote(page_route(site, path), safe='/')}"
def shared_blocks(root, path, source):
"""Render shared regions while keeping the page's body and metadata authored."""
site = root / "website/public"
metadata = PageMetadata()
metadata.feed(source)
for name, values in (("title", metadata.titles), ("description", metadata.descriptions)):
if len(values) != 1 or not values[0].strip():
raise ValueError(f"{path.relative_to(site)}: needs one nonempty head {name}")
depth = len(path.relative_to(site).parent.parts)
base = "../" * depth or "./"
route = page_route(site, path)
links = []
for label, destination in NAVIGATION:
attributes = ' class="nav-start"' if destination == "get-started/" else ""
if route.startswith(destination):
attributes += ' aria-current="page"'
links.append(f' <a href="{base}{destination}"{attributes}>{label}</a>')
values = {
"base": base,
"navigation": "\n" + "\n".join(links) + "\n ",
"canonical": escape(page_url(site, path), quote=True),
"origin": SITE_ORIGIN,
"title": escape(metadata.titles[0].strip(), quote=True),
"description": escape(metadata.descriptions[0].strip(), quote=True),
}
templates = root / "website/templates"
return {
name: indent(Template((templates / f"{name}.html.tmpl").read_text(encoding="utf-8")).substitute(values), " ")
for name in ("sharing", "header", "footer")
}
def sync_pages(root, check=False):
"""Update shared regions, or reject drift without changing any authored file."""
site = root / "website/public"
changes = []
for path in sorted(site.rglob("*.html")):
if path.relative_to(site).as_posix() in NOTES_FIXTURES:
continue
original = path.read_text(encoding="utf-8")
source = original
for name, block in shared_blocks(root, path, source).items():
start = f" <!-- website:{name}:start -->\n"
end = f" <!-- website:{name}:end -->"
if source.count(start) != 1 or source.count(end) != 1:
raise ValueError(f"{path.relative_to(site)}: needs one {name} region; use new-page for a new reading page")
before, _, remaining = source.partition(start)
_, found, after = remaining.partition(end)
if not found:
raise ValueError(f"{path.relative_to(site)}: {name} region is out of order")
source = before + start + block + end + after
if source != original:
changes.append((path, source))
if check and changes:
names = ", ".join(str(path.relative_to(site)) for path, _ in changes)
raise ValueError(f"Shared HTML is stale: {names}. Run python3 scripts/website.py sync and commit the updated pages.")
if not check:
for path, source in changes:
path.write_text(source, encoding="utf-8")
print(f"{'Checked' if check else 'Synced'} shared HTML ({len(changes)} pages changed).")
return [path for path, _ in changes]
def new_page(root, route, title, description):
"""Create a draft reading page with correct depth, metadata and shared regions."""
parts = PurePosixPath(route)
if not route or parts.is_absolute() or ".." in parts.parts or "\\" in route or parts.suffix:
raise ValueError("Use a relative directory route, such as blog/my-article.")
site = root / "website/public"
path = site.joinpath(*parts.parts, "index.html")
if not path.resolve().is_relative_to(site.resolve()):
raise ValueError("The new page must stay inside website/public.")
if path.exists():
raise ValueError(f"Page already exists: {path.relative_to(site)}")
base = "../" * len(parts.parts) or "./"
source = Template((root / "website/templates/page.html").read_text(encoding="utf-8")).substitute(
title=escape(title, quote=True), description=escape(description, quote=True), base=base,
)
# Render before writing, so invalid input cannot leave an incomplete page.
blocks = shared_blocks(root, path, source)
for name, block in blocks.items():
source = source.replace(f" <!-- website:{name}:start -->\n", f" <!-- website:{name}:start -->\n{block}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(source, encoding="utf-8")
print(f"Created {path.relative_to(root)}. Write the page, add its index link, then remove the Draft label and noindex directive when ready.")
return path
def run_script(root, name, arguments=()):
"""Use the active Python environment for the existing preparation and checks."""
subprocess.run([sys.executable, str(root / "scripts" / name), *arguments], cwd=root, check=True)
def preview_handler(site, prefix):
"""Serve one static directory at root or a Pages prefix, preserving redirects."""
class Handler(SimpleHTTPRequestHandler):
def send_head(self):
if not urlsplit(self.path).path.startswith(prefix):
self.send_error(404)
return None
return super().send_head()
def translate_path(self, path):
return super().translate_path("/" + path[len(prefix):])
return partial(Handler, directory=str(site))
def main():
"""Expose one small command for synchronizing, checking and previewing the site."""
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
sync = commands.add_parser("sync", help="Update shared HTML after editing templates or page metadata")
sync.add_argument("--check", action="store_true", help="Report drift without rewriting pages")
commands.add_parser("check", help="Check shared HTML, prepare assets, verify links and run regression tests")
links = commands.add_parser("links", help="Audit external targets using gh and HTTP; separate from offline checks")
links.add_argument("--report", help="Write the full external-link JSON report")
create = commands.add_parser("new-page", help="Create a draft reading page")
create.add_argument("route")
create.add_argument("--title", required=True)
create.add_argument("--description", required=True)
serve = commands.add_parser("serve", help="Prepare and preview the static site")
serve.add_argument("--port", type=int, default=4173)
serve.add_argument("--base-path", choices=("/", "/rea/"), default="/")
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
try:
if args.command == "new-page":
new_page(root, args.route, args.title, args.description)
elif args.command == "sync":
sync_pages(root, check=args.check)
elif args.command == "check":
sync_pages(root, check=True)
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
run_script(root, name)
elif args.command == "links":
run_script(root, "audit-website-links.py", ["--report", args.report] if args.report else [])
else:
sync_pages(root)
run_script(root, "prepare-website.py")
prefix = args.base_path
handler = preview_handler(root / "website/public", prefix)
with ThreadingHTTPServer(("127.0.0.1", args.port), handler) as server:
print(f"Preview: http://127.0.0.1:{server.server_port}{prefix}", flush=True)
server.serve_forever()
except (OSError, ValueError, KeyError) as error:
print(str(error), file=sys.stderr)
return 1
except subprocess.CalledProcessError as error:
return error.returncode
except KeyboardInterrupt:
return 0
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
import { resolveJavaScriptSourceMapReference } from "../../domain/javascript/javascriptSourceMapPaths.js";
import { analyzeParsedJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js";
import {
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptOpenReceivers,
@@ -190,7 +190,7 @@ function* analyzeArtifactFileSteps<
}
const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(parsed);
yield;
const analysis = analyzeParsedJavaScriptStaticSource(
const analysis = yield* analyzeParsedJavaScriptStaticSourceSteps(
file.text.value,
parsed,
openReceiverFacts,
+7 -2
View File
@@ -22,7 +22,11 @@ export class DirectoryArtifactReader implements ArtifactReader {
this.#rootPromise = realpath(root);
}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
/** Enumerate entries, optionally restricting descent while retaining directories. */
async *entries(
signal?: AbortSignal,
descendInto?: (path: string) => boolean,
): AsyncIterable<ArtifactEntry> {
const root = await this.#rootPromise.catch((cause: unknown) => {
throw directoryIoFailure("resolve artifact root", this.#rootPath, cause);
});
@@ -66,7 +70,8 @@ export class DirectoryArtifactReader implements ArtifactReader {
? "file"
: undefined;
if (kind === undefined) continue;
if (kind === "directory") directories.push(absolute);
if (kind === "directory" && (descendInto?.(path) ?? true))
directories.push(absolute);
yield {
path,
kind,
+13 -8
View File
@@ -148,8 +148,20 @@ export const inspectBundleKeyedArchive = async (input: {
// Match inventory spelling while retaining the raw path for I/O and evidence.
const wanted = selected.path.normalize("NFC");
const matches: ArtifactEntry[] = [];
for await (const entry of reader.entries(input.signal)) {
for await (const entry of reader.entries(input.signal, (directory) =>
wanted.startsWith(`${directory.normalize("NFC")}/`),
)) {
if (entry.path.normalize("NFC") !== wanted) continue;
if (entry.kind !== "file")
throw archivePathError(
"invalid_value",
`Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`,
);
if ((entry.declaredSize ?? 0) > MAX_BYTES)
throw new ArtifactReaderFailure(
"limit",
"Keyed archive exceeds 64 MiB",
);
matches.push(entry);
}
if (matches.length > 1)
@@ -163,13 +175,6 @@ export const inspectBundleKeyedArchive = async (input: {
"invalid_value",
`No regular file exists at ${selected.path} in the active app bundle.`,
);
if (entry.kind !== "file")
throw archivePathError(
"invalid_value",
`Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`,
);
if ((entry.declaredSize ?? 0) > MAX_BYTES)
throw new ArtifactReaderFailure("limit", "Keyed archive exceeds 64 MiB");
const stream = await reader.open(entry, input.signal);
const chunks: Buffer[] = [];
let size = 0;
@@ -33,7 +33,7 @@ import {
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { collectSemanticMemberMutations } from "./javascriptSemanticMemberMutations.js";
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
import {
collectSemanticReturns,
resolveSemanticModuleCallables,
@@ -126,8 +126,7 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
const state = createState(file.program);
collectDefinitions(file.program, state);
yield;
collectSemanticMemberMutations(file.program, state);
yield;
yield* collectSemanticMemberMutationsSteps(file.program, state);
traverseJavaScriptAst(file.program, {
enter: (node) => collectSemanticModuleLink(node, state),
});
@@ -5,6 +5,10 @@ import {
analyzeParsedJavaScriptSemanticsSteps,
} from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import {
traverseJavaScriptAst,
traverseJavaScriptAstSteps,
} from "./javascriptSemanticTraversal.js";
const source = `
import { readFile } from "node:fs";
@@ -32,3 +36,26 @@ it("yields between semantic phases and returns the synchronous result (#1462)",
if (reparsed === null) throw new Error("Expected parsed source");
expect(step.value).toEqual(analyzeParsedJavaScriptSemantics(reparsed));
});
it("traverses a large tree in steps with the synchronous visit order (#1462)", () => {
const parsed = parseJavaScriptSource(
Array.from(
{ length: 400 },
(_, index) => `const v${index} = [${index}];`,
).join("\n"),
"large.js",
);
if (parsed === null) throw new Error("Expected parsed source");
const visits = (record: string[]) => ({
enter: (node: { readonly type: string }) => record.push(`+${node.type}`),
exit: (node: { readonly type: string }) => record.push(`-${node.type}`),
});
const synchronous: string[] = [];
traverseJavaScriptAst(parsed.program, visits(synchronous));
const stepped: string[] = [];
const steps = traverseJavaScriptAstSteps(parsed.program, visits(stepped));
let yields = 0;
while (steps.next().done !== true) yields += 1;
expect(yields).toBeGreaterThan(0);
expect(stepped).toEqual(synchronous);
});
@@ -3,7 +3,7 @@ import type * as t from "@babel/types";
import { collectJavaScriptSemanticAsyncEffects } from "./javascriptSemanticAsyncEffects.js";
import { collectJavaScriptSemanticCalls } from "./javascriptSemanticCalls.js";
import { collectJavaScriptSemanticChildProcesses } from "./javascriptSemanticChildProcesses.js";
import { collectJavaScriptSemanticFingerprints } from "./javascriptSemanticFingerprints.js";
import { collectJavaScriptSemanticFingerprintsSteps } from "./javascriptSemanticFingerprints.js";
import { collectJavaScriptSemanticDataEffects } from "./javascriptSemanticDataEffects.js";
import type {
JavaScriptSemanticCallable,
@@ -84,19 +84,20 @@ export function* collectJavaScriptDerivedSemanticsSteps(
callables,
);
yield;
const functionFingerprints = collectJavaScriptSemanticFingerprints({
state,
callables,
calls,
promises: promiseOperations,
events: asyncEffects.eventOperations,
timers: asyncEffects.timerOperations,
childProcessSpawns: childProcesses.childProcessSpawns,
childProcessInteractions: childProcesses.childProcessInteractions,
requestOperations: dataEffects.requestOperations,
resourceOperations,
parserPartial,
});
const functionFingerprints =
yield* collectJavaScriptSemanticFingerprintsSteps({
state,
callables,
calls,
promises: promiseOperations,
events: asyncEffects.eventOperations,
timers: asyncEffects.timerOperations,
childProcessSpawns: childProcesses.childProcessSpawns,
childProcessInteractions: childProcesses.childProcessInteractions,
requestOperations: dataEffects.requestOperations,
resourceOperations,
parserPartial,
});
return {
...calls,
promiseOperations,
@@ -17,7 +17,7 @@ import type {
} from "./javascriptSemanticIr.js";
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
interface SemanticFingerprintCollectionInput {
readonly state: JavaScriptSemanticAnalysisState;
@@ -33,10 +33,10 @@ interface SemanticFingerprintCollectionInput {
readonly parserPartial: boolean;
}
/** Build bounded rename- and formatting-resistant components per callable. */
export const collectJavaScriptSemanticFingerprints = (
/** Build bounded rename- and formatting-resistant components per callable, yielding between callables. */
export function* collectJavaScriptSemanticFingerprintsSteps(
input: SemanticFingerprintCollectionInput,
): JavaScriptSemanticFunctionFingerprint[] => {
): Generator<void, JavaScriptSemanticFunctionFingerprint[]> {
const {
state,
callables,
@@ -67,11 +67,13 @@ export const collectJavaScriptSemanticFingerprints = (
);
const networkEffectsByOwner = groupByOwner(requestOperations, ownerId);
const resourceEffectsByOwner = groupByOwner(resourceOperations, ownerId);
return callables.flatMap((callable) => {
if (callable.kind === "class") return [];
const fingerprints: JavaScriptSemanticFunctionFingerprint[] = [];
for (const callable of callables) {
if (callable.kind === "class") continue;
const node = state.callableNodesById.get(callable.callableId);
if (node === undefined) return [];
const syntax = fingerprintSyntax(node, callable.callableId);
if (node === undefined) continue;
yield;
const syntax = yield* fingerprintSyntaxSteps(node, callable.callableId);
const promiseEffects = promiseEffectsByOwner.get(callable.callableId) ?? [];
const eventEffects = eventEffectsByOwner.get(callable.callableId) ?? [];
const timerEffects = timerEffectsByOwner.get(callable.callableId) ?? [];
@@ -80,50 +82,49 @@ export const collectJavaScriptSemanticFingerprints = (
const networkEffects = networkEffectsByOwner.get(callable.callableId) ?? [];
const resourceEffects =
resourceEffectsByOwner.get(callable.callableId) ?? [];
return [
{
callableId: callable.callableId,
status:
parserPartial || callable.returnCoverage.status !== "complete"
? "partial"
: "complete",
components: {
parameterArity: callableParameterArity(node),
normalizedAstSha256: digest(syntax.normalizedTokens),
controlFlowSha256: digest(syntax.controlFlowTokens),
relationShapeSha256: digest(
relationShape(
callable,
callSitesByCaller.get(callable.callableId) ?? [],
captureCountByCallable.get(callable.callableId) ?? 0,
promiseEffects,
),
fingerprints.push({
callableId: callable.callableId,
status:
parserPartial || callable.returnCoverage.status !== "complete"
? "partial"
: "complete",
components: {
parameterArity: callableParameterArity(node),
normalizedAstSha256: digest(syntax.normalizedTokens),
controlFlowSha256: digest(syntax.controlFlowTokens),
relationShapeSha256: digest(
relationShape(
callable,
callSitesByCaller.get(callable.callableId) ?? [],
captureCountByCallable.get(callable.callableId) ?? 0,
promiseEffects,
),
literalSetSha256: digest(
[...new Set(syntax.literals)].sort(compareUnicodeCodePoints),
),
effects: [
...(isAsyncCallable(node) ? (["async"] as const) : []),
...(childProcessEffects.length > 0
? (["child-process"] as const)
: []),
...(eventEffects.length > 0 ? (["event"] as const) : []),
...(networkEffects.length > 0 ? (["network"] as const) : []),
...(promiseEffects.length > 0 ? (["promise"] as const) : []),
...(resourceEffects.length > 0 ? (["resource"] as const) : []),
...(timerEffects.length > 0 ? (["timer"] as const) : []),
],
},
limitations: [
"The fingerprint is a static candidate and does not prove behavioral equivalence.",
...(parserPartial
? ["Incomplete semantic recovery makes this fingerprint partial."]
),
literalSetSha256: digest(
[...new Set(syntax.literals)].sort(compareUnicodeCodePoints),
),
effects: [
...(isAsyncCallable(node) ? (["async"] as const) : []),
...(childProcessEffects.length > 0
? (["child-process"] as const)
: []),
...(eventEffects.length > 0 ? (["event"] as const) : []),
...(networkEffects.length > 0 ? (["network"] as const) : []),
...(promiseEffects.length > 0 ? (["promise"] as const) : []),
...(resourceEffects.length > 0 ? (["resource"] as const) : []),
...(timerEffects.length > 0 ? (["timer"] as const) : []),
],
},
];
});
};
limitations: [
"The fingerprint is a static candidate and does not prove behavioral equivalence.",
...(parserPartial
? ["Incomplete semantic recovery makes this fingerprint partial."]
: []),
],
});
}
return fingerprints;
}
const ownerId = <T extends { readonly ownerCallableId: string | null }>(
value: T,
@@ -161,15 +162,15 @@ interface FingerprintSyntax {
readonly literals: readonly string[];
}
const fingerprintSyntax = (
function* fingerprintSyntaxSteps(
root: t.Node,
targetCallableId: string,
): FingerprintSyntax => {
): Generator<void, FingerprintSyntax> {
const normalizedTokens: string[] = [];
const controlFlowTokens: string[] = [];
const literals: string[] = [];
const callableStack: string[] = [];
traverseJavaScriptAst(root, {
yield* traverseJavaScriptAstSteps(root, {
enter: (node, parent) => {
const callableId = semanticCallableIdForNode(node);
if (callableId !== null) callableStack.push(callableId);
@@ -187,7 +188,7 @@ const fingerprintSyntax = (
},
});
return { normalizedTokens, controlFlowTokens, literals };
};
}
const normalizedNodeToken = (node: t.Node, parent: t.Node | null): string => {
if (t.isIdentifier(node))
@@ -25,7 +25,7 @@ import {
type JavaScriptSemanticPropertyPath,
} from "./javascriptSemanticPropertyPaths.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
@@ -34,13 +34,13 @@ import {
type PropertyPath = JavaScriptSemanticPropertyPath;
type ValueEffect = "write" | "escape";
/** Preserve uncertainty from property writes and references exposed to calls. */
export const collectSemanticMemberMutations = (
/** Preserve uncertainty from property writes and references exposed to calls, yielding during traversal and fixpoint iteration. */
export function* collectSemanticMemberMutationsSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): void => {
): Generator<void, void> {
const parents = state.parentsByNode;
traverseJavaScriptAst(program, {
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent) => {
if (parent !== null) parents.set(node, parent);
},
@@ -382,7 +382,7 @@ export const collectSemanticMemberMutations = (
);
}
};
traverseJavaScriptAst(program, {
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent) => {
for (const source of semanticIterationSources(node, parent))
deferIterable({
@@ -435,7 +435,8 @@ export const collectSemanticMemberMutations = (
const effectsBefore = recordedEffects.size;
const iterablesBefore = iterableReferences.length;
const iterationBefore: boolean = arrayIterationUnknown;
for (const reference of pendingReferences.splice(0)) {
for (const [index, reference] of pendingReferences.splice(0).entries()) {
if (index % 64 === 0) yield;
const { initializer } = reference;
if (referenceSourcesInactive(initializer, state))
pendingReferences.push(reference);
@@ -480,7 +481,8 @@ export const collectSemanticMemberMutations = (
reference.originAt,
);
}
for (const reference of [...iterableReferences]) {
for (const [index, reference] of [...iterableReferences].entries()) {
if (index % 64 === 0) yield;
if (referenceSourcesInactive(reference, state)) continue;
if (reference.iterationOnly) {
const expanded = expandedIterationReferences(reference, state);
@@ -518,7 +520,7 @@ export const collectSemanticMemberMutations = (
// Gathering effects can evaluate a primitive projection before its object
// escapes. Final values must use the completed mutation state.
clearSemanticPrimitiveBindingValues(state);
};
}
interface ReferencedValue {
readonly node: t.Node;
@@ -12,15 +12,28 @@ import * as t from "@babel/types";
*/
export const traverseJavaScriptAst = (
root: t.Node,
visitor: {
readonly enter: (
node: t.Node,
parent: t.Node | null,
readAncestors: () => readonly t.Node[],
) => void;
readonly exit?: (node: t.Node, parent: t.Node | null) => void;
},
): void => {
visitor: JavaScriptAstVisitor,
): void => completeSemanticSteps(traverseJavaScriptAstSteps(root, visitor));
/** Callbacks for one deterministic AST traversal. */
export interface JavaScriptAstVisitor {
readonly enter: (
node: t.Node,
parent: t.Node | null,
readAncestors: () => readonly t.Node[],
) => void;
readonly exit?: (node: t.Node, parent: t.Node | null) => void;
}
/**
* {@link traverseJavaScriptAst} with a yield every 1,024 visited nodes, so a
* caller can serve control messages during one large traversal. Visit order
* and callbacks are identical.
*/
export function* traverseJavaScriptAstSteps(
root: t.Node,
visitor: JavaScriptAstVisitor,
): Generator<void, void> {
const ancestors: t.Node[] = [];
const enter = (node: t.Node, parent: t.Node | null): TraversalFrame => {
visitor.enter(node, parent, () => [...ancestors]);
@@ -28,12 +41,15 @@ export const traverseJavaScriptAst = (
return { node, parent, children: childNodes(node), nextIndex: 0 };
};
const pending = [enter(root, null)];
let visited = 1;
while (pending.length > 0) {
const current = pending.at(-1);
if (current === undefined) break;
const child = current.children[current.nextIndex];
if (child !== undefined) {
current.nextIndex += 1;
if (visited % 1024 === 0) yield;
visited += 1;
pending.push(enter(child, current.node));
} else {
visitor.exit?.(current.node, current.parent);
@@ -41,6 +57,16 @@ export const traverseJavaScriptAst = (
pending.pop();
}
}
}
/** Run cooperative analysis steps to completion without yielding control. */
export const completeSemanticSteps = <Value>(
steps: Iterator<void, Value>,
): Value => {
for (;;) {
const step = steps.next();
if (step.done === true) return step.value;
}
};
interface TraversalFrame {
@@ -1,6 +1,9 @@
import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import {
completeSemanticSteps,
traverseJavaScriptAstSteps,
} from "./javascriptSemanticTraversal.js";
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
import {
@@ -57,9 +60,19 @@ export const analyzeParsedJavaScriptStaticSource = (
source: string,
file: ParsedJavaScriptSource,
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
): JavaScriptStaticAnalysis => {
): JavaScriptStaticAnalysis =>
completeSemanticSteps(
analyzeParsedJavaScriptStaticSourceSteps(source, file, openReceiverFacts),
);
/** Static analysis that yields during its source traversal; results are unchanged. */
export function* analyzeParsedJavaScriptStaticSourceSteps(
source: string,
file: ParsedJavaScriptSource,
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
): Generator<void, JavaScriptStaticAnalysis> {
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
traverseStaticSource(file, {
yield* traverseStaticSourceSteps(file, {
source,
accumulator,
...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
@@ -67,20 +80,20 @@ export const analyzeParsedJavaScriptStaticSource = (
});
addSourceMapDirectives(source, file.comments ?? [], accumulator);
return finalizeStaticAnalysis(source, file, accumulator);
};
}
const traverseStaticSource = (
function* traverseStaticSourceSteps(
file: ParsedJavaScriptSource,
findings: JavaScriptFindingContext,
): void => {
traverseJavaScriptAst(file, {
): Generator<void, void> {
yield* traverseJavaScriptAstSteps(file, {
enter: (node) => {
findings.accumulator.visitedNodes += 1;
inspectNode(node, findings);
return undefined;
},
});
};
}
const finalizeStaticAnalysis = (
source: string,
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { chmod, mkdir, readdir, truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { build } from "plist";
@@ -67,6 +67,82 @@ it.each([
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"rejects a selected unreadable directory before descending into it",
async () => {
const root = await bundle();
const selected = join(root, "Contents", "Resources");
await chmod(selected, 0);
try {
await expect(readdir(selected)).rejects.toMatchObject({ code: "EACCES" });
await expect(
inspect(root, { path: "Contents/Resources" }),
).rejects.toMatchObject({
issues: [
{
path: ["path"],
reason: "invalid_value",
message: expect.stringContaining("selects a directory"),
},
],
});
} finally {
await chmod(selected, 0o700);
}
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"inspects an active standalone archive without opening unrelated directories",
async () => {
const root = await createTestTempDirectory("rea-keyed-standalone-scope-");
const path = join(root, "Model.plist");
const bytes = Buffer.from(archive);
const sha256 = createHash("sha256").update(bytes).digest("hex");
await writeFile(path, bytes);
const blocked = join(root, "unrelated");
await mkdir(blocked);
await chmod(blocked, 0);
try {
await expect(readdir(blocked)).rejects.toMatchObject({ code: "EACCES" });
const result = await new ArtifactProvider(process.env, "darwin")
.createClient({ path, sha256, kind: "artifact", format: "plist" })
.execute("inspect_keyed_archive", {});
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({
archive_path: "Model.plist",
archive_sha256: sha256,
});
} finally {
await chmod(blocked, 0o700);
}
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"reports the selected oversized archive limit despite an unreadable sibling",
async () => {
const root = await createTestTempDirectory("rea-keyed-size-scope-");
const path = join(root, "Large.plist");
await writeFile(path, archive);
await truncate(path, 64 * 1024 * 1024 + 1);
const blocked = join(root, "unrelated");
await mkdir(blocked);
await chmod(blocked, 0);
try {
await expect(readdir(blocked)).rejects.toMatchObject({ code: "EACCES" });
await expect(
inspect(root, { path: "Large.plist" }),
).rejects.toMatchObject({
reason: "limit",
message: "Keyed archive exceeds 64 MiB",
});
} finally {
await chmod(blocked, 0o700);
}
},
);
it("reports an absent named root with the archive's available roots", async () => {
const root = await bundle();
await expect(
@@ -184,6 +260,38 @@ it("selects a decomposed bundle path by its normalized spelling and reports the
);
});
it("selects normalized intermediate directories while preserving their raw spelling", async () => {
const root = await createTestTempDirectory("rea-keyed-unicode-ancestors-");
const directory = "Cafe\u0301/Neste\u0301";
const rawPath = `${directory}/Model.plist`;
await mkdir(join(root, directory), { recursive: true });
await writeFile(join(root, rawPath), archive);
const result = await inspect(root, { path: rawPath.normalize("NFC") });
expect(result.archive_path).toBe(rawPath);
});
it.skipIf(process.platform === "darwin" || process.platform === "win32")(
"rejects equivalent archive paths beneath distinct Unicode-equivalent directories",
async () => {
const root = await createTestTempDirectory("rea-keyed-unicode-branches-");
for (const directory of ["Caf\u00e9", "Cafe\u0301"]) {
await mkdir(join(root, directory));
await writeFile(join(root, directory, "Model.plist"), archive);
}
await expect(
inspect(root, { path: "Caf\u00e9/Model.plist" }),
).rejects.toMatchObject({
issues: [
{
path: ["path"],
reason: "invalid_value",
message: expect.stringContaining("Unicode-equivalent"),
},
],
});
},
);
it("keeps an archive readable when a non-finite real is an explicit unknown", async () => {
const root = await bundle();
const bytes = Buffer.from(
+13 -3
View File
@@ -12,6 +12,12 @@ for asset notes and the source of case-study claims.
should explain a new fact or action.
- Reuse the shared styles, prompts, figures and code-step components. Keep a
case study focused on one question, with further examples in native details.
- Edit shared header, footer and sharing markup in `templates/`; run
`python3 scripts/website.py sync` and commit the rendered regions with their
templates. Keep page bodies outside the marked regions. Author each title and
description once; derive sharing fields and canonicals from them. Use
`new-page` for a reading-page scaffold, then add its index link. Keep a draft's
visible label and `noindex` until the content is ready.
- 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.
@@ -27,9 +33,13 @@ for asset notes and the source of case-study claims.
from HTML automatically. Mark example applications `noindex` in the head and
keep them crawlable. Maintain the SVG sharing source, not the generated PNG.
- Check both local root and `/rea/` paths, mobile layouts, expanded details,
copying and downloads. Run `scripts/prepare-website.py` followed by
`scripts/verify-website.py` and `scripts/test-website.py`, using the website
Python environment described in the README.
copying and downloads. Run `python3 scripts/website.py check`, using the Python
environment described in the README. It rejects drift rather than silently
rewriting shared HTML. Use `website.py links` for an optional network audit;
distinguish missing targets from access denials and unverified fragments.
- Check setup and support statements against the current installation/provider
guides and released package. Preserve historical case-study checkpoints and
versions unless a new investigation supplies replacement evidence.
- Keep `website-pages.yml` as the sole, manual production publisher for
Cloudflare and Pages. Default to `both`: both hosts receive the same verified
artifact and pass the published-commit check. Use `pages` only when explicitly
+86 -16
View File
@@ -7,8 +7,9 @@
An English static website with explanatory figures, worked guides and Aegis, DX-Ball, Notion, TH04 and CTF investigations.
The public files are in `website/public/`. The site uses HTML, CSS and small
scripts for copying code, following code comparisons and playing the dinosaur
speed reconstruction. Python prepares the example ZIP, sitemap and sharing
image; there is no frontend bundler or npm dependency.
speed reconstruction. Small Python commands maintain shared HTML, prepare
downloads and check the site. There is no frontend framework, bundler or npm
dependency. The checked-in HTML works as ordinary static files.
[style-guide.md](style-guide.md) explains the writing, page structure, figures,
visual system and review process. Read it before adding or revising a page.
@@ -21,8 +22,7 @@ From the repository root:
python3 -m venv website/.venv
source website/.venv/bin/activate
python3 -m pip install -r website/requirements.txt
python3 scripts/prepare-website.py
python3 -m http.server 4173 --bind 127.0.0.1 --directory website/public
python3 scripts/website.py serve
```
Open <http://127.0.0.1:4173/>. Refresh the browser after editing a file.
@@ -32,9 +32,81 @@ install them with `sudo apt-get install libcairo2 fonts-dejavu-core`. For other
systems, see [CairoSVG's installation instructions](https://cairosvg.org/documentation/#installation).
CI installs both packages explicitly before rendering the card.
After adding, moving or removing a page, or changing its indexing policy, rerun
`python3 scripts/prepare-website.py`. It regenerates the sitemap from the current
HTML files. No URL list needs to be maintained.
To test the GitHub Pages prefix, use
`python3 scripts/website.py serve --port 4174 --base-path /rea/` and open
<http://127.0.0.1:4174/rea/>.
## Contribute a page or correction
Edit the page's HTML in `public/`. Keep the question, evidence and result easy to
follow. The writing and design guidance in [style-guide.md](style-guide.md)
applies to every page.
| Change | Edit | Then run |
| ------------------------------ | ---------------------------------------------------------------- | ----------------------------------------------- |
| Page text, examples or figures | The page's HTML outside the marked shared regions | `python3 scripts/website.py check` |
| Search title or description | The page's `<title>` and first description meta tag | `python3 scripts/website.py sync`, then `check` |
| Main navigation | `NAVIGATION` in `scripts/website.py` | `sync`, then `check` |
| Header, social links or footer | `templates/header.html.tmpl` or `templates/footer.html.tmpl` | `sync`, then `check` |
| Shared link previews | `templates/sharing.html.tmpl` or `public/assets/social-card.svg` | `sync`, then `check` |
Create a reading page with:
```sh
python3 scripts/website.py new-page blog/my-article \
--title "My article title" \
--description "One sentence explaining the article."
```
The command creates `public/blog/my-article/index.html` with the correct relative
paths, navigation and sharing metadata. It starts as a visible draft with
`noindex`. Write the article, add its link to the Blog index, then remove the
Draft label and robots meta tag when it is ready. The same command accepts
routes under `guides/` and `showcase/`. New pages still need their own content;
the scaffold supplies the common structure.
The regions marked `website:sharing`, `website:header` and `website:footer` are
generated from the templates. Edit the template rather than a generated copy.
The `.html.tmpl` partials own their generated whitespace; HTML formatter-ignore
comments keep the repository formatter from changing those regions. Their
content is checked by `sync --check`, while ordinary page content stays subject
to the repository formatter.
`sync` updates only those regions; page bodies, figures and scripts remain
authored HTML. Commit the updated pages alongside the template. Titles and
descriptions are written once per page; their sharing fields are derived from
them. Routes come from file paths, including the sitemap, so there is no second
page list to register.
Before opening a PR:
```sh
python3 scripts/website.py check
```
This checks shared HTML without rewriting it, prepares generated assets, and
checks page links, copied-text targets, repository documentation links, SEO
metadata and the publishing configuration. It also runs the maintenance
regressions. A stale shared region fails with the command needed to fix it.
PR checks and the manual publisher run this same command. The checks work
offline after the Python dependencies are installed.
For an optional external-link audit, with GitHub CLI available:
```sh
python3 scripts/website.py links --report /tmp/rea-website-links.json
```
GitHub source links are checked through `gh`, including pinned commits and
branch names containing slashes. Other external links use HTTP. Missing targets
fail the command; access denials, timeouts and GitHub UI routes are reported as
unknown and need manual verification. External fragment IDs are not checked.
This network audit runs separately so third-party uptime does not block every PR.
Keep its report outside public assets.
Links are only part of a content review. When updating setup or support claims,
compare them with [installation.md](../docs/installation.md), the relevant
provider guide and the released package. A case study's pinned version and date
describe its recorded investigation; update them only when repeating that work.
## Pages
@@ -67,11 +139,11 @@ navigation and asset references remain relative so both hosts and local previews
keep working.
Maintain a descriptive `<title>` and a short meta description in each page's
`<head>`. The title names the page's subject; visible headings keep the wording
that best explains it to a reader. Open Graph and Twitter titles/descriptions
match those fields. `og:url` matches the canonical, and sharing images use
absolute rea.tools URLs. Search results and link previews should follow the same
clear, concise writing standard as the page.
`<head>`, then run `sync`. The title names the page's subject; visible headings
keep the wording that best explains it to a reader. The script derives Open
Graph and Twitter fields and the canonical from those inputs and the file's
route. Search results and link previews should follow the same clear, concise
writing standard as the page.
`scripts/prepare-website.py` scans `public/**/*.html` and generates
`public/sitemap.xml` with absolute rea.tools URLs. An `index.html` maps to its
@@ -101,8 +173,7 @@ canonicals, lost fixture exclusions, stale sitemap entries and missing previews.
Run it after preparing assets:
```sh
python3 scripts/verify-website.py
python3 scripts/test-website.py
python3 scripts/website.py check
```
### After publishing an SEO change
@@ -404,8 +475,7 @@ dependencies. The same asset preparation and verification run before each manual
deployment. With the preview environment active, run them locally:
```sh
python3 scripts/prepare-website.py
python3 scripts/verify-website.py
python3 scripts/website.py check
```
`.github/workflows/pages.yml` is a separate, manual-only VitePress build. It
+33 -28
View File
@@ -8,46 +8,46 @@
name="description"
content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/blog/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<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."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="REA Blog — Thoughts on Reverse Engineering" />
<!-- prettier-ignore -->
<meta property="og:description" content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/blog/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<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"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="REA Blog — Thoughts on Reverse Engineering" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@@ -63,8 +65,8 @@
>
<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="../blog/" aria-current="page">Blog</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Blog</p>
@@ -154,13 +157,14 @@
</section>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -170,5 +174,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
@@ -8,52 +8,46 @@
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/"
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/blog/touhou-reconstruction/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<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"
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Reverse Engineering’s Industrial Revolution — REA" />
<!-- prettier-ignore -->
<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." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/blog/touhou-reconstruction/" />
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<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"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Reverse Engineering’s Industrial Revolution — REA" />
<!-- prettier-ignore -->
<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." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -61,6 +55,8 @@
</head>
<body id="top" class="blog-article">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -69,8 +65,8 @@
>
<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="../../blog/" aria-current="page">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@@ -136,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -1010,21 +1007,23 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+38 -32
View File
@@ -8,46 +8,46 @@
name="description"
content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/examples/dino-lab/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Chrome Dinosaur Game: Rebuild Its Speed Rule — REA"
/>
<meta
property="og:description"
content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Chrome Dinosaur Game: Rebuild Its Speed Rule — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/examples/dino-lab/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Chrome Dinosaur Game: Rebuild Its Speed Rule — REA"
/>
<meta
name="twitter:description"
content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Chrome Dinosaur Game: Rebuild Its Speed Rule — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -56,6 +56,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -64,9 +66,10 @@
>
<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>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -396,21 +400,23 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+36 -31
View File
@@ -8,46 +8,46 @@
name="description"
content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/faq/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="REA FAQ: Setup, Updates and Troubleshooting"
/>
<meta
property="og:description"
content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="REA FAQ: Setup, Updates and Troubleshooting" />
<!-- prettier-ignore -->
<meta property="og:description" content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/faq/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="REA FAQ: Setup, Updates and Troubleshooting"
/>
<meta
name="twitter:description"
content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="REA FAQ: Setup, Updates and Troubleshooting" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@@ -64,9 +66,9 @@
<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="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="./" aria-current="page">FAQ</a>
<a href="../faq/" aria-current="page">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / FAQ</p>
@@ -495,21 +498,23 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+38 -32
View File
@@ -8,46 +8,46 @@
name="description"
content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/first-investigation/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Your First Reverse Engineering Investigation — REA"
/>
<meta
property="og:description"
content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Your First Reverse Engineering Investigation — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/first-investigation/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Your First Reverse Engineering Investigation — REA"
/>
<meta
name="twitter:description"
content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Your First Reverse Engineering Investigation — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@@ -63,9 +65,10 @@
>
<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>
<a href="../blog/">Blog</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -129,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -433,21 +437,23 @@ JS</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+70 -33
View File
@@ -8,46 +8,46 @@
name="description"
content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/get-started/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Install REA and Set Up Your Coding Agent"
/>
<meta
property="og:description"
content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Install REA and Set Up Your Coding Agent" />
<!-- prettier-ignore -->
<meta property="og:description" content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/get-started/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Install REA and Set Up Your Coding Agent"
/>
<meta
name="twitter:description"
content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Install REA and Set Up Your Coding Agent" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@@ -63,9 +65,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<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>
<a href="../blog/">Blog</a>
<a href="../get-started/" class="nav-start" aria-current="page">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -129,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Get started</p>
@@ -291,8 +295,9 @@
<div>
<p>
Use an existing Ghidra 12.1.x installation and a 64-bit
JDK 21 or newer on Linux x64 or macOS x64/arm64. Set the
paths before running setup:
JDK 21 or newer on Linux or macOS, on x64 or arm64. Your
Ghidra installation needs a native decompiler for your
host architecture. Set the paths before running setup:
</p>
<details class="evidence-details">
<summary>Connect your Ghidra installation</summary>
@@ -316,8 +321,38 @@ npx rea-agents@latest setup</code></pre>
<p>
Replace both paths with your existing installations.
Setup checks them and records valid settings for your
selected client. On macOS, use a Ghidra package with the
native decompiler matching your host architecture.
selected client. On Linux arm64, you may need to build
Ghidra's native decompiler separately.
</p>
</details>
<details class="evidence-details">
<summary>Connect Ghidra on Windows</summary>
<p>
Windows x64 has experimental, read-only support for
native x86 and x64 PE applications and DLLs on fixed
local NTFS drives. Use an existing Ghidra 12.1.x
installation and a 64-bit JDK 21 or newer:
</p>
<div class="code-block">
<div class="code-header">
<span>Ghidra connection · Windows PowerShell</span>
<button
class="copy-button"
type="button"
data-copy="ghidra-windows-connect"
aria-label="Copy Ghidra connection for Windows PowerShell"
>
Copy
</button>
</div>
<pre><code id="ghidra-windows-connect">$env:GHIDRA_INSTALL_DIR = "C:/tools/ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:/tools/jdk-21"
npx -y rea-agents@latest doctor --provider ghidra --json
npx rea-agents@latest setup</code></pre>
</div>
<p>
Replace the two paths with your installations, then
review setup's plan.
</p>
</details>
</div>
@@ -492,13 +527,14 @@ npx rea-agents@latest setup</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -508,5 +544,6 @@ npx rea-agents@latest setup</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -21
View File
@@ -8,40 +8,46 @@
name="description"
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/guides/browser/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<!-- prettier-ignore -->
<meta property="og:title" content="Browser and Network Analysis — REA" />
<meta
property="og:description"
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
/>
<!-- prettier-ignore -->
<meta property="og:description" content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/guides/browser/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<!-- prettier-ignore -->
<meta name="twitter:title" content="Browser and Network Analysis — REA" />
<meta
name="twitter:description"
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:description" content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -49,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -57,9 +65,10 @@
>
<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</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/" aria-current="page">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -123,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -416,13 +426,14 @@ link.click();</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -432,5 +443,6 @@ link.click();</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -21
View File
@@ -8,40 +8,46 @@
name="description"
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/guides/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<!-- prettier-ignore -->
<meta property="og:title" content="Reverse Engineering Guides — REA" />
<meta
property="og:description"
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
/>
<!-- prettier-ignore -->
<meta property="og:description" content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/guides/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<!-- prettier-ignore -->
<meta name="twitter:title" content="Reverse Engineering Guides — REA" />
<meta
name="twitter:description"
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:description" content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@@ -49,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@@ -57,9 +65,10 @@
>
<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="./" aria-current="page">Guides</a><a href="../faq/">FAQ</a>
<a href="../blog/">Blog</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/" aria-current="page">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -123,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Guides</p>
@@ -199,13 +209,14 @@
</section>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -215,5 +226,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+35 -29
View File
@@ -8,46 +8,46 @@
name="description"
content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/guides/javascript/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="JavaScript and Electron Analysis — REA"
/>
<meta
property="og:description"
content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="JavaScript and Electron Analysis — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/guides/javascript/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="JavaScript and Electron Analysis — REA"
/>
<meta
name="twitter:description"
content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="JavaScript and Electron Analysis — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -63,9 +65,10 @@
>
<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</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/" aria-current="page">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -129,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -418,13 +422,14 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -434,5 +439,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -21
View File
@@ -8,40 +8,46 @@
name="description"
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/guides/native/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<!-- prettier-ignore -->
<meta property="og:title" content="Native Binary Analysis — REA" />
<meta
property="og:description"
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
/>
<!-- prettier-ignore -->
<meta property="og:description" content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/guides/native/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<!-- prettier-ignore -->
<meta name="twitter:title" content="Native Binary Analysis — REA" />
<meta
name="twitter:description"
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:description" content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -49,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -57,9 +65,10 @@
>
<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</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/" aria-current="page">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -123,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -348,13 +358,14 @@ return (DxBallInt)pan;</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -364,5 +375,6 @@ return (DxBallInt)pan;</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+36 -31
View File
@@ -8,46 +8,46 @@
name="description"
content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="REA — Reverse Engineering for Your Coding Agent"
/>
<meta
property="og:description"
content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="REA — Reverse Engineering for Your Coding Agent" />
<!-- prettier-ignore -->
<meta property="og:description" content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="REA — Reverse Engineering for Your Coding Agent"
/>
<meta
name="twitter:description"
content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="REA — Reverse Engineering for Your Coding Agent" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="assets/styles.css" />
@@ -57,6 +57,8 @@
</head>
<body class="home-page" id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="./" aria-label="REA home"
@@ -64,11 +66,11 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<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>
<a href="./showcase/">Showcases</a>
<a href="./blog/">Blog</a>
<a href="./get-started/" class="nav-start">Get started</a>
<a href="./guides/">Guides</a>
<a href="./faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@@ -132,6 +134,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<details class="home-page-nav" data-home-nav open>
<summary class="container">
<span>On this page</span>
@@ -1095,13 +1098,14 @@ SPEED: 6</code></pre>
</div>
</section>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -1111,5 +1115,6 @@ SPEED: 6</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+36 -31
View File
@@ -8,46 +8,46 @@
name="description"
content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/aegis/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Aegis APK Reverse Engineering: Login Codes — REA"
/>
<meta
property="og:description"
content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Aegis APK Reverse Engineering: Login Codes — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/aegis/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Aegis APK Reverse Engineering: Login Codes — REA"
/>
<meta
name="twitter:description"
content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Aegis APK Reverse Engineering: Login Codes — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -56,6 +56,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -63,9 +65,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@@ -131,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -731,21 +734,23 @@ npx rea-agents@latest inspect-android-package \
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+36 -31
View File
@@ -8,46 +8,46 @@
name="description"
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/ctf/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="CTF Reverse Engineering: DownUnderCTF Flag Checker — REA"
/>
<meta
property="og:description"
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="CTF Reverse Engineering: DownUnderCTF Flag Checker — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Use REA to solve DownUnderCTF&#x27;s masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/ctf/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="CTF Reverse Engineering: DownUnderCTF Flag Checker — REA"
/>
<meta
name="twitter:description"
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="CTF Reverse Engineering: DownUnderCTF Flag Checker — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Use REA to solve DownUnderCTF&#x27;s masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -62,9 +64,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -701,21 +704,23 @@ chmod +x ms_flag_checker
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -28
View File
@@ -8,46 +8,46 @@
name="description"
content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/dx-ball/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="DX-Ball Reverse Engineering: Sound Panning — REA"
/>
<meta
property="og:description"
content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="DX-Ball Reverse Engineering: Sound Panning — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/dx-ball/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="DX-Ball Reverse Engineering: Sound Panning — REA"
/>
<meta
name="twitter:description"
content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="DX-Ball Reverse Engineering: Sound Panning — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -62,9 +64,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -709,13 +712,14 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -725,5 +729,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -28
View File
@@ -8,46 +8,46 @@
name="description"
content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Reverse Engineering Case Studies — REA"
/>
<meta
property="og:description"
content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Reverse Engineering Case Studies — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Reverse Engineering Case Studies — REA"
/>
<meta
name="twitter:description"
content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Reverse Engineering Case Studies — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@@ -62,9 +64,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="./" aria-current="page">Showcases</a>
<a href="../showcase/" aria-current="page">Showcases</a>
<a href="../blog/">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Showcases</p>
@@ -241,13 +244,14 @@
<a class="text-link" href="../guides/">Read the guides</a>
</section>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -257,5 +261,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -28
View File
@@ -8,46 +8,46 @@
name="description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/notion/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="Notion Electron Reverse Engineering: Clipboard — REA"
/>
<meta
property="og:description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="Notion Electron Reverse Engineering: Clipboard — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Follow Notion&#x27;s clipboard API through Electron&#x27;s preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/notion/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Notion Electron Reverse Engineering: Clipboard — REA"
/>
<meta
name="twitter:description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="Notion Electron Reverse Engineering: Clipboard — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Follow Notion&#x27;s clipboard API through Electron&#x27;s preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -62,9 +64,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -547,13 +550,14 @@ mimeTypes:
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -563,5 +567,6 @@ mimeTypes:
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+33 -28
View File
@@ -8,46 +8,46 @@
name="description"
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/th04/" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="TH04 Reverse Engineering: DOS Bullet Patterns — REA"
/>
<meta
property="og:description"
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
/>
<!-- prettier-ignore -->
<meta property="og:title" content="TH04 Reverse Engineering: DOS Bullet Patterns — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Inspect TH04&#x27;s original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/th04/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="TH04 Reverse Engineering: DOS Bullet Patterns — REA"
/>
<meta
name="twitter:description"
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
/>
<meta
name="twitter:image"
content="https://rea.tools/assets/social-card.png"
/>
<!-- prettier-ignore -->
<meta name="twitter:title" content="TH04 Reverse Engineering: DOS Bullet Patterns — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Inspect TH04&#x27;s original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@@ -55,6 +55,8 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@@ -62,9 +64,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@@ -130,6 +132,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@@ -724,13 +727,14 @@ no_aim:
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<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://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>
@@ -740,5 +744,6 @@ no_aim:
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>
+4 -2
View File
@@ -9,8 +9,10 @@ evidence the reader can inspect, and a useful next step. A clean, refined
appearance comes from precise wording, readable figures, consistent typography
and deliberate spacing.
Use this guide for new pages and revisions. Start from an existing page and
reuse `public/assets/styles.css` and `public/assets/main.js`. Local preview and
Use this guide for new pages and revisions. Create the common page structure
with `python3 scripts/website.py new-page`, then reuse the relevant components
from an existing page, `public/assets/styles.css` and `public/assets/main.js`.
Keep shared regions in sync with `templates/`. Contribution, preview and
publication commands are in [README.md](README.md).
## What a reader should understand
+16
View File
@@ -0,0 +1,16 @@
<!-- prettier-ignore -->
<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>
+70
View File
@@ -0,0 +1,70 @@
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="$base" 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">$navigation</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>
+38
View File
@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>$title</title>
<meta name="description" content="$description" />
<meta name="robots" content="noindex" />
<!-- website:sharing:start -->
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="${base}assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="${base}assets/styles.css" />
<script src="${base}assets/main.js" defer></script>
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="$base">Home</a> / Draft</p>
<h1>$title</h1>
<p class="lede">$description</p>
</div>
<article class="article">
<section class="article-section" id="question">
<h2>The question</h2>
<p>
Introduce one concrete question, then show the evidence and result.
</p>
</section>
</article>
</main>
<!-- website:footer:start -->
<!-- website:footer:end -->
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
<!-- prettier-ignore -->
<link rel="canonical" href="$canonical" />
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<!-- prettier-ignore -->
<meta property="og:title" content="$title" />
<!-- prettier-ignore -->
<meta property="og:description" content="$description" />
<!-- prettier-ignore -->
<meta property="og:url" content="$canonical" />
<!-- prettier-ignore -->
<meta property="og:image" content="$origin/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<!-- prettier-ignore -->
<meta name="twitter:title" content="$title" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="$description" />
<!-- prettier-ignore -->
<meta name="twitter:image" content="$origin/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
+1 -1
View File
@@ -11,7 +11,7 @@ not_found_handling = "none"
[build]
cwd = ".."
command = "python3 scripts/prepare-website.py && python3 scripts/verify-website.py"
command = "python3 scripts/website.py check"
watch_dir = "./public/examples/notes-electron"
# Public HTTPS depends on zone activation and certificate readiness.