The last ten pixels are expensive to describe
Coding agents produce a first layout quickly. The friction arrives later, when a ten-pixel correction becomes another round of prose, CSS edits, reloads and screenshots.
WIGSS uses the browser as the editing surface and turns each manipulation back into a constrained, reviewable source change. The repository stays the source of truth. Whether that translation lands on the element the user actually dragged is a separate question, and it is the one this report measures.
The screen and the file share one value, so that value became the key
A DOM node does not know which file produced it, and a file does not know where it was painted. Something has to link them.
The browser holds class="flex h-48 w-64". The file holds className="flex h-48 w-64". The class string is the only value present on both sides without adding anything to the user's build, so WIGSS made it the join key. The scan copies the whole string, the server parses each source file with Babel, and the first attribute whose value matches exactly becomes the target.
The choice follows from a product constraint rather than from convenience. The original PRD required npx wigss to run with nothing installed into the project, which rules out a build-time plugin and therefore rules out a real source coordinate. Within that constraint the class string is the best available key.
It is still a guess. The rest of this report is about where the guess is wrong and what it costs.
Half of the common patterns produced the wrong edit
Ten fixtures cover the React and Tailwind shapes a real codebase contains. Each defines one drag gesture and one correct outcome. Version A calls the shipped pipeline directly rather than reimplementing it.
Five of ten produced the intended change. Three wrote an edit that was wrong, which is worse than refusing: the save reports success, the file changes, and nothing signals that the wrong element moved. Two produced no diff and surfaced no reason, so the user sees only "could not generate a code change; try a larger edit", which is unrelated to the actual cause.
Class-string collisions across files were removed from this fixture set so that pattern handling and duplicate density are not measured together. Duplicates are measured separately in the next section.

Ten patterns, one edit each · A = shipped v0.2.0 · B = address-based prototype
| Pattern | A | B | What A wrote |
|---|---|---|---|
| Unique static className | Correct | Correct | h-48 → h-64 |
| Two identical siblings in one file | Wrong element | Correct | Edited the first card; the target was untouched |
| Template literal with interpolation | No diff | Correct | — |
| cn() call | No diff | Correct | — |
| Multi-line attribute | Correct | Correct | h-48 → h-64 |
| Responsive h-32 md:h-48 lg:h-64 | Wrong breakpoint | Correct | h-32 → h-80, editing base while the viewport was lg |
| Item inside .map() | Correct | Correct | h-48 → h-64 |
| Move a flex child by 12 px | Wrong element, arbitrary value | Correct | mt-[12px] on the preceding sibling |
| Single-quoted className | Correct | Correct | h-48 → h-64 |
| Static element beside a prop className | Correct | Correct | h-48 → h-64 |
- 01The responsive case is the most damaging of the three. findTwClass matches h- without regard to a breakpoint prefix, so an edit made at lg rewrites the base token. The desktop view looks correct, the post-apply check runs at the desktop width and passes, and the mobile height changes from 128 px to 320 px with nothing on screen to indicate it.
- 02The 12 px move is a second, smaller instance of the same shape. Twelve pixels is exactly mt-3 on the Tailwind scale, but when no top margin exists the rewriter appends a hard-coded mt-[12px] without consulting pxToTw, so a value that had a preset became an arbitrary one.
- 03Both faults were already described in the v2.2 PRD, which notes that the Tailwind strategy replaces utility classes by guesswork without knowing the parent layout. They were deferred, not missed.
Accuracy falls as one over the number of duplicates
Copy-pasted markup is normal. When N components carry the same class string, each of the N was made the edit target once.
The result is exactly 1/N, because a search that stops at the first match is correct only when the target happens to be first. An address does not depend on N.
Latency did not grow with N, because the rewriter stops iterating at the first match. That observation moved the cost measurement to the right place: the expense is not parsing but the collection step in the refactor endpoint, which reads the project on every save.
Duplicate density against join accuracy
| Duplicates N | A correct | A | B correct | B |
|---|---|---|---|---|
| 1 | 1/1 | 100% | 1/1 | 100% |
| 2 | 1/2 | 50% | 2/2 | 100% |
| 4 | 1/4 | 25% | 4/4 | 100% |
| 8 | 1/8 | 13% | 8/8 | 100% |
| 16 | 1/16 | 6% | 16/16 | 100% |
| 32 | 1/32 | 3% | 32/32 | 100% |
Two roots account for most of the faults
The failures are not independent. Almost all of them descend from one of two decisions, which is why a narrow change removes several at once.
The first root is the join key. Searching for a class string means duplicates resolve to whichever match comes first, dynamic class names resolve to nothing, and the project has to be read and parsed on every save. It also means the resolved position is a string rather than a coordinate, so the apply step looks the element up a second time and can disagree with the step that found it.
The second root is the translation. Converting a drag into a margin without knowing whether the parent is flow, flex or grid produces an edit that is sometimes absorbed and sometimes lands on the wrong axis. That uncertainty is what makes post-apply measurement necessary in the first place, and the measurement in turn introduced the rollback path that discards concurrent edits.

