| name | deobfuscate-javascript |
| description | Reverse-engineer minified, obfuscated, packed, or webpack/Vite/Rollup-bundled JavaScript back to readable code, including Codex.app `./ref/webview/assets` restoration. A WHOLE-TREE restore (an index.html + asset tree, or "restore the code/whole tree") DEFAULTS TO DEEP: Stage 1 only if obfuscated → Stage 2 smart-rename + polish → Stage 3 typed `.tsx` rewrite + acceptance review, npm-import resolution, import graph/ledger orchestration that drains EVERY reachable project-local chunk to promoted, full-target `quality-gate`; bundled vendor data (Shiki grammars/themes, 3Dmol) is detected and left as bare npm imports, not restored. A "quick"/"readable"/"快速" pass (or a lone pasted snippet) downgrades to the untyped readable restore where the hard bar is just naming quality. Use when the user provides a .min.js / dist bundle / obfuscated script / Vite chunk or asks to "humanify", "deobfuscate", "deminify", "unminify", "unpack", "decode", "make readable", "rename variables", "reverse engineer", "完整还原", "深度还原", "反混淆", "美化代码", "看懂这段 JS", "重命名变量", "反编译". |
Deobfuscate JavaScript
Reverse-engineer minified/obfuscated JavaScript back to readable code. Three stages: Stage 1 (only if obfuscated) and Stage 2 (rename + polish) do the readable restore; Stage 3 adds the typed rewrite + acceptance review and is the deep-tier add-on.
- Stage 1 — Deobfuscation: unwind obfuscation transforms (Packer/AAEncode unpack, Obfuscator.IO string arrays, hex/unicode/base64 escapes, dead code, opaque predicates). Pure Babel passes, no LLM dependency. Run only on obfuscated input. Detail: stages/stage-1-deobfuscate.md.
- Stage 2 — Restore to readable (two phases). (a) Rename cryptic identifiers (
a, e, t, _0x4f3, …) to meaningful names — a TypeScript+Bun port of humanify where you (the agent) play the renamer, running smart-rename.ts first so you only hand-name the residue; this is where readability is won. (b) Polish undoes bundler/compiler transforms — the reading-aid subset (--fast: React Compiler cache[N] strip, (0, fn)(...) unwrap, backtick → string, JSX-runtime → JSX, destructure defaults, aliased exports) is the default; the import-resolution tail (npm/shim/dead-shim elimination) only makes imports resolve against node_modules and is deep mode only. Detail: stages/stage-2-restore.md.
- Stage 3 — Finalize (deep mode only) (two phases). (a) Semantic rewrite turns the mechanical checkpoint into idiomatic typed TypeScript — semantic filenames, provenance header, import map, TypeScript types/
.tsx recipes, directory splits, import repair. (b) Acceptance review — the host agent reads every delivered file end-to-end against the four-category quality bar and iterates until each passes; no sub-agent and no authorization required (an independent reviewer sub-agent is optional, for extra eyes). The default tier ships the readable file from Stage 2 without Stage 3. Detail: stages/stage-3-finalize.md.
Default scope and depth
Two independent axes: scope (one file vs the whole import tree) and depth (readable vs deep). Pick scope from the input shape. For a whole-tree restore the default depth is deep (the user is asking to "restore the code", which means complete + typed); a lone pasted snippet defaults to readable. Drop to readable for a whole tree only when the user explicitly asks for a "quick" / "readable" / "快速" pass.
- Default scope — whole tree from
index.html. When the input is an app (an index.html plus a sibling-chunk asset tree), read index.html, auto-discover the entry (src/domain/check-entry.ts --discover --root <assets-dir> prints it; build-import-graph.ts auto-discovers when the positional is omitted), then recursively restore every reachable project-local chunk. The per-chunk pipeline is unchanged: Stage 1 (only if obfuscated) → wakaru mechanical-normalization pre-pass (wakaru-normalize.ts; recovers ES6 classes, async/await, optional chaining, destructuring, TS enums, … — runs by default, auto-skips if @wakaru/cli is absent) → Stage 2 rename (smart-rename first, then hand-name the residue) → Stage 2 reading-aid polish (polish.ts --fast) → format → organize the draft in the staging workspace, then promote into the shared src/ root (FSD layers, kebab filenames, provenance headers, one shared src/IMPORT_MAP.json). Script/batch output stages first and is never written straight into src/. Bundled vendor data (Shiki language grammars + themes, standalone data libs like 3Dmol) is detected by content and treated as npm-leaf — it is NOT restored into src/; consumers import the bare specifier (@shikijs/langs/<id>, @shikijs/themes/<id>). So src/ holds app code only, never hundreds of grammar/theme dumps. At readable depth the hard bar is naming quality; at the default deep depth it is also complete types + every reachable chunk promoted.
- Fallback scope — single file. When the input is a lone pasted snippet, or there is no
index.html / asset tree, restore just that one file — workflows/small-minified.md. Same per-file pipeline, no import graph.
- Depth — deep (default for whole tree) vs readable (quick opt-out). Deep is the whole-tree default: meaningful names plus Stage 3 (typed
.tsx rewrite + acceptance review), full npm-import resolution (Stage 2's deep-mode polish tail), and import-graph/ledger orchestration that drains every reachable chunk to promoted. It is also triggered explicitly on "deep" / "full" / "完整" / "深度" / "production" / "typed" / "restore the whole tree". Readable is the quick opt-out (meaningful names, reading-aid polish, untyped output) — use it only when the user says "quick" / "readable" / "快速" / "粗略", or for a lone pasted snippet. Both depths organize and promote; promote-organized.ts defaults to --tier deep (enforces Props/param types), and --tier readable relaxes the typing gate.
Deep is a completion bar, not an upsell: a whole-tree restore is "done" only when every reachable project-local chunk reaches stages.promoted and quality-gate.ts <target> passes (see Restoration contract). Readable is a deliberate downgrade the user asks for, not the resting state.
Output conventions
src/ is a clean-only deliverable zone — batch/script output never lands here directly. Anything a batch or script restore emits (auto-restore-full.ts checkpoints, a one-shot polish.ts swept over many chunks, --write-target-checkpoints files, any hash-basename .tsx) is a mechanical checkpoint, not a deliverable. It must be written into the temporary, gitignored staging tree under src/.deobfuscate-javascript/ (_full/checkpoints/<basename>.tsx for the batch executor; the per-chunk $WS/ otherwise) — never copied or written straight into src/. Promote a file into src/ only after the host agent has organized it. The promotion bar is good readability (semantic names throughout, no mechanical fallbacks like buttonValue3 / contextParam14), friendly filenames (semantic kebab-case), clear directory structure (FSD layers), and prettier-formatted (blank lines, wrapped long lines, parenthesized multi-line JSX returns — promote-organized.ts runs format.ts on every deliverable as it writes it, so src/ is never raw @babel/generator output) in every tier — plus complete types (Props interfaces on exported components, parameter/return types where they help) whenever the restore is deep/full, which is where all auto-restore-full.ts batch output lives. The only files that may promote untyped are genuinely hand-restored single readable-tier deliverables — and even those draft in $WS, get organized for names + structure, and only then land in src/; they are never raw script output. This staging → organize → promote discipline is the hard rule behind every convention below. For a whole-tree batch restore it is driven, not eyeballed: plan-organize.ts proposes a domain + kebab public path per chunk into the manifest's organize/promote stages, and promote-organized.ts drains the resulting promote frontier (typed deliverable → quality gate → copy into src/<domain>/ → IMPORT_MAP update → import rewrite). The restore is complete only when every reachable chunk reaches stages.promoted and quality-gate.ts <target-dir> passes — see workflows/full-restoration.md → Step 4.
- One shared restore root —
src/ — mirrors the source assets dir (e.g. ref/webview/assets). Every entry restores into this same root; there is no per-entry folder (no src/app-main/).
- FSD layers group the output (
app/, pages/, widgets/, features/, entities/, shared/). The original chunk identity is preserved in each file's provenance header, not in the directory layout.
- One shared import map —
src/IMPORT_MAP.json — reused regardless of which entry triggered the restore. Never create per-chunk, per-session, or per-entry maps (no APP_MAIN_IMPORT_MAP.json).
- kebab-case file and directory names (
button.tsx, download-icon.tsx, widgets/app-shell/). React component identifiers and type names stay PascalCase — JSX requires it. So button.tsx exports Button; download-icon.tsx exports DownloadIcon. quality-gate.ts enforces this (non-kebab-filename).
While restoring, if you hit a script bug, a step worth automating, or an unrecognized npm package, update the skill itself and commit it separately — see Maintaining this skill.
Restoration contract
Default tier (readable restore)
- Find the entry (whole tree is the default). When there is an
index.html + asset tree, auto-discover the entry: bun src/domain/check-entry.ts --discover --root <assets-dir> prints it, or omit the positional to build-import-graph.ts. Restore every reachable project-local chunk (the per-chunk steps below run across the tree, orchestrated by workflows/full-restoration.md). If there is no index.html/asset tree (a lone pasted snippet), fall back to the single-file flow — workflows/small-minified.md.
- Analyze first. Always run
sourcemap-check; use detect / extract per chunk.
- Restore to readable. Stage 1 only if obfuscated → wakaru-normalize (
bun src/infrastructure/wakaru-normalize.ts "$WS/original.js" -o "$WS/normalized.js"; auto-skips when the binary is absent, and always leaves a normalized.js for the next step) → Stage 2 rename (smart-rename first, then hand-name the residue until single-letter density is low) → reading-aid polish (polish.ts --rename --fast) → format. A one-shot polish.ts "$WS/normalized.js" --rename --fast --source <original-path> --out draft.tsx --format lands the draft (the file operand is the wakaru-normalized output; --source still points at the original input for the provenance header); you then hand-name what smart-rename left.
- Stage, organize, then promote. The rename/polish one-shot writes its draft into the per-chunk
$WS/ staging dir, not into src/. Promote into the shared src/ root (FSD layers, kebab filenames) — plus a provenance header (// Restored from <path>) and the shared src/IMPORT_MAP.json — only after organizing the draft (meaningful names, friendly kebab filename, clear structure). Never copy a mechanical checkpoint into src/ (see Output conventions). The hard bar at readable depth is naming quality — meaningful identifiers, no generated fallback names (buttonValue3, contextParam14); types, npm-import resolution, and the reviewer loop stay optional, and an optional single naming-only self-review is available when you want extra confidence.
Deep / production mode only
When the user asks for deep/full/typed/production output, continue past the readable draft:
- Build the graph (full mode). Build
manifest.json + ledger.json with build-import-graph.ts and build-symbol-ledger.ts so renaming is resumable and parallel-safe across the chunk tree.
- Create a mechanical checkpoint. Stage 2 scripts or
auto-restore-full.ts produce a broad readable checkpoint. A checkpoint may keep hash basenames and mechanical fallback names; it is never the final deep deliverable. wakaru-normalize may run per chunk body (src/infrastructure/wakaru-normalize.ts on each _full/files/<basename>/original.js) here too, but with two hard guards: never --unpack an already-split chunk tree (it re-derives its own module boundaries/filenames, forking the restore root), and treat any import/export specifiers wakaru emits as untrusted — build-import-graph.ts must (re)build the manifest from the real on-disk chunk files after wakaru, and resolve-npm-imports.ts + the Codex CHUNK_NAME_REGISTRY remain the authoritative import rewriter (wakaru's un_esm does not substitute for them).
- Rewrite semantically as the host agent. Read the checkpoint together with the original source, graph/ledger, producer/consumer imports, and project conventions. Rewrite the public output into code a human would maintain: meaningful filenames and identifiers, typed props, semantic exports, resolved npm imports, and clear structure.
- Pre-filter only. Run prettier and
quality-gate.ts to catch cheap failures. Passing these scripts does not mean the work is done.
- Acceptance-review LOOP. The host agent reads every delivered file end-to-end against the four-category quality bar in
src/infrastructure/acceptance-checklist.md, rewrites each NEEDS_FIX file, and re-reads until all pass — no sub-agent and no authorization required. An independent reviewer sub-agent is optional: use it for extra eyes only when the runtime offers one and the user has authorized delegation; never block on it, never ask for authorization to finish, and never fabricate verdicts or substitute a casual skim for a careful read. The deep restore is complete only after every delivered file passes.
- Full-target completion audit. For whole-tree restores, run
quality-gate.ts <target-dir> after the acceptance loop. This target-level audit checks the manifest plus the shared import map; it fails if any reachable local/oversized-local app feature chunk is still missing, mechanical, @ts-nocheck, an empty placeholder, a typed facade, or lacks Stage 3 acceptance/finalized evidence. Do not substitute a src/shared/boundaries/ grep or IMPORT_MAP.status === "done" scan.
Dual-track output (deep mode): hidden checkpoints preserve original hash basenames for traceability. Public final files/directories use semantic names without hash suffixes; preserve original chunk identity in provenance headers and, for multi-file/full restorations, an import map or report. Use one shared import map at the restore root — src/IMPORT_MAP.json — reused regardless of which entry triggered the restore; never per-chunk, per-session, or per-entry maps. If the restore root already has it, reuse and append; otherwise create it there. Create a separate map only when the user explicitly scopes a different project/root.
Full restoration mode is not a new stage — it's an orchestration layer on top of Stages 1–3 that coordinates the same per-file pipeline across an entire connected component of the import graph (e.g. an entry chunk plus all the sibling chunks it pulls in, transitively to the leaves). It adds a manifest.json (file-level dependency graph + per-file imports/exports + per-file stage status) and a ledger.json (per-file symbol-level checklist with cross-file binding table) so renaming is resumable across sessions and parallelizable across agents at file granularity. If the user says "deep", "full", "完整", "深度", or asks to restore referenced chunks, this means every reachable project-local sibling chunk is in scope by default. Detail: workflows/full-restoration.md.
Delta / boundary replacement inside an existing restore
When the target restore root already has a shared IMPORT_MAP.json and .deobfuscate-javascript/_full/manifest.json, first check whether the requested chunk already maps to a public file or typed boundary facade. If it does, treat the task as a delta restore unless the user explicitly asks to rebuild the whole reachable graph.
Delta restore contract:
- Reuse the existing target, manifest, ledger, and import map; do not create a parallel restore root.
- Restore the requested chunk in its existing
$TARGET/.deobfuscate-javascript/_full/files/<basename>/ workspace. Restore additional chunks only when they are missing from the import map or explicitly requested.
- Rewrite imports from the chunk's manifest entry through the shared
IMPORT_MAP.json: finalized local deps use semantic public paths; npm deps use bare specifiers; unresolved runtime/vendor deps keep their documented boundary facade paths.
- Replace the mapped boundary/public file with the semantic candidate, and update the import-map entry (
path, exports, status, and remove boundary when the facade is no longer a facade).
- Validate the delta: format, run
quality-gate.ts on the changed public path(s), run the target TypeScript check when the restore root has a tsconfig.json, and run Stage 3's acceptance review on changed public files only. Run the all-tree acceptance review only when the deliverable changed the whole public tree or the user asked for it.
What full-restoration restores — and what it deliberately doesn't
The mode makes three categories of dependencies terminal nodes in the manifest. They are recorded so consumers can reference them, but never have a ledger entry, never get renamed, and never get a <basename>/ workspace dir:
| Kind | Trigger | What we do |
|---|
npm-leaf | The chunk's basename is in CHUNK_NAME_REGISTRY (e.g. clsx-XXXX.js, react-XXXX.js, tslib.es6-XXXX.js, marked.esm-XXXX.js, floating-ui.react-dom-XXXX.js). | polish.ts → resolve-npm-imports.ts rewrites consumer imports to the bare npm specifier (import clsx from "clsx"). The chunk file itself is left untouched. |
oversized-local | Only when you explicitly pass --max-lines N and a local sibling exceeds that line cap. This is a quick/targeted mode, not deep restoration. | We parse imports/exports (so cross-file bindings link up) but stop BFS at the file. The consumer's import stays as import { xt as someAlias } from "./setting-storage-XXXX.js" — the agent renames someAlias based on usage in the consumer, not by following the producer. |
external | Bare specifier (react, vite, etc.) that wasn't seen as a chunk in the manifest. | Same as npm-leaf. |
faced-boundary | A genuinely third-party vendor/runtime-dominated chunk (Zod's 308-export src, a React+react-query scope layer, a host/vscode-api bridge, the Statsig SDK) imported by many feature chunks via cryptic aliases. Project/feature chunks are never faced — app-shell-*, app-main-*, feature pages/panels/views, components, contexts, and hooks are the app's own code and must be recursively restored even when large or widely imported. "Big" or "imported by many" is not a facing reason; only genuinely vendor/runtime content is. | Run make-facade.ts <chunk> to emit a typed export declare const … : any facade so consumers compile against it; record the alias map under dependencyBoundaryFacades in the project IMPORT_MAP; mark it ledger.ts mark-faced <basename> so its consumers become restorable. A facade is a temporary scaffold and an open boundary, not a finished deliverable — the deep/full restore stays incomplete until the chunk is restored or the user explicitly scopes it out. mark-faced refuses app-entry basenames unless you pass --force. Deep-restore the runtime only when the user explicitly scopes it. |
Depth rule (recursion): deep/full restoration recurses the entire reachable project-local import graph, not just the entry's direct imports. When you deep-restore app-main-* and it imports app-shell-*, you restore app-shell-* too — and everything it imports, transitively, to the leaves. An entry is not done while any project-local chunk it transitively imports is still a facade or placeholder. build-import-graph.ts already enqueues every local sibling (default --max-lines 0, no cap); the restoration loop must drain ledger.ts frontier until it is empty. Facing is reserved for genuine third-party vendor/runtime leaves and is an open boundary to report — never a way to call an app chunk "done". Use a positive --max-lines only when the user explicitly asks for a quick/targeted restore or accepts a partial dependency boundary. The final proof is quality-gate.ts <target-dir> over the whole public target, not a boundary-only audit and not import-map statuses alone.
Boundary lifecycle — three terminal states, never a permanent any-facade
src/shared/boundaries/*.ts is scaffolding in transit, not a resting place. A generated export declare const X: any facade type-checks but is dead weight and hides whether a dependency is third-party or unfinished app code. src/domain/chunk-classification.ts → classifyBoundary() splits every boundary into one of two kinds (driven by the IMPORT_MAP vendor field — anything other than the literal "runtime" marker names a real npm family), and each has a defined exit:
-
Third-party npm package → bare re-export shim (DONE). When the chunk is a recognised package (lodash, react-router, react-intl/FormatJS, framer-motion, mdast-util-*, @pierre/diffs, zod, @radix-ui/*, Segment…), the deliverable is a thin re-export, not a stub: src/infrastructure/make-facade.ts <chunk> --reexport <specifier> [--export-star | --reexport-named a,b,c] [--name-map alias→real.json]. It resolves to the package's real @types and is a finished deliverable — src/shared/boundaries/highlight-code.ts is the model. Verify first: the bundled chunk must be stock, not a Codex fork (the skill already warns @pierre/* and other vendors are forked — see reference/codex-ref.md), and the specifier must resolve in ref/node_modules. Consumers keep importing from src/shared/boundaries/; when they import cryptic aliases (appScopeC), the --name-map bridges { "<publicAlias>": "<realExport>" } so the re-export matches. quality-gate.ts <target> flags any vendor-npm boundary still left as an any-facade (full-restoration-npm-boundary-not-resolved).
-
Genuine app/host runtime → typed facade, or an opt-in runnable interim. For the app's own runtime (app-scope, the vscode/host bridge, rpc, host-config, product-logger, persisted-signal state…), the typed make-facade.ts any-facade is the type-check-only interim, recorded under dependencyBoundaryFacades + ledger.ts mark-faced. To make the app runnable before the runtime is restored, src/infrastructure/make-facade.ts <chunk> --passthrough <ref-relpath> [--name-map] emits a // @ts-nocheck + // TODO: deep-restore module that re-exports the original ref chunk (note: this makes src/ depend on the unrestored ref/ tree — it is a deliberate, clearly-marked stopgap, not a deliverable).
-
Restore and move OUT of src/shared/boundaries/ (the only completion for kind 2). A runtime facade/passthrough is an open boundary, never "done". Deep-restore the chunk, then ledger.ts set-organization it into its real FSD layer/domain and promote-organized.ts it out of src/shared/boundaries/. quality-gate.ts <target-dir> over the whole public target — not a src/shared/boundaries/ grep — is the proof.
Override flags on build-import-graph.ts:
--max-lines 0 — default; disable the cap entirely (restore everything project-local).
--max-lines 5000 — quick/targeted mode; skip local siblings above the cap.
--include foo-HASH,bar-HASH — force-restore specific basenames even if they exceed the cap. Repeat the flag is fine; comma-separated also works.
The entry is always exempt from the cap.
Run Stage 1 before Stage 2 on obfuscated input — extract.ts byte offsets are invalidated by Stage 1 rewrites. On purely minified (not obfuscated) input, skip Stage 1.
Current project profile — codex-app-code ./ref
When the workspace contains ref/package.json with name: "openai-codex-electron" or the user asks to restore ./ref, load reference/codex-ref.md before choosing a workflow.
Default assumptions for this repo:
- Primary bundled-code root:
ref/webview/assets.
- Main app entry: read
ref/webview/index.html; current builds usually route through index-*.js, app-main-*.js, and app-shell-*.js.
- Default restore root:
src/ (mirrors ref/webview/assets), with FSD layers (app/, pages/, widgets/, features/, entities/, shared/). Reuse the single shared src/IMPORT_MAP.json before creating any new map. Do not create a per-entry folder such as src/app-main/. Auto-discover the entry with src/domain/check-entry.ts --discover --root ref/webview/assets.
- Treat
ref/node_modules/**, ref/native-menu-locales/**, CSS, WASM, images, and obvious vendor/data chunks as references or terminal boundaries unless the user explicitly asks to restore them.
- Vendored UI packages →
@pierre/trees / @pierre/diffs boundaries, not app code. The file-tree and diff-view subsystems are the published @pierre/trees (Preact-based) and @pierre/diffs (Shiki-based) packages bundled inline. Don't deep-restore them as Codex features. But Codex FORKED them (a React fork of @pierre/trees; a settings-entangled @pierre/diffs gate), so a clean bare-import swap is usually infeasible — keep the forked wrapper, relabel provenance, and boundary-ize (quality-gate.ts --vendored); only the self-contained leaves are clean bare imports (highlight worker-* → @pierre/diffs/worker, parsePatchFiles-* → @pierre/diffs). Recognize by fingerprint: --trees-* / --diffs-* CSS vars, data-file-tree-* / data-diffs-* DOM attrs, pierre-light/dark(-soft) Shiki themes, the @pierre/truncate css here… comment. Engine chunks: file-tree-search-input-*, shiki-highlight-provider-gate-* (the @pierre/diffs core), file-diff-*, diff-unified-*, parsePatchFiles-*, worker-*. Basename traps that are NOT Pierre: diff-stats-*, diff-view-mode-*, use-diff-annotations-* (PDF.js), parse-diff-* (Codex wrapper), diff-* grammar (Shiki TextMate), treeView-SZITEDCU-*/treemap-* (Mermaid), worktree-* (git). Full detail + remediation mechanics: reference/codex-ref.md → Boundary classification.
- Before starting fresh, inspect existing
src/**/README.md, IMPORT_MAP.json, .deobfuscate-javascript/_full/manifest.json, and .deobfuscate-javascript/_full/ledger.json; resume rather than duplicating semantic modules.
When to use this skill
Trigger whenever the user:
- Provides a
.min.js, bundle.js, dist/ file, or any JS with cryptic short names and wants to understand or work with it.
- Provides obviously obfuscated code:
_0x identifiers, eval(function(p,a,c,k,e,d){...}), AAEncode emoji glyphs, String.fromCharCode(72,101,...) walls, hex/unicode-escaped string literals.
- Says "humanify", "deobfuscate", "deminify", "unminify", "unpack", "decode", "make readable", "rename variables", "reverse engineer".
- Asks "what does bundle.js do?" while pointing at obviously minified or obfuscated code.
- Writes in Chinese: "反混淆", "美化代码", "看懂这段 JS", "重命名变量", "反编译".
- Works in a project that already has a
modules/ or og/ folder full of previously deobfuscated chunks.
Don't trigger if the code is already readable — just read and explain. Don't trigger for plain beautification either — npx prettier --write <file> is the right tool when there are no cryptic names to rename and no obfuscation to unwind. wakaru alone is not a substitute for this skill — npx @wakaru/cli un-minifies (recovers classes/async-await/etc.) but its smart_rename is the same deterministic heuristic as smart-rename.ts, not semantic naming; use it as a pre-pass inside Stage 2 (below), not as the deliverable.
Routing — three questions, then load one workflow
Step 0 (always): run src/infrastructure/sourcemap-check.ts first. If a .map exists, recover originals via npx source-map-explorer instead — that beats any rename. For the current repo's ./ref tree, load reference/codex-ref.md first.
Step 0.5 (whole tree / ./ref / any multi-chunk app — the default): auto-discover the entry from index.html: src/domain/check-entry.ts --discover --root <assets-dir> reads index.html, picks the app entry, and prints its path (it also sanity-checks it). build-import-graph.ts runs the same discovery when you omit the positional entry. If discovery (or a manual check-entry.ts <entry> --root <assets-dir>) exits 3, the entry is a transitive vendor leaf, not the app — restoring from it yields a tiny dependency closure that looks complete (the classic "got 6 files, called it done" trap). A real app entry has a large local fan-out and is imported by ~nobody; a leaf is the inverse. Switch to the index.html <script> root (or a high-fan-out app-main-* chunk) before continuing.
Step 0.6 (existing restore delta): if the target already contains IMPORT_MAP.json plus _full/manifest.json, check whether the requested chunk is already represented as a boundary/public output. Prefer the delta/boundary-replacement contract above before launching a new whole-tree restore. In this case, "complete" means complete replacement for that scoped chunk unless the user explicitly asks to rebuild the reachable graph.
Step 0.7 (mechanical normalization — readable tier, default-on): on non-obfuscated minified/transpiled input the readable tier runs wakaru-normalize.ts as a pre-pass before extract.ts (recovers classes / async-await / optional-chaining / destructuring / enums that the renamer otherwise has to read around). It is byte-rewriting like Stage 1, so the order is sourcemap-check → detect → (Stage 1 if obfuscated) → wakaru-normalize → extract — extract/rename from the normalized output, never original.js. Skip it when a usable .map exists (recover via source-map-explorer instead). It auto-skips when @wakaru/cli is unavailable. In deep/full mode it is guarded, not default-on (see Restoration contract → deep step 2).
Then answer in order:
| Question | Then |
|---|
Obfuscated? (_0x arrays, Packer, AAEncode, hex/unicode walls, opaque predicates) | run Stage 1 first via workflows/full-obfuscation.md, then continue below |
Has an index.html + asset tree? (the default) — auto-discover the entry, restore the whole reachable tree | whole-tree restore → workflows/full-restoration.md at deep depth by default (typed .tsx + acceptance + drain every chunk to promoted); only "quick"/"readable"/"快速" downgrades to the untyped readable pass |
Otherwise — a lone pasted snippet or single chunk, no index.html/tree | fallback: single file → workflows/small-minified.md |
The single-file fallback workflow handles the sub-cases inline (it links the right technique): React/JSX → recover JSX in --fast polish; ≥ 3 exports / registry → multi-export-bundle.md split (a deep-tier step); ≥ 500 KB or > 1000 symbols → huge-single-file.md batching; webpack id:(e,t,n)=>{} modules → webpack-bundle.md webcrack pre-split; stdin/stdout chaining → piped.md. Load a technique doc only when its sub-case applies.
For end-to-end worked traces, see reference/examples.md. For caveats, gotchas, and troubleshooting, see reference/caveats.md.
Quality bar — anti-patterns to refuse before declaring done
Both tiers — naming/readability (the default tier's only hard bar)
These produce "looks OK if you skim it, falls apart on a real read" output. They are the hard bar in every tier, because they are about readability, not compilation:
- Program-scope-only rename. Symptom: top-level exports have meaningful names (
AppShellTabContent, RightPanelTabs) but the function bodies are still let k = useIntl(), [A, M] = useState(false), N = !h, …. Cause: renaming stopped at top-level scope. Cure: keep going into the function bodies (see stage-2-restore.md → Step 2.5) until single-letter density is low.
- Generated fallback names shipped as "readable". Symptom: code contains
ImportedBinding1, callbackValue1, localValue1, local274, argument8, elementNode1, hookValue1, restoredHelper1, param1, buttonValue3, buttonParam1, contextParam14, or DistO. Cause: a generic fallback rename hid cryptic identifiers without understanding them. Cure: use producer/consumer usage, JSX shape, call graph, and domain context to assign semantic names; quality-gate.ts --allow-flat flags the common mechanical patterns.
- React Compiler scaffolding left in load-bearing functions. Symptom:
let t = compilerCache.c(N), …; if (t[5] !== n || t[6] !== h …) { …compute…; t[5] = n; … } else { … = t[5]; … } still present. This hurts reading, so fix it in either tier. Cause: strip-react-compiler couldn't unwind a complex cache pattern. Cure: hand-strip the worst offenders — keep the compute branch, drop the cache slots.
- Lowercase JSX component aliases left as HTML intrinsics. Symptom: JSX conversion produces
<b id="…" defaultMessage="…" /> or another lowercase tag that is actually an imported component alias such as FormattedMessage. Cause: jsx-runtime.ts preserved the minified binding name and React now treats it as an intrinsic element. Cure: recover the imported component name from the producer/import map and rewrite opening/closing tags to the PascalCase component.
- Mechanical/batch checkpoint promoted into
src/. Symptom: auto-restore-full.ts or batch-polish.ts output, hash-basename files (button-bq66r8jD.tsx), or --write-target-checkpoints files sitting directly in src/. Cause: skipping the staging → organize → promote discipline and copying raw script output into the deliverable root. Cure: keep all batch/script output in the src/.deobfuscate-javascript/ staging tree; promote a file into src/ only after it meets the promotion bar — readable, friendly kebab filename, clear directory structure (and typed in deep/full mode, where batch restores live). See Output conventions.
- Mechanical checkpoints built, but
src/ left empty / nothing organized. Symptom: _full/checkpoints/ is full (and RESTORE_INDEX.json/manifest exist), but src/ has no promoted semantic tree — the batch executor ran and the restore was declared "done" at the checkpoint. This is a hard failure in both tiers: a checkpoint is an intermediate, never a deliverable. Cause: skipping Step 4's organize → promote loop. Cure: run plan-organize.ts → review/--apply → promote-organized.ts until ledger.ts frontier --stage promote is empty and quality-gate.ts <target-dir> exits 0. The gate now detects this directly (full-restoration-checkpoints-not-drained / -organize-incomplete). See workflows/full-restoration.md → Step 4.
Deep / production mode only
These are typing/structure/compilability requirements. They are not failures in the default readable tier — a readable, well-named untyped file is a valid default deliverable. They apply only when the user asked for deep/typed/production output, where Stage 3's D0 gate and Stage 3's acceptance review enforce them:
- Multi-export bundle delivered as a single file. Symptom: the polished
.tsx is > 1 000 lines, has a export const Foo = { Name1, Name2, … } registry, and lives at <target-dir>/<bundle>.tsx rather than <target-dir>/<bundle>/. Cure: run multi-export-bundle.md. (Default tier may leave a multi-export file flat.)
- Checkpoint shipped as deep completion. If you copy
$WS/polished.tsx, _full/checkpoints/<basename>.tsx, or an auto-restore-full.ts output directly to the public target and call the deep restore done, you are shipping the mechanical checkpoint. The host agent must semantically rewrite it, use semantic final filenames, run the script pre-filter, and then run Stage 3's acceptance review.
- Polished
.js instead of typed .tsx (deep mode). In deep mode, Stage 3's D5 requires a .tsx with a real Props interface (or type alias) on every exported component, parameter/return types where helpful, and ReactNode / ReactElement / event-handler types for JSX slots. function MyRow(props) with no annotation is fine in the default tier; it is a deep-mode fail only. See stage-3-finalize.md → D5.
- Pure icon module shipped with bundle residue. Symptom: a one-SVG file still has
jsx-runtime, __toESM(loadJsxRuntimeRaw()), var jsxRuntime = …, or an untyped export const i = props => <svg …>. Deep-mode cure: run src/application/semantic-finalize.ts --recipe icon; the typed deliverable uses SVGProps<SVGSVGElement>, IconProps, a semantic *Icon name, and a default export.
- Small multi-icon chunk left as one file. Symptom:
expand-XXXX.tsx exports two or more independent SVG icon components in one flat file. Deep-mode cure: split into <target>/<basename>/types.ts, one kebab *-icon.tsx per icon (e.g. expand-icon.tsx exporting ExpandIcon), and <target>/<basename>/index.ts barrel.
- Reusable UI component left mechanically renamed (deep mode). Symptom: a Button/control component still has
s, c, D, O, <spinnerT>, untyped props, or lookup tables without as const. Deep-mode cure: src/application/semantic-finalize.ts --recipe button — semantic lookup-table names, keyof typeof unions, a DOM props interface, forwardRef, displayName, default export. (In the default tier, meaningful local names + clear JSX are enough.)
- Deep request downgraded to quick mode. If the user says "deep", "full", "完整", "深度", or asks to include referenced chunks, running
build-import-graph.ts with a positive --max-lines and then skipping oversized-local chunks is a fail. Use the default no-cap graph, or explicitly tell the user you are producing a partial restore.
- App/feature chunk faced instead of recursively restored (deep/full mode). Symptom: a deep/full restore left a referenced project-local chunk — e.g.
app-shell-*, a feature page/panel/view, a context or hook — as a src/shared/boundaries/*.ts facade or placeholder, and declared the restore complete (the import map marks it boundary/faced). Cause: treating "large" or "imported by many feature chunks" as a facing reason, or stopping recursion at the entry's direct imports. Cure: follow the import graph to the leaves — every reachable project-local feature chunk must reach done, not faced. Drain ledger.ts frontier until it is empty before declaring the deep/full restore complete. Facing is valid only for genuine third-party vendor/runtime leaves (Zod, Statsig, the host/runtime bridge), and even then it is an open boundary to report, not a silent completion. (ledger.ts mark-faced refuses app-entry basenames without --force.) A boundary that is actually a known npm package is not "faced" — resolve it to a bare re-export shim (make-facade.ts --reexport); see Boundary lifecycle.
- Full/deep completion inferred from a narrow audit. Symptom:
src/shared/boundaries has few obvious placeholder files, or IMPORT_MAP.json says many chunks are done, so the whole restore is declared complete while composer-*, local-conversation-*, thread-*, review-*, or *-page / *-panel chunks are still mechanical. Cure: run quality-gate.ts <target-dir> over the whole target; the full-restoration coverage gate reads the manifest plus legacy/current import maps and refuses app-feature @ts-nocheck, export declare const, empty export {}, mechanical-readable-restored, unresolved oversized-local, and missing Stage 3 acceptance/finalized evidence.
- Deep restore shipped without the Stage 3 acceptance review. Symptom:
promote-final.ts exited 0, files copied to the target, deep task declared done — but no one read them end-to-end. The script gates miss grammatical-but-meaningless names, missing Props interfaces, missing forwardRef + displayName, missing as const, untyped props. Only an end-to-end read against the quality bar — the host agent's own self-review, or an optional independent sub-agent — can accept a deep deliverable. "No sub-agent available" is never a reason to skip it: the host does the read. (The default tier needs no LOOP; an optional naming-only self-review is enough.)
Tools at a glance
Full Restoration — multi-file orchestration (above Stage 1/2/3)
| Tool | Purpose | Run when |
|---|
src/domain/check-entry.ts | Sanity-check a restoration entry before building the graph: reads index.html, the entry's local fan-out (out-degree), and how many siblings import it (in-degree). Flags a transitive vendor leaf mistaken for an app entry (exit 3). build-import-graph.ts runs it automatically and warns; run standalone to confirm the entry first. | Step 0.5 of workflows/full-restoration.md, or any time the entry hash is uncertain. |
src/domain/build-import-graph.ts | BFS from an entry chunk, follow every import/re-export from literal. Stops at npm-leaf chunks recognised by resolve-npm-imports.ts's CHUNK_NAME_REGISTRY. Emits _full/manifest.json (file-level dependency graph + per-file imports/exports + per-file stage status) and stages a _full/files/<basename>/original.js for each local chunk. Calls check-entry.ts first and warns on a vendor-leaf entry (--no-entry-check to skip). | First step of workflows/full-restoration.md. Re-runnable; preserves prior stages + owner fields. |
src/infrastructure/make-facade.ts | Three emitters for a boundary chunk (see Boundary lifecycle): default → typed export declare const … : any facade (type-check only) for a huge vendored/runtime chunk (308-export Zod src, an app-scope/vscode-api runtime, the Statsig SDK); --reexport <specifier> (--export-star / --reexport-named a,b,c) → a bare third-party re-export shim when the chunk IS a known npm package (lodash, react-intl, @radix-ui/*, …), resolving to real @types — a finished deliverable; --passthrough <ref-relpath> → a @ts-nocheck + // TODO: deep-restore module re-exporting the original ref chunk so the app runs before the runtime is restored. Handles reserved-word exports (Zod exports in). --name-map map.json supplies semantic facade names, or bridges { alias → realExport } for reexport/passthrough; --provenance sets the header. | When the graph contains a large vendor/runtime-dominated chunk imported by many feature chunks: --reexport if it is stock third-party (verify not a fork), else default facade / --passthrough. Pair with ledger.ts mark-faced. |
src/domain/build-symbol-ledger.ts | For each kind: local file in the manifest, run extractSymbols, tag each binding with isExport/importedFrom, and assemble _full/ledger.json (per-file symbol checklist + cross-file binding table). Marks each file's stages.extracted = true. | Step 2 of workflows/full-restoration.md. --rebuild blasts prior progress; default keeps done symbols' restoredName intact. |
src/domain/ledger.ts | Status-machine CLI: status, next (suggest one next file × stage), frontier (list every file restorable right now — deps all done or faced — for leaf-first fan-out), claim/release (file-stage O_EXCL lockfile under _full/locks/), mark-done (apply renames.json into the ledger + auto-release lock + propagate to consumers), mark-faced (mark a boundary chunk as faced so consumers stop waiting on it), set-organization (record a chunk's chosen domain + semantic path; flip stages.organized), propagate-cross-file [--auto-fill], reset (roll a stage back to pending). Stages now run extract → rename → polish → finalize → organize → promote; frontier --stage promote lists chunks ready to promote. All writes go through atomic temp+rename. | Throughout the full-restoration loop. One agent per file × stage; multiple agents fan out across the frontier. |
src/application/auto-restore-full.ts | Batch checkpoint executor for a full-restoration workspace: reads _full/manifest.json + _full/ledger.json, generates cross-file import/export renames, merges smart-rename.ts, assigns deterministic fallback names, applies Stage 2 rename, runs Stage 2 polish, rewrites import/export aliases, fixes lowercase JSX component fallback tags, writes flat .tsx checkpoints under _full/checkpoints/ by default, and emits _full/auto-restore-report.json with needsAgentRewrite: true. | Use after graph + ledger initialization when the tree is too large for hand-written renames.json per file. It writes only into the staging tree (_full/checkpoints/) — never into src/. Treat the output as a broad mechanical checkpoint only; then run the organize → promote loop below. Do not pass --write-target-checkpoints — it drops checkpoint copies into src/ root, violating the staging rule; it is deprecated, and the staging copies under _full/checkpoints/ are the ones to organize from. |
src/application/plan-organize.ts | Batch organizer: propose a { domain, semanticPath, recipe, classification } for every local chunk from project-agnostic shape heuristics (icon → shared/icons/, button → shared/ui/, single-export → shared/utils/<kebab>.ts, vendor/runtime → shared/boundaries/; app-features → needs-review for the agent), surfacing fallbackRenameRatio and auto-downgrading colliding paths. Writes editable _full/organize-plan.json; --apply writes approved entries into manifest.organization + stages.organized. | First step of Step 4 in workflows/full-restoration.md, after the checkpoint/rename pass. Review/override (ledger.ts set-organization) before --apply. Optional --domain-map for project-specific domain assignment. |
src/application/promote-organized.ts | Promote orchestrator: drains the promote frontier (producers before consumers). Per chunk: builds the typed deliverable (semantic-finalize recipe, or the agent/checkpoint candidate), writes it at its semantic path, runs the same quality gate, and on pass copies into src/<domain>/, upserts src/IMPORT_MAP.json (path/exports/status: done), rewrites imports of promoted producers, and sets stages.promoted. Gate-fail → rollback + record + continue. Resumable, fleet-safe (promote lock), --dry-run. | The step that carries a batch restore out of _full/ into src/. Run --dry-run first. Re-run after hand-fixing reported chunks. Multiple agents can fan across the frontier. |
Stage 1 — Deobfuscation
| Tool | Purpose | Run when |
|---|
src/infrastructure/detect.ts | Classify obfuscation techniques + recommend next steps | First, on anything you suspect is obfuscated |
src/infrastructure/unpack.ts | Dean Edwards Packer + AAEncode + URLEncode unwrap (iterates layers) | Packed/encoded input. Uses new Function — gated by --no-eval |
src/infrastructure/string-array.ts | Inline Obfuscator.IO _0x... string-array references; remove rotation IIFE + dead array | Output has _0x arrays and indexed lookups |
src/infrastructure/decode-strings.ts | Decode \xNN, \uNNNN, String.fromCharCode(...), and literal atob('base64') | Hex/unicode escapes or constant base64 strings present |
src/infrastructure/simplify.ts | Constant folding, dead-code removal, (0, fn)(...) → fn(...), backtick-template → string, scope-aware literal inlining (loops to fixed point) | After string-array + decode-strings to clean up the residue; also useful standalone on Rollup/Vite output |
src/infrastructure/control-flow-report.ts | Detect while(true){switch} flatteners + opaque predicates — report only, no mutation | Last in Stage 1. Read the report; rewrite by hand |
src/infrastructure/deobfuscate.ts | Orchestrator: runs every Stage 1 pass in order with --skip / --stop-after | One-shot Stage 1. Emits final code + JSON report |
Stage 2 (rename)
| Tool | Purpose | Run when |
|---|
src/infrastructure/sourcemap-check.ts | Detect //# sourceMappingURL=… or adjacent .map; list original sources | Always first — if a sourcemap exists, renaming is wasted work |
npx webcrack <file> | Unpack webpack/browserify bundles into per-module files | Input is one giant bundle (≥ 500 KB or many id: (e,t,n)=>{} modules) |
src/infrastructure/wakaru-normalize.ts | Pre-rename mechanical decompiler — wraps external npx @wakaru/cli (Rust). Recovers ES6 classes, async/await from generator state-machines, optional chaining, ??, for-of, destructuring, TS enums, template literals, let/const (~66 rules the skill's polish lacks) so the rename starts from cleaner code. NOT a deobfuscator and NOT a semantic renamer. Auto-skips (passthrough) when the binary is absent. --level standard default; --level minimal for fidelity-critical/untrusted; aggressive only with verification; --dce opt-in; --unpack single-bundle only. | Readable tier: default-on, after sourcemap-check + detect (+ Stage 1 if obfuscated), before extract.ts. Skip when a usable .map exists. Deep/full mode: per chunk body only, never --unpack a split tree (re-derive the graph after). |
src/infrastructure/extract.ts | Parse JS, emit every binding + scope context as JSON. Filter flags (--kind, --scope-kind, --min-refs, --only-cryptic, …) trim the list for huge files. | Step 1 of rename pipeline |
src/application/plan.ts | Group symbols into ordered rename batches + emit CHECKLIST.md for stage-by-stage work | When symbols.json has > ~200 entries — break work into batches |
src/infrastructure/smart-rename.ts | Deterministic rename for the mechanical cases — React component props params, event handlers, .map/.reduce callbacks, hook returns. Emits id-keyed JSON compatible with apply.ts. | Run before judgment-naming params (Pass 3); covers ~80 % of the boring cases. Detail: reference/naming-heuristics.md |
| You (the agent) | Read symbols, write new names to JSON (one batch at a time for big files) | Step 2 of rename pipeline; handle whatever smart-rename.ts couldn't decide |
src/infrastructure/apply.ts | Apply rename map; scope-aware, collision-safe, reserved-word safe | Step 3 of rename pipeline |
npx prettier --write | Reformat output | After apply or polish, if you want different formatting than @babel/generator gave |
External tools (not bun deps): wakaru-normalize.ts shells out to npx @wakaru/cli@1.5.0 (or a global wakaru / prebuilt platform binary). wakaru is a Rust binary, not a Babel library — it is invoked as a subprocess (like webcrack/prettier) and is not added to package.json. The wrapper degrades gracefully (passthrough + stderr note) when it is unavailable, so the core bun+Babel path never depends on it.
Stage 2 (polish, post-rename)
Reading-aid subset (--fast, default tier): strip-react-compiler, simplify, jsx-runtime, inline-defaults, normalize-exports — these change how the code reads. Import-resolution tail (deep mode only): react-shim-elim, resolve-npm-imports, npm-cjs-shim-elim, dead-shim-elim — these only make imports resolve against node_modules. The default tier runs polish.ts --fast and stops at the subset.
| Tool | Purpose | Run when |
|---|
src/infrastructure/strip-react-compiler.ts | Strip the React Compiler memoization scaffolding (let cache = react.c(N) + every cache[i] === x ? readBranch : computeBranch conditional) | Renamed file contains react.c(N) or (0, x.c)(N) cache scaffolding |
src/infrastructure/simplify.ts | Same script as Stage 1; post-rename collapses (0, x.y)(...) → x.y(...), `svg` → "svg", restores {a: a} → {a}, collapses {true:"X",false:""}[cond?"true":"false"] → cond && "X" | Renamed file still contains (0, fn)(...) patterns, backtick string literals, or redundant long-form properties |
src/infrastructure/jsx-runtime.ts | Convert jsxRuntime.jsx("svg", { ...props, children: ... }) and jsxs / Fragment calls back to JSX syntax | Output of a React build (Vite, Rollup, esbuild) with JSX compiled to runtime calls |
src/infrastructure/inline-defaults.ts | Inline let X = p === undefined ? D : p and let X = p ?? D patterns into the parent destructure pattern as defaults ({ p = D }); merge let A, B; + ({A, B} = expr); into let { A, B } = expr; | Output has separate destructure + resolved-default declarations |
src/infrastructure/normalize-exports.ts | Collapse var X = expr; export { X as Y }; (and function/class variants) into export const Y = expr; | Output has tree-shaken aliased exports — common in Rollup/esbuild bundles |
src/infrastructure/react-shim-elim.ts | Collapse common React CJS namespace shims (toESM(loadReact())) to import React from "react" and prune the now-unused helper/runtime imports. | After JSX conversion/normalization, before npm import resolution, when a restored file still imports both a runtime helper chunk and a jsx-runtime chunk. |
src/infrastructure/resolve-npm-imports.ts | Rewrite hashed chunk imports (./clsx-DDuZWq6Y.js, ./jsx-runtime-XXXX.js) to bare npm specifiers (clsx, react/jsx-runtime). Splits imports when a ./shared re-export chain mixes npm and local exports. Renames the cross-file shim binding (_React → React) | Rollup/Vite bundles where vendored npm packages live alongside project code in the chunk graph |
src/infrastructure/npm-cjs-shim-elim.ts | Collapse leftover toESModule(defaultImport(), 1) namespaces after npm import resolution, replacing the namespace binding with the already-resolved bare npm default import. | After resolve-npm-imports.ts, before dead-shim-elim.ts, when files still have wrappers such as var dom = toESModule(ReactDOM(), 1). |
src/infrastructure/dead-shim-elim.ts | Final cleanup pass — iteratively removes top-level var X = lazyY() declarations whose binding has 0 references and whose callee is itself an unused import specifier (Vite/Rolldown's lazy-namespace pattern). Drops the import specifier when its last reference disappears, and the whole import declaration when it becomes empty. Also strips a trailing //# sourceMappingURL=… line on finalize since it points at a stale .map. | Always last in polish.ts. Quietly does nothing on hand-written files. Loops to a fixpoint so cascades (e.g. var jsxRuntime = loadJsxRuntime() → unused after jsx-runtime.ts rewrites the calls → its callee loadJsxRuntime import also unused → both gone) collapse in one run. |
src/infrastructure/format.ts | Shell out to bunx prettier --write (or npx prettier --write if bunx is unavailable). Accepts a file or directory; defaults to **/*.{ts,tsx,js,jsx,mjs,cjs} for directories | Final pipeline step, after polish.ts (and after the multi-export split when applicable) |
src/infrastructure/polish.ts | One-shot Stage 2 polish: strip-react-compiler → simplify → jsx-runtime → inline-defaults → normalize-exports → react-shim-elim → resolve-npm-imports → npm-cjs-shim-elim → dead-shim-elim. --rename runs smart-rename + apply as a pre-step (so polish.ts <file> --rename --fast --format is the default-tier one-shot). --fast skips the import-resolution tail (react-shim-elim, resolve-npm-imports, npm-cjs-shim-elim, dead-shim-elim) — that tail is deep mode only (it resolves npm imports, a compilability concern, not readability). Defaults to --prefer local; pass --format to chain prettier. | --rename --fast after analysis for the readable tier; the full chain (no --fast) in deep mode to resolve npm imports |
Stage 3 (semantic finalize)
| Tool | Purpose | Run when |
|---|
src/application/semantic-finalize.ts | Recipe-based finalizer for high-confidence semantic modules. --recipe icon turns one or more exported SVG JSX components into typed IconProps modules; multi-icon chunks become a directory plus index.ts barrel. --recipe button turns the common Button/control chunk into named variant tables, typed props, forwardRef, displayName, and default export. --rewrite-imports --import-map map.json updates consumers from bundle aliases (t, n) to semantic named imports and split barrel paths. | After polish.ts, before final formatting. Use it for pure SVG icons, small multi-icon chunks like expand-XXXX, and Button/control chunks like button-XXXX. |
src/application/plan-split.ts | Draft a split-plan.json for multi-export polished modules by reading exports and registry objects. The generated plan is a starting point, not a substitute for semantic grouping. | Before split-bundle.ts on any file with ≥ 3 exports, a registry object, or > 1000 lines. |
src/infrastructure/split-bundle.ts | Execute a reviewed split plan: extract top-level bindings into a directory, generate cross-file imports for simple helper references, emit an index.ts barrel, and rebuild registry-object exports. | After Stage 2 polish / semantic naming and before final formatting/gating for multi-export bundles. |
src/application/promote-final.ts | Gate-before-copy helper: runs quality-gate.ts on a candidate file/dir, writes an optional report, and only then replaces the public target. It is not final acceptance; Stage 3's acceptance review still follows. | Use instead of direct cp/manual copy when moving a host-agent rewritten candidate into the user's target directory. |
src/domain/quality-gate.ts | Hard finalization gate: fails on missing provenance headers, too many cryptic params/bindings, cryptic public export names, generated fallback names, dense one-letter references, stale runtime/compiler residue, suspicious lowercase JSX component tags, or flat files that should be split. On a full-restoration target, it also audits _full/manifest.json plus IMPORT_MAP.json / legacy chunks + boundaries maps and rejects app-feature chunks that are missing, mechanical, placeholders, facades, @ts-nocheck, oversized-local, or not Stage 3 accepted/finalized — and a third-party npm boundary still left as an any-facade (full-restoration-npm-boundary-not-resolved; convert via make-facade.ts --reexport). --check-format runs prettier --check and fails unformatted files (soft-skips when prettier is unavailable). --vendored (and auto-detected make-facade.ts output) relaxes semantic checks only for faithful vendored modules / boundary facades. | Run before copying a polished file to the target and again after final split/format. For whole-tree deep/full restores, the final run must be quality-gate.ts <target-dir> --check-format, not a boundary-only check. A non-zero exit means the deliverable is partial. Use --allow-missing-provenance only for known intermediate candidates; --vendored only for faithful vendored modules / boundary facades. |
Import map default: when rewriting imports for files that belong to the same restored project, pass the shared project map by default:
IMPORT_MAP="src/IMPORT_MAP.json"
bun <skill-dir>/src/application/semantic-finalize.ts --rewrite-imports <file-or-dir> \
--import-map "$IMPORT_MAP"
Append new hash-to-semantic entries to that file as the project grows, and reuse it across future chunks from the same app so consumers do not accumulate duplicate import-map files. The map is always src/IMPORT_MAP.json, regardless of which entry triggered the restore.
For delta/boundary replacements, derive import rewrites from the existing project map instead of hand-inventing paths. Semantic local imports take priority over hashed paths; unresolved runtime/vendor boundaries should continue to import the documented facade path until that boundary is explicitly restored.
Stage 3 (acceptance review) — deep mode; default = optional naming review
| Tool | Purpose | Run when |
|---|
src/application/prepare-stage-e-review.ts | Batch the deep-mode public tree into review packets under <target>/.deobfuscate-javascript/_stage-e: index.json, batches.tsv, README.md, and one batch-XX.md per batch with the acceptance checklist and inlined public source files. Excludes hidden checkpoints. --verify checks the packets still cover the current public tree exactly once. Optional aid — useful whether the host agent reads the packets itself or hands them to a sub-agent. | On a large deep-mode tree, after quality-gate.ts passes, to batch the read. --max-files / --max-bytes keep batches readable. Not required — for small targets, read the files directly. |