mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 17:16:46 +03:00
engine-v0.1.0
632
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
44a25fa241 |
Release: publish the npm platform packages in one command
bun run release:platform-packages downloads each engine-v<ENGINE_VERSION> binary with its .sha256 sidecar (required; nothing unverified is published), stages the package from cli/platform-packages/<target> with the version stamped, the executable at bin/ and the repo LICENSE, and runs npm publish --access public. Targets already on the registry are skipped so a re-run resumes after a partial failure. Preconditions: package.json pins equal ENGINE_VERSION and npm is logged in. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
a2cd029548 |
Live: a discard releases every wrapper it hid
Bugbot on #720: the non-restoreOriginal discard now hides every matching wrapper, but the delayed fallback still released only the first querySelector hit. A target inside a `.map()` renders one wrapper per item, so the rest stayed at display:none and their original content never came back on the static and missed-HMR flows that fallback exists for. The hide, the existence checks, and the release now all speak about the same set. discardedWrappers(sessionId) is the one place that collects it; releaseDiscardedStaticWrappers takes the stylesheet down once and releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId argument and just unwinds the node it is given. The HMR-ownership decision still reads the first wrapper, which is fair: duplicates all render from one source element, so ownership is uniform across them. The reload branch is unchanged because a reload restores every original at once. Covered by a source-shape test rather than an e2e scenario: hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime fixture, so all of them take the watcher path and none can reach the static release. The existing framework-ownership guards in the same file move to the new shape and keep their intent, including the one that says only non-discard cleanup may blank the wrapper while waiting for HMR. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
d2750e5198 |
Live: every active-session wrapper lookup goes through the resolver
Cursor Bugbot on #720: findVariantsWrapper alone was not enough. resolveBarAnchor, the visible-variant element, mountedParameterCount, readVisibleVariantFromDOM, showVariantInDOM, the source injection, and the whole accept path still took the first [data-impeccable-variants] match, so in the relocated-wrapper case Tune never bound and the bar kept anchoring to the empty scaffold even after the resume reached CYCLING. Thirteen call sites now resolve through findVariantsWrapper. The resolver split in two so a missing id cannot silently widen the lookup to any session: findVariantsWrapper(sessionId) returns null without an id, and findAnyVariantsWrapper() is the entry point for the two resume paths that have no id yet. Both share pickPopulatedVariantsWrapper, which is the old querySelector whenever there are fewer than two matches. Discard cleanup now hides every duplicate wrapper rather than the first, since a target inside a `.map()` renders one per item and hiding one left the rest of the discarded variants on screen. What still takes a raw first match is deliberate: bare existence checks, selector strings for stylesheets and observers (which want to cover every match), querySelectorAll sweeps, the parsed source document, and the Svelte component wrapper, which holds no variant children at all. The source-shape test pins that exact set by name, so a new raw lookup fails until it is either routed through the resolver or justified there. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
670b1c3fb8 |
Live: the shader teardown can no longer race its own construction
The new cycling assertion caught a real defect on CI: vite8-react-insert
reached CYCLING with #impeccable-live-shader still painted over the page.
showShaderOverlay is async. It appends its canvas synchronously, then
awaits createImageBitmap and finishes the GL setup before it publishes
shaderState. hideShaderOverlay returned early on a null shaderState, so a
teardown that landed inside that window did nothing, and the construction
then published itself over a session that had already left GENERATING,
with no teardown left to run. The scroll tick kept repositioning it,
which is why the CI page.html shows the canvas sized from the capture
rect but styled to the cycling anchor.
Every teardown now bumps a shader epoch before it does anything else, and
a construction pins the epoch it owns and abandons its canvas (releasing
the GL context) at every point past an await and before any publish,
including both bitmap-fallback publishes. A teardown also drops a shader
node that no shaderState owns, so an already-orphaned canvas cannot
survive one.
Reproduced by widening the append-to-publish window: with a 400ms delay
after uiAppend, vite8-react-insert failed with the CI error and the probe
showed the teardown arriving at CYCLING with shaderState still null.
The same run passes with this change, as does a 1500ms window on insert
and plain. Locally that window is about 4ms, which is why it only showed
on a slower runner.
The four remaining setLiveState('CYCLING') sites that did not lower the
loader now do: the SSE done handler (the one route that can reach CYCLING
from GENERATING), the Svelte republish remount, and the two accept
failure recoveries.
The e2e assertion already waits up to 5s for the shader to clear, so it
was never racing a legitimate teardown; it is left as it is.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
|
||
|
|
26cb1f0193 |
Live server: stop ends the process, SSE skips the mutation lane
Two Rust-only regressions found while investigating #719, both of which can leave a tab waiting on a broadcast that never comes. /stop ran shutdown() but never set shutting_down, and the accept loop only breaks on that flag or a signal, so a stopped server kept its port and kept answering while its server.json was already deleted. The next `impeccable live` then booted a second server on another port and a tab could reattach to the zombie. Node's shutdown() ended in process.exit(0). The flag is now set after the response is written, so `stop` still reads "stopping" instead of a reset connection, and the accept loop (already non-blocking) exits on its next pass. GET /events took a turnstile ticket and waited its turn before registering, even though handle_sse releases that ticket two statements later and needs no arrival ordering. A peer that stalls mid-request holds the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s on Node); broadcast is fire-and-forget, so a `done` landing in that window reaches an empty client set and is gone. Registering early can only make a stream see more broadcasts. The one cost is that the connected frame's activeSessions snapshot may miss a mutation still in flight, and the browser treats that snapshot as a hint. Preflights still take a turn: answering those out of order reorders the POSTs the browser issues behind them. The route classification moved into releases_ticket_up_front so it can be unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped server's pid is gone and its port is free. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
5e626e2d9f |
Live: the loader now hands off when the resume is the arrival
The overlay could sit in its generating shader over a DOM that already held all three variants, and only a page refresh cleared it (#719). The server's generation preflight runs live-wrap with --defer-source-write, so the wrapper and every variant reach the DOM in a single HMR batch. The deferred-wrapper scout is constructed at init and the variant MutationObserver at Go; observer callbacks run in construction order, so on that batch the scout resumes first and resumeSession, not the observer, is the transition into CYCLING. It set the state and the bar but never called hideShaderOverlay(), so the frozen capture of the original stayed painted over the variants. It also reported browser_resumed, which does not count as publication progress, and then disconnected and re-created the observer, dropping the records that observer had already queued for the same batch, so variants_ready never fired at all. resumeSession now finishes the same transition the observer does (shader down, inline edit off, insert session finalized, params panel rebuilt) and reports variants_ready when it already holds every variant. The deferred scout names itself in the journal as browser_resumed_deferred_wrapper, so the two resume paths are no longer indistinguishable. Wrapper resolution goes through findVariantsWrapper, which prefers a wrapper that actually holds non-original variants. A target inside a .map() renders one wrapper per item, and an agent that relocates the wrapper out of the shared primitive live-wrap scaffolded leaves an empty one behind; first match could pin either and strand the session at 0/N. With zero or one match this is the querySelector it replaces. Tests: waitForCycling now asserts the generating shader is gone once the bar cycles, across every runtime fixture (it failed on vite8-react-plain before this change and passes after), marked no-retry so the reload recovery cannot hide it. Source-shape tests pin the transition, the variants_ready report, and the wrapper preference. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
3a857af9ea |
Shim test: run from a staged copy and prove the download happened
The three fail-closed cases cleared IMPECCABLE_BIN and pointed IMPECCABLE_HOME at a temp dir, but locate() prefers an installed @impeccable/cli-<os>-<arch> before the cache or a download. Those platform packages ship with every engine release and are a merge prerequisite, so as soon as one is installed under the repo the cases would resolve it and go green without fetching anything. Confirmed by hand: with a platform package staged in node_modules, running the shim against an unreachable download base still exits 0 from the package. The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a copy of the repo's package.json, with no node_modules on the lookup path above it, so require.resolve of the platform package fails the way it does on a machine without the optional dependency. Production code is unchanged; there is no test-only branch in the shim. The fixture server also records every request now, and each download case asserts the asset and sidecar URLs were actually requested, so a future lookup shortcut fails loudly instead of passing on an untested path. A sixth case installs a fake platform package next to the staged shim and asserts the shim prefers it with the server untouched, which pins the precedence the other cases depend on being absent. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
689e5150d9 |
Vectors: drop the 12,208 byte-identical repeat lines
The recorder deduplicated by arguments per run, not across runs, so the frozen call snapshot arrived with 12,208 lines (43% of 28,266) that repeat an earlier line byte for byte. Every one re-asserts what its first occurrence already asserts, and `crates/core/tests/vectors.rs` replays line by line with no count anywhere, so removing them changes nothing it checks: the replay still reports 8,321 pass, 0 fail. Duplicates were removed with `awk '!seen[$0]++'`, keeping first occurrences and file order, and every changed file was checked to equal that transform of its old contents. No line was added, reordered, or rewritten, and no vector file gained or lost a distinct call. The tree drops from 9.2 MB to 5.7 MB. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
80ef6bf06e |
Oracle fixture: declare the vite plugin the web workspace imports
`live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react` but the workspace's package.json listed only `vite`. No oracle case installs or evaluates that config (the three `live-boot-workspaces-*` cases stop at root resolution), so the fixture was never wrong at runtime, only self-contradictory to read. Adding the devDependency keeps the goldens byte-equal. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
884c9aaf3d |
npm shim: refuse a download with no verifiable sidecar
The skill launcher and `impeccable install` both fail closed when a release binary's `.sha256` sidecar cannot be fetched or carries no hash: they refuse rather than cache an unverified binary. The npm shim did not. It only compared when a hash was present, so a 404, an empty sidecar, or a truncated one all wrote the payload straight into `~/.impeccable/bin/<version>/` and exec'd it. It now refuses in the same cases, with wording that matches the launcher, and writes nothing until the hash matches, so a refusal leaves the cache dir empty. IMPECCABLE_BIN and the optional-dependency lookup are untouched: neither downloads. tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP server and covers missing, empty, and mismatched sidecars, plus the matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail against the old shim. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
a91c226b2b |
Merge origin/main into rust-swap (#718 live-server leak guard)
Brings the leak guard from #718 onto the branch and makes its guarantee hold
for the Rust engine instead of the Node scripts it was written against.
Conflicts and how each was resolved:
- tests/live-poll-stream.test.mjs, tests/live-server.test.mjs,
tests/live-target-context.test.mjs (modify/delete): kept deleted. They drove
skill/scripts/live-server.mjs, which does not exist here; the verb behavior
they covered is the oracle's job now. Their entries came out of
test-suites.mjs along with the rest of main's live list, which is Node-script
coverage this branch already retired.
- scripts/test-suites.mjs: took main's two new entries that still apply,
process-group.test.mjs into core and live-server-leak.test.mjs into live, plus
the infra trigger patterns for the three new scripts/lib modules. Dropped
main's pin.test.mjs (no such file here).
- package.json: kept test:cleanup, dropped test:cli-e2e (no cli-e2e suite here).
- scripts/run-tests.mjs: rewritten to hold both sides rather than picking one.
From #718: the createGroupShutdown state machine, the per-suite run-id marker
env, the post-suite leak check, and --cleanup. From
|
||
|
|
4c5243fcd4 |
Tests: stop the harness leaking live-server processes (#718)
* Tests: stop the harness leaking live-server processes
Nothing owned a live server past the exit paths JavaScript can observe. The
live unit tests spawn the server as a direct child and stop it with an HTTP
/stop plus proc.kill() inside an after() hook; the e2e session and the
target-context tests boot it through `live-server --background` / live.mjs,
which spawns a detached, unref'd daemon that only the `stop` verb ever ends.
A POSIX child does not die with its parent, and a detached daemon is orphaned
to pid 1 from birth, so any exit that skipped teardown (a node:test timeout, a
SIGKILL of the runner, a Ctrl-C, an assertion that threw before the hook) left
the server listening on a fixed live-suite port for good. scripts/run-tests.mjs
did not compensate: it used blocking spawnSync, so no signal handler could run;
it left suite commands in its own process group with nothing that could kill
that group; and it never checked afterwards whether anything survived. Days of
local runs accumulated 197 orphans on one machine, the oldest four days old,
until `bun run test:live` could not claim its ports.
The fix is structural rather than a cleanup sweep bolted on the end, and it is
deliberately implementation-agnostic so it holds for the Node scripts here and
for the Rust `impeccable live-server` on rust-swap:
- tests/lib/live-servers.mjs. armLiveServerReaper(), called once at module
scope by every test file that starts a server, stamps the process env with a
unique marker, installs exit and signal handlers, and spawns a detached
reaper holding a pipe to the process. SIGKILL the process and the pipe closes,
the reaper wakes on EOF and kills the servers carrying that marker. That is
the one case no in-process cleanup can reach. trackServerChild() also
registers direct children (live servers and fixture dev servers) so the
ordinary exits are a cheap kill by handle.
- scripts/lib/live-server-processes.mjs. The scan and kill primitives, shared
by the reaper and the runner. Processes are matched by the environment marker
the harness exported, never by name or port, so a sweep can only ever reach a
server this repo's tests started.
- scripts/run-tests.mjs. Each suite command now runs as its own process-group
leader with SIGINT/SIGTERM/SIGHUP forwarded to the group, and after every
suite the runner checks for live servers carrying that suite's run id. A
survivor is killed and fails the run, so the next leak surfaces in the run
that caused it instead of on a laptop days later. IMPECCABLE_SKIP_LEAK_CHECK=1
bypasses it. `bun run test:cleanup` sweeps leftovers from earlier runs.
- tests/live-server-leak.test.mjs pins the guarantee: it boots a real server
under a process it then SIGKILLs, and fails if the server outlives it. With
IMPECCABLE_NO_TEST_REAPER=1 the test fails, which is what makes it a
regression test rather than a tautology.
Verified: bun run test:live green with zero survivors; scoped live-e2e
(vite8-react-plain) matches pristine main test for test; the SIGKILL repro goes
from 2 orphans to 0; SIGINT and SIGKILL of the runner itself both leave nothing
behind; bun run build green.
Fixes #717
AI assistance: prepared by Claude Code under pbakaus's direction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Review fixes: scope the sweep to whole env entries only
Five review findings on #718, all in the matching layer that decides which
processes a sweep may touch.
The repository-path fallback is gone (Greptile P1). `bun run test:cleanup`
passed REPO_ROOT to findLiveServers, which then also matched any live-server
command line under the checkout, marker or not. A developer running
`impeccable live` in this repo has exactly that command line, so the cleanup
could have killed their own session. The PR promised matching on the exported
environment marker and nothing else; now it does. The cost is that a server
from a run predating the marker is no longer found and has to be killed by
hand, which is the right trade.
Environment entries are compared whole on macOS and BSD (Greptile P1). `ps -E`
flattens the environment into the command column, and that line was searched
with a plain substring test, so IMPECCABLE_TEST_REPO=/work/impeccable also
matched /work/impeccable-copy and one checkout's cleanup could reach a
neighbouring checkout's servers. envLineHasEntry() now requires the marker to
start an entry (line start or whitespace) and to end one (line end, or
whitespace followed by the next KEY=), which is the same whole-entry
comparison the Linux /proc branch already did. Six unit tests cover it,
including the adjacent-path negative case, and a live probe against real
`ps -E` output confirms an exact repo matches while /work/impeccable-copy and
a run-id prefix do not.
The SIGKILL regression test now skips on win32 with a stated reason (Copilot).
The reaper is a POSIX mechanism and armLiveServerReaper() does not arm it
there, so the test asserted a guarantee Windows does not make yet.
Signal exits use the shell convention 128 + signum in both the runner and the
test helper (Copilot, two threads). SIGHUP returned 143; it is 129. Read from
os.constants.signals rather than a hand-written table.
Verified: leak test 7/7 (2 guard, 5 matcher); bun run test:live 895 tests, 0
fail, 0 survivors; scoped live-e2e (vite8-react-plain) 3 pass / 1 fail,
matching pristine main; SIGKILL repro 3 servers up, 0 after; bun run build
green.
AI assistance: prepared by Claude Code under pbakaus's direction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Review fix: make marker values opaque so the matcher has no ambiguous case
Greptile's follow-up P1 on the parser was right, and the parser was the wrong
place to answer it. envLineHasEntry ended an entry at "whitespace followed by
the next KEY=", so a checkout path that extended another one with whitespace
plus a KEY=-shaped token still defeated it, which is exactly the ambiguity the
docblock admitted to. A format that cannot be parsed unambiguously should not
be handed ambiguous input.
So the fix is at the source: no marker value is a path any more. IMPECCABLE_TEST_REPO
now carries repoMarker(), the first 16 hex characters of the sha256 of the
checkout's real path, and the runner and the cleanup command both compute it
the same way from REPO_ROOT. Two checkouts whose paths share a prefix get
unrelated hashes, so a substring cannot arise in the first place, and every
spelling of one checkout (trailing slash, `.` segment, symlink, /private
prefix) resolves to one marker. The run id is now repoMarker plus 8 random
bytes of hex, and the process id p<pid> plus the same, both from a
whitespace-free alphabet.
With every value fixed-alphabet, envLineHasEntry needs only "starts an entry
and ends at whitespace or line end". The KEY= lookahead is gone and so is the
documented unresolvable case. assertMarkerValue keeps the invariant honest: it
refuses any value outside [A-Za-z0-9_-] with a message that says to hash it,
so a future caller that passes a path gets a loud error instead of a silent
mismatch. The readable path is still available for a human reading `ps -E`
output, exported separately as IMPECCABLE_TEST_REPO_PATH, which nothing
matches on and the docblock says so.
Matcher tests: the space-in-value case is gone, since that value can no longer
exist. Added a strict-prefix case (a longer hash-shaped value starting with the
marker), an adjacent-checkout case asserting the two hashes do not even share a
prefix, a symlink/trailing-slash case against real directories, an alphabet
check on all three generators, and one asserting assertMarkerValue throws.
Verified: leak test 10/10; bun run test:live 898 tests, 0 fail, 0 survivors;
scoped live-e2e (vite8-react-plain) 3 pass / 1 fail, matching pristine main;
SIGKILL repro 1 server up, 0 after; bun run build green. A probe against real
`ps -E` output with a hashed marker: this checkout 1 match, its trailing-slash
spelling 1, an adjacent checkout 0, exact run id 1, a run-id prefix 0.
AI assistance: prepared by Claude Code under pbakaus's direction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Review fixes: async group shutdown, and a Windows-safe symlink test
Two Cursor Bugbot findings, both real.
killCurrentGroup busy-waited on alive(child.pid) after sending SIGTERM, which
could never work. A dead child stays a zombie until its parent reaps it, the
parent here is the runner, and the runner reaps through libuv when the event
loop runs. The spin blocked the very loop that would have done the reaping and
then read the unreaped zombie as alive, so every SIGINT, SIGTERM and SIGHUP
burned the full 2s grace and ended in a needless SIGKILL. There is no waitpid
from JavaScript that sees through this, so the wait is now asynchronous and
keyed on the child's own exit event. The logic moved to
scripts/lib/process-group.mjs: trackChildExit exposes the exit as a flag and a
promise, stopGroup races that promise against the grace period and escalates to
SIGKILL only if it loses, and killGroupSync stays synchronous for
process.on('exit'), where nothing can be awaited, so it sends SIGTERM then
SIGKILL without pretending to wait. A second Ctrl-C now skips the grace period
entirely rather than queueing behind it.
Measured on a real SIGINT to a running live suite: 2027ms before, 34ms after.
tests/process-group.test.mjs pins both halves, including the escalation path
against a child that traps SIGTERM, which is not otherwise reachable from a
registered suite.
The repoMarker symlink test called symlinkSync with no type, which throws EPERM
on Windows without Developer Mode. It now passes 'junction' there and 'dir'
elsewhere, the same shape tests/concept-seed.test.mjs uses, and the
trailing-slash and dot-segment cases split into their own test so they keep
running on every platform regardless.
Merged origin/main (through #716) to re-level the branch.
Verified: leak and process-group tests 16/16; bun run test:live 900 tests, 0
fail, 0 survivors; scoped live-e2e (vite8-react-plain) now 4/4, with the
orphaned-session test that #716 fixed passing in 7.2s; bun run build green.
AI assistance: prepared by Claude Code under pbakaus's direction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Review fix: a second Ctrl-C must reach the group the first one is stopping
Cursor Bugbot caught a bug I introduced with the async shutdown, and it is the
same class of leak this PR exists to close. The signal handler cleared
currentChild before awaiting stopGroup, so a second Ctrl-C read a null handle:
killGroupSync did nothing, process.exit walked away from the SIGKILL escalation
still in flight, and because the suite is spawned detached it kept running
after the runner was gone. Impatience with a stuck suite produced exactly the
orphan the change is supposed to prevent.
The shutdown state machine moved into scripts/lib/process-group.mjs as
createGroupShutdown, which holds the group in `stopping` for as long as it is
being ended rather than dropping the only reference to it. A second signal
kills that handle and leaves; process.on('exit') looks at `current` or
`stopping`, so the last-resort path reaches a group mid-shutdown too. The
runner keeps no shutdown state of its own now, which is what made the bug
possible to write in the first place.
The extraction is what makes it testable: `exit` is injectable, so
tests/process-group.test.mjs can drive two signals at a stubborn child that
traps SIGTERM and assert the group dies in under 2s against a 30s grace. Point
that test at the old logic (killGroupSync on the cleared reference) and it
hangs out the full grace and fails, which is the check that it pins something
real. Five cases in all, including the exit-handler path and the no-child case.
Verified: process-group 10/10, live-server-leak 11/11; real double SIGINT to a
running live suite exits in 24ms with zero group members and zero servers left;
bun run test:live 900 tests, 0 fail, 0 survivors; scoped live-e2e
(vite8-react-plain) 4/4; bun run build green.
The core suite wedged twice locally in tests/build-phase.test.mjs, the
pre-existing unbounded-spawnSync hang noted in the PR description that
rust-swap's
|
||
|
|
eac24af113 |
Merge origin/main into rust-swap (#716 orphaned-session fix)
Restores tests/live-browser-source.test.mjs from main: the page JS it pins is unchanged by the engine swap and the file passes as-is. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
3f815865ab |
Self-discard orphaned JSX live sessions again (#716)
* Self-discard orphaned JSX live sessions again (#715) #694 stopped the source fallback from fetching and DOMParser-injecting raw JSX, which was painting {expressions} and comment markers into the page. The JSX gate it put in front of the fetch decided everything from the live DOM alone, and an unmounted wrapper looks exactly like a wrapper that was deleted from the file, so it treated both as "wait for mount": the orphan branch counted down its retry budget and then fell out of the function with no terminal action. A resumed CYCLING session whose region had been edited out of source therefore never reached discardOrphanedSession, the durable snapshot stayed out of the discarded phase, and the picker stayed frozen, which is the #439 regression the live-e2e scenario pins. The fix restores the decision without restoring the parse: probeJsxWrapperForOrphan reads the file as plain text and matches the session marker, so no DOM is ever built from JSX. Marker present means the component is simply not mounted and the observer keeps waiting; marker absent after the same retry budget the HTML path uses means the file moved on, and the session self-discards. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Orphan probe: a source read that fails also retries, then discards Review on #716 (Greptile, Copilot): the probe's empty catch swallowed a failed /source read, so a session whose file had been renamed or deleted (404), or that hit a transient fetch failure, neither retried nor reached a terminal action, which is the frozen-picker failure the probe exists to end. A read that cannot answer now shares the retry budget with a read that answers without the marker, and after the budget the session is discarded with a reason that names the failure. Unit test pins that the probe has no empty catch and that the failure path discards. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * Orphan probe: only evidence that the wrapper is gone may discard Review on #716 (Greptile, second pass): after the previous change a transient /source failure that outlasted the 3.6 s retry budget discarded a valid session, and a discard is durable. Now a read that answers without the marker, or a 404 (the file renamed or deleted), retries on the budget and then discards; any other failure retries on the budget and then keeps the session, warns, and tells the user it is checked again on the next event. The unit test pins both halves. AI assistance: prepared by Claude Code under pbakaus's direction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c5adf55bc8 |
CI: the first full run on the branch, three fixes
- The oracle harness masks the climb to the root a /-prefixed target produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth (7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash re-recorded. - Two context test helpers canonicalized their temp dir, which on Windows yields a \\?\ verbatim path that takes `/` literally; they strip the prefix like Node's realpathSync. The critique-storage identity test compares against the platform's own resolved path. - Every job that drives the binary end to end (live-e2e smoke and full, accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from the checkout first; before, they looked for a release that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
88ccc9c42b |
Port: the installer half of the OpenCode command bridge (#483)
Upstream sha
|
||
|
|
074c6a715d |
Oracle: goldens for the three fixtures the merge added
`tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and `linked-url-patterns.{css,html}` (#709) with the merge, so the corpus generator produced six `detect-fixture-*` cases with no goldens and the directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`) moved. Every golden here was recorded from the binary and then cross-checked against `node cli/bin/cli.js` on an origin/main worktree over the same files: the six per-fixture cases agree byte for byte in JSON and text, and a full scan of `tests/fixtures/antipatterns` produces 432 findings identical on both engines after normalizing the repo path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
5ea8787b02 |
Port: Fix skill subcommand help handling (#708)
Upstream sha
|
||
|
|
50684ececb |
Port: OpenCode slash command bridge (#483)
Upstream sha
|
||
|
|
ed50cc5ea0 |
Port: fail URL scans when the browser is unavailable (#711)
Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans when the browser is unavailable). `detect` gains an operational-failure flag. Exit 1 now means at least one requested target could not be scanned, and it takes precedence over exit 2, because findings from the targets that did scan do not turn a partial scan into a complete one. The flag is set by an unreachable path, an unreadable directory or file in a dir walk, a per-file scan that throws, a URL scan that throws, and a shared-browser setup failure. - `walk_dir_reporting` and `build_import_graph_reporting` take a read-error callback; the plain wrappers stay for callers that do not report. A file the graph could not read is skipped for the scan too. - `SharedBrowser::ensure_launched` is the eager half of `createBrowserDetector()`: the CLI brings the browser up before the loop so a launch failure prints one `Error:` line and every URL target is skipped, instead of the lazy launch reporting once per URL. - The static engine and the text path spell a permission failure the way Node does (`EACCES: permission denied, open '<path>'`), which is what `Error: cannot scan <target>: <message>` prints. - Usage text and docs/CLI-CONTRACT.md carry the exit-status block. Verified against origin/main's JS: missing target, missing target alongside a flagging file, unreadable file, unreadable file beside a readable sibling, unreadable directory, unreadable nested directory, a clean scan, and a browser-unavailable scan of one and of two URLs all agree on exit code, stdout and stderr (the browser-not-found wording is the pre-existing puppeteer-vs-discovery difference). Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at exit 1, plus new `detect-missing-file-with-findings`, `detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each cross-checked against origin/main. `detect-help` carries the new block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
bc45026706 |
Port: Fix Next.js 16 CSP and parent hook discovery (#710)
Upstream sha
|
||
|
|
c9429e46f6 |
Port: resolve unique --target names in monorepos (#706)
Upstream sha
|
||
|
|
8ac3886a9c |
Port: Fix detector URL scans and advisory handling (#709)
Upstream sha
|
||
|
|
66482a9808 |
Port: Fix flat type hierarchy false positives (#702)
Upstream sha
|
||
|
|
0375e219f1 |
Merge origin/main into rust-swap
Textual merge only. The JS engine and skill scripts stay deleted on this branch; every behavior change they carried is ported to the Rust crates in the commits that follow. Conflict resolutions: - skill/SKILL.src.md, skill/reference/new-work.md: main's new wording, with the branch's launcher invocations kept in place of `node <script>.mjs`. - scripts/test-suites.mjs: registers main's new build-tooling tests (copy-provider-commands, root-commands-sync, opencode-commands) and leaves the tests for deleted JS modules deregistered. - Every modify/delete conflict under cli/engine, cli/bin/commands, skill/scripts/*.mjs and tests/ for deleted modules keeps the deletion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
4369ad538d |
Open the detector: the rule crates join the workspace, the C-ABI goes away
The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
32b270f4e8 |
Fix: stop gray-on-color false positives on Tailwind opacity and JSX (#633) (#707)
* Fix: stop gray-on-color false positives on Tailwind opacity and JSX (#633) Do not treat bg-*/10 tints as solid fills, and pair gray text with chromatic backgrounds only inside the same tag and ternary arm. AI assistance: prepared with Cursor Grok under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: keep nested ternary arms and post-ternary classes exclusive (#633) Recurse exclusive class scopes so nested else-arms do not pair, and treat classes after a finished ternary as shared across both arms. AI assistance: prepared with Cursor Grok under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: ignore nullish coalescing when scoping gray-on-color (#633) The second ? in ?? was treated as a ternary delimiter, so exclusive arms stayed in one scope. Prepared with Cursor Grok under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f2f9958b3d |
Fix: fail URL scans when the browser is unavailable (#711)
* Fix URL scan failure exit codes Return exit 1 when browser setup or a URL scan fails, including partial multi-target scans, while preserving JSON findings output. Document the detector exit contract and cover isolated installs without Puppeteer.\n\nAI assistance disclosure: Codex implemented and tested this fix under maintainer direction. * Fix local target failure exit codes AI assistance disclosure: Codex implemented and tested this fix under maintainer direction. * Handle unreadable detector targets AI assistance disclosure: Codex implemented and tested this fix under maintainer direction. * Report unreadable detector directories AI assistance disclosure: Codex implemented and tested this fix under maintainer direction. |
||
|
|
8b326fc81e |
Fix: resolve unique --target names in monorepos (#700) (#706)
* Fix: resolve unique --target names in monorepos (#700) Bare child names such as Cantaro.Web now match a unique workspace candidate instead of being reported missing. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: resolve --target once in the context CLI Reuse the resolved path for loadContext so a bare name does not walk workspace candidates twice. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: match unique --target names after cwd absolutizing Live and other helpers resolve --target against cwd before context.mjs sees it. Treat a missing single-segment path the same as a bare workspace name so those callers still select the unique child. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com> |
||
|
|
fa44839f72 |
Fix detector URL scans and advisory handling (#709)
* Fix detector URL and advisory handling Recover joined URL arguments without splitting local paths, derive advisory behavior from registry severity across consumers, inspect readable linked CSS in URL scans, and report only the dominant primary font. AI assistance disclosure: Implemented and verified with Codex under maintainer direction. * Filter linked CSS to rendered selectors Flatten linked stylesheet grouping rules and collect only selector rules that target the live DOM, preventing unused grouped and selector-less patterns from leaking into URL findings. AI assistance disclosure: Implemented and verified with Codex under maintainer direction. * Fix detector review edge cases AI assistance disclosure: Codex implemented and verified these fixes under maintainer direction. * Preserve unresolved linked CSS selectors AI assistance disclosure: Codex implemented and verified this fix under maintainer direction. * Fix linked CSS selector filtering Resolve pseudo-element selectors to live hosts, reject unresolvable linked CSS findings, and make the regression assertions independent. Also ignore comment delimiters when recovering CSS rule selectors. AI assistance disclosure: This commit was prepared with Codex under maintainer direction. * Skip unresolved container query CSS Exclude linked container-query groups when their current applicability cannot be resolved, with a browser regression proving inactive styles do not leak. AI assistance disclosure: This commit was prepared with Codex under maintainer direction. * Detect active container query CSS Use a temporary custom-property probe so the browser decides whether a nested style rule actually applies in the current container layout. AI assistance disclosure: Codex helped implement and test this fix under maintainer direction. * Filter inactive linked CSS states Keep valid empty pseudo-class matches authoritative and omit selector-less linked at-rules that cannot be tied to rendered nodes. AI assistance disclosure: Codex helped implement and test this fix under maintainer direction. * Parse pseudo-elements without rewriting literals Preserve quoted attribute values and escaped identifiers while resolving real pseudo-elements to live hosts. AI assistance disclosure: Codex helped implement and test this fix under maintainer direction. * Restore live linked keyframes AI assistance disclosure: Codex helped implement and verify this fix under maintainer direction. * Handle grouped linked keyframes AI assistance disclosure: Codex helped implement and verify this fix under maintainer direction. * Respect keyframe definition order AI assistance disclosure: Codex helped implement and verify this fix under maintainer direction. * Resolve effective linked keyframes AI assistance disclosure: Codex helped implement and verify this fix under maintainer direction. * Fix keyframe easing detection Serialize effective per-keyframe easing back into the linked stylesheet corpus so overshoot motion is detected. Add a browser regression with a neutral animation name.\n\nAI assistance disclosure: Codex helped implement and test this fix under maintainer direction. |
||
|
|
672ca29642 |
Fix Next.js 16 CSP and parent hook discovery (#710)
* Fix CSP and hook ancestor discovery Recognize Next.js 16 proxy files when detecting runtime CSP and mirror harness ancestor lookup when locating active hook manifests for nested projects. AI assistance disclosure: Implemented and verified with Codex under maintainer direction. * Tighten hook and proxy discovery AI assistance disclosure: Codex implemented and verified these fixes under maintainer direction. * Honor ancestor hook disable config AI assistance disclosure: Codex implemented and verified this fix under maintainer direction. * Keep hook discovery within target repository Stop manifest discovery at the target repository boundary instead of re-adding an outer workspace root, with regression coverage for nested Git targets. AI assistance disclosure: This commit was prepared with Codex under maintainer direction. * Detect proxy CSP in nested Next apps Recognize proxy files at root or src placement relative to nested Next project markers while continuing to ignore unrelated proxy helpers. AI assistance disclosure: This commit was prepared with Codex under maintainer direction. * Resolve external targets from their own repository Scope explicit sibling targets to their own Git root so caller context and hook manifests cannot suppress required detector guidance. AI assistance disclosure: This commit was prepared with Codex under maintainer direction. * Isolate explicit targets at Git boundaries Keep nested repositories and external targets out of caller and home-level context or hook discovery. AI assistance disclosure: Codex helped implement and test this fix under maintainer direction. |
||
|
|
a264199177 |
Fix skill subcommand help handling (#708)
Ensure install, link, update, and check render static help before entering any operational path. Covers top-level and legacy routing for both -h and --help. AI-assisted implementation under maintainer direction. |
||
|
|
482368511a |
Fix Codex skill version metadata (#703)
Move Codex and .agents skill versions under metadata while keeping all version readers compatible with legacy top-level frontmatter.\n\nAI assistance: prepared with Codex under maintainer direction. |
||
|
|
36037ea8c5 |
oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
6580c47f1f |
oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json')
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
9961848ce2 |
oracle: replay byte-for-byte on Linux too
The corpus was recorded on macOS and eight cases failed on ubuntu CI for reasons that were all environment, not behavior: - stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a symlink and two goldens (context-dir-override, live-accept-source-locked) had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The source-locked case now actually exercises the lock it is named for. - context-lowercase-product-name declares platforms: ['darwin', 'win32']; run.mjs skips such cases elsewhere and says so in the summary. - The hook-project workspace's empty provider skill folders (.claude, .cursor) are now tracked with .gitkeep; git cannot track empty directories, so a fresh checkout had none and hooks on found nothing to repair. - crates/live's read_dir_raw sorts entries by name: the goldens hold the order macOS returned, Linux returns hash order, and the source-candidate lists in live-commit output depended on it. macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the public repo's node_modules on the machine that runs them (CI now installs them). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
f2c9aeab5b |
build:extension: ship the wasm-core extension shell and vendor its detector from the detector release
`bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
9736a9f6e9 |
Fix OpenCode slash command bridge (#483)
Add a first-class OpenCode command bridge across builds, installs, updates, linked installs, and pinned shortcuts. Preserve current provider behavior while backfilling missing or drifted command files.\n\nAI assistance: contributor and maintainer work used AI tools as disclosed in the PR discussion and commits. |
||
|
|
84728e9ce4 |
Fix flat type hierarchy false positives (#702)
* Fix flat type hierarchy false positives Use rendered semantic roles and dominant size frequency, align the adjacent-step guidance, and abstain in source-only scans.\n\nAI assistance: prepared with Codex under maintainer direction. * Fix static hidden typography filtering Honor the hidden attribute in the static wrapper and use raw browser findings in regression coverage. AI assistance: prepared with Codex under maintainer direction. * Align typography sampling with painted content Count visibly painted aria-hidden text and exclude content-visibility hidden subtrees in both static and browser scans. AI assistance: prepared with Codex under maintainer direction. |
||
|
|
0547ed6a63 |
reorg C: the open Rust runtime joins this repo as one Cargo workspace
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.
The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.
What changed versus the engine repo copy:
- Every crate manifest moves from `license-file.workspace` to
`license.workspace` (this workspace declares Apache-2.0), and the workspace
gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
a sibling `launcher/` dir, and `engine_binary` downloads from
`github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
of the retired dist repo. No oracle golden carried the old URL, so no
re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
`../impeccable-second` or a hardcoded home directory now resolve the root as
`CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
live mode serves that. `scripts/check-detector-release.mjs` now requires it
and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
`modern-screenshot.umd.js` directly rather than through vendored copies, so
the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
plain `cargo build --release -p impeccable` is enough to run `bun run test`.
Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
|
||
|
|
38e102f0b2 |
Fix: never inject raw JSX in live-mode fallback (#454) (#694)
* Fix: never inject raw JSX in live-mode fallback (#454) On React/JSX targets, missed HMR used to fetch source and DOMParser-inject it, painting {expressions} and comment markers as page text. Adopt a live wrapper that already has variants, otherwise leave HMR alone. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: wait for unmounted JSX variants instead of tearing down (#454) A missing live wrapper on React is often a closed modal or other route, not a failed generation. Leave the observer armed so mount can still reach CYCLING. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: recover empty JSX replace wraps after fallback retries (#454) Insert scaffolds still wait for HMR. A replace wrapper with no variants after retries is a failed generation and should leave GENERATING. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: align live-reference setup assertions with current SKILL.src.md #689 shortened Setup step 2, but the live-reference tests still expected the old playbook sentence, which kept CI red on main and this branch. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6bc4f242c7 |
Fix live cleanup races with framework HMR (#695)
Guard delayed accept and discard DOM fallbacks when framework/HMR ownership is present, while preserving static-page cleanup. Add unit/source regressions for both paths and refresh stale Setup wording assertions from #689. AI-assisted: prepared with Codex under @pbakaus direction. |
||
|
|
e355ebf714 |
reorg: public plumbing for the in-repo Rust workspace and the two-release flow
The engine binaries move from the impeccable-dist channel to this repo's own GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the engine links arrives as detector-v<DETECTOR_VERSION> releases on the same repo. This commit wires the public side for that; the crates themselves land in the next commit. - Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/. - release.mjs gains `engine`: verifies ENGINE_VERSION against the platform package pins and the detector release, tags, pushes; release-engine.yml builds the five targets and publishes. check-detector-release.mjs is the matching release-order guard (with tests). - Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml (exact pin), DETECTOR_VERSION, /target ignored. - CI: rust + rust-windows jobs and an oracle job that replays the goldens against a source build, warn-only until the first detector release exists; ci-test-plan exposes a `rust` output. - docs/ENGINE.md (the crate map and the closed-detector mechanism) and the CLAUDE.md engine, release-order and rules sections. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY |
||
|
|
6c474b1c79 |
Node-free swap: comp-fidelity verbs move to the engine
The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase)
and their six libs are ported into the impeccable-engine binary. This removes
the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'`
now returns nothing.
- reference/new-work.md, reference/visualize.md, and the asset-producer /
finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead
of `node <script>.mjs`.
- Deleted the ten ported .mjs and the four JS unit tests that imported them
(their behavior is now covered by the engine's Rust tests and the oracle);
removed those files from scripts/test-suites.mjs.
- Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic
workspace, recorded from the engine binary; the deterministic outputs are
byte-identical to the JS the scripts left behind.
- docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and
the runtime-resolved (never-committed) font-index catalog.
The font-index catalog JSON stays shipped in the skill (data/font-index.json);
the engine resolves it at run time and never vendors it.
Prepared with AI assistance (Claude Code).
|
||
|
|
47f1871385 |
Tests: stop two harness hangs from wedging a whole run
Two suites could hang forever and never print a tally, because the one mechanism that could interrupt the wedged work was missing on both paths. Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper spawned every child with spawnSync and no timeout. spawnSync blocks the test worker's thread, so node's --test-timeout (an event-loop timer) cannot interrupt a child that wedges (a fork/exec blocked on OS resources under concurrency, a gate's comp-diff grandchild, or a stray browser launch). Bound every child with spawnSync timeout + killSignal SIGKILL so a wedge becomes a fast, named failure the next test survives. Hang 2 (bun run test:skill-behavior): runTurn called generateText with no client-side deadline, so a stalled provider stream kept the fetch (and the whole node process) alive past the per-test timeout, producing no tally. Attach a real AbortSignal (default 840s, under the 900s per-test cap): on expiry the fetch aborts, the turn throws, and the scenario fails-and-continues. The unref'd timer is cleared on completion. Runner backstops: run-tests.mjs now spawns each command as a detached process-group leader and enforces a per-suite wall-clock cap that SIGKILLs the entire group (workers, grandchildren, browsers) on expiry, with SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node batch gets a finite --test-timeout (180s); skill-behavior gets a 60min group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS, IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS, IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS. Proof: bun run test green twice (~60s); scoped claude-sonnet-5 skill-behavior sweep terminates with a tally (20 tests, ~32min) where the 840s abort caught a wedged redesign turn and the sweep continued instead of hanging. Prepared with AI assistance (Claude Code). |
||
|
|
c55cd49895 |
oracle: pin E8 stale-hook-manifest detector fallback (context)
Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine binary and hand-reviewed: - context-stale-hook-manifest: a .claude/settings.local.json naming the retired `node .../hook.mjs` script under the claude-code provider emits MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an active hook (its script is gone after the update). - context-launcher-hook-active: the same manifest in the launcher form still suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is recognized as active. The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED block. No existing golden moved: every other context case runs under the source provider, whose hook-manifest list is empty, so none of them scan a manifest. Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's value cannot leak. DELTAS.md records the intentional divergence from JS parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx |
||
|
|
2dccbefefe |
Tests: fix pre-existing release-guard staging on the swap branch
release.test.mjs was already red on the swap branch: release.mjs imports check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order guard), which the temp work tree never staged, so every dry run failed to resolve the module instead of exercising the guard. Stage both modules and set IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this suite predates the guard and only covers the version/changelog/artifact checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx |
||
|
|
a7a63180f5 |
Oracle: re-record the Sep-1 verb fixes ported to the Rust engine
Five fixes landed on main in JS between the swap branch and its rebase and were ported to the engine; the goldens they touch are re-recorded from the fixed binary, each engine output first diffed byte-for-byte against the upstream JS on the same inputs. DELTAS.md documents every case with its upstream hash. - critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child): the #660 critique close path (identity + fingerprint freshness, ~NNNN collision suffix, closed flag, close verb, latest --json). Upstream |
||
|
|
632912b5ae |
Fix live script response encoding (#690)
Declare UTF-8 on the generated live and detector JavaScript responses and cover both endpoints with integration assertions.\n\nAI-assisted: prepared with Codex under @pbakaus direction. |
||
|
|
187790826d |
Fix concept seed under symlinked installs (#686)
Resolve the CLI entry path through realpath and cover linked skill directories on Unix and Windows junctions. AI-assisted change: implemented with Codex under @pbakaus direction. |