Repairing spintax templates

Repair is detect the defect → apply the one canonical recipe for its class → re-check. Never a free rewrite. And it respects the order the series taught: structure, then variables, then synonyms — you cannot fix at the synonym layer what belongs in the variable layer.

Why templates break at render time, not in the source

validate() judges the template. A template can be flawless and still emit broken text, because the defect lives in the combination of choices: two adjacent slots happen to pick the same word, a noun from one slot meets a relative pronoun from another and they disagree in gender, an empty branch leaves a bare pair of brackets behind. None of it is visible in the source, and none of it survives a human reading of the source — it only appears in some renders.

That is why repair starts from rendered variants, not from the template. The n8n node's Lint operation exists for exactly this: it judges the render, never the template, and on a real pool it scored 2% clean over 200 renders on the first pass and 100% after the slots it pointed at were fixed. The single most common finding in that pool was a noun from one slot meeting a pronoun from another — сюжет, в которой landed in 18% of the articles before anyone noticed.

The practical consequence: if you only ever read the template, you will ship the defect. Render tens of variants, lint them, and repair what the lint points at.

The golden rule: fix by recipe, not by vibe

A defect has a class, and every class has one canonical repair. Free-form fixing — by a person in a hurry or by a model told "fix this" — breaks grammar while fixing grammar. A model asked to repair дополнение, который will happily swap in a synonym of a different gender and hand back расширение, который: the pronoun it was asked about is still wrong, and it has now invented a new branch. Given a named recipe instead — bind the governed word into each branch — it does the one transformation and stops.

The second half of the rule is order. The grammar guide says it for authoring: "Never add {a|b} before permutations, variables, and sentence shape are locked." For repair it reads: structure → variables → synonyms. A defect that comes from a noun being repeated in three cases is a variable-layer problem; patching the synonyms around it fixes one render and leaves the next one broken.

Two repairs, in this order

  1. Syntax repair — from validate() diagnostics. Each one carries a line, a column and a code; the model is shown the numbered template and told to change as little as possible and leave everything else byte-for-byte identical. @spintax/authoring-prompt builds that prompt (buildRepairPrompt()), the n8n node exposes it as Build Repair Prompt, and Spintax Studio's Fix button runs the same round. If this step reports errors, do it first — a template that does not parse has no renders to lint.
  2. Render-lint repair — from findings over rendered variants. A finding carries a code, the offending fragment and a message, but no position in the template. This guide is about this step: map the code to its recipe, find the branches that produced the fragment, apply the recipe, re-render, re-lint.

Defect → recipe catalogue

Each class below names who finds it — the lint, by code, or you, reading samples — and gives the one recipe with a before and an after. The "before" renders are real engine output; the lint messages are quoted from the n8n node's lint as it prints them. The grammar behind each recipe is taught once, in the grammar-safe guide; the links go to the exact section.

Article or determiner (English)

Found by: you, in samples — not machine-checkable. Symptom: a encrypted connection. Recipe: bind the determiner into the branch.

a {secure|encrypted} connection
{a secure|an encrypted} connection

Taught in Article and determiner binding.

Subject & predicate, gender & number

Found by: you, in samples. Symptom: the team respond; in Russian сайт удобна, команда доступны. Recipe: bind subject and predicate in one branch. Two independent slots can never be made to agree; one slot holding both halves always does.

{the team|all members} {responds|respond}
{the team responds|all members respond}
{сайт|платформа} {удобен|удобна}
{сайт удобен|платформа удобна}

Taught in English subject-verb agreement and Gender and number.

Governed word: the relative pronoun (Russian)

Found by: the lint, code agreement.relative. It looks back at most two words from each form of который, guesses the noun's gender from its ending, and stays silent when the ending is ambiguous (путь, тень) rather than guess. Symptom, exactly as the lint prints it:

"дополнение … который" — neuter noun with a masculine relative pronoun

Recipe A: bind the governed word into each branch, in the form that branch needs.

{дополнение|плагин}, который ускоряет сайт
{дополнение, которое|плагин, который} ускоряет сайт

Over 40 renders the lint flagged 18 of the first template and none of the second.

Recipe B: hoist the noun into a one-gender #def when it recurs. Pick synonyms of the same gender, roll once, and every later mention agrees by construction:

#def %Ext% = {дополнение|расширение}
%Ext%, которое ускоряет сайт

Also 40 of 40 clean. The condition that comes with recipe B: it holds only while the noun appears in one case. A separately declared %ExtGen% does not correlate with %Ext% — two definitions are two rolls — so a noun that needs several cases takes the case-family ladder: synonyms that decline alike with the ending outside the definition, or one #def stem that every case form references.

