Build Lex
YESreplaces $12/mosaves $144/yrback to the verdict
A document editor whose AI reads what you wrote and responds as a reader would, in margin comments you can accept, argue with or ignore, with the constraint that it never edits your text and never writes a sentence you then keep.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the editor and the local application.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree, needs memory and patience
Why The default policy is local, and a draft is the most private writing most people do.
Get it Ollama or llama.cpp with a model that follows structured output instructions reliably. open ↗
Verify
It returns valid structured output for a comment request ten times in a row - have readyfree
Why Comment quality, coverage and anchor drift are only observable on real writing with real weaknesses.
Get it Something you are actually working on and would revise.
Verify
It is long enough that comment coverage across sections is testable - decidefree, a couple of hours of reading
Why Everything about comments depends on positions that survive editing, and this is the hardest decision to change later.
Get it Read how your editor framework represents positions and how they map through changes before writing anchor code.
Verify
A written note explains how a position maps through an insertion above it
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- documents(id, title, created_at, updated_at, model_policy) · model_policy is local or a named hosted model, per document - versions(id, document_id, body, word_count, at) · a snapshot per writing session, not per keystroke - comments(id, document_id, version_id, from_pos, to_pos, kind, body, state, at) · kind is question, observation, structure or reader_reaction - comment_state · open, addressed, dismissed. Dismissed is kept, because dismissing the same note three times is a signal - anchors(comment_id, quoted_text, prefix, suffix) · so a comment can be relocated after the text moves rather than becoming orphaned - There is deliberately no table for AI-generated body text, because the tool does not produce any.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DB_PATH | required | ~/.local/share/lex/lex.db | Documents, versions and comments. Back this up like the drafts it holds. |
DEFAULT_MODEL_POLICY | required | local | What a new document gets. Local, so sending is a decision rather than a default. |
LOCAL_MODEL | required | llama3.1:8b | The local model for comments. Must follow structured output reliably. |
SESSION_GAP_MINUTES | required | 30 | The activity gap that ends a writing session and snapshots a version. |
HOSTED_API_KEYsecret | optional | sk-... | Only for documents explicitly set to a hosted model. Absent means local only, which is a fine way to run. |
The build, in order
An editor that keeps your words
You can write for an hour and lose nothing.
Comments anchor to ranges. A model without stable positions makes the next phase impossible.
Files
src/editor.tsTwo different mechanisms. Conflating them ruins the history.
Files
src/versions.tsMid-sentence, not at a clean point.
Files
tests/recovery.test.ts
done when · tick each as it passeswatch out- Snapshotting per keystroke fills the database with thousands of near-identical versions and makes the history unusable for the thing it is for, which is seeing how the draft changed. Snapshot per session, defined by a gap in activity, and keep the current text saved continuously and separately.
Comments in the margin, anchored to the text
The AI responds to a passage as a reader, and the note stays on that passage.
Range, kind, body. No replacement field, because the absence is the design.
Files
src/comment.tsWhat confused them, what they wanted next, what the paragraph is doing.
Files
prompts/comment.mdModels attend to the opening. Chunk so later sections get read too.
Files
src/chunk.ts
done when · tick each as it passeswatch out- A model asked to comment will offer a rewritten sentence unprompted, and the moment a rewrite is one click from being applied the tool has become a writing tool rather than a reading one. Make the output structure hold no replacement text field at all, so there is nothing to apply.
Comments that survive editing
You revise a paragraph and its comments follow the text or say they cannot.
Prefix and suffix, so relocation by content is possible.
Files
src/anchor.tsPosition first, quote match second, orphan third. A pure function, unit tested.
Files
src/anchor.tssrc/anchor.test.tsWith the text they referred to. Silently dropping them loses feedback you paid for.
Files
src/margin.ts
done when · tick each as it passeswatch out- Comments anchored only to character offsets drift onto unrelated text after an edit above them, and a note about a weak argument appearing next to a paragraph it does not describe is worse than no note at all. Keep a quoted excerpt and relocate by content, and orphan the comment when the text no longer exists.
Argue with a note, and keep the argument
You can disagree with a comment and that disagreement persists.
Addressed, dismissed, replied. Deletion loses the signal.
Files
src/state.tsThe pattern is the interesting output, not any single dismissal.
Files
src/patterns.tsAddressing a comment is a state change. The revision is yours to make.
Files
src/state.ts
done when · tick each as it passeswatch out- Deleting dismissed comments throws away the most useful signal in the system. A note you dismiss every time is either a deliberate stylistic choice, which the tool should learn to stop raising, or a blind spot, which is worth knowing. Keep dismissals and surface the pattern.
Where the draft goes, per document
You choose per document whether it is ever sent anywhere.
Not a global setting. The unit of decision is the draft.
Files
src/policy.tsRaise rather than send. Interface enforcement is not enforcement.
Files
src/model.tssrc/model.test.tsSo you know which reader you were listening to.
Files
src/comment.ts
done when · tick each as it passeswatch out- A global setting means one careless change sends every draft, including the ones you had decided were local only. Put the policy on the document, enforce it where the request is made rather than in the interface, and test that a local-only document raises rather than sends.
Getting the writing outproduct builder
The draft leaves in a format you can use, and comments come too if you want.
Byte for byte, in a test. This is the promise made checkable.
Files
src/export.tssrc/export.test.tsSome formats support them. Omission is the default.
Files
src/export.tsFinding the paragraph you cut three drafts ago is why versions are kept.
Files
src/search.ts
done when · tick each as it passeswatch out- An export that silently applies a suggestion, or includes a phrase from a comment, breaks the one promise the tool makes. Compare the export byte for byte against the stored document text and treat any difference as a defect.
That is the whole plan for Lex. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Collaborative editing.
- Publishing and distribution.
- Any generation of body text.
- Style enforcement against a house guide.
- collaborative editor polish, sync, and embedded model access
- proprietary ranking data
- brand-trained models
- team workflows
- large template libraries
- Comment kinds you can turn off, once dismissal patterns show which you never want
- A reading pass that reports structure rather than commenting inline
- Version comparison across drafts, which is where a long project's history becomes useful
Need the files? The project pack on the verdict page hands your agent the whole brief · more ai writing.