QUILL Lite — Product Requirements

Version 1.0.0 · released 2026-09-25 · Windows · MIT · part of the QuillVille family

1. Statement

QUILL Lite is QUILL with everything removed except the editor. One document per window, plain text or rich text, and nothing else.

It exists for one person: someone who wants Notepad or WordPad with QUILL’s accessibility, and who finds the full writing environment more than they need. That is not a small group. QUILL for All is a writing environment — AI, dictation, conversion, comparison, publishing, braille tooling, extensions — and for a great deal of what people actually do with a text editor, all of that is in the way. Opening a .txt file to change one line should not start a product with a setup wizard.

QUILL Lite is not a replacement for QUILL and not a place new features go. Anyone who wants any of the list above wants QUILL. This is stated here, in the requirements, because the pressure will be to add things: it is built so that it cannot grow into QUILL without somebody deliberately deciding to abandon the product it is.

Provenance

QUILL Lite began as PR #1490 by Steven Scott (doubletaponair), offered under this repository’s MIT licence. That PR also isolated a real bug in QUILL’s own Rich Edit surface — see §7 — and deliberately did not fix it, so that adding an app and changing the editor stayed separate decisions. Both decisions were taken; the fix ships with this release and reaches every QUILL user, not only QUILL Lite’s.

2. Architecture

Native Preferences search uses the shared preferences_search navigator, installed by the dialog contract. It indexes labels, accessible names, and help text, never field values; results focus existing controls without accepting the dialog or mutating settings. Control+F returns to search, Enter navigates, and Escape clears a query before closing. Disabled controls retain their state.

Activation repair is bounded to an event-loop check and one 75 ms settling check. Every activation/deactivation changes a generation, so old callbacks cannot affect a new activation. Repair acts only on shell/MDI containers or missing focus, never another interactive control. Inactive or hidden shells, hidden documents, menus, and shutdown suppress repair. It makes no app speech announcement; native focus changes remain the screen reader’s responsibility.

Settings writes return success or failure through LiteSettingsPersistenceMixin. Failed writes keep session values active and set settings_dirty; a coalesced, user-safe warning is spoken and remains in the status message until a later write succeeds. Preferences never claims a failed write was saved. Reopening Preferences retries already accepted settings; new edits still require OK. Existing settings changes and session-close saves also retry without a loop. Shutdown flushes a pending warning before teardown. Logs contain only the stable QUILL-LITE-SETTINGS-WRITE diagnostic and storage category, not raw exception text or user paths.

Document Open and Save As dialogs use the configured default document folder when no saved document location applies. A blank setting resolves to the Windows Documents folder; a document already saved continues to use its own folder.

Application shutdown owns the inbox timer through QuillLiteApp.stop_background_sources(). Confirmed Exit and shell close stop polling before document destruction; OnExit repeats that cleanup safely. Cancelling a close keeps polling active. A queued inbox tick must not consume requests after shutdown starts, and requests left after a reentrant close are reposted for the next launch. A deferred launch-update check must not start after shutdown. Document timers remain each document’s responsibility.

QUILL Lite is not a fork. That is the single most important architectural statement in this document, and it is the difference between a sibling and a copy that rots.

Layer Where What
The app quill/apps/lite*.py The shell, the windows, their menus, commands, status bar and dialogs
Its own domain logic quill/core/lite/ Settings, paths, recovery, the command table, file encoding, backups, the inbox, the feature areas
Logic QUILL should also have quill/core/numbered_bookmarks.py Nine slots that move with the text – shared, not QUILL Lite’s; see 2.2
Its F1 catalogue quill/core/lite_surface_help.py What every window is for
The editor surface quill/ui/richedit_editing.py over quill/ui/richedit_rtf_surface.py QUILL’s own control, extended — not copied
The product shell standalone/quilllite/ Entry point, icon, installers, docs

Everything under quill/ is therefore covered by the repository’s existing gates — module-size budget, access-key uniqueness, the over-announce gate, the dialog registry, the F1 audit — which is the reason the app code lives there rather than in the product folder. A surface that no gate can see is a surface that silently rots; QUILL Cast shipped an unreachable first-run dialog for two releases exactly that way.

2.1 What QUILL Lite reuses rather than reimplements

Each of these is a QUILL finding that cost real on-device testing to establish. QUILL Lite gets them by using QUILL’s code, not by copying its conclusions:

2.2 QUILL Lite is never allowed to be ahead of QUILL

This is a rule, not an aspiration, and it has already cost work twice.

If QUILL Lite needs something the editor cannot do, the capability goes into the shared package and the editor gets a way to reach it in the same release. A feature the small product has and the big one does not is exactly backwards – and worse, it is invisible: nobody opens QUILL and notices the absence of a thing they have only ever seen elsewhere.

In this release that meant:

Where the two must differ, they differ on purpose and in writing, and the reason is a comment in keymap.py rather than folklore.

That principle stands; the example it used to give does not. This paragraph recorded Ctrl+Alt+J and Ctrl+Alt+V as deliberate divergences from WordPad’s Ctrl+J and Ctrl+Shift+V, on the grounds that an existing binding somebody’s hands already know outranks a new command’s convention. The family keymap reversed both (bad.md §3, rule 1: Microsoft’s key wins where Word, WordPad or Notepad bind one for a function both editors have). Keeping the paragraph would have left the PRD describing a divergence the products no longer have – which is worse than describing none, because somebody reads it and writes code to match.

The divergences that remain are listed in the family keymap’s exception table and each carries its own reason. QUILL Lite may never be ahead of QUILL remains the rule (CLAUDE.md); value crosses both ways, violations cross one.

2.3 What QUILL Lite adds to the shared package

quill/ui/richedit_editing.py is a subclass of QUILL’s surface, not a copy of it. A document window — one file, one window, switchable between plain and rich, themed by itself — needs five things a notebook tab does not, and every one is a Rich Edit message or a TOM call that wx has no spelling for: text-mode switching (EM_SETTEXTMODE), word wrap (EM_SETTARGETDEVICE), view background and zoom (EM_SETBKGNDCOLOR, EM_SETZOOM), whole-document recolour for the theme, and heading enumeration.

The zoom detail is load-bearing: in rich text the text-size control is a view zoom rather than a font change, because run point sizes are the heading ladder and rewriting them would silently re-level every heading in the document.

3. Scope

3.1 What it keeps, and why

