# Report-Gates Defect Glossary

This reference catalogs every defect code the report gates (`static`, `fidelity`, `layout`, `visual`, `proofread`) can raise, their severity and impact, and how to repair them.

## Defect Codes by Category

### Static Gate (Content & Structure)

| Code | Severity | Integrity | What It Detects | How to Fix |
|------|----------|-----------|-----------------|-----------|
| `korean_no_keep_all` | major | false | Text containing Hangul uses default word breaking, causing character-level wrapping and orphaned characters on mobile or narrow containers. | Add "word-break: keep-all; overflow-wrap: anywhere" to the body or the slide text container. |
| `palette_off_token` | minor | false | Color values are hardcoded literals (hex, rgb) instead of design-system tokens or are missing from the design spec palette. | Replace the literal color with a design-spec token (var(--...)) or add the color to the spec palette. |
| `emoji_in_prose` | major | false | Emoji characters appear in body text, headings, or figure captions. Emoji render at inconsistent sizes and lack semantic meaning. | Remove the emoji; use an inline SVG icon or plain text. |
| `em_dash_in_prose` | major | false | Em dashes (—) are used in body text for sentence breaks or parenthetical asides. Readability and accessibility suffer without a rewrite. | Rewrite the sentence with a comma, colon, or period instead of an em dash. |
| `en_dash_in_prose` | minor | false | En dashes (–) are used incorrectly outside of numeric ranges. Only numeric ranges (e.g., "pages 5–10") should use en dashes. | Use a hyphen for compounds; keep the en dash only between numbers. |
| `heading_too_long` | minor | false | A heading exceeds one visual line: 26 Korean characters (52 display width units) or 12 English words. | Shorten the heading to one line (26 Korean characters or 12 English words). |
| `heading_generic` | minor | false | A heading uses generic labels (e.g., "Introduction", "Background") without conveying the section's specific claim or finding. | Make the heading carry the section's claim instead of a generic label. |
| `unsourced_number` | major | false | A numeric value (percentage, count, duration, ratio) appears without source attribution or lineage. Under Sources, an element is split at line breaks and sentence ends, and a sentence is checked exactly as under any other heading unless every numeral in it is a citation-field number: a year, a full date, or a month/day (`10/26`, `Oct 26`, `2026. 10. 26.`; a day count such as `30 days` is checked); a page, volume, issue, number, or edition marker; a number inside a URL, DOI, arXiv id, ISBN, or ISSN; a number inside a quoted title (straight or curly quotes, corner brackets, `<cite>`, or an `<i>`/`<em>` title directly before a URL or date); a number inside link text; or a citation tag's digits. A year range (`2019-2023`, `2019~2023`) is a date. A digit run glued to letters that are not a unit, multiplier, or currency is a name, not a numeral (a commit hash, `#123`, `GPT-4`, `v2.3.1`, `Q3`, `FY27`, `10-Q`), and so is a dotted version (`0.30.14`); `50M`, `10k`, `3x`, `x3`, `2.5GB`, `320m`, `12%`, `5mn`, `40pct`, `5yrs`, and the European grouping `1.234.567` stay numerals. A unit, multiplier, or percent on the right is read before the letters on the left, so a word glued in front of a measure (`rate40%`, or a Korean word written solid before `300` and a counter) keeps it a numeral; Latin letters on both sides make an identifier (`W3C`). A Korean ordinal (the je prefix, the jjae suffix, the cha counter) is a name and a Korean quarter (N bungi) is a date, like `Q3`; a two-digit dotted date (`26.10.26`) is a date. Ruling consequences: `10-K` and `8-K` stay numerals because `K` is a multiplier while `10-Q` is a name, so quote a title that carries a filing code; a slashed month/day also covers fractions (`2/3`) and table scores (`7/12`), which are therefore not checked under Sources (#11386). A version, figure, or count in an unquoted title (`Llama 3.1 model card`, `Buyback $94B`, `Revenue grew 40%`) is checked; quote the title to exempt it. | Tag the number MEASURED / ASSUMED / DERIVED or cite its source in the same paragraph; under Sources only citation-field numbers and names (a hash, #12, GPT-4, Q3, 10-Q) are exempt, so quote a reference title that carries a code or a figure, or wrap it in <cite>: an unquoted one is checked, and 10-K / 8-K stay numerals because K is a multiplier. A slashed N/M reads as a month/day, so a fraction or a score (2/3, 7/12) under Sources is not checked (#11386): write it as a percent or tag it. |
| `chart_without_figure` | major | false | An image or chart lacks a figure wrapper with a caption. Uncaptioned charts isolate visuals from narrative flow. | Wrap the image or chart in a figure container with a caption. |
| `chart_svg_no_text` | major | false | An SVG chart or diagram lacks a title, axis labels, units, or value labels. Visuals cannot stand alone without text guidance. | Give the chart a title, axis labels, units, and value labels as SVG text. |
| `chart_svg_missing_labels` | minor | false | An SVG chart has some labels (e.g., title) but is missing others (e.g., axis ticks or unit). | Add the missing title, axis ticks, or unit label to the chart. |
| `report_without_visual` | major | false | The report body has no chart, diagram, or figure image at all (header logos, icons, and images marked decorative do not count; only rendered output counts, so a Mermaid source block or an image with any zero dimension (attribute, inline style, or viewBox) does not; an element with no declared size counts, because the browser sizes it, and the layout gate is where rendering is proven). A text-only report hides structure and trends the reader would see at a glance. | Add at least one captioned visual of the findings: a chart from the data table, a rendered Mermaid diagram, or a figure image (references/report-visualization.md). |
| `image_missing_alt` | minor | false | A figure image has a missing or empty `alt` and is not marked decorative. Screen readers and failed image loads leave the reader with nothing. An image is decorative only with an empty `alt` plus `role="presentation"` (or `role="none"`, or `aria-hidden="true"`): it raises nothing here and does not count as a visual. | Give the image an alt attribute that states what it shows, not its file name; mark a purely decorative image alt="" with role="presentation". |
| `korean_serif_font` | major | false | A serif or brush Korean font family is applied to Korean text. The design spec mandates gothic (sans) Korean typefaces. | Use a gothic (sans) Korean family; serif and brush families are banned by the design spec. |
| `section_without_citation` | major | false | A major section or subsection contains no in-text citations or reference marks. Source attribution is missing. | Cite at least one source inside this section or merge it into a cited one. |
| `missing_closing_section` | blocker | true | The report lacks a closing section explaining the methodology, sources, and gate outcomes. Delivery is incomplete without narrative closure. | Add the closing section that explains how the report was made (method, sources, gates). |
| `missing_citation_section` | blocker | true | The report has no sources, references, or citation definitions (e.g., [S1], [S2]). Attribution is impossible. | Add a sources or references section, or [S<n>] definition lines. |
| `broken_asset_reference` | blocker | true | An asset file (image, diagram, spreadsheet) is missing or unreadable. The deliverable cannot be rendered or is incomplete. | Re-render or restore the missing asset file before delivery. |
| `missing_required_section` | blocker | true | A section explicitly required by the brief (e.g., via `--require-section`) is absent. | Add the section the brief requires (matched by --require-section). |
| `missing_promised_deliverable` | blocker | true | A deliverable (e.g., a slide deck, a chart, a data file) is promised in the brief but not delivered, not marked blocked_capability, and not skipped with a reason. | Produce the promised deliverable or record it as blocked_capability or skipped with a reason. |

