Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
591b910e31 | ||
|
|
9ffc6210a4 | ||
|
|
b973b1b1ec | ||
|
|
47947eb01c | ||
|
|
2d99e1e3b2 | ||
|
|
5d6eeb22fb | ||
|
|
e964a06b83 | ||
|
|
5aa30b8b86 | ||
|
|
8c7569aca7 | ||
|
|
539a0f95b5 | ||
|
|
2309cebb7b |
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <test> & "result"", 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()
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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)),
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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'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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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: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'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 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>
|
||||
|
||||
@@ -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'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'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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
/>
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user