Build Lex

YESreplaces $12/mosaves $144/yrback to the verdict

0%0 of 19 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

EditorProseMirror over a document model with stable positionsRuntimeTypeScript with a local application and SQLiteCommentsGenerated on demand, anchored to ranges, never applied automaticallyVersionsEvery session's text kept, diffableModelsLocal by default, hosted as an explicit per-document choice

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

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

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

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

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

VariableNeededExampleWhere the value comes from
DB_PATHrequired~/.local/share/lex/lex.dbDocuments, versions and comments. Back this up like the drafts it holds.
DEFAULT_MODEL_POLICYrequiredlocalWhat a new document gets. Local, so sending is a decision rather than a default.
LOCAL_MODELrequiredllama3.1:8bThe local model for comments. Must follow structured output reliably.
SESSION_GAP_MINUTESrequired30The activity gap that ends a writing session and snapshots a version.
HOSTED_API_KEYsecretoptionalsk-...Only for documents explicitly set to a hosted model. Absent means local only, which is a fine way to run.

The build, in order

  1. An editor that keeps your words

    You can write for an hour and lose nothing.

    1. Comments anchor to ranges. A model without stable positions makes the next phase impossible.

      Files src/editor.ts

    2. Two different mechanisms. Conflating them ruins the history.

      Files src/versions.ts

    3. Mid-sentence, not at a clean point.

      Files tests/recovery.test.ts

    done when · tick each as it passes
    watch 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.
  2. Comments in the margin, anchored to the text

    The AI responds to a passage as a reader, and the note stays on that passage.

    1. Range, kind, body. No replacement field, because the absence is the design.

      Files src/comment.ts

    2. What confused them, what they wanted next, what the paragraph is doing.

      Files prompts/comment.md

    3. Models attend to the opening. Chunk so later sections get read too.

      Files src/chunk.ts

    done when · tick each as it passes
    watch 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.
  3. Comments that survive editing

    You revise a paragraph and its comments follow the text or say they cannot.

    1. Prefix and suffix, so relocation by content is possible.

      Files src/anchor.ts

    2. Position first, quote match second, orphan third. A pure function, unit tested.

      Files src/anchor.tssrc/anchor.test.ts

    3. With the text they referred to. Silently dropping them loses feedback you paid for.

      Files src/margin.ts

    done when · tick each as it passes
    watch 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.
  4. Argue with a note, and keep the argument

    You can disagree with a comment and that disagreement persists.

    1. Addressed, dismissed, replied. Deletion loses the signal.

      Files src/state.ts

    2. The pattern is the interesting output, not any single dismissal.

      Files src/patterns.ts

    3. Addressing a comment is a state change. The revision is yours to make.

      Files src/state.ts

    done when · tick each as it passes
    watch 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.
  5. Where the draft goes, per document

    You choose per document whether it is ever sent anywhere.

    1. Not a global setting. The unit of decision is the draft.

      Files src/policy.ts

    2. Raise rather than send. Interface enforcement is not enforcement.

      Files src/model.tssrc/model.test.ts

    3. So you know which reader you were listening to.

      Files src/comment.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more ai writing.