Build TextCortex
A local writing workspace whose hard problems are never losing the paragraph a rewrite replaced, tracking which spans a model wrote through every subsequent edit, and exporting in a way that carries that record with the text.
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 application and the test suite.
Get it nodejs.org or your version manager. open ↗
Verify
node --version prints v22 or higher - API keypay per run; drafting a few thousand words a week is small change
Why One call per instruction run, and the only part of this build that leaves the machine or costs anything.
Get it Create a key with your preferred provider and set a spend limit in their console as well as in the application.
Verify
Running an instruction produces a new version and a run row with token counts - decidefree
Why The disclosure obligation attaches to publication rather than to generation, and the editorial review exemption only helps if somebody actually reviews it.
Get it Read the current transparency article, decide which side of it your use sits on, and write that decision into the README. open ↗
Verify
The README states the decision and the date it was made - have readyfree
Why Provenance remapping only breaks on real editing, where text is moved, pasted over and partly rewritten, and a synthetic test document never does any of that.
Get it Take something you actually drafted with a model and rebuild it in the application.
Verify
Every paragraph shows an origin and none of them is unknown by accident
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, current_version_id) - versions(id, document_id, ordinal, body, created_at, reason, parent_version_id) · every version is kept and reason says why it exists - spans(id, version_id, start_offset, end_offset, origin, model_id, instruction_id, at) · origin is typed, generated, edited or unknown - instructions(id, name, body, created_at, archived_at) · reusable prompts, archived rather than edited in place - runs(id, version_id, instruction_id, model_id, input_hash, output_hash, tokens_in, tokens_out, cost_minor, status, at) - snippets(id, name, body) · the boilerplate you paste often, kept out of the prompt library so it is not confused with instructions - No row in this schema is ever overwritten with generated text. A generation creates a new version whose parent is the one you had.
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 |
|---|---|---|---|
MODEL_API_KEYsecret | required | sk-...redacted | One call per instruction run. Set a provider-side spend limit as well. |
MODEL_ID | required | a text model identifier | Recorded on every run and every generated span, because output differs by model. |
DATABASE_PATH | required | ./data/writing.db | Documents, versions, spans, instructions and runs. Back this up. |
MONTHLY_COST_CEILING | required | 4.00 | Generation stops here and the editor keeps working. |
EXPORT_DIR | required | ./exports | Markdown plus the companion provenance file for each export. |
The build, in order
A document that cannot lose the paragraph you liked
Every change that a model makes creates a new version and the previous body survives byte for byte.
Append only, at the storage layer rather than by convention.
Files
src/lib/versions.tstests/versions.test.tsSo the history reads as a story rather than a stack.
Files
src/lib/schema.sqlGoing back should not destroy what you are going back from.
Files
src/lib/restore.tstests/restore.test.ts
done when · tick each as it passeswatch out- Applying a rewrite by replacing the document text and trusting the editor's undo stack means the paragraph you had before the rewrite is gone the moment the page reloads, and losing work to the assistant is the fastest way to stop using it. Make every generation a new version row with a parent, keep the previous body untouched, and give the save path no way to write over an existing version.
Knowing which words are yours
Provenance is recorded at insertion and survives every subsequent edit.
A diff between versions is a guess about who typed what.
Files
src/lib/spans.tstests/spans.test.tsThe span has to follow the words, not the position.
Files
src/lib/remap.tstests/remap.test.tsBetter a gap you can see than a claim you cannot support.
Files
src/components/Provenance.tsx
done when · tick each as it passeswatch out- Storing one flag saying the document was AI generated is useless by the second paragraph, because half of it is now yours and the half that is not is scattered through it; recomputing provenance later by diffing versions is guesswork dressed as a record. Record the origin at the moment of insertion with offsets, remap those offsets on every keystroke batch, and make unknown a real state you can see.
Instructions you can reproduce, and a bill that stops
A rewrite that worked last week can be run again with the same instruction and the same model.
An instruction referenced by old spans is history, not configuration.
Files
src/lib/instructions.tstests/instructions.test.tsReproducibility and the bill come from the same table.
Files
src/lib/runs.tsA writing tool should not stop being a text editor.
Files
src/lib/cost.tstests/cost.test.ts
done when · tick each as it passeswatch out- Keeping the prompt in the code and tweaking it until the output looks right means the rewrite that worked last week cannot be reproduced and you cannot tell whether the model changed or you did. Store instructions as rows, archive instead of editing, reference the instruction identifier on every run and every generated span, and record the model identifier next to it.
Export that carries the record with the textproduct builder
What leaves this application says which parts a model wrote.
Named in the export dialogue so nobody exports half of it.
Files
src/lib/export.tstests/export.test.tsEvery table, round-tripped, or it is not a backup.
Files
src/lib/backup.tstests/roundtrip.test.tsA test is a better commitment than a promise in a README.
Files
tests/no-evasion.test.tsREADME.md
done when · tick each as it passeswatch out- Exporting Markdown with generated text indistinguishable from your own throws away the only record of which is which, and reconstructing it afterwards is guesswork; worse, the export is exactly the moment the text starts travelling to places you will not be present at. Write the provenance as a companion machine-readable file, name that file in the export dialogue, and make the JSON backup the complete record rather than a convenience.
That is the whole plan for TextCortex. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A browser extension for other applications.
- Team workspaces and shared prompt libraries.
- Any humanising or detection-evasion feature.
- A retrieval-backed knowledge base.
- team governance and integrations
- vendor-managed prompt and quality tuning
- proprietary models or classifiers
- brand-trained workflows
- A side-by-side version diff that colours by origin rather than by change
- A per-document provenance summary, which is the sentence you would put under a published piece
- A second model configured alongside the first, so an instruction can be compared rather than trusted
Need the files? The project pack on the verdict page hands your agent the whole brief · more ai writing.