Kept Why
Plain text and rich text, one document each The two things Notepad and WordPad are
Headings 1–4, bold/italic/underline, alignment, fonts The formatting a WordPad user expects, on QUILL’s ladder
Heading navigation and a headings list, in both modes The reason a screen-reader user prefers a structured document at all; plain text has Markdown headings and they are real
A spoken “Heading 2” on arrival Nothing else in the stack can say it — see the heading ladder above
Describe Formatting at the cursor QUILL’s own answer to “what am I standing in?”
Find, Replace, Go to Line Non-negotiable in any editor
A focusable status bar (F6) See §4.2
Unsaved-work recovery, and session restore See §5.2
Byte-honest encoding and line endings, and a way to change them See §5.1
Theme, following the system by default See §4.4
Nine numbered bookmarks that move with the text No scrollbar to remember the position of
Structural selection (word, line, paragraph, expand) Shift+arrow is the wrong tool for a paragraph
Describe Character A reader says “space” for four different characters
A twelve-slot copy tray, a collector and a clip library The system clipboard holds one thing
Abbreviations, over QUILL Lite’s own library Expansion is an accessibility feature, not a typing one
Sort, de-duplicate, trim, and three case conversions Work, not writing — and expensive by keyboard
Printing and Page Setup Every text editor Windows has ever shipped has Ctrl+P
The Command Palette Answers “how do I sort lines?”, which is the real question
Switchable feature areas See §3.3

3.2 What it drops, and why

Tabs, dictation, self-voicing, preview, Quillins, remote files, GitHub, braille tooling, the setup wizard, comparison, publishing, and every companion app.

The command palette is not on that list any more, and was listed here in error: §3.1 and the command table have always had it, and it ships. A document that says a shipped feature was dropped is worse than one that says nothing, because somebody reads it and does not go looking for the key.

AI came off the list on 2026-09-23, and the distinction matters because the list is not meaningless now. What arrived is five commands behind one pad on QUILL’s own free hosted service, under two gates (§5.6). What is still dropped is everything else QUILL’s AI is: bring-your-own-key, the provider list and the default cascade, local models, the agent harness, the conversation, alt text, and the setup wizard that configures them. The reason for the exception is the reason the product exists: summarizing and explaining a passage is disproportionately valuable to somebody who cannot skim, and an editor that is only usable by somebody who reads a screen quickly is not much of an accessible editor.

Two of those deserve their reasoning stated rather than assumed:

3.3 Two decisions that shape everything else

Numbered documents in one window. Documents are MDI children of a single QuillLiteShell, numbered from 1, and a number is never reused while its document is open. “Document 3” is a name a person can hold and say out loud; “the other Untitled” is not.

The cost is real and is written into the module that causes it: MDI children do not appear in Alt+Tab. The whole burden of moving between documents falls on the app, so it carries it four ways – Ctrl+F6 (the Windows convention), Ctrl+Tab (what people actually press), Alt+1 to Alt+9, and the Window menu – and every document’s title leads with its number, because the title is the one string a screen reader announces on arrival.

Switchable feature areas. QUILL Lite stays small by being switchable, not by being poor. Tools > Customize Features turns whole areas off, and switching one off removes its menu and its keys – a key that still fires for a feature somebody turned off is the feature not being off. Somebody who wants Notepad unchecks rich text, and the Format menu is gone entirely.

Four areas ship switched off and are found in that same checklist rather than hidden, because a feature nobody can find is a feature that does not exist: autocorrect (welcome in prose, actively wrong in a config file), timestamped backups (reassuring, and they fill a folder), Go To Anything (the palette, the headings list and the bookmark list each already answer their own part), and AI help, which is off because it sends what you ask about off this computer – a different kind of reason, and the one that needs a second question as well as a switch (§5.6).

Nineteen areas now. Markdown and HTML arrived on 2026-09-15 with the rule that decides what Ctrl+B writes in a .md, and AI help on 2026-09-23. Both are areas rather than bare settings for the same reason the rest are: switching one off has to take its menu rows and its keys with it, and a setting cannot do that.

Seventeen areas, not twelve (2026-09-10). Five features belonged to no area at all, which meant no switch in the dialog could reach them: the Matches submenu, Go Back and Go Forward, the Command Palette, Describe Character, and text size. That is a different failure from a feature being un-switchable on purpose – there was nothing to find and nothing to complain about. Going the other way, File Encoding and Line Endings stopped being switchable: it belonged to the line-tools area, so turning off Sort Lines also took away the dialog that decides whether a file round-trips byte-for-byte, which for a Notepad replacement is most of the job.

3.3a Profiles: a name is a promise, not a preset

Nineteen checkboxes is the right way to change one thing and the wrong way to say “give me the small one”. :data:quill.core.lite.features.PROFILES is four named starting points – Recommended, Everything, WordPad, Notepad – and the design rules behind them are worth stating, because each one has a cheaper wrong version.

A profile is a baseline, not a mode. Applying one checks and unchecks every box, and then the boxes are the truth again. There is nothing to escape from: the very next change is an ordinary per-area override, and the Choice reads back Custom the moment the boxes stop matching. The alternative – a mode that owns the dialog and reverts what you do – makes the checklist a lie.

A profile is written as what it takes away. AppProfile.disabled lists the areas switched off, never the ones kept, so an area added in a later version is on in all four profiles until somebody decides otherwise. A profile written before a feature existed must not silently remove it.

Two of them carry a setting, and only those two. “Notepad” does not mean these menus; it means the thing you type in is plain text. A Notepad profile that removed the Format menu and went on creating rich text documents on Ctrl+N would keep the letter of its name and break its promise – and the user would find out one document later, at a Save As dialog offering a format they thought they had turned off. So Notepad claims default_mode = "plain" and WordPad claims "rich". Recommended and Everything claim nothing: they are statements about which areas exist and have no opinion about the rest, and a profile that quietly rewrote a setting the user chose would be the mode this design refuses to be.

A description you cannot read is not documentation. Every profile has carried a paragraph since profiles existed and nothing displayed it – the Choice listed four bare names. Every other profile system in the codebase shows its description; this was the one that did not. It is now under the Choice, it is the Choice’s F1 answer, and it is spoken once when Use Profile is pressed. Not on every arrow of the Choice: the reader is already saying the name, and a paragraph over the top of that is what GATE-13 exists to stop.