### Fidelity Gate (Facts, Quotes & Meaning)

Raised only by the fidelity verifier (the fact ledger, fill, verify, meaning, and polish steps), never by `check`. Until that verifier ships in an edition, the manifest records `fidelity: not_run`.

| Code | Severity | Integrity | What It Detects | How to Fix |
|------|----------|-----------|-----------------|-----------|
| `unresolved_placeholder` | blocker | true | A fact or quote placeholder is left in the artifact, names an unknown ledger id, is malformed, or uses an unsupported mode. | Register the fact with fact add or remove the sentence; a placeholder is never filled with a guess. |
| `value_mismatch` | blocker | true | A displayed value differs from its ledger row's rendering, or a stored derived value differs from exact recomputation (a hand edit after fill). | Re-run fact fill; never edit a filled value by hand. |
| `unit_mismatch` | blocker | true | A displayed, adjacent, or header unit conflicts with the ledger row's unit or scale, or a value-mode fact lost the unit bound to it at fill (the fill map records it: the unit after the value, its currency sign, or its column header). | Re-run fact fill, or derive a converted row with fact derive --op convert. |
| `unit_unbound` | minor | false | A value-mode fact with a ledger unit shows no recognizable unit: none right after its value (with or without a space, a Korean particle allowed), no currency sign right before it, no :unit element after it, and no unit in its visual column header (a table with a rowspan or without a th row binds no header). | Use the full fact placeholder, or write the unit right after :value (with or without a space; a Korean particle may follow), as its :unit element, or in the matching table column header. |
| `quote_not_verbatim` | blocker | true | A quotation bound to a quote row does not match the source snapshot contiguously after normalization, or a rendered cut is not the registered cut. | Re-insert the quote through its {{Q<n>}} placeholder; after a second failure the sentence is replaced by the quote. |
| `claim_status_invalid` | blocker | true | A sentence's claim binding names an unknown claim, a refuted or unresolved claim outside Gaps and Contradictions, or a high-risk claim absent from the verified claims. | The claim id is unknown, refuted, unresolved, or not in verified-claims; move the sentence to Gaps or Contradictions, or remove it. |
| `incompatible_operands` | blocker | true | A derivation's operands differ in unit, population, period, or accounting basis where the operation requires them to match; derive refuses and writes no row. | The operands differ in unit, population, period, or basis for this operation; fix the rows or declare the operation that fits. |
| `pointer_label_mismatch` | blocker | true | A placeholder's label differs from the referenced ledger row's label (a typo guard on the id). | The placeholder's label is not the row's label; check the id, then the label. |
| `quote_anchor_unresolved` | blocker | true | A quote cut's from or to anchor is absent, occurs more than once, or appears in reverse order in the quote. | A from or to anchor is missing or occurs more than once in the quote; pick unique anchor words. |
| `unit_duplicated` | blocker | true | A unit-bearing placeholder is followed by another copy of its own unit. | The unit after the placeholder repeats the row's unit; use :value or remove the unit. |
| `conversion_altered_number` | blocker | true | `fact add` located a value (or a number in its sentence) that the source's conversion probe recorded as altered in `sources/S<n>/conversion.json`, such as a footnote digit glued onto it; the converted text cannot back the number, so no row is written. | The source conversion altered this number (sources/S<n>/conversion.json lists it); snapshot a verified text of the source over it with `source snapshot --id S<n> --text <verified.txt> --replace` (refused while an active row cites S<n>; then retract those rows or use a new id), then re-add the fact. |
| `polish_diff_fail` | blocker | true | The polish pass changed protected fact spans, numerals or units, tickers, table rows, or any markup byte (including every attribute, comments, and script/style/template content). | Revert to the pre-polish artifact and apply text-node edits only; markup changes require the full fidelity gates. |
| `fidelity_interview_incomplete` | blocker | true | A fidelity field in the brief is still pending or missing when a fidelity command runs. | A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun. |
| `derived_not_allowed` | blocker | true | A placeholder resolves to a derived, converted, or assumed row while the brief allows no derived numbers. | The brief allows no derived or assumed numbers; cite a source row directly or change the brief with the requester. |
| `source_outside_floor` | blocker | true | A ledger row's source lies outside the source floor the brief records (pointed sources only, primary only, or the allowed source classes), or the row rests on no source under any floor but `standard`. | The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact. |
| `brief_changed` | blocker | true | `fact verify` reads an edition's fill map whose recorded brief hash is not the hash of the current locked `## Fidelity` block, or that records none. | The edition's fill map was not written under the current ## Fidelity block (it changed after fill, or the map predates the binding); re-run fact fill, then fact verify. |
| `snapshot_hash_mismatch` | blocker | true | The pinned snapshot body (raw.bin; text.txt for converted-only and legacy sources) differs from its metadata or from a cited row's pinned sha256, or a cited row's sources dir, meta.json, raw.bin, or text.txt cannot be read (the message names the cause). | The source snapshot changed after it was pinned or cannot be read; restore it, or run source refresh to re-anchor its rows. |
| `snapshot_not_raw` | blocker | true | A measured row rests on a converted-only source (no raw bytes), or `fact add` was run on such a source or on a legacy text.txt snapshot. | Measured rows need the original bytes: snapshot the source with --url, --html, or --raw <bytes> --text <extraction>; converted-only sources support warned quotations only. |
| `chart_value_untraceable` | blocker | true | A chart data label, axis tick, or chart data file does not match the ledger-backed data the chart declares, or the chart declares no data source. | A chart value, tick, or data file does not match its ledger-backed data source; re-render the chart from the exported data. |
| `quote_unbound` | blocker | true | A direct quotation in the report has no ledger quote row behind it; a citation marker alone does not bind it. | A direct quotation has no ledger quote row; register it with fact quote and insert it through its placeholder. |
| `edition_unverified` | blocker | true | A delivered edition has no fact verify record whose artifact hash matches the delivered source. | The delivered file has no matching fact verify record; run fact verify on that edition and record it with --verified-by. |
| `untraceable_number` | major | false | A number or date in prose or a table cell resolves to no ledger row and is not structural; a citation does not exempt it. Under Sources a sentence is traced unless every numeral in it is a citation-field number (see `unsourced_number`). | Replace the typed number with a {{F<n>}} placeholder backed by a ledger row, or register it as assumed with a rationale. |
| `unsupported_verification` | major | false | A fact bound to a `verified` mark (`data-claim-kind="verified"`, from a `{{V:F<n>:label}}` placeholder) cites a row whose own locator artifact does not sit in a recorded command-output snapshot, or the mark binds no fact of its own. Another command row in the same sentence never counts. Unmarked prose is never raised here. | A sentence marked verified (data-claim-kind="verified") cites a row that is not recorded command output; cite the recorded output row or drop the verified mark. |
| `claim_kind_mismatch` | major | false | A fact bound to a claim kind cites a row of a kind that claim kind does not accept, a fact sits under marks of two different kinds (nested marks), a mark binds no fact of its own, or a mark names an unknown kind; the kinds are in [fidelity-claim-kinds.md](fidelity-claim-kinds.md). | An element marked with a claim kind (data-claim-kind) cites a row that kind does not accept, or the kind is unknown; cite a row of that kind or fix the mark (references/fidelity-claim-kinds.md). |
| `conflicting_readings` | major | false | Two active ledger rows with the same value and unit but different periods or qualifiers are cited as one fact. | Two ledger rows with different periods or qualifiers are cited as one fact; split the sentence or retire one row. |
| `hedge_dropped` | major | false | A hedge, condition, or negation class present in the source quote is absent from the report sentence. | Restore the qualifier the source uses (estimated, about, not yet). |
| `direction_flipped` | major | false | The source states an increase and the sentence a decrease, or the reverse. | The source states the opposite direction; rewrite from the quote. |
| `causality_added` | major | false | The sentence carries a causal connective that no supporting quote or claim statement contains. | The source states no cause; state the correlation or quote the source. |
| `qualifier_dropped` | major | false | A bounding or approximation qualifier recorded on the ledger row (up to, at least, about) is absent near the value in the sentence. | The source bounds this value (up to, at least, about); restore the qualifier or use :q. |
| `unbound_sentence` | major | false | A findings sentence carries no fact, quote, citation marker, or claim binding and is not marked as opinion or recommendation. | Bind the sentence to a fact, a quote, a citation, or a claim-graph node with data-claim. |
| `meaning_drift_confirmed` | major | false | Two independent meaning readers flag the same drift kind on the same sentence and quote pair. | Two independent readers found the same drift; rewrite from the quote or dispute with a reason. |
| `meaning_unverified` | major | false | `fact meaning-check` found a pair no reader judged, or a force_strong pair no strong reader judged; an empty drift list is not a clean check. | No meaning reader judged this pair, or no strong reader judged a force_strong pair; run the missing reader on pairs.json and rerun fact meaning-check. |
| `polish_edit_rejected` | major | false | A proposed polish edit changes a protected number, hedge, qualifier, link, or claim id. | The edit changed a number, a hedge, a qualifier, a link, or a claim id; re-propose the language fix only. |
| `polish_hedging_decreased` | major | false | The polish pass removed a hedge the source grounds. | The polish pass removed qualifiers; restore them. |
| `polish_text_churn` | major | false | More than half of the visible prose changed during the polish pass (`POLISH_CHURN_LIMIT` in `scripts/polish-rules.mjs`). | More than half the text changed during polish; the pass composed instead of proofreading. |
| `snapshot_mismatch` | major | false | An operation that needs one collection window uses operands from unequal windows or observations outside the window. | The operands come from different collection windows; use rows from one window or state the windows separately. |
| `operand_timestamp_missing` | major | false | A windowed operation cites an operand without an observed_at timestamp. | Record the operand's observed_at timestamp before checking its collection window. |
| `operand_missing` | major | false | A windowed operation names an operand id that is not in the fact ledger (a dangling reference, not a missing timestamp). | A windowed operation names an operand that is not in the fact ledger; restore the row or re-derive from existing rows. |
| `join_integrity_fail` | major | false | A table row combines facts from different source observation rows, or a declared observation id contradicts the row's lineage. | A table row combines facts from different source observations; keep each row to one observation or split it. |
| `quote_stale_version` | major | false | The quoted source revision differs from the revision the report targets for the same scope. | The quoted source revision differs from the revision the report targets; re-snapshot the target revision or date the quote. |
| `quote_fuzzy_match` | minor | false | An exact quote match failed but a candidate with similarity of at least 0.95 and identical protected tokens exists (OCR noise); diagnostic only. | The source text is OCR-noisy; confirm the snapshot and prefer the raw document. |
| `relocated` | minor | false | After `source refresh` repinned a source, the row's exact quote and context occur exactly once at a new byte range; old and new offsets are reported. | After source refresh the exact quote and context sit at one new byte range; review the old and new offsets (verification never rewrites the row). |
| `quote_converted_only` | minor | false | A quote row rests on a converted-only source, so it is bound to converted text, not to the fetched bytes; `fact quote` and `fact verify` both report it. | The quote comes from a converted-only source with no raw bytes; re-snapshot the original with --raw and --text, or keep the warned quote. |
| `name_outside_quote` | minor | false | An uppercase Latin token of 2 to 6 letters in the sentence is absent from the supporting source text and that source's acronym table. | A name in the sentence is not in the cited quote; add the source or remove the name. |
| `verification_claim_unmarked` | minor | false | A sentence matches the unmarked cue rule of the `verified` claim kind (a first-person run: we/I (have) ran, run, re-ran, re-run, executed, verified by (not ran out of), or by running after we/our in the sentence; a first-person check: we/I (have) verified, checked, confirmed, measured, tested, double-checked, cross-checked (not checked in/into/out) with a result object in the clause (value, values, result, results, figure, figures, number, numbers, total, totals, count, counts, measurement, measurements, reading, readings, output, outputs, span, length, rate, rates, latency, throughput, score, scores, benchmark, benchmarks, metric, metrics, data, estimate, estimates, speed, duration, size, ratio, growth, or a digit) and no document named before the clause ends (report, reports, filing, filings, document, documents, paper, papers, article, articles, publication, statement, disclosure, website, page, docs, documentation; a comma, a semicolon, or as, which, who, whom, whose, where ends it, and a document word followed by a number is a locator); \uC6B0\uB9AC or \uC800\uD76C as the subject word (+ \uAC00, \uB294, or \uD300\uC774), then \uC2E4\uD589\uD588, \uC2E4\uD589\uD558\uC5EC, \uC2E4\uD589\uD574, \uB3CC\uB838, or \uB3CC\uB824 in the clause with no third-party subject between, or then \uD655\uC778, \uAC80\uC99D, \uCE21\uC815, \uC810\uAC80 + \uD588, \uD558\uC600, \uD558\uC5EC, \uD574, \uD55C with a result object in the clause (\uACB0\uACFC, \uAC12, \uC218\uCE58, \uAE38\uC774, \uD569\uACC4, \uBE44\uC728, \uC18D\uB3C4, \uC9C0\uD45C, \uB370\uC774\uD130, or a digit) and no document before the clause ends (\uBCF4\uACE0\uC11C, \uBB38\uC11C, \uACF5\uC2DC, \uB17C\uBB38, \uAE30\uC0AC, \uC790\uB8CC; a comma, a semicolon, or \uCC98\uB7FC, \uB4EF\uC774, \uAC19\uC774 ends it, and a document word followed by a number is a locator); \uC9C1\uC811 with no third-party subject, then \uC2E4\uD589 or \uB3CC\uB9AC in the clause; a curly apostrophe reads as a straight one; rendered from CLAIM_KINDS, see fidelity-claim-kinds.md) and cites a fact not marked `verified` (or cites none and its block is unmarked). A warning only; it never blocks. | The sentence reads as a check we ran but carries no verified mark; mark it {{V:F<n>:label}} on a recorded command-output row, or reword it. |
| `scope_widened_warn` | minor | false | The sentence uses a broader scope term than the quote's narrow term, and no supporting fact carries the broad scope. | The sentence uses a broader scope word than the quote; confirm the scope or narrow it. |
| `particle_unverified` | minor | false | A particle after a filled value could not be checked against the value's final reading. | The particle after this value could not be checked; read it aloud and fix it by hand if wrong. |
| `fallback_without_entity` | minor | false | A quote that replaced a twice-failed sentence has no entity for its attribution line. | The replacement quote has no entity for attribution; set one on the row. |
| `meaning_drift_warn` | minor | false | One meaning reader flags a change of scope, subject, or certainty without independent agreement. | A separate reader found the sentence changes scope, subject, or certainty; rewrite from the quote. |
| `polish_edit_unlocated` | minor | false | A polish edit's before-text matches zero or several places in the current text. | The edit's before-text was not found exactly once; re-propose against the current text. |
| `polish_link_changed` | minor | false | The polish pass altered or removed an href link target, or an internal target stopped resolving. | A link was altered or removed during polish; restore it. |
| `polish_name_changed` | minor | false | A ledger-backed proper noun present before polish is absent afterward. | A proper noun changed during polish; restore it. |

