A human and an agent trade turns on an HTML page. The human leaves directives and edits prose in the browser; everything lands in the file.
What's decided about editing as markdown and element identity, beside examples: SCOPES-DESIGN.html. How everything the tool adds to a page looks, and the design decisions so far: DESIGN.html.
⌘-click a box for its step below. (A plain click opens a directive box.)
./deploy runs from main only and refuses on a dirty tree. If the tests fail in the copy, it puts the copy back and commits nothing. Details: README, “Deploy” and “Contract with iterate-on-docs”.
The pipelines above, stood on end. Green: today. Dashed purple: directional.
Parse with linkedom, rewrite into canonical shape: add html/head/body if missing, b→strong and the other forms markdown writes back, one block per line, inline runs on one line, whitespace collapsed, two-space indent. Idempotent. Writes to disk when it differs.
<h1>Plan</h1> <p>Ship the <b>formatter</b> first.</p><ul><li>one</li><li>two</li></ul> <img src="map.png">
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>Plan</h1>
<p>Ship the <b>formatter</b> first.</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
<img src="map.png">
</body>
</html>
Normalize is its own command, in formatter.mjs; GET runs the same code. The agent runs it after writing the page, before the human opens it. Directional: validation rules, for what normalizing can't fix. None yet; one a rule could report:
$ node formatter.mjs OVERVIEW.html OVERVIEW.html: <p> holds a <div>; a paragraph holds phrasing content only
One element ID per scope (every block), and per link, image, hr, and part of an SVG. The formatter writes them into the file, from a counter whose high-water mark sits on <html>, so an edit can't shift them. It assigns IDs on every GET and first thing on every PUT. An ID never changes and is never reused; duplicates (an agent's copy-paste) retire the ID, and every copy gets a fresh one.
<html data-hrv-next="8"> … <p data-hrv="4">Ship the <b>formatter</b> first.</p> <ul data-hrv="5"> <li data-hrv="6">one</li> <li data-hrv="7">two</li> </ul>
<p> <!-- @AI: say why first --> Ship the <b>formatter</b> first. </p>
<p data-hrv="4">Ship the …</p>
…
<script type="application/json"
id="hrv-directives">
{"4":"say why first"}
</script>
Like an HTML comment, a type="application/json" script is inert: the browser never runs it. The client reads it with JSON.parse(el.textContent).
Ship the formatter first.
⌘-click the marker. Clicking a real one opens its directive.
<html>
<head>
… the page's head …
<style> hover, markers, box </style>
</head>
<body>
… the page, with its IDs …
<script type="application/json" id="hrv-directives"
data-version="3f9a0c1b2d4e"> {…} </script>
<script type="application/json" id="hrv-bases"> {"4":"a1c4…"} </script>
<script> hover · click · box · PUT </script>
</body>
</html>
The green lines exist only in the served copy, never in the file, so the client's own elements have no ID and can't take a directive. data-version fingerprints the file on disk; a save's answer says whether the file changed (see conflict check). hrv-bases holds each element's base, which a save sends back.
{id, base, directive} or {id, base, markdown}
Sent 0.4 s after you stop typing. Carries one change, a directive or a scope's markdown, with the base it started from. Changes go out one at a time.
No file-wide version. The server compares the change's base with the element now, directives and IDs stripped. Only that change can conflict. A changed child changes its parent, so nesting needs no special case.
{id: "6", base: "a1c4", directive: "say more"}
base matches li 6 → applied; answer has new bases
base differs, or no li 6 → 412, nothing written: error, reload suggested
A conflict means we weren't taking turns; it's an edge case. An agent's edit elsewhere doesn't block the save. The answer carries the file's hash before the save; if that isn't the page the tab loaded, the tab says "the page changed on disk; reload to see it". directional Later the tab hears of it live.
{id: "4", base, markdown: "Ship the **formatter** last."}
→ <p data-hrv="4">Ship the <strong>formatter</strong> last.</p>
Any scope. The answer carries the new HTML, as served, and the new markdown of the scopes around it; the tab swaps the HTML in. Blank markdown, body, and elements that aren't scopes (hr, an SVG shape) are refused.
Open, then save unchanged: the HTML stays byte-for-byte the same. See Prose editing.
Saving reconciles with the prior HTML: an element the edit leaves alone, moves, or changes keeps its ID and what markdown can't say (img width and height). New elements get fresh IDs, sent back to the tab.
A save carries one directive; empty text removes it. The rest stay where they are, so a directive the agent finished and removed can't come back. First child of its element; before a void or raw-text element (img, hr, pre…).
<p> <!-- @AI: say why first --> Ship the <b>formatter</b> first. </p> <!-- @AI: needs alt text --> <img src="map.png">
Write the canonical format, IDs and all. IDs are part of the file. The first format of each page is one noisy diff; after that a diff shows only new IDs.
directional Before a page leaves the tool, a cleanup script strips them.
A paragraph with bold and a link.
Pick the paragraph, the list, one item, or the section around them: each is a scope and edits as markdown. Saving sends the markdown; the server answers with the HTML, and the page swaps it in. Elements you left or changed keep their IDs and directives.
<!-- @AI: … -->, attached to exactly one element.
@AI:. Avoid: note, comment, annotation.
data-hrv, written into the file by assign IDs.
b, a, code…). See MDN: content categories.
<p>.
Work lives in yx. Run yx list.
html-revisor-spec.mdREADME.md8683442