Report index

Editing source from the browser by address, not by class string

WIGSS joins a rendered element to its source by searching every project file for a matching className string. Over ten common React and Tailwind patterns that join produced the intended edit 5 times out of 10, and its accuracy falls as 1/N when N components share a class string: 3% at N = 32. Reading an address that the development JSX transform already carries brought both figures to 100% and cut the bytes read per save from 162,606 to 284.

Eric Kim 김진모
DevOps Engineer, WIGTN
WIGSS, WIGTN Style Sync Studio
01Problem

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.

02Mechanism

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.

03Pattern coverage

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.

Six outcomes of searching every file for an exact class-string match, and the two token faults that remain when the search returns the right element
FIG.Only the leftmost branch reaches the intended element. Two of the remaining five write into the wrong place, and the token edit can still be wrong after a correct join.

Ten patterns, one edit each · A = shipped v0.2.0 · B = address-based prototype

PatternABWhat A wrote
Unique static classNameCorrectCorrecth-48 → h-64
Two identical siblings in one fileWrong elementCorrectEdited the first card; the target was untouched
Template literal with interpolationNo diffCorrect
cn() callNo diffCorrect
Multi-line attributeCorrectCorrecth-48 → h-64
Responsive h-32 md:h-48 lg:h-64Wrong breakpointCorrecth-32 → h-80, editing base while the viewport was lg
Item inside .map()CorrectCorrecth-48 → h-64
Move a flex child by 12 pxWrong element, arbitrary valueCorrectmt-[12px] on the preceding sibling
Single-quoted classNameCorrectCorrecth-48 → h-64
Static element beside a prop classNameCorrectCorrecth-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.
04Duplicate density

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 NA correctAB correctB
11/1100%1/1100%
21/250%2/2100%
41/425%4/4100%
81/813%8/8100%
161/166%16/16100%
321/323%32/32100%
05Root causes

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.

Two root decisions and the faults that descend from each: the searched class string above, the layout-blind translation below
FIG.Everything in the upper tree follows from resolving by search. The lower tree explains why verification exists, and why the rollback it depends on can lose work.
06Approach

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.

01Wrap the development runtime

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.

02Ask for one line of configuration

"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.

03Read one file instead of the project

The address names the file, so the collection step reads 1 file rather than 40, and parses 1 rather than 10.

04Carry the character range forward

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.

Two paths from a screen element to a source location: a class-string search across every file, and an address read from the JSX development transform
FIG.The shipped path searches; the proposed path reads. Removing the search removes the duplicate-collision class of failure along with the per-save file I/O.
07Apply path

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.

08Rollback safety

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.

A save passing through three fixed delays to a verification that ends in done, skipped or a warning, with no edge returning to the writer
FIG.Three fixed delays put at least 4.5 seconds between a save and its verdict, and the slowest branch is the one that gives up. Nothing returns to the writer: the automatic re-edit the first PRD specified was never built.

Save, then a concurrent edit to another line of the same file, then a failed fidelity check

OutcomeA · whole-file restoreB · range reversal
Style change revertedYesYes
User's concurrent edit keptLostKept
User had also changed the written lineOverwritten silentlyRefused, 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.
09Editing surface

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.

The shipped editor on first run, showing only two small tabs and no visible controls
FIG.First run. Two tabs are the entire interface until the pointer finds them.
Overlay boxes after a scan, with labels overlapping each other and ten border colours in use
FIG.After a scan of 12 components. Section 12 sits under another label and cannot be read, Grid 10 and Section 10 overlap at the project list, and the depth badges scatter across the corners.
The agent panel sliding over the right side of the page being edited
FIG.The agent panel covers the region it is commenting on. Reading a suggestion and editing the element it refers to are mutually exclusive.
  • 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.
10Canvas prototype

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.

A pannable canvas holding the same route at 375, 768 and 1280 pixels, with the active breakpoint shown in the top bar
FIG.One route at three widths. The 375 card is marked stale and waiting for its update, and the top bar states that edits will land on lg.
A drag in progress with an insertion line, a ghost following the pointer, and a list of candidate interpretations with confidence
FIG.Releasing a drag opens the candidate list: reorder, margin, parent gap, then absolute position with its warning. Each candidate names the code it would write.

The same page in both surfaces

Shipped editorCanvas prototype
Controls visible on first run212 or more
Active breakpoint shownNoAlways
Viewports reviewable at once13
Label collisions at 12 componentsPresentNone; labels appear on hover and selection
Warning before a risky editNoneHygiene and reuse badges
Panel occludes the edit targetYesNo; the rail displaces the canvas
11Escalation

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.

Escalation from a deterministic AST edit to a scoped model edit to a user prompt, with every tier passing the same linter, apply guards and on-screen check before a failed check rolls back and returns the next candidate
FIG.Every tier writes through the same linter, the same guards and the same check. The retry edge on the right is the loop the build did not have when this was measured; the addendum below records it landing.
12Evidence status

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.
13Addendum

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 canvas route in pull request 5: a left rail with routes and a component tree, three viewport cards, and the active breakpoint in the top bar
FIG.The canvas as built in pull request 5. Routes from a static walk on the left, the live component tree under them, and the breakpoint that will receive the edit always in the top bar.
Mid-drag on the hero: the page itself is taller inside the iframe while the drag is still in progress
FIG.Mid-drag, before release: the hero is already 428 pixels tall in the page itself, neighbours reflowing, while the source still says lg:h-96. The styles exist only in the DOM.
After release the toast reports that the lg token was rewritten and the on-screen check passed in 733 milliseconds
FIG.Release plus 733 milliseconds: the edit landed on the lg token, the page was re-measured, and the toast reports the check that passed — not the write that happened.
The arbitration panel in pull request 5 after resizing a grid card: the element's own size, or the parent grid dropping to one column, each with the tokens it would write
FIG.The arbitration sketch became a panel. A grid card resized past its track offers two readings — its own width, or the parent's column count — each naming the tokens it would write; Escape writes nothing.
The right-click prompt tier: a natural-language instruction scoped to one element's class fragment
FIG.The prompt tier, scoped. A right click asks for an instruction, the model may answer with one line of class tokens, and the same fences, screen check and undo apply.
A margin edit on a centred container fails verification: rendered 96 pixels against an expected 102, rolled back automatically
FIG.The screen keeps the last word. A margin written into a centred container rendered 96 pixels of movement against an expected 102; by the time the toast is readable, the file is already back to what it was.

The report's numbers, then and after pull request 5

MeasurementAt writingIn pull request 5
Intended edit, ten patterns5/1010/10
Join accuracy at 32 duplicates3%100%
Files read per save401
Feedback during a dragNone until reloadSame-frame DOM preview
A wrong edit's fateA warning that waitsRolled back, one repair attempt, re-verified
Release to verified, demo pageNot built0.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.
14Limitations

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.