Build ClosersCopy

KINDAback to the verdict

0%0 of 17 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 copywriting workspace whose hard problems are refusing to manufacture the social proof that the genre invites, forcing every factual claim to be resolved before a draft can ship, and keeping a personal voice from a corpus rather than from a slider.

estimated effort weekendthe files for this build are in the project pack

RuntimeNext.js 15 with TypeScript, single user, running on your own machineStorageSQLite with append-only draft versionsGenerationOne model call per section with the framework version recordedClaimsA deterministic extractor plus a blocked ready stateVoiceConstraints derived from your own writing samples, checked after generation

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

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. API keypay per call; a draft is fractions of a penny and the ceiling is what keeps it there

    Why Generation is one call per section, and it is the only part of this build that leaves your machine or costs money.

    Get it Create a key with the provider you prefer and set a spend limit in their console as well as the ceiling here.

    Verify One section generates and the model identifier is stored on the draft version

  3. have readyfree

    Why This build refuses a whole category of output because of it, and the refusal list should be written from the rule rather than from a summary of it.

    Get it Read the rule text and the Commission's business guidance, note the date you read it, and record the current civil penalty figure. open ↗

    Verify The refusal patterns in the code map to named sections of the rule

  4. have readyfree

    Why The voice constraints are derived from these, and without them the output is generic marketing language with your product name in it.

    Get it Collect writing you were happy with: emails, posts, an about page.

    Verify Constraints are derived and differ between two different sample sets

  5. decidefree

    Why The extractor is deterministic on purpose, which means its coverage is exactly the list you wrote, so the list is a design decision rather than an implementation detail.

    Get it Write down the patterns for numbers, superlatives, guarantees, comparisons, deadlines and quoted speech.

    Verify The list is in a readable file and the fixture document produces the expected claim count

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- frameworks(id, name, description, created_at)
- framework_versions(id, framework_id, version, sections_json, published_at) · immutable, and every draft records which one it used
- projects(id, name, audience, offer, created_at) · the brief, kept separate from the copy
- drafts(id, project_id, framework_version_id, title, created_at, state) · state is writing, blocked or ready
- draft_versions(id, draft_id, ordinal, body_md, model_id, prompt_version, created_at, note) · append-only, never updated
- claims(id, draft_version_id, kind, sentence, offset, state, source_url, source_note, resolved_at) · kind is number, superlative, guarantee, comparison, deadline or quoted_speech
- voice_samples(id, project_id, body, words, banned_terms_json) · your own writing, not a personality setting
- A draft moves to ready only when every claim row on its latest version is resolved. That is a query, not a checkbox, and it is the only path to the ready state.

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
DATABASE_PATHrequired./data/copy.dbFrameworks, projects, drafts, versions, claims and voice samples.
MODEL_API_KEYsecretrequiredsk-...redactedThe text model. Give it a provider-side spend limit too.
MODEL_IDrequireda text model identifierStored on every draft version, because output quality changes with it.
MONTHLY_COST_CEILINGrequired5.00Generation stops here. Editing, claim resolution and export continue.
CLAIM_PATTERNS_PATHrequired./config/claim-patterns.jsonThe readable pattern list. Extending it is the normal maintenance of this app.
EXPORT_DIRrequired./exportsMarkdown with front matter carrying framework, model and resolved claims.

The build, in order

  1. A framework is data, not a prompt in a file

    Frameworks are versioned ordered sections with their own instructions, and every draft records the version it used.

    1. A framework you can read is a framework you can fix.

      Files src/frameworks/schema.ts

    2. So a draft from March is still explicable in December.

      Files src/frameworks/publish.tstests/framework-version.test.ts

    3. And require an explicit action to move a draft to a newer one.

      Files src/drafts/create.ts

    done when · tick each as it passes
    watch out
    • Keeping the prompt text in the code means that when you improve it, every previous draft silently becomes unreproducible and you can no longer tell whether last month's better output came from a better brief or a better prompt. Version the framework as data, stamp the version on the draft, and let an old draft be regenerated against the framework it was written under.
  2. Drafts that accumulate instead of overwrite

    Every generation and every edit adds a version, and any two versions can be compared.

    1. Enforced in the schema so it cannot be forgotten in a route.

      Files src/db/schema.sqltests/append-only.test.ts

    2. The only way to explain a quality change later.

      Files src/drafts/generate.ts

    3. A linear history is one you can reason about.

      Files src/drafts/diff.tssrc/drafts/restore.ts

    done when · tick each as it passes
    watch out
    • Regenerating in place means the version somebody actually liked is gone the moment they press the button again, and in this genre people press it a dozen times before deciding the third one was best. Append every version, never update one, and make restore an append too so the history stays linear.
  3. The claims check, and the shapes that are refused outright

    Factual claims are extracted deterministically and block the ready state until resolved, and testimonial-shaped output is rejected.

    1. So the same draft always produces the same rows.

      Files src/claims/extract.tsfixtures/claims.md

    2. Refuse the generation, do not edit the sentence out.

      Files src/claims/refuse.tstests/refuse.test.ts

    3. The state transition runs the query itself.

      Files src/drafts/state.tstests/ready-gate.test.ts

    done when · tick each as it passes
    watch out
    • Asking a second model call to check the first one for false claims produces a confident approval and no record, which is worse than no check at all because it looks like one. Use a deterministic extractor with a pattern list you can read and extend, so the same draft always produces the same claim rows and a missed pattern is a bug you can fix.
  4. Voice from your own writing

    Tone is a set of measurable constraints derived from samples you supply, checked after generation.

    1. Banned terms, preferred terms, sentence length, reading level.

      Files src/voice/derive.tstests/voice.test.ts

    2. The prompt is a request; the check is the enforcement.

      Files src/voice/check.ts

    3. A list of offending sentences, not a score.

      Files src/voice/report.ts

    done when · tick each as it passes
    watch out
    • A tone dropdown labelled friendly or authoritative changes nothing you can measure and gives the user the feeling of control without any of it, so the output drifts back to generic marketing language within three sections. Derive constraints from real samples and check the generated text against them afterwards, because the check is the part that actually holds.
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.