The development JSX transform already knows the answer
React compiles JSX to jsxDEV(type, props, key, isStatic, source, self) in development, and the fifth argument carries the file, line and column. React DevTools opens a file from it. The value is already flowing; it simply never reaches the DOM.
The same package can inject the scan runtime, which resolves a second problem. The script that answers a scan request currently lives only inside the bundled demo page, so an arbitrary project returns nothing and the editor falls back to an empty component list after four seconds.
Knowing where an element is does not always mean it can be written. A cn() call is resolvable but not rewritable as a string, so the prototype edits the first string argument and escalates when there is none. The address turns a class of silent failures into a class of explicit ones.
A thirty-line module re-exports jsxDEV and adds data-wigss="file:line:column" to DOM elements. Components are left alone; the attribute lands on the tag whose class literal is being edited.
"jsxImportSource": "wigss" in tsconfig. No Babel plugin, so a Next project keeps its SWC pipeline and its build times. Production is unaffected because it does not use jsxDEV.
The address names the file, so the collection step reads 1 file rather than 40, and parses 1 rather than 10.
The resolved range travels into the apply step, so nothing is looked up a second time. The next section explains why this last step is not optional.

Resolving by address is not enough if apply looks the element up again
The first version of the prototype resolved the correct element and still failed the duplicate and move cases, exactly as the shipped pipeline did.
CodeDiff carries an original and a modified snippet and nothing else, and the apply route locates the edit with content.indexOf(original). An address resolved upstream is discarded at that line, and with two identical siblings the search returns the first one again.
Passing the character range through to apply made both cases pass. The type for this already exists: TargetLocation has a range field, and the dispatcher supplies { start: 0, end: 0 } with a comment saying that later locators will produce real values.
This did not appear in the design notes. It appeared when the experiment ran, which is the argument for running the experiment before writing the plan.
The rollback path restores whole files
Every save issues a rollback token so the user can undo an edit whose on-screen result drifted. The token restores the file, not the edit.
In the simulation WIGSS changed a className, the user then edited a different line of the same file in their editor, and the fidelity check failed. Rolling back returned the file to the pre-save snapshot and removed the user's line: const total = items.filter(Boolean).length; became const total = items.length; again. A style tool reverted logic.
Reversing only the range that was written keeps the user's edit. When the written text is no longer present, because the user changed that line too, the operation has to be refused rather than performed, and the reason shown.

Save, then a concurrent edit to another line of the same file, then a failed fidelity check
| Outcome | A · whole-file restore | B · range reversal |
|---|---|---|
| Style change reverted | Yes | Yes |
| User's concurrent edit kept | Lost | Kept |
| User had also changed the written line | Overwritten silently | Refused, with a reason |
- 01The apply route also writes a .bak.<timestamp> sidecar next to every file it touches and never removes it, which leaves artefacts in a repository that already has version control.
The interface hides the one thing the user needs to see
The captures below come from a scan of the bundled demo page: 75 DOM elements grouped into 12 components. What follows is about how those 12 are presented, not how they were found.
Because the target page fills the viewport, every control was moved behind a hover reveal. On first run the screen offers a small tab at the top and another at the right edge, and nothing indicates that a scan has to be run before anything can be selected.
Overlays carry a colour per component type across ten types, plus a depth badge from L1 to L5. At twelve components the labels already collide and the colours stop separating anything. The names come from a running index, so a hero section reads as Section 12 and a card grid as Grid 10, neither of which points at a file.
Nothing on screen states which breakpoint an edit will land on. The mobile toggle narrows the viewport to 375 px and changes nothing in the edit path, so the breakpoint fault measured earlier is invisible while it happens and stays invisible afterwards.



