User guide · Phase 1
Drafting in DraftLint
DraftLint is a contract editor that treats your document as structured law — articles, sections, clauses — not loose prose. This guide covers how to use the editor today and the underlying vocabulary you’ll see referenced in issues, PRs, and the schema.
01 Using the editor
Opening a document
The editor lives at /dev/editor. A sample Master Services Agreement loads by default so you can see numbering, outline, and signature rendering without typing anything first.
To start fresh, click New in the topbar. You get a minimal scaffold — one article with one section and one empty body paragraph. The numbering plate already reads Article 1 / Section 1, and the caret lands inside the empty paragraph ready for typing.
Empty headings show muted placeholder copy — “Untitled article”, “Untitled section” — until you type into them. The placeholder is purely visual; it never enters the document text and never persists.
The workspace
Three regions, top to bottom and left to right:
- Topbar — wordmark on the left; New, Find, and a Guide link in the middle; the Auto / Light / Dark theme toggle on the right. The theme choice persists per-browser (defaults to Light).
- Toolbar — inline-formatting buttons (B / I / U) on the left; a Reading / Full paper-width toggle and the Default / M&A numbering scheme toggle on the right. Pinned to the top of the viewport. Reading width is the default; Full lets the document expand to fill the available column for editing wide tables or side-by-side review.
- Outline rail— a live table of contents on the left. Click any entry to jump to its heading. The active item highlights as you scroll or move the caret. Drag the rail’s right edge to resize it (or focus the handle and press ← / →, hold Shift for larger steps). Double-click the edge — or press Homewhile it’s focused — to reset to the default width. Your chosen width persists per browser.
- Paper — the document surface itself, with a statline below reporting article count, section count, word count, and the active numbering scheme.
Reshaping structure
The depth of a heading is a property of the document, not a font size. Use Tab to push the current node one level deeper into the previous peer, and ShiftTab to lift it out. Numbering renumbers immediately. If a move would violate the schema (for example, trying to nest below subclause), the keystroke is silently absorbed — nothing happens to the document.
To reorder siblings at the same level, place the caret inside the node and press Alt↑ or Alt↓. This moves the entire subtree, headings and children together.
Adding new sections inline
Enter behaves differently depending on where the caret sits in a heading:
- At the endof a heading, the caret moves forward into the body — the first paragraph of the current section (or the first nested child’s heading at higher levels). No new section is created; this is the “finish the title, start writing” path.
- In the middle, the heading splits — the remainder text moves into a new sibling at the same level and the caret follows.
- At the start, an empty sibling is inserted before; the caret stays put.
To create a brand-new sibling at the same level after you’ve already dropped into the body, press ⌘Enter. That works from anywhere inside an article / section / subsection / clause / subclause and lands the caret in the new heading. Numbering reflows on its own.
Deleting a section
Two paths, same effect: ⌘⌫while the caret is inside the section’s heading, or hover the row in the Outline rail and click the × button that appears on the right. Children go with the parent — deleting a section takes its subsections, clauses, and subclauses along.
A toast at the bottom of the page confirms the delete and offers Undo (or just ⌘Z). Cross-references that pointed at the deleted node turn into broken refs — the red squiggly underline + Refs-rail Broken section surface them. No confirmation dialog; the toast + undo carry the safety.
The schema requires at least one section per article (and the doc requires at least one article). When deleting would leave a required-non-empty parent empty, the command is a silent no-op. One known limitation: once you delete and save, the node’s stable id is gone for good — recreating the section by hand won’t bring back cross-references that used to point at it. Undo restores it; save-then-undo doesn’t.
Inline formatting
Bold, italic, and underline are the supported inline marks for prose formatting. Toggle them with the toolbar or with ⌘B / ⌘I / ⌘U. The defined-term mark is documented separately below.
Defining terms
Select the text you want to define — typically the quoted, capitalized phrase right after “means” — and press ⌘ShiftD. A small popover appears in the right edge of the document with the selected text pre-filled as the canonical term. Edit it if the canonical form differs from what you selected (for example, selection “Affiliates” with canonical term Affiliate), then press Enter or click Define to wrap the selection. Esc or Cancel dismisses without changes. Definitions render in a brilliant midnight-blue ink, heavy weight, small-caps; in-prose usages of the same term echo the chord at a lighter weight in soft midnight ink — no underline, so the term-network reads at a skim without fragmenting the surrounding prose.
Automatic usage binding
The editor watches for capitalized phrases that match a registered canonical term and acts on its own when the choice is unambiguous:
- Exactly one definition — the match is wrapped in a
definedTermmark silently. The Defs rail’s usage count ticks up; nothing pops over the prose. The auto-bind is its own undo step, so ⌘Z reverts just the bind without erasing the word you typed. - Zero or two-plus definitions — the match gets a faint dotted underline in muted ink (distinct from the numbering-blue phantom underline). Hover or Tab onto the underline to surface a small Bind as usage of “X” pill; Enter or click confirms the binding.Esc dismisses the pill; the underline stays for the next interaction. Auto-binding is held back here because silent binding could introduce the wrong cross-reference.
Behavior is not retroactive: adding a second definition of an already-bound term leaves existing usages alone. Remove the conflicting definition and subsequent matches resume auto-binding.
Jumping from a usage to its definition
⌘click on any defined-term usage moves the caret to the first definition mark for the same term. Same gesture as ⌘click on an xref. Use Ctrlclick on Windows/Linux. Plain click stays a normal selection; only the modifier triggers the jump.
Marking usages manually
Once a term is defined, you can mark later references to it as usages so the Defs rail counts them and rename-cascade picks them up. Select the reference and press ⌘ShiftK. A picker appears listing every canonical term in the document, alphabetically. Type to filter, ↑ / ↓ to move the highlight, Enter to commit, Escto cancel. If the selection text matches a canonical term exactly (case-sensitive), that row is pre-selected — so the common case is “select the word, hit ⌘ShiftK, hit Enter.” The picker is keyboard-first; there’s no toolbar entry yet (the command palette will host one in a later phase). With no definitions in the document the picker shows an empty state pointing back at ⌘ShiftD.
Active term highlight
When the caret sits inside a definition or usage, the active span picks up a saturated green highlight and every sibling — the canonical definition and all other usages of the same term — picks up a lighter shade of the same hue. Move the caret away and every highlight disappears; defined terms read as normal prose at rest. The effect makes it possible to audit a term’s reach from the prose itself, without leaving for the Defs rail.
With the caret on any span, ⌘Shift→ jumps to the next occurrence of the same canonical term and ⌘Shift←jumps to the previous; both wrap at the ends. The definition is part of the cycle at its document position. The same gesture works on terms-of-art spans — see the § 02 keymap. When the caret is not on a qualifying span the default selection-extending motion runs as before. If a span happens to be both an authored defined-term and a dictionary term-of-art, the defined-term cycle wins.
Cross-references
Press ⌘ShiftR to open the cross-reference picker. The picker lists every article and section (and clause / subclause as the doc deepens) in document order, each row showing the computed label — e.g. Section 1.02 — and the first ~60 characters of the heading text. Arrow keys move the highlight; Enter commits; Esc cancels. Typing in the input filters by either the label or the heading text (substring, case-insensitive).
Three formats: Long (Section 4.2(b)), Short (§4.2(b)), and Number-only (4.2(b)). The default is Long; your selection is remembered for the rest of the browser session.
With an empty caret, confirming the picker inserts the computed text wrapped in an xrefmark. With a non-empty selection, confirming wraps the existing text as an xref without changing it — the path for converting prose like “the foregoing Section” into a tracked reference.
Tab-to-bind suggestion
As you type a citation that resolves unambiguously to a single hierarchy node — Section 4.2, §4.2(b), Article III, abbreviations like Sec.— the matched text picks up a dashed blue underline and a small chip appears just after it previewing the target’s heading and a Tab hint. Press Tab (or click the chip) to commit the binding in a single undo step — separate from the typing transactions beneath it, so undo reverts just the bind and leaves your prose alone. Esc, moving the caret out of the matched range, or typing past the citation dismisses the suggestion silently. Only confident, unambiguous matches surface — when the resolver isn’t sure, the chip stays out of your way and ⌘ShiftR is the explicit path.
In the body, xrefs render as a thin blue underline (same hue as the numbering plates — they encode the same kind of structural identity). When the caret sits inside one, the underline strengthens. ⌘clickon an xref jumps the caret to its target’s heading (use Ctrlclick on Windows/Linux); plain click stays a normal selection, matching the defined-term gesture. If the target was renamed or removed, the underline turns into a bright-red squiggle (same shape spellcheck uses), and ⌘click flashes the source span instead of jumping anywhere — a visible hint that the reference is broken and needs the picker re-run.
When you add, remove, or reorder a section — or flip the numbering scheme — every xref’s visible text updates in the same transaction. There’s no separate refresh step and no flicker; the renumber and the relabel land together, and a single undo reverts both. Xrefs whose target has gone missing read as a loud [broken ref]placeholder paired with the red squiggly underline; the rail’s Broken section keeps tabs on each one by id so you can find and re-point it. Same-transaction salvage still runs first: if your structural change happens to mint a section whose computed label uniquely matches the xref’s current text, the xref silently rebinds to the new node before the placeholder kicks in. Ambiguous matches stay broken on purpose. Undo always reverts the placeholder swap together with the structural change that triggered it.
Hover the pointer over an xref and a small card surfaces with the target’s full label, its heading text, and the first sentence of its body — along with a Jump to target button that works the same as ⌘clickon the underline. Moving off the xref dismisses the card. Alt-click on an xref shows the card immediately and pins it (no dwell delay, no hide-on-leave). Broken references show a muted “Target not found” card with a pointer back to ⌘ShiftR for retargeting. Press Esc to dismiss a pinned card; typing also dismisses.
Scan refs
The topbar’s Scan refs button finds plaintext citations anywhere in the document — Section 4.2, §§ 1, 2, and 3, Sections 4.2 through 4.5, Article III, parenthesized sub-locators, and the common abbreviations — and routes each match through a review dialog. Each row shows the surface text, the resolver’s best-guess target, and a confidence chip (confident, fuzzy, or no match). Confident + fuzzy rows are checked by default; no-match rows must be overridden via the row’s ⋯ menu before they can apply. Confirming binds every accepted row in a single transaction — one undo step regardless of how many references the scan caught. The scan only surfaces unbound plaintext; spans already wearing an xref mark are skipped.
The Defs rail
The left rail has three tabs: Outline, Defs, and Legal. Switching tabs preserves the outline state — collapsed sections, focus, and scroll position survive the round trip.
The Defs tab lists every defined term alphabetically. Each row shows:
- Term — the canonical string carried by the mark.
- Usage count — number of
definedTermmarks pointing at this term. (Auto-suggested usages land in a later story; today this counts only usages that already carry the mark.) - Snippet — the marked text from the first definition, italicized and muted.
- State stripe — a 2px right-edge stripe encoding state: muted for dead (defined but never used), numbering blue for phantom (placeholder for the upcoming detection pass), warning red for conflict (same term defined more than once).
Clicking a row jumps the caret to the first definition. The kebab (⋯) opens a per-term menu with Jump to next usage and Rename term….
A filter strip at the top of the rail — All / Defined / Phantom— narrows the list. Each chip carries its own count. Defined hides phantoms (so you can audit just the terms with bindings); Phantom hides everything else (so you can triage the “did you forget to define this?” flags in isolation).
Healthy defined-term rows wear a leading 3px midnight-blue marker and render the term itself in the same midnight ink at a heavier weight — the rail reads as every term is anchored, not as “one row out of a quiet list.” The row the caret currently sits inside picks up a saturated wash + an outer glow ring. Dead and phantom rows opt out of the accent so the chord is reserved for terms that actually carry a binding.
Renaming a term
Rename term… opens a preview dialog: the header holds a new-term input, and the body lists every marked surface form of the old term (one row per unique form, most-common first). Each row carries an editable suggested replacement and a Skip toggle. Enter from the header commits; Esccancels with no changes. Apply runs every selected row’s text rewrite and re-attributes every mark — including skipped spans, whose visible text stays but whose term attribute still updates — in a single transaction (one undo step).
Smart-suggest fills in confident transformations only:
- Case —
affiliate→subsidiary,Affiliate→Subsidiary,AFFILIATE→SUBSIDIARY. - Possessive —
Affiliate’s→Subsidiary’s. - Plural —
Affiliates→Subsidiaries. Powered by thepluralizepackage so irregulars likeIndicesandCounselcome out right.
Anything else — verb / adjective derivations (Affiliated, Affiliation), quoted forms (“Affiliate”), multi-word phrases — is left blank-and-not-confident on purpose. The row defaults to Skip; type your own replacement and untick Skip to apply. Better to type than to accept a wrong suggestion in haste.
If the new term you type is already defined elsewhere in the document, Apply is blocked with an inline error so you can’t accidentally create a conflict. Phantoms (capitalized matches with no definition mark) are not touched by rename — only marked spans for the renamed term are.
When every span happens to be an exact-case match of the old term (the common case after defining a clean canonical form), the command-palette / programmatic rename path skips the dialog and applies silently. The rail kebab always opens the dialog so you can see what changed.
Footer totals follow the rhythm N TERMS · N DEAD · N PHANTOM · N CONFLICT in mono uppercase — handy as a glanceable health indicator while drafting.
Phantom detection
A phantom is a phrase that looks like a defined term but has no matching definition. The rail flags three patterns:
- Quoted capitalized phrases —
“Affiliate”used without a definition. The strongest signal; legal drafting almost always quotes a term where it is defined. - Multi-word capitalized sequences —
Material Adverse Effectmid-sentence with no definition. - Single capitalized words mid-sentence —
each BorrowerwhereBorrowerisn’t defined. Sentence-initial capitalization, all-caps acronyms (LLC), and a small stoplist of unambiguous proper nouns (US states, months, days, common country names) are suppressed.
Phantoms surface as rows with a numbering-blue right-edge stripe and the snippet unresolved usage. Clicking a phantom row jumps to the first occurrence so you can wrap it as a definition with ⌘ShiftD or accept it as expected prose. The heuristic intentionally favors false negatives over false positives — when in doubt, it stays quiet.
Not yet shipped:per-document dismissal of phantoms (so “Closing” doesn’t keep nagging you on a doc that uses it casually) and the section-gutter stripe coordination with the gutter feature in Epic 1.6.
The Legal rail
The Legal tab inventories every dictionary term the editor recognizes in the current doc, grouped by rigidity tier in the order Hard → Semi-rigid → Negotiated → Convention. Empty tiers are omitted, so a contract that never touches a Hard term simply skips that section.
Each section header carries a tier dot in the tier’s hue and a count summary in the rhythm N TERMS · N USES. Each row shows the same dot, the canonical term, and a tabular usage count. Clicking a row jumps the caret to the term’s first occurrence; because the rigidity plugin already promotes the caret-adjacent match to .dl-rigidity--current, the jump doubles as a flash without a separate animation.
Where the Defs tab lists terms you have defined in the document via ⌘ShiftD, the Legal tab lists terms drawn from the built-in M&A dictionary — the same entries that drive the per-tier underlines and the Term Inspector. Empty state links to the dictionary section of this guide.
Pasting from Word and Google Docs
When you paste HTML from Word or Google Docs, the editor strips inline styles, spans, and image tags, then promotes paragraphs styled as headings (Word’s MsoHeading, Docs’ title) up to real heading nodes. Tables, images, and footnotes are dropped in Phase 1. Plain-text paste splits on blank lines into paragraphs.
Find and replace
Press ⌘F or click Find in the topbar to open the find panel above the document. Type a query and every hit is highlighted; the active match is emphasized and scrolled into view. ⌘G cycles to the next match, Shift⌘G to the previous one. Esc closes the panel, clears the highlights, and returns focus to the editor.
Three scopes are available, selected from the segmented control in the panel:
- Whole document — the default. Matches everywhere in the doc.
- Current section — narrows to the nearest ancestor
sectionof the caret. Moving the caret to a different section updates the scope automatically. - Selection — locks to the selection range that was active when you chose this scope. Useful for replacing inside a specific clause without touching the rest of the document.
Two option toggles sit beside the scopes: Aa for case-sensitive matching and ab| for whole-word matching. Both can be combined with any scope.
The lower row holds a replacement input plus Replace and Replace all. Replace swaps the active match and advances; Replace all rewrites every match in the current scope in a single, undoable transaction. Replacements always stay inside a single block, so node boundaries (headings, paragraphs, list items) are never crossed.
Accessibility & assistive tech
The editor surface, outline, and find panel are designed for keyboard-only and screen-reader use:
- Each heading in the document carries an
aria-levelmatching its schema depth — article = 1, section = 2, subsection = 3, clause = 4, subclause = 5 — so a screen reader announces “heading level 2” for a section, regardless of how the heading is rendered visually. - The outline rail is exposed as a real ARIA tree (
role="tree",role="treeitem",aria-level,aria-expanded). Only one item is in the tab order at a time; arrow keys move focus inside the tree. - Focus rings are always visible. Closing the find panel with Escreturns focus to the document so you don’t land back at the top of the page.
- No focus traps: Tab moves through the topbar, toolbar, outline, document, and signature block in source order.
Screen-reader walkthrough
The following flow has been verified manually with NVDA 2024 on Windows / Firefox and VoiceOver on macOS / Safari:
- Land on
/dev/editor. The page title is read first, then the topbar landmark, then the document outline tree. - Tabinto the outline. The first article is announced as “Agreement, treeitem, level 1, expanded.” ↓walks down the visible items; ←on an expanded item collapses it (“collapsed” is announced).
- Press Enteron any treeitem to jump the caret into that heading in the editor. Type-ahead works too — pressing “s-i-g” in quick succession jumps to the next heading whose text starts with “sig”.
- In the editor, headings are announced with their schema-derived level. Pressing ⌘F opens the find panel; the find input is auto-focused. Esc closes it and returns focus to the document — the screen reader picks up reading from the caret.
Signature block
The footer of the sample document renders a real signature block: party name, authorized-signature rule, and date rule, laid out on a grid. The party name is editable; the rules are visual chrome and aren’t part of the document text.
02 Reference
Keymap
Structure
| Tab | Demote — push the current article/section deeper into the previous sibling |
| ShiftTab | Promote — lift the current section out of its parent |
| Alt↑ | Move the current peer up among its siblings |
| Alt↓ | Move the current peer down among its siblings |
| Enter | In a heading: at end → into body; mid-heading → split into sibling; at start → empty sibling above |
| ⌘⌫ | Delete the section / article / clause containing the caret. Children go with it. Toast offers Undo. |
| ⌫ | At the start of an empty hierarchy node (blank heading, no body): delete the node. Toast offers Undo. |
| ⌫ | At the start of a non-empty heading: first press selects the whole hierarchy node (heading + body + children) with a soft accent ring; second press deletes it. Click or arrow keys exit the selection. |
| ⌘Enter | From a body paragraph: insert an empty sibling after the current hierarchy node |
Formatting
| ⌘B | Bold · Ctrl+B on Windows/Linux |
| ⌘I | Italic |
| ⌘U | Underline |
| ⌘ShiftD | Define term — wrap the selection as a definition · Ctrl+Shift+D on Windows/Linux |
| ⌘ShiftK | Mark as usage — wrap the selection as a usage of a registered term · Ctrl+Shift+K on Windows/Linux |
| ⌘ShiftR | Insert cross-reference — open the picker for any hierarchy node · Ctrl+Shift+R on Windows/Linux |
| Tab | Accept the live cross-reference suggestion when a chip is showing beside the caret. Plain Tab only; Shift-Tab still promotes the current hierarchy node. · Esc dismisses the suggestion silently. With no suggestion active, Tab falls through to the hierarchy demote behavior. |
| ⌘Shift→ | Next sibling — when caret is on a defined term or term of art, jump to the next occurrence (wraps) · Ctrl+Shift+→ on Windows/Linux |
| ⌘Shift← | Previous sibling — same as ⌘⇧→, in reverse · Ctrl+Shift+← on Windows/Linux |
Find and replace
| ⌘F | Open the find panel · Ctrl+F on Windows/Linux |
| ⌘G | Next match |
| Shift⌘G | Previous match |
| Enter | Next match (focus inside the find input) |
| ShiftEnter | Previous match (focus inside the find input) |
| Esc | Close the find panel and restore focus to the editor |
Outline navigation
| ↑ | Move focus to the previous visible item |
| ↓ | Move focus to the next visible item |
| → | Expand a collapsed item, or move into its first child |
| ← | Collapse an expanded item, or move out to its parent |
| Home | Move focus to the first outline item |
| End | Move focus to the last visible outline item |
| Enter | Navigate the editor to the focused heading |
| A–Z | Type-ahead — jumps to the next heading whose text starts with what you typed |
History
| ⌘Z | Undo |
| Shift⌘Z | Redo |
| ⌘Y | Redo (alternate) |
Hierarchy vocabulary
The schema enforces exactly five typed levels of legal hierarchy. The same vocabulary is used everywhere — in commands, in issue titles, in the outline rail — so it’s worth learning.
| Level | Depth | Role |
|---|---|---|
| Article | 1 | Top-level chapter. Renders centered with an "ARTICLE N" plate. |
| Section | 2 | Numbered subdivision inside an article. Default scheme: "Section 1." |
| Subsection | 3 | Hanging-label block. Used for grouping clauses. |
| Clause | 4 | Operative paragraph with a parenthetical label. |
| Subclause | 5 | Deepest level. Inner enumeration; further nesting is blocked by the schema. |
Numbering schemes
Numbering is derived from structure, not typed by the author. Two schemes ship today, switchable from the toggle in the toolbar:
- Default
- Plain Arabic numerals. Articles are labelled
Article 1,Article 2; sections areSection 1,Section 2. Deeper levels use decimal nesting. - M&A
- Roman articles and decimal sections.
Article I→Section 1.01→Section 1.02. Subsections and below use lower-case letters and lower-case roman numerals.
Switching schemes does not touch document content. Only the numbering widgets rerender — the same persisted document opens correctly under either scheme.
Legal-term recognition
The editor recognizes terms from a built-in Legal Terms of Art dictionary — covering both M&A and Master Services Agreement vocabulary — and stains the glyphs themselves with a per-tier accent (no underline): oxblood for hard (statutory) terms, dark mustard for semi-rigid, dark purple for negotiated, dark moss for drafting conventions. When the cursor lands in or beside a recognized term, the same hue brightens a notch so the active match pops without swapping identity. The Legal-rail legend dots and tier labels mirror the resting accent so the rail key is the literal color you see in prose.
The Lens toggle in the topbar controls how much recognition shows through: Off hides all decorations, Dim shows only Hard (statutory) terms and fades the surrounding prose to 80% alpha so defined-term marks and Terms of Art read as the only substance on the page, and On shows all four tiers at full ink. The choice persists per-browser; default is On.
Term Inspector
Hover the pointer over a recognized term and a small card surfaces below it with the term name, tier, plain-language definition, a watch note when one applies, and — if the term is also defined locally via ⌘ShiftD — a Jump to definition in documentlink. The card’s left stripe carries the tier hue; the rest of the card stays ink-on-paper so the page never reads as a Christmas tree.
Moving off the term dismisses the card. Alt-click on a term to pin the card without moving the caret — useful when you need to read the card while moving the pointer elsewhere. Dismiss a pinned card with Esc, by clicking outside, or by starting to type.
Known gaps in Phase 1
- Defined-term marks ship with a wrap command, a rename command, the Defs rail tab, and phantom detection. Per-doc phantom dismissal, auto-suggest for usages, and the section-gutter coordination still need to land.
- No cross-reference marks yet (Phase 2).
- No axe-core CI gate yet. A11y is verified manually plus a vitest unit test on the heading-level decorations; full pipeline integration is tracked separately.
- No real-time collaboration (Phase 3).
- Tables, images, and footnotes are not preserved when pasted.