Preposition + case (Russian, German)

Found by: you, in samples — the lint does not judge case inside a slot. Symptom: после первом заказе, für dem Team. Recipe: the preposition travels with its governed form into the branch.

{после|при} первом заказе
{после первого заказа|при первом заказе}
{mit|für} dem Team
{mit dem Team|für das Team}

When the same noun is governed in several places, the alternative is a case-form variable — %OrderGen%, %OrderLoc% — declared as a family. Taught in Preposition + governed form.

Repeated root

Found by: the lint, code repeat.word. It scans a six-word window, so it catches a repeat up to five words apart, and it compares stems, so вопрос / вопросов count as one word. Six is measured: at nine, 82% of the hits were ordinary cohesion; at six every hit was real. Symptom, as printed:

"сразу" and "сразу" repeat 4 words apart

Recipe: keep the root in one slot. Either the enumeration or the fixed text carries it, never both; when the repeated thing is an entity, hoist it into a #def and mention it once.

{Сразу|Немедленно} после установки сайт сразу ускоряется
{Сразу|Немедленно} после установки сайт начинает отвечать быстрее

18 of 40 renders flagged before, none after. The English shape is the same — Repeated-word collision shows need to acquire… can acquire.

The seam the lint cannot see. A permutation makes any two blocks neighbours, so a heading and the intro that follows it can share a root across the block boundary. The lint deliberately does not flag heading-to-body repeats (that is where the "ordinary cohesion" lived), so seams are held by discipline in the source — split the vocabulary at the boundary — or by a whole-template lint pass over the assembled document.

Register and slot homogeneity

Found by: you, in samples. Symptom: a formal branch next to a casual one, or a narrative connective that changes register mid-paragraph (Therefore / So yeah). Recipe: one register per slot; pin the connectives. Remove the branch that does not belong rather than rewrite the slot. The rules are in Synonymization rules.

Punctuation left inside a branch

Found by: the lint, code punctuation.empty-pair. Symptom: empty brackets or quotes — an optional branch that left its wrapper behind. Recipe: move the punctuation into the branch that needs it.

Works in any browser ({fast|})
Works in any browser{ (fast)|}

18 of 40 renders clean before, 40 of 40 after.

Three more codes exist for the same family — punctuation.double-space, punctuation.space-before and punctuation.duplicated — and you will rarely see them from the engine: its post-processing pass normalizes a double space and a space before a comma in the render itself. They are the safety net for text assembled outside the engine, or by a host that renders with post-processing off. The recipe is the same: punctuation goes inside the branch that owns it.

Variables: the strongest fix