Profile Areas on Ctrl+N For
Recommended 17 of 21 unchanged the shipped answer, and the way back from experimenting
Everything 21 of 21 unchanged turning things off as they annoy you rather than finding them one at a time
WordPad 5 of 21 rich letters and notes that should look like something
Notepad 2 of 21 plain config files, logs, and anything where hidden formatting would be a problem

The four Recommended leaves off are autocorrect, timestamped backups, Go To Anything and AI help – and the last of those is off for a different kind of reason from the other three. Those would be wrong on; this one is off because using it sends the passage you ask about over the internet, which is not a choice a default gets to make on somebody’s behalf. See §5.6: the area switch and the agreement are two separate questions, and Everything can turn the first on without ever answering the second.

Notepad keeps two areas and still has Find, Replace, Go To Line, Select All, Insert Date and Time, Word Wrap, the font picker, the status bar, Undo, printing and text size – everything Notepad has had since 1985 – because those live outside the switchable areas by design. It also keeps File Encoding and Line Endings, per the paragraph above.

3.4 Non-goals

4. Accessibility

The four family rules, applied here.

4.1 Every menu item shows its key, and no key is claimed twice

The menu bar is generated from one table, so the keys the menus advertise, the keys that are bound and the keys the Keyboard Shortcuts window lists are one list read three times. tests/unit/core/lite/test_lite_commands.py asserts, of that table: every item carries a key; no key is claimed twice; no & mnemonic is claimed twice inside one menu (Windows cycles focus between duplicates instead of pressing, so one of a pair silently cannot be reached); and every key string is one wx.AcceleratorEntry can actually parse — wx silently drops what it cannot, leaving a menu advertising a dead key.

4.2 Every window answers F1, and so does every control

F1 answers with what this window is for, then what the focused control does, through the family’s shared engine. GATE-LITE-HELP (quill/tools/lite_help_audit.py) refuses to let a new window ship without a purpose or a new control ship without help.

QUILL Lite adds a stricter check of its own (tests/unit/tools/test_lite_control_help.py), because the shared scanner has two blind spots that matter here: it does not treat wx.CheckBox as helpable (and the Find and Replace windows are half checkboxes), and it cannot see a control built by a factory in another module — which is the document itself, the control a listener is in for all but a few seconds of a session.

4.3 The status bar is navigable, not decorative

QUILL’s status bar is a row of focusable cells rather than painted text, and QUILL Lite’s is the same: F6 lands in it, arrows and Home/End move between cells, each cell announces its value, Enter acts on it, and Escape returns to the document. Ten cells: the last announcement, position, words, characters, selection, format, heading, encoding, line endings, and saved state.

Two of those exist nowhere else in the app. Encoding and line endings are what decide whether a file round-trips byte-for-byte, which for a Notepad replacement is most of the job — and in every other editor they are invisible.

Refreshes are coalesced through a restarting 90 ms timer (QUILL’s own window): counting words is O(document), and a refresh per keystroke is several full scans per keypress.

4.4 Say only what the screen reader does not already say (GATE-13)

The reader announces window titles, focus moves, control names, roles, states and selection changes. QUILL Lite announces only outcomes it alone knows: “Bold on”, “Saved notes.txt”, “Replaced 4 occurrences”, “Wrapped to the start”, “Heading 2: Installing”. Nothing is announced from a focus handler; the one exception is the F6 landing in the status bar, which names the region once, because moving from a text control to a strip of buttons is described by the reader as “button” and nothing more.

Announcements also land in the status bar’s message cell, so speech that was missed can be read again.

Two smaller decisions in the same spirit: OK, Cancel and Close carry no & mnemonic at all — Enter and Escape already serve them, and every letter they give up resolves a collision elsewhere (GATE-14) — and dark mode is the default, because the users this editor is for are disproportionately light-sensitive and a first launch that is bright white is a first launch some of them cannot read.

4.5 Speech, and not braille

QUILL Lite announces through a running screen reader and nowhere else. ScreenReaderVoice reaches NVDA and JAWS through Prism or accessible_output2 and Narrator through a UIA notification, and hands nothing to the engine unless a reader is actually running. There is no self-voicing fallback and there is no braille output: nothing in the QUILL Lite tree routes an announcement to a braille display, where QUILL has a braille service that does.

This is a decision, recorded so it stops reading as an oversight.

Why. A braille route is not a line of code; it is a delivery channel with its own verbosity rules, its own truncation problems, and its own way of being wrong – QUILL’s braille surface is several modules and a settings group. The editor this PRD describes is meant to stay small enough to be read in an afternoon, and adding a second output channel is the kind of thing that makes a Notepad-scale product stop being one.

What a deafblind user gets instead, and why it is not nothing. Every message QUILL Lite speaks also lands in the status bar’s message cell (4.3), and the status bar is a row of real focusable buttons rather than a painted strip. A braille display reaches it by cursor routing like any other control, and F6 puts the caret there in one keystroke. So the information is available; it is pull rather than push. What is genuinely lost is the unprompted announcement – a background result a listener would have heard, a deafblind user has to go and look for.

What would have to change. The announcement path is one seam (ScreenReaderVoice.speak), so the work is not in QUILL Lite: it is deciding whether QUILL’s braille service can be lifted into the shared package without bringing its settings group with it. If it can, QUILL Lite gets braille for the cost of one call. Until somebody has answered that question, this section is the honest description of where the product stands.

5. Safety and data

5.1 Byte honesty

Open a file, change nothing, save it, and the bytes must be the bytes you started with. quill/core/lite/textfile.py owns this: a UTF-8 BOM is kept, a UTF-16 file stays UTF-16, and the enormous installed base of Windows text files that are neither is cp1252 — tried last, because it can never fail to decode. Line endings are remembered and re-applied, so a file that arrived from a shell script does not silently gain carriage returns because it was opened once.

Every write is atomic (temp file in the same directory, then os.replace), so a power cut during a save leaves either the old file or the new one, never a truncated one. In rich mode the theme colour is reset to automatic before the save and restored afterwards, so dark mode never leaks light grey text into somebody’s document.

5.2 Recovery

Every modified window owns one recovery slot: a copy of the content beside the original, written on a timer and removed the moment the document is saved or closed cleanly. A clean shutdown leaves the folder empty; anything still there on the next start is work the last session did not keep, and is reopened in its own window, unsaved. The original file is never touched, so a crash mid-save cannot leave somebody with a truncated original and no recovery.