### Layout Gate (Rendering & Presentation)

| Code | Severity | Integrity | What It Detects | How to Fix |
|------|----------|-----------|-----------------|-----------|
| `layout_overflow` | major | false | An element exceeds its container width or height, causing horizontal scroll or content spillage in PDF, DOCX, or slide renders. | Constrain the element to its container (max-width: 100%, overflow-wrap: anywhere, or a narrower measure). |
| `layout_text_clipped` | major | false | Text is clipped or hidden by `overflow: hidden` or a fixed container height. Content is lost or unreadable. | Remove the overflow: hidden clip or give the text room to wrap. |
| `layout_scroll_container` | minor | false | A table or list is wrapped in a scrollable container for HTML deliverables. Scrolling content prints clipped in PDF or DOCX. | Scrolling content prints clipped; make the content fit or wrap the table in a scroll wrapper only for HTML deliverables. |
| `layout_image_distorted` | major | false | An image is distorted, stretched, or squeezed (aspect ratio is not preserved). The visual information is misrepresented. | Preserve the image aspect ratio (object-fit: contain inside a fixed container). |
| `layout_sibling_overlap` | major | false | Two adjacent block elements overlap (via negative margins, absolute positioning, or grid collision). Layout is broken. | Separate the overlapping elements (margins, grid, or a slide split). |