Half the catalogue above has a variable-layer cure, and it beats every synonym-layer patch because it removes the combination the defect lived in. The method:

  1. Hoist before you synonymize. A declined entity that recurs through the text is pulled into a #def first; only then does the synonym pass run on the copy around it. Done the other way round, the inline synonym swap silently changes the form the rest of the sentence agreed with. In the wpps-ru campaign, one article went from 52 clean renders out of 200 to 200 of 200 on this step alone: #def %Ext% = {дополнение|расширение}, both neuter, and the inline synonyms never touched it again.
  2. Name the forms. %PascalCase% for the nominative, a suffix for every other case you use — %OrderGen%, %OrderDat%, %OrderLoc% — following the variables guide's convention.
  3. Keep a family on one roll, the only way the engine correlates forms. Synonyms that decline alike with the ending outside the definition (#def %e% = {магазин|сайт|проект}для %e%а); or one #def stem that every case form references (#def %s% = {посетител|гост}, #def %VisitorsGen% = %s%ей); or the whole phrase in one enumeration. Two independent definitions are two rolls and never correlate — not with #set, not with #def.
  4. #def, not #set, for anything two mentions must agree on. #set re-rolls at every reference and is the right tool when you want the variation; #def rolls once per render.

When not to spin

Some spans are not repaired; they are taken out of the enumeration entirely. Proper nouns, brand names, prices, URLs and legal wording never go inside a branch. Neither does a noun with a long tail of dependents — an adjective, a participle and a relative clause all agreeing with it — because every branch would have to carry the whole tail. Vary the copy around these, and keep them fixed.

Repairing with an LLM

A model repairs well when it is given four things and denied a fifth: the template, the concrete defect with its fragment, the named recipe to apply, and the instruction to apply only that — and no licence to improve anything. The request is never "fix this"; it is "apply recipe X to the span that produced Y". The prompt below is the shape that held on real material.

You are repairing a spintax template. Do NOT rewrite freely and do NOT improve the wording.

A lint pass over rendered variants found this defect:
  code:     <agreement.relative | repeat.word | punctuation.empty-pair | …>
  fragment: "<the offending rendered text>"
  message:  "<the lint message>"

Apply exactly ONE recipe from https://spintax.net/docs/repairing-spintax.md
  recipe: <its name, e.g. "bind the governed word into each branch">
to the branch or branches that produced the fragment — and to nothing else.

Rules:
- Keep meaning, structure, variables and every other branch byte-for-byte unchanged.
- Do not add words, clauses, prepositions or relative pronouns the template does not
  already have.
- If the recipe cannot be applied without changing meaning, leave the template as it is
  and reply: unfixable
- Return only the corrected template — no explanation, no quotes, no code fences.

Two habits from the same campaign keep the model inside the lines. Substitution-only markup when the model marks up a finished text: it may replace words and short phrases, and it may not introduce conjunctions, subordinate clauses, prepositions or relative pronouns the source did not have — the invented который and the invented preposition were the source of most agreement and case defects, and the rule removed them. And a lint pass on every marked-up paragraph, because even substitution-only drifts: one paragraph came back as {Совпадает|Совпадает ли} — careful, no. {Совпадает ли|…}, English thinking leaked into a Russian branch plus a doubled root. The loop caught it; a human would not have.

Two more pointers for the model: the nine mistakes models make in spintax, and the grammar audit fragment in the grammar-safe guide, which is the authoring-side twin of this prompt.

Automating repair

For a pipeline, the shape that reached lint-clean 200 of 200 on three long-form templates is this, per paragraph:

draft = markup(concrete_text)            # substitution-only
repeat up to K:
  errors = validate(draft)
  if errors:        draft = repair(draft, errors); continue   # syntax: the model fixes the spans
  if any(lint(render(draft, seed_i)) for i in 1..N):
                    draft = markup(concrete_text); continue   # dirty: regenerate, do not free-fix
  return draft                                                # clean
fail loud

Read the asymmetry: a syntax error is repaired by the model from precise spans; a lint finding either sends the paragraph back to be regenerated or — in the full version of the loop — is repaired by applying the named recipe from the catalogue to the span. The loop never lets the model fix a lint defect freely. With N = 8 renders per paragraph and K = 3 attempts, the second template in the campaign went from 95 clean of 200 to 200 of 200.

In n8n the same loop is four nodes: Validate routes the item to Valid or Invalid, Build Repair Prompt turns the Invalid output into the fix-it prompt for your LLM node (cap the loop, two attempts is plenty), Render Many draws the sample, and Lint routes it to Clean or Defective — a sample is clean only when every drawn document was. The same rules also run as a plain function over any text — the campaign's pipeline ran its own copy of them, the one the node's lint was ported from.

Three things to know before you wire it:

  • Provenance is yours to build. A lint finding carries the rendered fragment, not a position in the template. To apply a recipe you find the branch that produced the fragment — search the template for the fragment's words, branch by branch — and that is the piece of plumbing every repair tool ends up writing.
  • Sampling is probabilistic. Eight renders per paragraph catch the common combinations; the rare one surfaces at volume. The final gate over the assembled template — 200 renders, every one linted — stays in place as the net, and the lint's sample operation is built for it (50 renders by default, up to 500).
  • Seams stay human. The per-paragraph loop cannot see a repeat across block boundaries; the whole-template pass or source discipline holds those, as above.

Cheat-sheet

DefectFound byRecipeTaught in
Article / determiner (EN)you, in samplesbind the determiner into the branchdeterminers
Subject & predicate, gender & numberyou, in samplesbind subject and predicate in one branchagreement, gender and number
Relative pronoun disagrees (RU)lint agreement.relativebind the governed word into each branch; or hoist the noun into a one-gender #defcase families
Preposition + case (RU, DE)you, in samplesthe preposition travels with its form into the branch; or a case-form variablepreposition + case
Verb governance, quantifiers (RU)you, in samplesbind verb and object / quantifier and noun in one branchgovernance, quantifiers
Repeated rootlint repeat.wordkeep the root in one slot; hoist an entity into #def and mention it oncecollision
Register / slot homogeneityyou, in samplesone register per slot; pin the connectivesrules
Empty pair, stray space, doubled marklint punctuation.empty-pair, punctuation.double-space, punctuation.space-before, punctuation.duplicatedmove the punctuation into the branch that owns itthis page

One line to remember: detect, name the recipe, apply it, re-check. If there is no recipe for what you see, the defect is in the structure or the variables, and the series before this guide is where it gets fixed.


Continue the series