- 01A drag that produces no diff reports "could not generate a code change; try a larger edit". The actual causes are a template literal or a cn() call, and neither is affected by the size of the drag.
A canvas makes the breakpoint fault visible while it happens
The second prototype is an editing surface rather than a pipeline. It runs the same scan protocol against the same demo page, and the cards hold live iframes.
Placing one route at three widths side by side turns the responsive fault into something a person can see. An edit made at lg shows its effect on sm and md in the same glance, which is the check that no amount of care in the pipeline can replace.
The active breakpoint is stated in the top bar and never hidden. Colour is reserved for hover and selection; component identity moves to badges that carry the hygiene score and the reuse count, so a name says which file it came from and a badge says whether an edit there is likely to fail.
A drag ends in a decision rather than a write. The insertion line shows the destination before the pointer is released, and the arbitration list names the candidates with the code each would produce. Absolute positioning is present as the last option and marked as breaking the responsive layout.


The same page in both surfaces
| Shipped editor | Canvas prototype | |
|---|---|---|
| Controls visible on first run | 2 | 12 or more |
| Active breakpoint shown | No | Always |
| Viewports reviewable at once | 1 | 3 |
| Label collisions at 12 components | Present | None; labels appear on hover and selection |
| Warning before a risky edit | None | Hygiene and reuse badges |
| Panel occludes the edit target | Yes | No; the rail displaces the canvas |
What happens when the deterministic path cannot write
Today an unwritable edit falls back to an inline style attribute. The v2.2 PRD recorded that as a deliberate temporary trade-off and added a cleanup pass to convert such diffs back to classes when every property maps to a preset.
If the tool is not permitted to leave code that a reviewer would reject, the fallback has to go, and with it the cleanup pass that exists to repair its output. What replaces them is an escalation: a scoped model edit, then a prompt at a range the user chooses, then an explicit refusal that leaves the file alone.
A model edit is bounded by the same machinery as a deterministic one. It receives the node and its parent rather than the file, its output is spliced into a fixed range, it is re-parsed and checked for token parity, and the on-screen measurement is the last word. A plausible but wrong edit does not reach the user because the result is judged by the rendered page, not by reading the code.