The interval is a setting (autosave_seconds, 15–600 seconds, default 30), reachable only from Preferences – Copy unsaved work aside every (seconds). It has no key and no menu row, deliberately: it is a number somebody sets once.

5.3 Its own data folder

Settings, recent files and recovered work live in %LOCALAPPDATA%\QuillLite, deliberately not in %APPDATA%\Quill. QUILL Lite is offered as an alternative to QUILL rather than as a client of it: a Notepad-scale editor that silently adopted a writing environment’s settings would be making a decision nobody asked it to make, and uninstalling it could then cost somebody something QUILL owns. A machine that has never had QUILL installed must not grow a Quill folder because somebody opened a text file.

This is why QUILL Lite does not use quill.core.ipc for its single-instance guard — that lock lives in QUILL’s data directory. It uses wx.SingleInstanceChecker (a kernel object, with no file to go stale) plus its own inbox directory. Uninstalling never removes the data folder either: recovery slots are the one thing somebody may not have finished with, and an uninstaller is the worst possible moment to discover that.

The one bridge, and why it does not contradict any of that (2026-09-18). QUILL Lite has always been able to read QUILL’s abbreviations and personal dictionary, behind a switch the person turns on. QUILL now has the other half: quill/core/lite_bridge.py and Tools > Customize and Support > Bring My QUILL Lite Settings… (Alt+Shift+F11) merge QUILL Lite’s copies of five content stores into QUILL’s folder, copy the settings and rebound keys that translate, and turn those sharing switches on so the two editors stop keeping separate copies. Five properties keep this inside the principle above rather than against it:

Both write sites are classified in quill/tools/persistence_audit.py (merge_json_store as content, enable_lite_sharing as marker), so neither can be added to or moved without somebody re-stating what they do.

5.3a Two instances, one settings file (F-11, 2026-10-01)

--new-instance runs two processes against one settings file. Each used to write its whole in-memory copy, so the last to save undid every preference the other had changed – atomic writes stop a half-written file, not a valid older snapshot replacing a valid newer one. A save is now a three-way merge (quill/core/lite/settings_merge.py): the app keeps the settings it loaded or last wrote as a baseline; at save it re-reads the file, writes its own value for every field it changed since the baseline, and keeps the file’s value for every other field. Last writer wins per field, not per file. The running instance’s in-memory settings are never replaced by the other’s – a silent change to theme, font or speech mid-session is the thing this product must not do – so the other’s choices apply at its next launch. An unreadable file is not merged against (that would reset untouched fields to defaults); the save falls back to the whole-object write.

5.3b When a focus repair does not take (F-04, 2026-10-01)