## Exit Codes

Gates emit a manifest-level outcome with one of three statuses:

| Status | Meaning |
|--------|---------|
| `pass` | All checks passed; no defects raised. |
| `fail` | At least one blocker defect was raised. Major and minor defects alone leave the gate at `pass`; they still go to the repair loop. |
| `not_run` | The gate was not invoked (e.g., no probe file provided for layout, no visual inspection conducted). |

### Layout Gate Probe File Requirement

The `layout` gate only runs when a probe file (a rendering artifact: PDF, PNG, or HTML snapshot) is provided alongside the manifest. If no probe file is available, the manifest records `layout: not_run` and the gate does not emit defects.

## Polish Text-Node Contract

Polish edits change text nodes only; markup changes go through the full fidelity gates, not polish.

The primary rule is a whole-document skeleton comparison. Both gates scan the pre-polish and the polished document with the same tokenizer (`scripts/html-markup.mjs`) and require the skeleton to be byte-identical in document order: every tag token, comment, bogus comment, the frozen content of `script`, `style` and `template`, and every character reference of a text node (`&` up to the first `;` or non-name character). An accepted edit carries no `<`, `>` or `&` and may not touch a character reference, so an edited span holds no skeleton token and a legitimate polish never moves this sequence. Any difference names the first differing token and its byte offset: `polish-apply` refuses the edit, and `polish-diff` reports the blocker `polish_diff_fail` ("skeleton changed at token ..."). The character guards below are the early, actionable refusals; this comparison is what makes the guarantee complete, so a hand edit from `R&amp;D` to `R&lt;D` is blocked even though no edit was involved.

