diff --git a/docs/PORTING-GUIDE.md b/docs/PORTING-GUIDE.md new file mode 100644 index 000000000..ee7f476a4 --- /dev/null +++ b/docs/PORTING-GUIDE.md @@ -0,0 +1,124 @@ +# Porting guide + +Conventions every porting task follows. The bar is byte-equal behavior against +the JS goldens; read this before touching a crate. + +## Sources of truth + +- JS source: the public repo checkout (`../impeccable-second` locally, + `../impeccable` in CI; env `IMPECCABLE_PUBLIC_REPO` overrides). +- Contract: `docs/CLI-CONTRACT.md` in the public repo. Every verb's argv, env, + stdout/stderr, exit codes, files, network. +- Goldens: `tests/oracle/` in the public repo. `node tests/oracle/run.mjs` + with `IMPECCABLE_BIN=` replays every case and diffs. +- Function vectors: `tests/oracle/vectors/calls//.jsonl`, generated + by `node tests/oracle/vectors/record-calls.mjs` (needs the JS engine present). + `crates/core/tests/vectors.rs` replays them. + +## Rules + +1. Port the behavior, including bugs. Mark a knowingly-odd port with a + `// JS-PARITY:` comment naming the JS function and what it does. Never + change a threshold, message string, or ordering while porting. +2. Improvements go through `tests/oracle/DELTAS.md` in the public repo, one + line per case id, after review. Until then the golden wins. +3. JS number and string semantics live in `impeccable_core::js` + (`number_to_string`, `to_fixed`, `parse_float`, `parse_int`, `trim`, + `to_lower_case`). Use them everywhere output text is produced; never + `format!("{}", f64)` a value that ends up in stdout or a snippet. +4. Regexes: `regex` crate. JS features it lacks (lookbehind, backreferences, + `\b` on unicode) get hand-written equivalents with a test. +5. Field order in emitted JSON matters. Use serde structs with the JS field + order, or `serde_json::Map` built in order (`preserve_order` is on). +6. Every crate that produces stdout does so through the `cli` crate's writer so + trailing newlines and stdout/stderr split match the contract exactly. +7. Exit codes are part of the contract. `std::process::exit` only in `cli`. +8. No panics on user input. `panic = "abort"` is set in release; a panic is a + crash for the user. +9. Keep `core` free of I/O and `std::process`; it compiles to wasm. + +## Crate boundaries + +| crate | owns | must not | +|---|---|---| +| core | pure rules, color, registry, findings, inline ignores; the browser rules over the `browser::dom::Dom` probe trait | touch fs/process/network | +| html | static engine: DOM model, cascade, adapters, text/regex engine, design system, visual | spawn browsers | +| browser | CDP client, browser discovery, page injection | parse HTML itself | +| context | context/doctor/pin/briefs/critique/palette/concept-seed/question/image/embed/signals/csp | | +| hook | hook, hook-before-edit, hooks admin | | +| live | live server and all live verbs | require Node except the documented Svelte/Vue exception | +| cli | argv router, exit codes, stdout writer | business logic | +| wasm | wasm-bindgen exports over core; `JsDom` (the probe over JS imports); see docs/WASM-BUNDLE.md | | + +## Workflow per module + +1. Read the JS file top to bottom. List exports and internal helpers. +2. Port helpers first, then exports, keeping names (`snake_case`) and a doc + comment `/// JS: #`. +3. Run the vector test for the module until green. +4. If the module feeds a verb, build the verb, run the oracle for that verb's + prefix, fix until green. +5. Commit with a message that names the JS module ported and the vector/oracle + result. End with `Prepared with AI assistance (Claude Code).` + +## Windows + +The JS ran on Node, which picks `path.win32` on Windows and `path.posix` +elsewhere. `impeccable_common::jsp` does the same: the top-level functions +(`join`, `resolve`, `relative`, `dirname`, `basename`, `extname`, +`normalize`, `is_absolute`, `SEP`) dispatch on `cfg!(windows)`; the two +implementations sit under `jsp::posix` and `jsp::win32`, each with tests whose +expected values came from `node -p "path.win32.X(...)"`. Rules: + +1. Use `jsp::*` wherever the JS used `path.*`. Use `jsp::posix::*` only where + the JS wrote `path.posix.*` (tanstack-adapter's dirname, live's + `posix_normalize`). +2. Where the JS did `.split(path.sep).join('/')` (display paths, glob matching, + manifests another platform may read), the port calls `jsp::to_posix`. + Where the JS built a prefix with `path.sep`, use `jsp::SEP` / `SEP_CHAR`. + Where the JS split on a literal `'/'`, keep the literal: that is the + behavior on Windows too, bugs included. +3. `resolve` / `relative` take an explicit cwd where Node read + `process.cwd()`. Callers that only pass absolute paths may hand in `"/"`; + on Windows that is the drive-less root, exactly what `path.win32.resolve('/')` + yields with no cwd for the device. +4. Process plumbing that differs per OS lives in `impeccable_common::proc`: + `kill0` / `pid_reachable` (`process.kill(pid, 0)`; OpenProcess on Windows), + `terminate` (`process.kill(pid)`), `detach` (`spawn({ detached: true })`: + `setsid` on unix, `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP` on + Windows, so no console window), `hide_window` for short-lived helpers, + `on_interrupt` (`process.on('SIGINT'|'SIGTERM')`; a console control + handler on Windows), `shell` (`{ shell: true }`: `/bin/sh -c` or + `cmd.exe /d /s /c`), `node_exe` (`node.exe`), `tool_on_path` + (`which` / `where`). +5. Chrome discovery already carries the Windows candidate list; the launcher + for Windows is `launcher/impeccable.cmd`. + +Cross-checking from macOS or Linux: `rustup target add x86_64-pc-windows-msvc` +then `cargo check --workspace --target x86_64-pc-windows-msvc`. `ring` +(under `ureq`'s rustls) compiles C for the target, so point `cc` at a clang +with the Windows CRT/SDK headers (`cargo install xwin && xwin --accept-license +splat --output ~/.xwin`), e.g. + +``` +CC_x86_64_pc_windows_msvc=clang \ +CFLAGS_x86_64_pc_windows_msvc="--target=x86_64-pc-windows-msvc -Wno-everything -I$HOME/.xwin/crt/include -I$HOME/.xwin/sdk/include/ucrt -I$HOME/.xwin/sdk/include/um -I$HOME/.xwin/sdk/include/shared" \ +AR_x86_64_pc_windows_msvc= \ +cargo check --workspace --target x86_64-pc-windows-msvc +``` + +CI runs `cargo test --workspace` on `windows-latest` (`.github/workflows/ci.yml`), +which is the only real Windows execution the project has. What a check +cannot show and only that job (or a Windows machine) can: the live server's +`--background` spawn surviving the parent's console, Ctrl-C reaching +`on_interrupt`, `cmd.exe` quoting for `proc::shell` scripts, and the Svelte +bridge finding `node.exe`. + +The oracle goldens in the public repo were recorded on macOS: `` / +`` masks assume `/`-separated paths, `tests/oracle/lib.mjs` compares +`stdout` byte-for-byte, and several cases stage posix-only workspaces +(`core.hooksPath=/dev/null`, `sh` steps). Running the oracle on Windows would +need the harness to normalize `\` to `/` inside ``/``-masked paths +before diffing, drive-letter-aware masks, and a Windows recording pass for +the verbs whose output embeds `path.sep` (hook file lists, live manifests). +That work belongs to the public repo and is out of scope here. diff --git a/docs/RUNTIME-ENV.md b/docs/RUNTIME-ENV.md new file mode 100644 index 000000000..7d9965cd2 --- /dev/null +++ b/docs/RUNTIME-ENV.md @@ -0,0 +1,26 @@ +# Runtime environment the binary reads (context crate) + +The JS scripts learned two things from their own file location that a single +binary cannot: which harness built them (`lib/provider.mjs`) and where the +skill's `reference/` and `SKILL.md` live (`../reference`, `../SKILL.md`). +`crates/context/src/provider.rs` resolves both at run time. + +| Variable | Meaning | Default | +|---|---|---| +| `IMPECCABLE_SKILL_DIR` | The skill directory (holds `SKILL.md`, `reference/`, `scripts/`). Used for the native platform references `context` inlines, the local version in the update check, `pin`'s `command-metadata.json`, and `concept-seed`'s default catalog dir (`/scripts`). | Walk up from the executable's directory until a directory containing `reference/ios.md` is found (the binary ships at `/scripts/bin/-/`). None if nothing matches: native refs are then skipped silently, like a missing file in the JS. | +| `IMPECCABLE_PROVIDER_ID` | The build provider id (`claude-code`, `codex`, `cursor`, ...). Selects the hook manifest paths `context` and `doctor` inspect and the `$`/`/` command prefix (`$` only for `codex`). | Derived from the skill dir's harness folder (`/.codex/skills/impeccable` -> `codex`); otherwise `source`, which is what the JS reads in a source checkout. | +| `IMPECCABLE_SELF` | How to spell this binary in printed commands where the JS printed `node /