The activation repair (one check after the event loop, one 75 ms later) can still be overruled – Windows restoring focus after the second check, or a control that refuses it. That is the residue a listener reports as “sometimes it works”, so lite_shell._note_repair_miss counts it per shell (focus_repair_misses) and logs QUILL-LITE-FOCUS-MISS with the class of whatever holds focus and the session count – never a title, a label or a word of a document, because the log travels in support bundles. Nothing is announced. It is not a family helper yet: QUILL’s own repair (#170) is a different mechanism, and a shared helper with one caller is speculation; it is extracted when a second app adopts this shape.

5.3c Opening is prepare, then commit (F-05, 2026-10-01)

DocumentFrame.load read, decoded and RTF-scanned on the UI thread, so a large file, a slow share, a scanner holding the read or a damaged RTF froze the window with nothing said. Opening is now two halves: quill/core/lite/open_prepare.prepare (wx-free, any thread: read, decode or scan, return an immutable PreparedDocument) and DocumentFileMixin._commit_load (UI thread only: the Rich Edit control and everything a load implies). load() still runs both synchronously, so every existing caller is unchanged. open_prepare.prepare_in_background routes a file of 1 MiB or more, or a UNC path, through lite_window_open.DocumentBackgroundOpenMixin.begin_background_load: a new window, read-only while empty-and-loading, “Opening ” announced once and on the status bar, the read on the app’s lazily created two-worker TaskManager, the commit on the UI thread only when the open generation still matches, the window’s UiLifetimeToken is alive and the app is not shutting down. Closing the window is Cancel. Failure asks Try Again; declining closes a window that holds nothing else. A file still opening counts for the already-open check, so a second request focuses it instead of reading it twice. The app’s shutdown owner stops the pool. Not done: Open as Plain Text on failure (a rich file that fails the scan still reports through the box).

5.4 File associations

The full installer offers “Open .txt and .rtf files with QUILL Lite” as an optional component that registers an Open With verb, never a default handler. A text editor that quietly takes over every .txt on the machine is a text editor people uninstall, and Notepad, WordPad and QUILL stay exactly where they were.

5.5 Getting help, without an account and without an audience

Help > Get Help from Support… (Ctrl+Alt+F2) writes an email to support@community-access.org, where a person reads it and the reply comes back to the person who wrote it.

That is a correction, not a feature. The family’s reporting item used to file a GitHub issue in a public repository, through a token baked into every installer, and it left the reporter with no way to be answered: a GitHub issue is not a conversation you can join without an account, which is precisely the account a screen-reader user reporting that an editor went silent is least likely to have. Worse, somebody describing a failure names their configuration, their employer, or the document they were working on — and every word of it was published, permanently and searchably, the moment they pressed Submit.

QUILL Lite had neither half of that. It shipped with no reporting item at all, only an address printed in the About box to copy out by hand.

The form is the same one every app in the family opens (quill/ui/support_dialog.py over quill/core/support_message.py): what kind of message this is, a subject, and what happened. What you expected and how to reproduce it are optional. Your email address is optional too — a problem can be reported without giving one, and because the message goes from the reader’s own mail account, support can still reply to that (corrected 2026-09-26; this used to say nobody could answer). Send opens your own mail program with the message already written, with QUILL Lite’s name and version, your Windows version and your screen reader appended. Nothing leaves the machine until you send it there, and the app says so out loud rather than claiming to have sent something it has not. A machine with no mail program set up — webmail only — gets the whole message and the address on the clipboard, so nothing typed is ever lost.

Writing to support@community-access.org directly works exactly as well. There is no form anybody is required to use.

No GitHub credential, anywhere (2026-09-26). Every piece of feedback from QUILL Lite and Quill Radio goes to support@community-access.org; nothing is filed as a GitHub issue. So the build no longer generates or embeds the bundled “feedback token” (scripts/build_release.ps1 lost -TokenFile/-SkipToken, and a public build no longer fails without one), and the support form no longer tries the feedback-hub dialog first: email is its only route. The mail handoff is support_dialog.send_by_mail, shared with Quill Radio’s Suggest a Station or Podcast so both cut, copy and fall back identically.

5.6 AI help: two gates, not one

QUILL Lite carries five AI commands – summarize, rewrite, proofread, explain, and ask a question about the open document – on QUILL’s own hosted service. They are the only part of this product that sends anything off the machine, which is why they sit in this section rather than in Scope.

Two independent gates, both of which must be true before a byte leaves. The feature area answers does this exist in my copy; the agreement answers have I agreed to what it does. Collapsing them into one switch gets the second one wrong, because an area can be switched on by a profile, by a settings import, or by another person using the machine, and none of those is consent.

The agreement is therefore:

Withdrawal signs the machine out. Keeping a sign-in for a service just declined would leave the key in a door the user has closed. Signing out, by contrast, does not withdraw the agreement: it is a smaller action and must not silently perform a larger one.

Shape: one pad behind one key, not five keyed commands. Measured across Ctrl+Alt, Ctrl+Shift, Alt+Shift and Ctrl+Alt+Shift, the chords free in both editors are few, and family rule 2 requires a command both products have to keep its chord – so five separate keyed commands were never available. The single pad is the better shape regardless: one surface showing what is about to be sent, one showing what remains, one consent surface.

Family rule 10 holds. The capability is shared rather than QUILL Lite’s own, so QUILL runs the same five commands on the same five chords from the same module. QUILL Lite is not ahead of QUILL here; QUILL has more AI, not less, and none of its existing provider list, default cascade, bring-your-own-key, agent or local-model paths changed to make room for this.

5.6a Conversations: memory without a multiplier (1.1)

A conversation was the one AI shape deliberately left out of the free tier, because it resends its history on every turn and so each reply costs more than the last. It ships in 1.1 on the condition that removes the multiplier: the conversation so far is trimmed to the same input ceiling every request has, newest turns first, on this computer (quill/core/ai/hosted_chat.py) and again, authoritatively, on the service. A turn is one request and never costs more than any other; conversations have a monthly share of their own (feature_cap.chat, 40 of 100). What a long conversation loses is memory of its opening, and the window says so aloud the first time that happens rather than leaving somebody to infer it from a puzzling answer.

No new command, no new chord. Chords free in both editors are the scarcest resource in the family (section 5.6), so a conversation is reached from the pad (Have a conversation, a seventh choice) and from the result window (Follow Up, which seeds the conversation with that question and answer, and keeps a document question’s excerpts attached). The window is modeless; focus lives in the message box and each reply is announced, because it lands in a transcript that does not have focus – the one case GATE-13 says the app must speak.

With the user’s own API key (OpenAI or Google Gemini) there is no trimming: the whole conversation goes, and is shortened only past what the model can read at once.

Family rule 10 holds: the window, the pad row and Follow Up live in the shared quill/ui/hosted_ai_* modules, so QUILL has them in the same release on the same keys.

5.6b Ten writing tools (1.1)

The pad grows from seven choices to seventeen: Shorten, Simplify, Make more formal, Make friendlier, Turn into a list, Find action items, Suggest headings, Continue writing, Write an email reply, and Translate. The rule for admitting a tool is the same one that admitted Summarize: one passage in, one result out, at the ordinary size, so no tool is a new worst case for the free service (each is one request, with a monthly share of 60). Each is a fixed template on the service; the client sends only the passage, and Translate’s target language is the one value it fills in, from a fixed list of twenty checked on both ends, because a free-text field there would be a place to smuggle instructions.

The choice of tools is aimed at the people this product is for: Simplify for letters from banks, hospitals and lawyers; Turn into a list and Find action items because a list is far quicker to review by ear than a paragraph; Suggest headings because headings are what a screen reader navigates by, so it turns a flat passage into one you can move through. The list of seventeen is a list box, so a first letter jumps to a tool.

5.6c Your own API key: a provider chosen, never inferred (1.2, qc.md X-07)

Use My Own API Key (Alt+F2, formerly Use My Own OpenAI Key) sends AI help straight to the provider the person chooses – OpenAI or Google Gemini – on their own key, with no QUILL server and no allowance. The requirement that shaped it came from reviewing PR #1615, which made Gemini keys work by inferring the provider from a model name and falling back to whichever key was stored. That half was not taken. The requirements instead:

5.7 Every switch has a way back, from inside the app

A preference somebody can turn off and cannot turn back on is not a preference; it is a trapdoor. The rule is that the surface that sets a value is a surface that can also unset it, and that the way back is reachable without editing a file – because the population this product is written for is exactly the one that cannot be told “open the JSON in your profile folder”.

The one place this was broken until 2026-09-24 is worth recording, because it is the shape the rule exists to catch. The Reopen Last Session window’s Never Ask Again wrote session_restore_ask = "never". Nothing else in either editor ever wrote that key: not Preferences, not the window itself on any later visit. The window then stopped appearing at launch, and although File > Reopen Last Session... still opened it, there was no control in it that could undo the setting. The button’s help text made it worse by naming a real setting that is not this one – “Reopen last session” is restore_session, whether to reopen at all – so following the instruction produced a different effect and no clue.

Ask Me Next Time is its twin. Exactly one of the pair is enabled, decided by the mode the caller passes in, and the disabled one is greyed rather than hidden: a removed control leaves somebody hunting the window for it, where a disabled one announces itself as unavailable the moment a reader arrives on it. The same choice the two tag pickers make, for the same reason.

Not a Preferences row, deliberately. The window that asks the question is where somebody looks to answer it, and adding a row to QUILL Lite’s Preferences that QUILL’s Settings does not have would put the small product ahead of the large one (§2.2) over a control that belongs in the shared window anyway. Both editors gained it in the same change, which is what rule 10 requires.

5.8 Dictation: the reliability pass, and words taught in a window (2026-09-28)

Nine sections of the dictation plan (dict.md 2.2, 2.3, 2.5, 2.6, 2.7, 2.8, 3.1, 3.3 and 5; that plan is now docs/design/2026-10-05-dictation-plan-and-status.md, whose Appendix A maps its section numbers), implemented in the shared Live Dictation stack so QUILL has every one of them on the same keys (rule 10). The design constraint from that plan holds throughout: nothing here costs the baseline machine anything while dictation is idle, and the watchdog’s only running cost is one comparison per audio block.

Requirements, and where each lives.

Tests. The core behaviours against a fake recogniser and a fake document (tests/unit/core/test_windows_dictation.py), the words model round-trip and its edits (test_dictation_words_file.py), and each Lite handler through the window it runs in (tests/unit/apps/test_lite_dictation.py); the command coverage gate stays at zero shape_only.

5.9 Dictation: holding the key, live words, the AI, and OpenAI (2026-10-05)

The rest of the dictation plan, after studying VS Code’s dictation (credited, behaviour by behaviour, in docs/design/2026-10-05-dictation-plan-and-status.md, which also holds everything still to do). All of it is shared code, and tests/unit/ui/test_dictation_parity.py fails if QUILL and QUILL Lite differ in a dictation command, key or setting.

5.10 Transcribing a recording (2026-10-06)

QUILL Lite’s Transcribe a Recording command (Shift+F5) transcribes supported audio files in the background using the shared dictation speech engines. Its window reports recording length and estimated transcription time; on Windows, duration lookup falls back to Media Foundation when the other metadata readers cannot determine the length. Several selected recordings run in sequence.

The transcript is saved privately and is not inserted into the document the user was editing. A completion notice in Notifications opens the saved transcript when activated; Activity retains the outcome and failure reason. There is no destination picker.

6. Release packaging

Two artifacts: the portable build and the system installer. That is the family contract for every app (2026-10-11), and QUILL Lite reached it first:

Artifact Built from Output Measured
Installer installer/quilllite.iss QuillLite-Setup-Shared-1.0.0.exe 117 MB
Full portable build_portable.py --product quilllite QuillLite-Portable-1.0.0.zip 94 MB

Why two (2026-09-15). QUILL Lite used to ship the thin QuillLite-Lite-Setup and the launcher-only QuillLite-Companion zip as well, and both were wrong for this product. The Companion zip installs nothing, so it binds to whatever shared runtime is already on the machine – including one frozen before QUILL Lite existed, which fails at launch with No module named quill.apps.lite and cannot repair itself, because the launcher’s runtime bootstrap only fires when no runtime resolves and a stale one resolves perfectly well. The thin installer traded a 117 MB download for a 110 MB first-launch download plus a network dependency, on the one app somebody installs because they have nothing else – so the runtime is usually absent and the saving is notional. Retiring both is safe for anyone already on one: the two installers share an AppId, so this one upgrades a thin install in place, and core/updates.py falls through to an installable asset when a release publishes none for the running edition. On 2026-10-11 the rest of the family followed: every app now ships only the portable build and the installer.

The download names spell the product as one mixed-case word, QuillLite-*, never Quill-Lite-*: a screen reader speaks QUILL Lite as the product’s name, where a hyphenated form is read out punctuation and all.

The installer script is named quilllite.iss, not quill-lite.iss: the family used the *-lite.iss suffix for the retired thin installer flavour, and quill-lite.iss ends in it – so the installer was being audited as a thin one. Every other identifier for this product is already quilllite (the folder, the AppRefId, the launcher product key), so the installer matches it.

portable-inventory.json is committed and enforced: the portable build fails on any drift from it, which is the class of defect that once put 82 MB of undeclared payload into a runtime installer.

Both come from scripts/build_release.ps1. The shared QuillVille Runtime at %LOCALAPPDATA%\QuillVille\Runtime\3.13\ is reused: the installer ships it and lays it down only when what is already there is older, and the portable bundle carries its own interpreter and never touches it at all. Neither ffmpeg nor libmpv is staged — QUILL Lite declares no media components, and the build strips any a sibling app left in the shared work area, so build order cannot change what ships.

The installer is per-user: PrivilegesRequired=lowest, with no elevated all-users override. App files and the shared runtime are installed for the signed-in user. The optional runtime setting that replaces Notepad for every user remains a separate, explicitly elevated action.

Code signing is opt-in (-Sign / QUILL_SIGN=1), the same convention as every sibling; see docs/code-signing.md.

Releases publish to Community-Access/quill rather than to a repo of their own, and are told apart by the QuillLite-* asset prefix. Deviation from the siblings, noted deliberately: Radio, Cast, Weather and Studio each have their own release repo. Revisit if it bites.

6.1 Updating in place

Help > Check for Updates… on Ctrl+Alt+U, the key eight sibling apps already answer, plus a silent once-a-day check at launch (check_updates_on_launch, on by default, one checkbox in Settings). Page Setup gave up Ctrl+Alt+U for it and took Ctrl+Alt+P, which is free in both editors and a better letter besides: a chord that means one thing in eight apps and something else in the ninth is the kind of difference nobody finds until it does the wrong thing.

Before this, QUILL Lite could not tell its user that a newer version existed. It is the app most likely to be somebody’s only Quill product — installed by a person who wanted Notepad — and therefore the app whose users are least likely to go looking on GitHub. They would have stayed on 1.0.0 forever with no way to know that was happening.

The dialog is shared, and it always shows what changed. quill/ui/ update_notice.py is the one “an update is available” surface for QUILL, the eight companion apps and QUILL Lite: the release notes flattened out of Markdown, capped and never empty, with Update as the affirmative and default and Close as the escape. Focus lands on the notes rather than on a button, so the first thing read is the release notes and not the word “Update”. QUILL alone adds Skip this version, because QUILL alone has a settings field to remember the answer in. The download, its spoken 25/50/75 milestones, and the Install-and-restart dialog are quill/ui/update_download.py, shared the same way — QUILL Lite has no TaskManager, so it supplies a one-shot thread instead and gets the identical experience.

Releases resolve from RELEASE_REPO and RELEASE_ASSET_PREFIX in quill/core/lite/__init__.py, deliberately not from companion_install.ASSET_PREFIX: that table is the set of QuillVille apps QUILL can offer to install for you, and QUILL Lite is a separate product rather than a sibling QUILL launches.

The silent launch check says nothing in every direction but one: nothing while it runs, nothing when there is nothing, and nothing when the network is down. Only a genuine newer version speaks, and even then it only offers — nothing is downloaded until the user presses Update.

Two family-wide defects were fixed to make this honest (2026-09-12), both older than QUILL Lite and both live in every companion app:

6.2 When it cannot start, it says so (2026-09-28)

QuillLite.exe is the family’s native launcher, and until 2026-09-28 it spawned pythonw.exe, waited, and threw the exit code away. A portable Quill Radio that “just doesn’t return anything” was the report that ended that. Now the launcher writes the engine’s stdout and stderr to a launch log (data\logs\launch.log beside a portable copy, %APPDATA%\Quill\logs\QuillLite-launch.log for an installed one, replaced on every launch) and, on a non-zero exit, shows one MessageBox a screen reader reads unaided: “QUILL Lite did not start”, the reason in words, the log’s path and the support address. Windows’ silent endings (a DLL not found, a damaged or 32-bit file, access denied) are translated; a Python traceback’s last line is quoted with a hint. Pressing Enter on the exe inside the zip is recognised from the archive tool’s scratch-folder name and explained as “extract the whole zip first”. Exit 0 stays silent, so the single-instance hand-off is unchanged. Shared code (quill/native/launcher/launch_report.c, pinned to a Python mirror in tests/unit/native/test_launch_failure.py); QUILL Lite gains it at its next launcher build, and the user guide’s Getting help chapter already opens with “If QUILL Lite does not start”.

6a. The family pass (2026-09-18)

A twenty-nine item audit of the two editors against each other, closed in September 2026. It is recorded here because three of its outcomes are now constraints on this product rather than one release’s work.

The eleven rules are code. quill/core/family_rules.py holds them and both keymap.py and lite/parity.py cite them by number; a test checks that every citation resolves, so the rules cannot be renumbered into lies. Lower number wins when two conflict. Rule 1 – Microsoft’s key wins for a function both editors have – is itself a gate (tests/unit/core/test_microsoft_habits.py), checked against both keymaps rather than believed; it found the last disagreement on the day it was written, and Word’s F12, Ctrl+F12 and Ctrl+Shift+F12 are aliases in both products because of it.

Divergence is a written argument. DIVERGENCES in quill/core/lite/parity.py holds the eight handlers whose chord is allowed to differ, each with the reason, and the gate prints the reason when it reports the pair – so the next person reads the argument instead of re-having it. SETTINGS_ALIASES does the same for the five settings one concept is stored under two names for, and the vocabulary gate (quill/tools/settings_vocabulary_audit.py, in platform_report) fails a new setting that is neither shared, aliased, nor a reviewed one-sided field. Two of the five real pairs share no word at all, which is why it is a reviewed table and not a name-similarity check.

Rule 10 now cuts both ways in practice. QUILL Lite may never be ahead of QUILL, and the audit found places where it was: QUILL got the DocumentText model QUILL Lite already had, the spelled suggestions QUILL Lite already spoke, the Spelling Announcements window, and the swallowed-formatting-key guard. Where QUILL Lite was behind, it gained Word’s F12 family and the shared-store bridge (5.3). Neither direction is a favour: a capability in one product and not the other is invisible to the person who only has the other one.


7. The QUILL-side fixes this work produced

Both were isolated by PR #1490 and both ship in quill/ui/richedit_rtf_surface.py, where every QUILL user gets them.

_TOM_TRUE was tomUndefined. tom.h defines tomTrue as -1; -9999999 is tomUndefined, which means “leave this property alone”. Assigning it to ITextFont.Bold therefore asked the control to change nothing — and succeeded. set_heading applied the point size, silently never applied the bold, and raised nothing. Because heading_level_for_font requires bold before it will call a paragraph a heading, QUILL could not then find the headings QUILL had just made: heading navigation and Describe Formatting both went blind in rich mode. Measured on RICHEDIT50W / Riched20 10.0.26100:

Bold = -9999999 (tomUndefined)  ->  Bold=0   Weight=400
Bold = -1       (tomTrue)       ->  Bold=-1  Weight=700

standalone/quilllite/tests/repro_tom_true.py reproduces it in isolation and imports neither QUILL nor QUILL Lite, so it can be run against a clean checkout.

A collapsed caret described the wrong paragraph. A collapsed TOM range reports the formatting of the character before it, so standing at the start of a heading described the paragraph above it. caret_format_description now probes the character after the caret, which is what a screen reader describes.

7.1 The spell checker, and the exception that makes it usable

QUILL Lite ships a spell checker because the products it is measured against have moved: WordPad never had one and never will, but the Notepad on a Windows 11 machine today has had spell check and autocorrect since 2024. An editor claiming Notepad parity without one is behind the thing it is copying.

None of the checking is new. The wordlist, the suggestion ranking and the guided F7 review are QUILL’s, reached through the same helper the Mastodon compose box uses. What QUILL Lite added – and what QUILL now shares – is the rule about when not to speak.

A live spell checker in settings.json flags every key; in main.py it flags every identifier. Those alerts are not merely useless, they are asymmetrically expensive: a sighted user’s eye skates over a red underline it has learned to distrust, while for this product’s actual users each one is a status line to read past. A feature that interrupts fifty times a minute is one people switch off permanently, and a feature switched off permanently is a feature that was never shipped.

So the default follows the file’s extension, decided once per document, and three things follow from it:

QUILL already suppressed live alerts inside URLs, code spans and fenced blocks (spellcheck_live), which is the right granularity in prose and no help at all in a source file, where the whole document is the exception. It now consults the same file-type rule QUILL Lite does, behind spellcheck_skip_code_files.

Markdown is deliberately not on the code list. It is where people write, and suppressing the whole file would silence the prose along with the code; its code regions are handled by spellcheck_live, at the granularity that fits.

7.2 Selection is three features, not one

Edit > Selection is the largest single surface QUILL Lite adds, and it is worth saying why an editor this small carries one at all. It is a submenu: nineteen items poured into Edit would swamp it, and another top-level menu on a small editor’s bar is a cost paid on every visit by everyone. A submenu is one Alt+E, N away, and gives the group its own mnemonic namespace.

Selecting text without sight is a different job from selecting it with a mouse, and the difference is not one of degree. There is no drag. There is no glance afterwards that confirms you took what you meant to. Shift and an arrow key – the answer every editor offers – is fine for two letters and useless for four paragraphs, because it gives no feedback and no sense of position.

So the menu offers three genuinely different mechanisms rather than one with variations:

Mark and extend (F8). Anchor, move by any means, complete. The only way to take an arbitrary run of text without holding a modifier down throughout, and the only one that composes with every navigation key the editor has rather than with a fixed set of chords. Ported key for key from QUILL, because a user of both should not have to hold two versions of it.

Structure. Word, line, sentence, paragraph, block – taken whole, without needing to know where they start, then grown or shrunk a rung at a time. This is the one that removes counting.

Marks. Throwaway positions, deliberately distinct from the nine bookmarks: a bookmark is a place you decided to keep, a mark is where you were standing before you went to look something up, and conflating them makes both worse.

One rule runs through all of it: every command says how much it took. “Selected paragraph, 412 characters” is a fact somebody can act on. A selection that announces nothing has to be verified by pressing something destructive and seeing what disappears, which is not a test anybody should have to run.

Select All stays in Edit itself on Ctrl+A, outside this switchable area, so turning the submenu off can never remove the one selection command that predates all of it.

7.8 The thesaurus and the dictionary came to QUILL Lite, and Look Up came back to QUILL (2026-10-02)

The family rule ran both ways in one change. QUILL Lite had no thesaurus at all – a comment in the command table said it needed an asset download, which was never true: the 18 MB MyThes file ships in every copy. QUILL’s thesaurus, meanwhile, looked up the word as typed and offered headwords, so “running” found nothing and “sprint” was offered where “sprinting” was needed. Both are fixed once, in shared code: quill/core/word_lookup.py walks a word back to the headwords the data knows and inflects every replacement forward, and quill/ui/word_tools_commands.py is the one WordToolsMixin both editors mix in through HostedAiMixin – Shift+F7, the Thesaurus for “word” and Dictionary for “word” submenus on the context menu, Say Word Summary, and the AI dictionary. QUILL’s show_thesaurus now delegates to it.

Look Up (quill/ui/lookup_window.py, DICT-2’s window) had been reachable in QUILL only when the thesaurus data was missing, and it went online whenever the dictionary feature was on, which is not consent. It is now Tools > Writing

Look Up Word (Alt+F10) in QUILL and Tools > Look Up Word in QUILL Lite, the online half is behind Use online sources in the window itself (dictionary_online_lookups, the same field name in both settings files, so the vocabulary gate sees one setting), and the online answer arrives on a worker and replaces the offline one with a sentence said when it lands.

The AI dictionary (quill/core/ai/word_tools.py: one feature id, one system instruction asking for JSON – prose for listening and exact replacement texts – and thirteen prompts; quill/ui/word_tools_window.py: the answer window) runs only on a direct route, because the free service is metered per person and a dictionary is asked a hundred times a day. QUILL reaches the same thirteen commands through a Dictionary submenu of its AI menu (quill/ui/main_frame_hosted_ai.py), on QUILL Lite’s chords exactly.

Gates that moved: a new Customize Features area, dictionary, with the profile counts now 17, 21, 5 and 2 of 21; commands_words.py split out of the command table under GATE-11; the Lite coverage snapshot has sixteen new handlers, every one called by a test in tests/unit/apps/test_lite_words.py; and menu_lint’s rule against bare Ctrl+Alt chords (AltGr) decided the keys the less-used rows carry.

8. Future directions

8.1 Accepted for a later release (decided 2026-09-08; shipped 2026-09)

Everything in this section has shipped. It is kept as the record of why each item was accepted, because the reasoning is the part worth having and the list itself is now the command table. Overwrite mode, tab mode, the line operations, the deletion ring, persistent bookmarks, back and forward, Earlier Versions, promote and demote, sentence and toggle case, reverse and number lines are all in the shipped product; so, since 2026-09-17, are three of the four things the last paragraph declined — Extend Selection Mode, the Heading Organizer and folding over Markdown sections — which arrived by the other route, as crossings from QUILL rather than as inventions here (bad.md 4.2).

Read it as history. The live list of what is still open is the family parity plan, not this section.

QUILL’s editor surface was diffed against QUILL Lite’s, command table against command table and user guide against user guide, and the result reviewed against one test: does the absence read as a defect against a promise this product already makes – “Notepad and WordPad’s keys, unchanged”, and “QUILL’s accessibility” – or as the restraint §1 asks for? Thirteen were accepted on that basis. Every one of them already exists in QUILL, so §3.4’s “not a feature destination” is not in play: none is a new invention, each is shared code given a door here, behind its own switchable area in the §3.3 sense.

The Notepad baseline. Insert/Overwrite mode – Notepad has had it since 1985, and it becomes one more readable cell in the status bar §4.2 already argues for. Tab as indent versus a literal tab, with Indent and Outdent on a selection. Line operations: Move Line Up/Down, Delete Line, Join Lines – today reordering two lines costs a select, cut, move and paste, which is four keystrokes and a lost cursor for the reader this editor is for. Structured deletion (to line start/end, delete paragraph) with QUILL’s deletion ring, which re-inserts recently deleted text at the cursor and so turns a delete into a move – something undo cannot do.

The first two settle an argument this document currently leaves half-made: §5.4 already accepts that people edit .json and .py here, which is why spell check goes quiet in them. Shipping that concession without Tab control and block indent is one half of a position.

Find and Replace, which has Match case and nothing else. Whole word and regular expressions; extended escapes (\n, \t, \uNNNN) so the invisible characters Describe Character already names can also be searched for – the two are halves of one workflow; Count Occurrences and a match list to arrow through; and peek navigation with a spoken match count, so a search’s shape is known before it is committed rather than discovered one F3 at a time.

Navigation. Bookmarks that persist between sessions, and the last cursor position restored – §3.1 justifies bookmarks with “no scrollbar to remember the position of”, and that argument does not stop when the document closes. Back and Forward location (Alt+Left/Right), the undo for navigation.

Parity and the safety net. A way to browse and restore the timestamped backups QUILL Lite already writes – a safety net nobody can reach is not one, and this is the missing half of a feature that ships today. Heading promote and demote, and moving a section with its body, so a document can be restructured without the cut-and-paste most likely to lose a reader’s place. Sentence Case and Toggle Case, and the line transforms people actually reach for (numeric sort, sort by length, reverse, number lines, hard wrap).

Considered in the same pass and declined, so the reasoning is not lost: browse mode / Quick Nav single-key jumping – the strongest accessibility case of anything reviewed, and a modal layer over every keystroke, so it is its own project rather than an item on a list; match bracket and first-non-blank, too narrow for prose; strikethrough, superscript, subscript and numbered lists – revisit numbered lists first if this is reopened, since an ordered list is structure a reader announces rather than decoration.

8.2 Out of scope

Out of scope for 1.0, and each stated so it is a decision rather than an oversight:

9. Validation