An edit's `before` and `after` may not contain `<`, `>` or `&` at all. `polish-apply` refuses such an edit before any other check, as `polish_edit_rejected` with the reason `edit contains markup characters (<, >, &): route this section through the full fidelity gates`, so the refusal is visible, never a silent drop. The rule holds whatever the tokenizer models: an edit without these characters cannot create, alter, close or escape a tag, comment, bogus comment, CDATA section, entity or script. The characters alone are not enough, because a located region can start right after an existing `&` or inside a character reference: `polish-apply` also refuses, as `edit touches a character reference: route this section through the full fidelity gates`, any edit whose located region overlaps a character reference or opens one. Literal `&` and `<` in prose are entities in the source (`R&amp;D`, `p &lt; 0.05`) and stay usable only by leaving their bytes untouched: the proofread lane narrows an edit to the words around an entity or tag (`budget grew` to `budget rose`), never one that spans it. Wording that cannot be fixed without touching an entity or markup goes through the full fidelity gates.

Before comparing, both gates refuse with `polish_diff_fail` ("ambiguous markup: ...") any document holding a construct the polish tokenizer does not model exactly: `</` followed by a non-letter (`</>` included), `<!--` inside a `script` or `style` element, `<![CDATA[`, `<?`, `--!>`, or `<` inside a tag or attribute value. Renderer output contains none of these. The tokenizer still follows the HTML spec for the bogus comment (`</` + non-letter, `<?`, `<!` up to the next `>`), the abrupt comment closes `<!-->` and `<!--->`, and the script-data escaped and double-escaped states.