This reproduces defects; it does not benchmark the tool
The harness runs ten fixtures, a duplicate-density sweep and a rollback simulation, and calls the shipped pipeline for the A side rather than a description of it. The 340 existing unit tests pass and tsc --noEmit is clean at the measured commit.
Ten fixtures chosen for the shapes they exercise are not a sample of any codebase, and the B side implements three axes rather than a product. The figures below are what the measurements support and what they do not.
- 01Supported: an address-based join produced the intended edit on all ten patterns and is unaffected by duplicate density; the shipped build rewrites the wrong breakpoint, emits an arbitrary value where a preset exists, and discards concurrent edits on rollback; per-save reads fall from 162,606 bytes to 284.
- 02Not supported: coverage of the deterministic path on real projects, the share of edits a model tier would rescue, and whether the address survives Next's SWC pipeline.
- 03Required before this becomes a measured system: a fixed set of open-source Next and Tailwind repositories with a scripted edit suite, and telemetry for tier outcome, latency and convention violations recorded from the first release rather than added later.
The roadmap ran, and the screen check now has teeth
This report argued for an address join, a verification loop and a canvas surface. Between its measurements and pull request 5 on the repository, all three were built. This section records what that pull request built and what building it surfaced, from the same test bench.
The gate at the head of the roadmap opened. Next 14's SWC honours a jsxImportSource override, so the development transform stamps file, line and column onto every element it renders: 12 of 12 detected components carry an address on the demo page. The harness columns moved with it — the pipeline in pull request 5 lands the intended edit on 10 of 10 patterns, holds 100% at every duplicate density, and reads one file per save instead of forty. Addresses exist only in development builds; production falls back to the search join this report measured.
The canvas mock-ups became a route. A rail lists the routes found by a static walk of the target's app directory and, under them, the component tree of the active card; the active card's width decides which breakpoint token an edit rewrites; an insertion line is the only gesture that commits a reorder. The first sessions on it also produced the honest embarrassments: a scripted resize wrote a 1,901-pixel height into the demo because the test picked its target by geometry, and a margin written into a centred container moved nothing on screen. Both became same-day fixes, and the second became the test case below.
Dragging now shows the page's answer while the pointer is still down. Each frame posts one message into the iframe and the styles land on the live DOM only — the source is never touched — so the reflow a resize will cause is visible as it happens, neighbours included. Releasing writes the code edit, and the reload that follows replaces the preview with whatever the code actually renders.
The verification edge the escalation figure named as missing is closed on the canvas too: apply, rescan, verify, and on mismatch an automatic reverse-edit rollback, one scoped model repair, and a re-verify. The centred-container case shows why the screen keeps the last word. Dragging the main column sideways writes a margin class, and whether that margin beats the auto-centring depended on the order the generated stylesheet happened to take: across builds the same edit rendered as 0 pixels of movement in one run and 96 in another. Both miss the 102-pixel expectation, and both end as a clean file.
The arbitration list and the parent tier followed in the same pull request. A drop now returns candidates rather than a decision: the element's own margin or size, the parent's gap, the parent's column count when a grid child is resized to a width its track cannot give, and absolute positioning last — offered only under a positioned ancestor and labelled as breaking the flow. Each candidate names the exact tokens it would write, Escape writes nothing, and a right click opens a prompt whose model edit is fenced to the same single class fragment and reversed by the same reverse-edit undo. The grid case closed the acceptance test: resizing a card until the row cannot hold three columns offers grid-cols-3 to grid-cols-1 on the lg token, and undo restores the file byte for byte.
Closing the loop surfaced two failures no plan listed. A rescan can measure the page before the dev server has compiled the edit, and judging that stale render rolled back a correct edit — so the loop now waits until the class it wrote is visible in the scan before it judges. And the development server bundles API routes separately, which quietly gave apply and rollback two different in-memory backup stores; the store moved to the process global, and the canvas run is what caught it — the unit tests share one module instance and never could.






The report's numbers, then and after pull request 5
| Measurement | At writing | In pull request 5 |
|---|---|---|
| Intended edit, ten patterns | 5/10 | 10/10 |
| Join accuracy at 32 duplicates | 3% | 100% |
| Files read per save | 40 | 1 |
| Feedback during a drag | None until reload | Same-frame DOM preview |
| A wrong edit's fate | A warning that waits | Rolled back, one repair attempt, re-verified |
| Release to verified, demo page | Not built | 0.8 s |
- 01Still open, in order: candidates are enumerated deterministically, not ranked by a model; the absolute candidate cannot yet make its parent positioned, which needs a two-address atomic apply; production builds still join by search; React Server Components remain unmeasured.
Where the claim stops
- L01Ten fixtures were written to exercise particular code shapes. They show that each failure occurs and how, not how often any of them occurs in a given repository.
- L02The comparison holds the px-to-Tailwind scale table and the apply-time safety guards constant across both sides, so it measures the join, the breakpoint handling and the output policy, and nothing else.
- L03The 5/10 figure is specific to this fixture set. Removing the cross-file class collisions raised it from 2/10, which shows how strongly the number depends on how much duplication the sample contains.
- L04The address approach was unverified end to end when this was measured; the addendum records jsxImportSource working under Next 14's SWC pipeline in pull request 5. Behaviour under React Server Components, where a change re-renders a route rather than a component, has still not been measured.
- L05The prototype measured here implements resolution, breakpoint-aware token editing and range-based application only. Structure edits, the model tiers and the editing surface came later in pull request 5, and the addendum's figures for them come from sessions on the demo page rather than from this fixture set.
- L06Latency was measured in-process on one machine. It excludes the fixed delays the current save flow adds after writing, which dominate the wall-clock time a user experiences.
- L07The rollback result comes from a simulation of the documented restore behaviour rather than from a running session, though the restore path it models is a single call that rewrites the file with its pre-save contents.

