{"version":3,"sources":["../src/index.ts"],"names":["normalizeBaseLang","pluralArity"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Canonical LLM Spintax authoring prompt — spec `docs/spec-llm-authoring-prompt.md`.\n *\n * ONE prompt, shared by every surface (Telegram bot, n8n node, playground, API), so that a\n * template drafted anywhere is drafted the same way. Product content, deliberately NOT part of\n * `@spintax/core` — the engine must not grow authoring opinions (spec §2.2).\n *\n * The engine is a RUNTIME dependency for exactly one thing: plural arity. This package used to keep\n * its own copy of the 3-form language table to stay dependency-free, and the copy drifted twice —\n * once in content (a locale added to the engine and not here) and once in shape (`slice(0, 2)`\n * instead of the engine's normalization, which disagrees on any 3-letter tag). A prompt that\n * disagrees with the engine about arity teaches templates the engine then rejects, which is the one\n * failure this package exists to prevent. Weightlessness lost to correctness, deliberately.\n */\nimport { normalizeBaseLang, pluralArity, type Diagnostic } from '@spintax/core';\n\n/** Bump when the prompt text changes in a way that can change model output. */\nexport const PROMPT_VERSION = '6';\n\nexport type VariationLevel = 'conservative' | 'balanced' | 'aggressive';\n\n/**\n * Length and tone of ONE block of copy — nothing else.\n *\n * `landing` is \"a headline plus one supporting sentence\", not landing-page markup; every channel,\n * `generic` included, describes short copy. The prompt never emits markdown or HTML (hard rule 4)\n * and no option relaxes that. A host that needs structure — an article of `<h2>` / `<p>` / `<ul>`,\n * a document of sections — calls the prompt once per block and composes the structure itself,\n * which removes a whole class of template defects by construction (spec §3, #76). To get a longer\n * block, say so in `brief` (\"one paragraph of four to five sentences …\"): the channel is a\n * default, the brief is the instruction.\n */\nexport type Channel = 'email' | 'sms' | 'push' | 'landing' | 'generic';\n\n/**\n * The grammatical case of a variable's VALUE.\n *\n * In an inflected language a variable is not a neutral hole: `%Visitors%` = \"посетители\" only fits\n * a nominative slot. Hosts that serve such languages declare one variable per case — the pattern\n * real Russian template sets converge on (`%Visitors%`, `%VisitorsGen%`, `%VisitorsDat%`,\n * `%VisitorsLoc%`, `%VisitorsInstr%`). Declaring the case here is what lets the prompt tell the\n * model WHICH member of the family a given sentence needs.\n */\nexport type GrammaticalCase =\n  | 'nominative'\n  | 'genitive'\n  | 'dative'\n  | 'accusative'\n  | 'instrumental'\n  | 'prepositional';\n\nexport interface VariableSpec {\n  name: string;\n  /** Case of the value. Load-bearing in ru/uk/be; ignorable in English. */\n  case?: GrammaticalCase;\n  /** Free-form hint, e.g. \"brand name — does not decline\". */\n  note?: string;\n}\n\n/** A bare name (case unknown) or a full spec. */\nexport type AllowedVariable = string | VariableSpec;\n\nexport interface AuthoringPromptOptions {\n  /** Plain-language description of the copy to write. */\n  brief: string;\n  /** BCP-47-ish language tag. Selects the grammar block — it is NOT a translation hint. */\n  locale?: string;\n  /** The ONLY variables the model may use. Anything else is a hallucination. */\n  allowedVariables?: readonly AllowedVariable[];\n  channel?: Channel;\n  variationLevel?: VariationLevel;\n}\n\nconst asSpec = (v: AllowedVariable): VariableSpec => (typeof v === 'string' ? { name: v } : v);\n\n/**\n * How to present the allow-list, and the rule the model breaks most often.\n *\n * A variable is substituted VERBATIM — the engine never inflects it, and neither can the model by\n * gluing a suffix on the outside. So the SENTENCE has to be built around the form the value already\n * has. In English that surfaces as the article trap (\"a %product%\" is a coin-flip on a/an); in\n * Slavic languages it surfaces as case, which is far more destructive.\n */\nfunction variableRulesBlock(locale: string | undefined): string {\n  const universal = `VARIABLES\nUse ONLY the names given in ALLOWED VARIABLES, exactly as written.\n\nA variable is substituted VERBATIM. The engine does not inflect, pluralize or re-case it, and\nneither can you by bolting text onto the outside of it. Build the SENTENCE around the form the\nvalue already has.`;\n\n  const profile = teachingProfile(locale);\n\n  // EVERY case language gets the case rules — they are language-neutral, and a 7-case language\n  // handed only the English a/an rule is worse off than one handed them in the wrong prose.\n  // (Gating this on 'east-slavic' alone is what silently stripped them from sr/hr/bs.) Only the\n  // WORKED EXAMPLES below are language-specific, so only they are profile-gated.\n  if (profile === 'east-slavic' || profile === 'bcs') {\n    const caseRules = `${universal}\n\nCASE IS PART OF THE VALUE, not a suggestion:\n- Variables that differ only by a suffix (%X%, %XGen%, %XDat%, %XLoc%, %XInstr%) are NOT synonyms.\n  They are the same word in different cases. Choose the one the sentence actually governs.\n- NEVER glue an ending onto a variable. A suffix bolted onto the outside of %X% renders as literal\n  text, not as an inflected word.\n- A preposition and the variable after it move TOGETHER. If you swap the preposition, you must swap\n  to the matching variable — or the sentence breaks.\n- If the case you need is NOT in the list, REWRITE THE SENTENCE so it needs a case that is. Never\n  approximate with the wrong one: a wrong case reads as broken copy, which is worse than plainer\n  copy.\n- A brand or proper name does not decline.`;\n\n    if (profile !== 'east-slavic') return caseRules;\n\n    return `${caseRules}\n\nWorked examples (Russian):\n      для %VisitorsGen%      (родительный — after \"для\")\n      к %VisitorsDat%        (дательный — after \"к\")\n      о %VisitorsLoc%        (предложный — after \"о\")\n      с %VisitorsInstr%      (творительный — after \"с\")\n- \"%Visitors%ов\" is not a genitive; it renders as the literal text \"посетителиов\".\n- Write \"в %CasinoName%\", never \"%CasinoName%а\".`;\n  }\n\n  return `${universal}\n\n- Do not assume the shape of a value you have not seen. \"a %product%\" is a coin-flip between \"a\"\n  and \"an\"; write \"our %product%\" or restructure the sentence.\n- Do not pluralize or possessivize a variable by hand — if you need a count, use {plural …}.`;\n}\n\n/**\n * The per-request allow-list.\n *\n * Deliberately NOT part of the system prompt: the rules above are stable and cacheable, while this\n * list changes with every item a host processes (in n8n it comes from the current row). Mixing them\n * would defeat prompt caching and blur what is policy versus what is data.\n */\nfunction variableListBlock(vars: readonly VariableSpec[]): string {\n  if (vars.length === 0) {\n    return 'ALLOWED VARIABLES: (none — do not use any %variable%)';\n  }\n\n  const lines = vars.map((v) => {\n    const bits = [v.case, v.note].filter(Boolean).join('; ');\n    return bits ? `  %${v.name}% — ${bits}` : `  %${v.name}%`;\n  });\n\n  return ['ALLOWED VARIABLES (the case is part of the value, not a suggestion):', ...lines].join(\n    '\\n',\n  );\n}\n\nexport interface BuiltPrompt {\n  systemPrompt: string;\n  userPrompt: string;\n  allowedVariables: readonly string[];\n  promptVersion: string;\n}\n\n/* ── Blocks ──────────────────────────────────────────────────────────────── */\n\n/** Indent a worked example so it reads as a block inside the prompt. */\nfunction indent(block: string): string {\n  return block\n    .split('\\n')\n    .map((l) => `        ${l}`)\n    .join('\\n');\n}\n\nconst ROLE = `You are a copywriter who writes SPINTAX TEMPLATES.\nA spintax template is ONE piece of copy that a renderer expands into many different — but equally\ngood — variants.`;\n\nconst GOAL = `GOAL — readable template first, variety second.\nWrite the final copy as if for a human, then add markup only where a word or clause could genuinely\nbe said another way. EVERY variant the renderer can produce must read like a human wrote it. A\ntemplate that can produce one awkward variant is a broken template, no matter how much variety it\noffers.\n\nTWO PASSES, never merged. First write the copy straight through with NO markup at all, as one\nfinished piece of text. Only then go back over that finished text and add the markup. Inventing the\nprose and choosing the branches at the same time is what produces variants that disagree with\nthemselves, and the longer the copy, the more reliably it happens — past a few sentences the two\npasses stop being a preference and become the only way to get it right.\n\nSCOPE — you write ONE block of copy: a paragraph, a headline, a message. Never a document. No\nheadings, no lists, no markdown, no HTML (hard rule 4), and no brief overrides that. If the brief\ndescribes something with structure — an article, several sections, a page — write only the single\nblock it most directly asks for. The host calls you once per block and assembles the structure\nitself; a template that tries to hold a whole document breaks in ways nobody can see in the\nsource.`;\n\n/**\n * Which language the prompt TEACHES IN for a locale — the examples, and the language-specific\n * advice that goes with them.\n *\n * This is a separate question from plural arity, and conflating the two was a real bug: arity was\n * used as a stand-in for \"is this Russian-ish\", so adding sr/hr/bs to the 3-form family silently\n * handed Croatian a Russian case block and Cyrillic examples. Arity says how many forms a\n * `{plural}` takes; it says nothing about which language to write an example in.\n *\n * Ask the ENGINE for arity (`pluralArity` from `@spintax/core`). Ask this for anything else.\n */\ntype TeachingProfile = 'east-slavic' | 'bcs' | 'default';\n\n/**\n * Base tags → profile. Explicit, because every implicit rule tried here has been wrong: the tags\n * that share a plural rule do NOT share a script, an alphabet, or a case system worth teaching the\n * same way.\n */\nconst PROFILE_BY_LANG: Readonly<Record<string, TeachingProfile>> = {\n  ru: 'east-slavic',\n  uk: 'east-slavic',\n  be: 'east-slavic',\n  sr: 'bcs',\n  hr: 'bcs',\n  bs: 'bcs',\n};\n\nfunction teachingProfile(locale: string | undefined): TeachingProfile {\n  return PROFILE_BY_LANG[normalizeBaseLang(locale)] ?? 'default';\n}\n\nexport interface PromptExamples {\n  def: string;\n  set: string;\n  permutation: string;\n  optional: string;\n  conditional: string;\n  plural: string;\n}\n\n/**\n * The worked examples the prompt teaches from, for a given locale.\n *\n * Exported and interpolated into the prompt rather than retyped inside it, so the test suite can\n * `validate()` the exact strings the model is shown — under the same locale. A prompt that teaches\n * invalid syntax poisons everything downstream of it; that must be a test failure, not a surprise\n * in production.\n *\n * Examples are written IN a language, so they follow the teaching profile, never the arity. The\n * conjunction inside a permutation (`lastsep=\" и \"`) is baked into the example text rather than\n * interpolated, so it cannot drift out of agreement with the sentence around it. {@link syntaxBlock}\n * derives the same word for its abstract `a|b|c` illustration from the same profile.\n *\n * `bcs` deliberately teaches from the English set: there are no BCS examples yet, and Croatian\n * copy invented to fill the slot would be worse than neutral English. The BCS-specific advice\n * lives in {@link grammarBlock}, which does carry BCS words.\n *\n * The plural example is the one that cannot ride on the example language alone: every example is\n * validated UNDER THE TARGET LOCALE, so a 2-form `item|items` fails arity for a 3-form locale\n * taught in English. Its forms therefore follow the engine's arity while the carrier sentence\n * follows the profile. `sat|sata|sati` is used because all three BCS buckets differ there and the\n * shared corpus already pins it — no invented copy.\n */\nexport function promptExamples(locale?: string): PromptExamples {\n  const russian = teachingProfile(locale) === 'east-slavic';\n  const arity = pluralArity(locale);\n  return {\n    def: [\n      '#def %product% = {course|training}',\n      '#set %offer% = our new %product%',\n      'Get %offer% today — the %product% starts on Monday.',\n    ].join('\\n'),\n    set: [\n      '#set %greeting% = {Hi|Hello|Hey}',\n      '%greeting% there. And later, %greeting% again — which may well read differently.',\n    ].join('\\n'),\n    permutation: russian\n      ? 'Мы [<minsize=2;maxsize=3;sep=\", \";lastsep=\" и \">перезвоним за час|подберём тариф|перенесём данные].'\n      : 'We can [<minsize=2;maxsize=3;sep=\", \";lastsep=\" and \">reply within the hour|set up your account|migrate your data].',\n    optional: 'Get our {|brand new }{course|training} today.',\n    conditional: '{?discount?Save %discount% today|Get started in minutes}',\n    plural: russian\n      ? 'У вас %n% {plural %n%: товар|товара|товаров} в корзине.'\n      : arity === 6\n        ? // Arabic (#88): six forms, and the one/two forms carry the number in the word itself\n          // (كتاب واحد, كتابان), so %n% goes INSIDE the forms that print it, not before the block.\n          'في سلتك {plural %n%: %n% كتاب|كتاب واحد|كتابان|%n% كتب|%n% كتابًا|%n% كتاب}.'\n        : arity === 3\n          ? 'The sale ends in %n% {plural %n%: sat|sata|sati}.'\n          : 'You have %n% {plural %n%: item|items} in your cart.',\n  };\n}\n\n// The permutation note is not pedantry: the default separator is a single SPACE, so a bare\n// [clause|clause] joins into mush (\"we reply fast we migrate your data\"). Models get this wrong\n// unless told explicitly.\nfunction syntaxBlock(locale: string | undefined): string {\n  const ex = promptExamples(locale);\n  // Genuinely arity — the SHAPE the engine will accept. Straight from the engine, so it cannot\n  // disagree with the validator the drafted template is about to meet.\n  const forms = pluralArity(locale);\n  const pluralForm =\n    forms === 6\n      ? '{plural %n%: zero|one|two|few|many|other}'\n      : forms === 3\n        ? '{plural %n%: one|few|many}'\n        : '{plural %n%: one|many}';\n  // Six slots are not guessable from their names the way one|few|many is: say which counts land\n  // where, and that the number may live inside a form.\n  const slotOrder =\n    forms === 6\n      ? '\\n    The six forms go in this order: zero (0), one (1), two (2), few (3–10, 103–110, …),\\n' +\n        '    many (11–99, 111–199, …), other (100–102, 200–202, …). A form may carry the number\\n' +\n        '    itself — write %n% inside the forms that print it and leave it out of the ones that do not.'\n      : '';\n  // The conjunction belongs to the example LANGUAGE, not the arity: a Cyrillic \"и\" inside an\n  // English sentence is what keying this on arity produced for Latin-script hr/bs.\n  const and = teachingProfile(locale) === 'east-slavic' ? 'и' : 'and';\n\n  return `SYNTAX — this is the COMPLETE list. Nothing else exists.\n\n{a|b|c}\n    Pick exactly one. Rerolls at EVERY occurrence: two separate {Hi|Hello} may disagree.\n    An EMPTY branch makes a word optional — deliberate and useful:\n${indent(ex.optional)}\n    But a stray double pipe is an ACCIDENTAL empty branch: {a|b||c} silently renders nothing one\n    time in four. Re-read every {…} for a doubled \"|\".\n\n#def %v% = value\n    Define once, reuse. The value is chosen ONCE per message, and every %v% in the copy is the\n    SAME. Use it for anything that must stay consistent across sentences (a product name, a\n    tone), and for any number you both print and agree grammatically.\n    A #def value may itself contain {a|b} AND other variables — variables nest:\n${indent(ex.def)}\n    Here %offer% contains %product%, so the two can never contradict each other. Inline {a|b}\n    would reroll and could say \"course\" in one sentence and \"training\" in the next.\n\n#set %v% = value\n    A macro: the value is re-picked at EVERY use, so one name can read differently across the\n    copy. Use it when variety is the point, or for text you mention only once:\n${indent(ex.set)}\n    Choose between them by asking whether two mentions disagreeing would read as a mistake.\n    A product name, a price, a count: #def. A greeting repeated down the page: #set.\n\n%name%\n    A variable, filled in by the host at render time. Use ONLY names from ALLOWED VARIABLES.\n\n[<minsize=2;maxsize=3;sep=\", \";lastsep=\" ${and} \">a|b|c]\n    Permutation: shuffles the items and joins them.\n    ALWAYS set sep when the items are clauses — the DEFAULT SEPARATOR IS A SINGLE SPACE, which\n    turns clauses into mush.\n    ALWAYS set lastsep for a human-readable list: \"a, b ${and} c\"\n    instead of the robotic \"a, b, c\".\n    minsize/maxsize pick a SUBSET, so the list itself varies in length.\n    Use permutations only for items of EQUAL weight, where the order genuinely does not matter\n    (benefits, features, providers):\n${indent(ex.permutation)}\n\n{?VAR?then|else}\n    Conditional. Emits \"then\" if VAR has a truthy value, otherwise \"else\".\n    The \"else\" half is OPTIONAL: {?VAR?then} emits NOTHING when VAR is empty. That is the form to\n    use for a detail that should simply disappear when the host has no value for it — do not pad\n    it with a filler sentence.\n    Use it when the copy must adapt to data that may be missing:\n${indent(ex.conditional)}\n\n${pluralForm}\n    Plural agreement by count. This target language takes EXACTLY ${forms} forms —\n    writing any other number of forms is a hard error, not a style choice. NEVER hand-roll counts\n    as {item|items}:\n${indent(ex.plural)}${slotOrder}`;\n}\n\nconst RULES = `HARD RULES\n1. Grammar-safety. Every option inside {…} must fit the surrounding sentence identically — same\n   part of speech, same agreement. Read each branch back into the FULL sentence before keeping it.\n2. Variables. Use only the names given in ALLOWED VARIABLES. Never invent one. If you need a value\n   that is not offered, rewrite the copy so it is not needed.\n3. Counts. Any number followed by a noun goes through {plural …}. You cannot pick bucket forms by\n   hand — the engine does it per locale.\n4. No syntax outside the list above. No markdown, no HTML.\n5. Do not spin proper nouns, brand names, prices, URLs, or legal wording. Vary the copy AROUND them.\n6. Directives. #def and #set must START THEIR OWN LINE, one per line, above the copy that uses them.\n   A directive written mid-sentence is NOT a directive: it stays literal text and gets printed to\n   the reader, and nothing downstream reports it as an error.\n7. Addresses. Never end a sentence with an email address, a URL or a domain; put it inside the\n   sentence: \"Write to info@example.com and we will reply within a day.\" Right after an address the\n   formatter cannot always tell where the address ends and the next sentence begins, so a sentence\n   glued on can be printed as part of the address. Write the domain ending in lower case: a\n   capitalised ending such as \"example.Com\" can be split into \"example. Com\".`;\n\nconst OUTPUT = `OUTPUT CONTRACT\nReturn the template and NOTHING else — no explanation, no quotes, no code fences, no \"Template:\"\nprefix. Your entire reply is fed straight into the renderer.`;\n\nconst SELF_CHECK = `SELF-CHECK — do this before you answer\n- Mentally render 5 variants. If any reads awkwardly or breaks agreement, fix that BRANCH; do not\n  rewrite the whole sentence.\n- Check every %var% against ALLOWED VARIABLES.\n- Check every [ … ] has a sep and really holds equal-weight items.\n- Check every count goes through {plural …}.\n- Check every #def and #set is alone on its own line, not buried in a sentence.\n- Check no sentence ends with an email address, a URL or a domain, and every domain ending is lower case.\n- Check you returned ONE block, with no heading, list or markup of any kind.`;\n\n/**\n * Grammar is language-specific, and the Slavic cases are where a model quietly produces garbage:\n * it will happily hand-roll `{товар|товара|товаров}` and get the bucket boundaries wrong. The\n * engine's locale-aware plural is the whole point — the prompt has to push the model into it.\n */\nfunction grammarBlock(locale: string | undefined): string {\n  const lang = normalizeBaseLang(locale);\n  const profile = teachingProfile(locale);\n\n  if (profile === 'east-slavic') {\n    return `LANGUAGE: ${lang} — agreement is strict and unforgiving. This is the hard part; slow down here.\n- Every option inside {…} must preserve GENDER, CASE and NUMBER agreement with the words around it.\n    WRONG: {хороший|отличная} курс     ← gender breaks in the second branch\n    RIGHT: {хороший|отличный} курс\n- If a branch changes the case the sentence needs, the preposition that governs it must move INSIDE\n  the branch. A preposition left outside governs both branches, and only one of them survives:\n    WRONG: {после|при} первом заказе   ← \"после первом заказе\": после takes the genitive\n    RIGHT: {после первого заказа|при первом заказе}\n- SPINNING A DECLINED NOUN OR ADJECTIVE. One rule underneath all of it: forms that must agree have\n  to come from ONE roll. In order of preference:\n  1. Choose synonyms that DECLINE ALIKE and keep the endings OUTSIDE the definition — then a single\n     roll serves every case at once:\n        #def %e% = {магазин|сайт|проект}\n        О %e%е говорят, для %e%а есть скидка, к %e%у можно вернуться.\n     Test every branch against every case you need. ONE differing ending disqualifies the branch:\n     {больш|нов} breaks in the nominative alone (\"Большый проект\") and is correct everywhere else —\n     exactly the kind of defect that survives proofreading and shows up in one variant out of many.\n  2. Endings agree but the stems differ — cut at the STEM, and name the cases by REFERENCING it, so\n     they still come from that one roll:\n        #def %s%  = {посетител|гост}\n        #def %V%  = %s%и\n        #def %VG% = %s%ей\n        %V% — для %VG%.\n     The stem must be #def. Under #set it re-rolls at every use and you get \"Гости — для\n     посетителей\".\n  3. They genuinely decline differently — put the whole span inside ONE enumeration:\n        #def %p% = {посетители — для посетителей|гости — для гостей}\n  NEVER give each case its own list. Two definitions are two independent rolls:\n        WRONG: #def %V% = {посетители|гости}\n               #def %VG% = {посетителей|гостей}     ← renders \"Гости — для посетителей\"\n- NEVER hand-roll count forms: {товар|товара|товаров} is WRONG. Write {plural %n%: товар|товара|товаров}\n  and let the engine choose the bucket for the actual number — you cannot do it correctly, it depends\n  on the count.`;\n  }\n\n  if (profile === 'bcs') {\n    return `LANGUAGE: ${lang} — agreement is strict and unforgiving. This is the hard part; slow down here.\n- Every option inside {…} must preserve GENDER, CASE and NUMBER agreement with the words around it.\n    WRONG: {dobar|odlična} kurs      ← gender breaks in the second branch\n    RIGHT: {dobar|odličan} kurs\n- If a branch changes the case the sentence needs, the preposition that governs it must move INSIDE\n  the branch. A preposition left outside governs both branches, and only one of them survives:\n    WRONG: {u|iz} gradu     ← \"iz gradu\": iz takes the genitive\n    RIGHT: {u gradu|iz grada}\n- Forms that must AGREE have to come from ONE roll. Prefer synonyms that DECLINE ALIKE and keep the\n  endings outside the definition, so a single #def serves every case; test every branch against\n  every case you need, because one differing ending is enough to break it. NEVER give each case its\n  own list — two definitions roll independently, so the nominative from one and the genitive from\n  the other will not match. If the synonyms genuinely decline differently, put the whole span inside\n  one enumeration instead.\n- Counts take THREE buckets, same boundaries as Russian. NEVER hand-roll them:\n  {bonus|bonusa|bonusa} is WRONG. Write {plural %n%: bonus|bonusa|bonusa} and let the engine pick.\n- Write the WHOLE template in ONE script. Do not mix Latin and Cyrillic inside a template or, worse,\n  inside a single {…} — every branch must be the same script as the text around it.\n- The worked examples below are in ENGLISH, and only the plural forms are in yours. Write YOUR\n  language throughout, including the words inside a permutation's sep/lastsep: the examples show\n  lastsep=\" and \", and yours is \"i\" in Latin script or \"и\" in Cyrillic — never the English word.`;\n  }\n\n  return `LANGUAGE: ${lang}\n- Every option inside {…} must agree with the surrounding sentence: same part of speech, same\n  number, same tense, and the article before it must still be correct (\"a offer\" is a broken branch).\n- Never hand-roll counts as {item|items} — use {plural %n%: item|items}.`;\n}\n\nfunction levelBlock(level: VariationLevel): string {\n  switch (level) {\n    case 'conservative':\n      return `VARIATION LEVEL: conservative\nUse {a|b} only, and only on words that carry no agreement risk (adverbs, interchangeable synonyms\nof the same gender/number). No permutations, no nesting. Prefer 2 options per choice.`;\n    case 'aggressive':\n      return `VARIATION LEVEL: aggressive\nUse every construct, including [ … ] permutations and {…} nested inside branches. Push for real\nvariety — but the grammar rules above still beat variety, every time. A broken variant is worse\nthan a boring one.`;\n    default:\n      return `VARIATION LEVEL: balanced\nUse {a|b}, %variables%, #set, {?…} and {plural …}. Use permutations only where the items are\ngenuinely equal-weight. Aim for 2–3 options per choice and vary whole clauses, not just adjectives.`;\n  }\n}\n\nfunction channelBlock(channel: Channel): string {\n  switch (channel) {\n    case 'email':\n      return 'CHANNEL: email — 2–5 sentences, conversational, one clear call to action.';\n    case 'sms':\n      return 'CHANNEL: SMS — under 160 characters TOTAL in every variant. Be terse. No permutations.';\n    case 'push':\n      return 'CHANNEL: push notification — one sentence, under 120 characters in every variant.';\n    case 'landing':\n      return 'CHANNEL: landing copy — a headline plus one supporting sentence.';\n    default:\n      return 'CHANNEL: generic short marketing copy.';\n  }\n}\n\n/* ── Builders ────────────────────────────────────────────────────────────── */\n\n/** Build the canonical authoring prompt: brief → a spintax template. */\nexport function buildAuthoringPrompt(opts: AuthoringPromptOptions): BuiltPrompt {\n  const specs = (opts.allowedVariables ?? []).map(asSpec);\n  const level = opts.variationLevel ?? 'balanced';\n  const channel = opts.channel ?? 'generic';\n\n  const systemPrompt = [\n    ROLE,\n    GOAL,\n    syntaxBlock(opts.locale),\n    grammarBlock(opts.locale),\n    variableRulesBlock(opts.locale),\n    levelBlock(level),\n    RULES,\n    OUTPUT,\n    SELF_CHECK,\n  ].join('\\n\\n');\n\n  const userPrompt = [\n    channelBlock(channel),\n    variableListBlock(specs),\n    '',\n    'BRIEF:',\n    opts.brief,\n    '',\n    'Write the spintax template now. Output only the template.',\n  ].join('\\n');\n\n  return {\n    systemPrompt,\n    userPrompt,\n    allowedVariables: specs.map((v) => v.name),\n    promptVersion: PROMPT_VERSION,\n  };\n}\n\nexport interface AuthoringRulesOptions {\n  /** Selects the grammar block. The rules are worth far more for an inflected locale. */\n  locale?: string;\n  /** Omit to leave the level unstated — a reader choosing for itself needs no instruction. */\n  variationLevel?: VariationLevel;\n}\n\n/**\n * The authoring rules as a DOCUMENT TO READ, for a reader that is already a model.\n *\n * {@link buildAuthoringPrompt} drives a model on someone else's behalf: a host holds a brief, calls\n * an LLM with these rules as its system prompt, and feeds the reply straight to the renderer. An\n * agent reached over MCP is the other shape entirely — it already has the user, the brief and its\n * own conversation, and what it lacks is the craft. It cannot install a system prompt for itself,\n * so handing it `buildAuthoringPrompt({ brief: 'x' }).systemPrompt` and discarding the rest would\n * be an abuse of the builder, not a use of it.\n *\n * The difference is not cosmetic, which is the reason this is an export and not a caller-side\n * `.systemPrompt`: OUTPUT is deliberately EXCLUDED. It says \"return the template and NOTHING else —\n * your entire reply is fed straight into the renderer\", which is true for a host's LLM call and\n * false, even harmful, for an agent that has to talk to a person. Everything else is kept verbatim\n * so the two surfaces cannot teach different craft: same ROLE (it defines what a template IS), same\n * GOAL, syntax, grammar, variable and hard rules, same self-check.\n */\nexport function authoringRules(opts: AuthoringRulesOptions = {}): string {\n  return [\n    ROLE,\n    GOAL,\n    syntaxBlock(opts.locale),\n    grammarBlock(opts.locale),\n    variableRulesBlock(opts.locale),\n    ...(opts.variationLevel ? [levelBlock(opts.variationLevel)] : []),\n    RULES,\n    SELF_CHECK,\n  ].join('\\n\\n');\n}\n\nexport interface RepairPromptOptions {\n  /**\n   * MUST be the same locale the template was authored under and will be rendered with — the\n   * plural arity the model is told to produce depends on it, and a mismatch is the very bug the\n   * repair is supposed to fix.\n   */\n  locale?: string;\n  /** Restated so a repair cannot smuggle in a variable the host does not have. */\n  allowedVariables?: readonly AllowedVariable[];\n}\n\n/**\n * Build a repair prompt from `validate()` output.\n *\n * Without this the authoring loop dead-ends the first time a model returns something invalid —\n * and it will. The precise spans added in core 0.1.3 are what let us point the model at the exact\n * offending token instead of saying \"something is wrong\".\n *\n * It carries the SAME authoring constraints as the draft prompt (locale, allowed variables): a\n * repair that fixes a bracket while inventing a `%variable%` or the wrong plural arity is not a\n * repair.\n */\nexport function buildRepairPrompt(\n  template: string,\n  diagnostics: readonly Diagnostic[],\n  opts: RepairPromptOptions = {},\n): BuiltPrompt {\n  const specs = (opts.allowedVariables ?? []).map(asSpec);\n  const errors = diagnostics.filter((d) => d.severity === 'error');\n\n  const numbered = template\n    .split('\\n')\n    .map((line, i) => `${String(i + 1).padStart(2, ' ')} | ${line}`)\n    .join('\\n');\n\n  const list = errors\n    .map((e) => `- line ${e.line}, column ${e.column} [${e.code}]: ${e.message}`)\n    .join('\\n');\n\n  const systemPrompt = [\n    ROLE,\n    'You are FIXING an invalid spintax template. Change as little as possible: repair the reported',\n    'spans and leave everything else byte-for-byte identical. Do not rewrite the copy.',\n    syntaxBlock(opts.locale),\n    grammarBlock(opts.locale),\n    variableRulesBlock(opts.locale),\n    OUTPUT,\n  ].join('\\n\\n');\n\n  const userPrompt = [\n    'This template failed validation.',\n    '',\n    variableListBlock(specs),\n    '',\n    'TEMPLATE (line-numbered):',\n    numbered,\n    '',\n    'ERRORS:',\n    list || '- (none reported — return the template unchanged)',\n    '',\n    'Return the corrected template. Output only the template.',\n  ].join('\\n');\n\n  return {\n    systemPrompt,\n    userPrompt,\n    allowedVariables: specs.map((v) => v.name),\n    promptVersion: PROMPT_VERSION,\n  };\n}\n\n/**\n * Strip what a model wraps its answer in despite being told not to.\n *\n * The output contract forbids fences and quotes; models emit them anyway. Contract in the prompt,\n * tolerant parsing in the host — never trust the contract alone.\n */\nexport function cleanModelTemplate(raw: string): string {\n  let t = raw.trim();\n  t = t\n    .replace(/^```[a-z]*\\r?\\n?/iu, '')\n    .replace(/\\r?\\n?```$/u, '')\n    .trim();\n  t = t.replace(/^(?:template|шаблон)\\s*:\\s*/iu, '').trim();\n  if ((t.startsWith('\"') && t.endsWith('\"')) || (t.startsWith('“') && t.endsWith('”'))) {\n    t = t.slice(1, -1).trim();\n  }\n  return t;\n}\n"]}