The shared rule in `scripts/polish-rules.mjs` runs per edit against the current and original report in `polish-apply`, and against the whole document in `polish-diff`. The sequence of opening and closing tags, every attribute name and value (including `href`, `id`, `class`, and `style`), comments, declarations, and all content of `script`, `style`, and `template` elements must be byte-identical. Attribute reordering, different quoting, tag case, whitespace inside a tag, and a self-closing slash are markup changes too. Any difference raises the blocker `polish_diff_fail`; apply rejects it with reason `markup changed`.

Text nodes remain subject to the existing fact-span, number/unit, ticker, hedge, qualifier, name, and churn checks. Protected numbers, tickers, and required hedges cannot migrate between text nodes. A text edit inside an existing fact span is still a fact-span change, including under `aria-hidden`. Script/style/template content is frozen through the end of input when unclosed. No CSS or HTML visibility model is used by polish.

## Lineage and Citation Markers

Defect codes and their hints are delivered via the outcome manifest under `residualDefects` (if repair did not exhaust). Each defect entry includes:

- **code**: A string key from the table above (e.g., `"em_dash_in_prose"`)
- **message**: A human-readable narrative or location hint describing where the defect was found (e.g., "Line 42: em dash found in paragraph 2")

Citations within reports follow one of two lineage modes:

- **inline**: Source marks appear as `[Sn]`, `[Source n]`, or `[^n]` hyperlinks inline with the cited text.
- **section**: Sources are listed in a dedicated section with numbered entries ([S1], [S2], etc.).

Both modes are valid; choose based on the report's narrative flow.
