{
  "id": "BFLNewsReader04",
  "name": "БФЛ — чтение первоисточника",
  "active": true,
  "versionId": "b6723070-4210-43f4-be9a-872c535864be",
  "nodes": [
    {
      "id": "9fd4ddc2-d30e-42f5-aee9-c1839ad321df",
      "name": "Вход",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "parameters": {
        "inputSource": "passthrough"
      },
      "position": [
        0,
        0
      ]
    },
    {
      "id": "414eef2f-f876-4efb-b71a-cea2f84bd167",
      "name": "Начать чтение",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\n// Small, deliberately restrictive URL adapter for the n8n sandbox (global URL is absent).\n// Only the official ASCII hosts used by this workflow are representable.\nclass StrictURL {\n constructor(value){\n  if(typeof value!=='string'||value.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value))throw Error('url_invalid');\n  const m=/^https:\\/\\/((?:www\\.)?(?:nalog\\.gov\\.ru|cbr\\.ru|vsrf\\.ru|pravo\\.ru|probankrotstvo\\.ru|kommersant\\.ru|rbc\\.ru))(\\/[^?#]*)?(\\?[^#]*)?(?:#[^\\s]*)?$/i.exec(value);\n  if(!m)throw Error('url_forbidden');\n  this.protocol='https:';this.hostname=m[1].toLowerCase();this.pathname=m[2]||'/';this.username='';this.password='';this.port='';this.hash='';\n  if(/%|\\/{2}|(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(this.pathname))throw Error('url_path_encoding_forbidden');\n  this.params=[];\n  for(const entry of (m[3]||'').slice(1).split('&').filter(Boolean)){\n   const at=entry.indexOf('=');const key=decodeURIComponent((at<0?entry:entry.slice(0,at)).replace(/\\+/g,' '));const val=decodeURIComponent((at<0?'':entry.slice(at+1)).replace(/\\+/g,' '));\n   if(/[\\u0000-\\u001f\\u007f]/.test(key+val)||this.params.some(x=>x[0]===key))throw Error('url_query_invalid');this.params.push([key,val]);\n  }\n  this.searchParams={keys:()=>this.params.map(x=>x[0]),get:key=>this.params.find(x=>x[0]===key)?.[1]??null,delete:key=>{this.params=this.params.filter(x=>x[0]!==key)},sort:()=>this.params.sort((a,b)=>a[0].localeCompare(b[0]))};\n }\n toString(){return 'https://'+this.hostname+this.pathname+(this.params.length?'?'+this.params.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'):'');}\n}\nfunction resolveOfficialLocation(location,base){\n if(typeof location!=='string'||!location||location.startsWith('//'))throw Error('redirect_forbidden');\n const current=new StrictURL(base);let target;\n if(location.startsWith('https://'))target=location;\n else if(location.startsWith('/'))target='https://'+current.hostname+location;\n else if(location.startsWith('?'))target='https://'+current.hostname+current.pathname+location;\n else throw Error('redirect_relative_path_unsupported');\n const next=new StrictURL(target);if(next.hostname.replace(/^www\\./,'')!==current.hostname.replace(/^www\\./,''))throw Error('cross_source_redirect');return next.toString();\n}\n\nconst QUALITY=(()=>{const URL=StrictURL;\n'use strict';\n// Standalone helpers. These do not fetch pages, classify their meaning, mutate state or publish.\n// Merge into actual deployed nodes. Do not replace the server's modified v0.4 wholesale.\nconst POLICY={\"version\": \"0.4.1\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"max_per_publisher_first_pass\": 2, \"require_exact_quotes_to_offer_topic\": false, \"verify_selected_topic_before_image\": true, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\"}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\"}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\"}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\"}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\"}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\"}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\"}], \"categories\": {\"bankruptcy\": \"Банкротство граждан\", \"credit\": \"Кредиты и МФО\", \"collection\": \"Приставы и взыскание\", \"property\": \"Жильё и имущество\", \"family\": \"Семья, наследство и долги\", \"fraud\": \"Финансовое мошенничество\", \"consumer\": \"Права потребителей финансовых услуг\"}};\nfunction error(code){throw Object.assign(new Error(code),{code});}\nfunction safeText(value,max=20000){\n  if(typeof value!=='string'||!value.trim()||value.length>max)error('text_invalid');\n  if(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u.test(value))error('text_controls');\n  return value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n}\nfunction normalizeExcerpt(value){\n  return safeText(value,60000).replace(/[\\u00ad\\u200b\\ufeff]/gu,'')\n    .replace(/[«»“”„‟]/gu,'\"').replace(/[‘’‚‛]/gu,\"'\")\n    .replace(/[\\u2010-\\u2015\\u2212]/gu,'-').replace(/\\s+/gu,' ').trim();\n}\nfunction checkExcerpt(quote,article){\n  try{\n    const q=normalizeExcerpt(quote),body=normalizeExcerpt(article);\n    // A short factual phrase may be legitimate. No arbitrary 15-character cutoff.\n    if((q.match(/[\\p{L}\\p{N}]+/gu)||[]).length<2)return {matched:false,reason:'fragment_too_short'};\n    const at=body.indexOf(q);\n    return at<0 ? {matched:false,reason:'text_differs'} : {matched:true,reason:'normalized_match',normalized_excerpt:q};\n  }catch(e){return {matched:false,reason:e.code||'evidence_invalid'};}\n}\nfunction partitionEvidence(facts,article){\n  if(!Array.isArray(facts))error('facts_invalid');\n  const matched=[],needs_review=[];\n  facts.forEach((fact,index)=>{\n    const result=checkExcerpt(fact?.evidence,article);\n    const row={index,fact,result};\n    (result.matched?matched:needs_review).push(row);\n  });\n  // Matching is provenance, NOT semantic proof. Caller must review the selected news.\n  return {matched,needs_review,topic_must_not_be_dropped_for_this_alone:true,semantic_review_required:true};\n}\nfunction sourceBlocks(article,documentKey){\n  if(typeof documentKey!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(documentKey))error('document_key_invalid');\n  const paragraphs=safeText(article,60000).split(/\\n\\s*\\n/u).filter(p=>p.trim());\n  const chunks=[];\n  for(let paragraph of paragraphs){\n    while(paragraph.length>1800){\n      let cut=paragraph.lastIndexOf(' ',1800);\n      if(cut<900)cut=1800;\n      chunks.push(paragraph.slice(0,cut));paragraph=paragraph.slice(cut).trimStart();\n    }\n    if(paragraph.trim())chunks.push(paragraph.trim());\n  }\n  return {document_key:documentKey,blocks:chunks.map((text,i)=>({id:'p'+String(i+1).padStart(3,'0'),text}))};\n}\nfunction resolveEvidenceRefs(fact,document){\n  if(!fact||fact.document_key!==document.document_key)error('wrong_source_document');\n  if(!Array.isArray(fact.source_ids)||!fact.source_ids.length||fact.source_ids.length>3||new Set(fact.source_ids).size!==fact.source_ids.length)error('source_ids_invalid');\n  const map=new Map(document.blocks.map(b=>[b.id,b.text]));\n  return fact.source_ids.map(id=>{\n    if(!map.has(id))error('source_block_not_found');\n    return {id,text:map.get(id)};\n  });\n}\nfunction sourceURL(raw){\n  if(typeof raw!=='string'||raw.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(raw))error('url_invalid');\n  let u;try{u=new URL(raw);}catch{error('url_invalid');}\n  if(u.protocol!=='https:'||u.username||u.password||u.port)error('url_forbidden');\n  const source=POLICY.sources.find(s=>u.hostname===s.host||u.hostname==='www.'+s.host);\n  if(!source)error('source_not_allowed');\n  u.hostname=source.host;u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  if(source.host==='pravo.ru')u.pathname=u.pathname.replace(/(\\/(?:news|story)\\/\\d+)\\/amp\\/?$/,'$1/');const p=u.pathname;\n  const article = source.host==='nalog.gov.ru' ? /^\\/rn\\d+\\/(?:news\\/[^/]+|ifns\\/imns\\d+_\\d+\\/info)\\/\\d+\\/?$/.test(p)\n    : source.host==='cbr.ru' ? /^\\/press\\/(?:event|regevent)\\/\\/?$/i.test(p)&&/^\\d+$/.test(u.searchParams.get('id')||'') || /^\\/press\\/pr\\/\\/?$/i.test(p)&&!!u.searchParams.get('file')\n    : source.host==='vsrf.ru' ? /^(?:\\/news|\\/press_center\\/news)\\/\\d+\\/?$/.test(p)\n    : source.host==='pravo.ru' ? /^\\/(?:news|story)\\/\\d+\\/?$/.test(p)\n    : source.host==='probankrotstvo.ru' ? /^\\/(?:news|article|articles)\\/[a-z0-9-]+-\\d+\\/?$/.test(p)\n    : source.host==='kommersant.ru' ? /^\\/doc\\/\\d+\\/?$/.test(p)\n    : source.host==='rbc.ru' ? /^\\/[a-z0-9_-]+\\/\\d{2}\\/\\d{2}\\/\\d{4}\\/[a-f0-9]{24}\\/?$/i.test(p)\n    : false;\n  if(!article)error('article_path_needs_adapter');\n  if(source.host==='vsrf.ru')u.pathname='/news/'+p.match(/\\d+/)[0];\n  else if(source.host==='nalog.gov.ru'||source.host==='pravo.ru')u.pathname=p.replace(/\\/?$/,'/');\n  else if(source.host==='cbr.ru')u.pathname=p.toLowerCase().replace(/\\/?$/,'/');\n  else u.pathname=p.replace(/\\/$/,'');\n  const keep=source.host==='cbr.ru'?(/event/i.test(p)?'id':'file'):null;\n  for(const key of [...u.searchParams.keys()])if(key!==keep)u.searchParams.delete(key);\n  return {url:u.toString(),publisher:source.host,source_name:source.name,source_kind:source.kind};\n}\nfunction dateValue(value){\n  if(typeof value!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))error('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if(!Number.isFinite(d.getTime())||d.toISOString().slice(0,10)!==value)error('invalid_date');\n  return value;\n}\n// read is trusted HTTP/extractor output; judgment is untrusted classifier output.\n// Actual reading, redirects, trace provenance and publication-date extraction stay in n8n.\nfunction offerTopic(read,judgment,window){\n  if(!read||!judgment||judgment.read_id!==read.read_id)error('read_id_mismatch');\n  const src=sourceURL(read.url);\n  if(read.transport!=='http'||read.status_code!==200||read.date_kind!=='published'||read.discovery_bound!==true)error('source_read_required');\n  safeText(read.article_text,60000);\n  const day=dateValue(read.published_date),from=dateValue(window.from),to=dateValue(window.to);\n  if(from>to)error('date_window_invalid');\n  if(day<from||day>to)return {offer:false,reason:'outside_window'};\n  if(judgment.relevance==='unrelated')return {offer:false,reason:'unrelated'};\n  if(!['direct','adjacent','context'].includes(judgment.relevance))error('relevance_invalid');\n  if(!Object.hasOwn(POLICY.categories,judgment.category))error('category_invalid');\n  if(!['news','analysis','opinion','explanation','statistics','advertisement'].includes(judgment.content_type))error('content_type_invalid');\n  if(judgment.content_type==='advertisement')return {offer:false,reason:'advertisement'};\n  return {offer:true,read_id:read.read_id,...src,published_date:day,\n    title:safeText(judgment.title,160),angle:safeText(judgment.angle,350),\n    relevance:judgment.relevance,category:judgment.category,category_label:POLICY.categories[judgment.category],\n    content_type:judgment.content_type,verification_state:'on_selection',\n    score:judgment.relevance==='direct'?30:judgment.relevance==='adjacent'?20:10};\n}\nfunction rankTopics(topics,{excludedURLs=[],limit=POLICY.max_topics}={}){\n  if(!Number.isInteger(limit)||limit<1||limit>POLICY.max_topics)error('limit_invalid');\n  const seen=new Set(excludedURLs.map(url=>sourceURL(url).url)),unique=[];\n  for(const topic of [...topics].filter(t=>t.offer===true).sort((a,b)=>b.score-a.score||b.published_date.localeCompare(a.published_date))){\n    const url=sourceURL(topic.url).url;\n    if(seen.has(url))continue;seen.add(url);unique.push(topic);\n  }\n  const selected=[],deferred=[],counts=new Map();\n  for(const t of unique){\n    if((counts.get(t.publisher)||0)>=POLICY.max_per_publisher_first_pass){deferred.push(t);continue;}\n    selected.push(t);counts.set(t.publisher,(counts.get(t.publisher)||0)+1);\n  }\n  // Diversity is a preference. Do not discard useful topics when only one source has them.\n  return [...selected,...deferred].slice(0,limit);\n}\nfunction topicText(topic){\n  return safeText(`${topic.title}\\n\\nИдея поста: ${topic.angle}\\n\\n${topic.category_label} · ${topic.published_date}\\n${topic.source_name}\\n${topic.url}\\n\\nПосле выбора проверю факты и подготовлю черновик.`,4096);\n}\nreturn {POLICY,normalizeExcerpt,checkExcerpt,partitionEvidence,sourceBlocks,resolveEvidenceRefs,\n  sourceURL,offerTopic,rankTopics,topicText};\n\n})();\nconst NEWS=(()=>{const URL=StrictURL;\n'use strict';\n// Pure helpers. No network, database, paid calls or Telegram mutations.\n// Port to n8n Code nodes against the actual runtime; require is not assumed there.\nconst CONFIG={\"version\": \"0.4.1\", \"timezone\": \"Europe/Moscow\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"selection_ttl_hours\": 24, \"caption_limit_utf16\": 1024, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\", \"enabled\": true}]};\nconst STAGES = Object.freeze({\n  proposal:'Инициатива', bill:'Законопроект', adopted:'Принято, вступление в силу не подтверждено',\n  in_force:'Действует', court:'Судебная практика', explanation:'Разъяснение', statistics:'Статистика'\n});\nconst CONTROLS = /[\\u0000-\\u0008\\u000b-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u;\nfunction fail(code) { throw Object.assign(new Error(code), {code}); }\nfunction clean(value, max, field='text') {\n  if (typeof value !== 'string') fail(field+'_type');\n  const s=value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n  if (!s || s.length>max || CONTROLS.test(s)) fail(field+'_invalid');\n  return s;\n}\nfunction instant(value) {\n  const t=value instanceof Date ? value.getTime() : Date.parse(value);\n  if (!Number.isFinite(t)) fail('invalid_clock');\n  return t;\n}\nfunction isoDay(value) {\n  if (typeof value!=='string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) fail('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if (!Number.isFinite(d.getTime()) || d.toISOString().slice(0,10)!==value) fail('invalid_date');\n  return value;\n}\nfunction parseDateEvidence(value) {\n  const s=clean(value,150,'publication_date_raw').toLowerCase();\n  let m=s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:$|t\\d{2}:)/);\n  if (m) return isoDay(`${m[1]}-${m[2]}-${m[3]}`);\n  m=s.match(/^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:$|\\s)/);\n  if (m) return isoDay(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  const months=['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];\n  m=s.match(/^(\\d{1,2})\\s+([а-я]+)\\s+(\\d{4})(?:$|\\s)/);\n  if (m && months.includes(m[2])) return isoDay(`${m[3]}-${String(months.indexOf(m[2])+1).padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  fail('publication_date_unreadable');\n}\nfunction windowFor(days=7, now=new Date()) {\n  if (!CONFIG.allowed_days.includes(days)) fail('days_not_supported');\n  const parts=new Intl.DateTimeFormat('en-CA',{timeZone:CONFIG.timezone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(instant(now)));\n  const get=t=>parts.find(p=>p.type===t).value;\n  const to=`${get('year')}-${get('month')}-${get('day')}`;\n  const from=new Date(Date.parse(to+'T00:00:00Z')-(days-1)*86400000).toISOString().slice(0,10);\n  return {from,to,days,timezone:CONFIG.timezone};\n}\nfunction parseCommand(text, botUsername='bflcontentbot') {\n  if (typeof text!=='string') return null;\n  const match=text.trim().match(/^\\/news(?:@([a-z0-9_]+))?(?:\\s+([\\s\\S]*))?$/i);\n  if (!match) return null;\n  if (match[1] && match[1].toLowerCase()!==botUsername.toLowerCase()) return null;\n  let tail=(match[2]||'').trim(),days=CONFIG.default_days;\n  const d=tail.match(/^(\\d+)(?:\\s+|$)/);\n  if(d){ days=Number(d[1]); if(!CONFIG.allowed_days.includes(days))fail('days_not_supported'); tail=tail.slice(d[0].length).trim(); }\n  return {days,topic:tail ? clean(tail,200,'topic') : ''};\n}\nfunction canonicalURL(value) {\n  if (typeof value!=='string' || value.length>1500 || /[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value)) fail('url_invalid');\n  let u; try{ u=new URL(value); }catch{ fail('url_invalid'); }\n  if(u.protocol!=='https:' || u.username || u.password || u.port) fail('url_forbidden');\n  const source=CONFIG.sources.find(s=>s.enabled && (u.hostname===s.host || u.hostname==='www.'+s.host));\n  // Exact known hosts; do not follow arbitrary subdomains just because web_search allows them.\n  if(!source)fail('source_not_allowed');\n  if(u.hostname==='www.'+source.host)u.hostname=source.host;\n  u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  return u.toString();\n}\nfunction articleURL(value){return QUALITY.sourceURL(value).url;}\nfunction sourceName(url) { const host=new URL(canonicalURL(url)).hostname; return CONFIG.sources.find(s=>s.host===host).name; }\nfunction normalizedEvidence(text) { return text.normalize('NFC').replace(/\\s+/gu,' ').trim(); }\nfunction quoteIn(quote,body) {\n  const q=clean(quote,800,'evidence');\n  if(q.length<15 || !normalizedEvidence(body).includes(normalizedEvidence(q)))fail('evidence_not_found');\n  return q;\n}\n// read MUST come from an actual HTTP reader/extractor. Never pass a model-produced read object.\nfunction validateRead(read, searchURLs, win, now=new Date()) {\n  if(!read || read.transport!=='http' || read.status_code!==200 || !/^text\\/html(?:;|$)/i.test(read.content_type||''))fail('page_not_read');\n  const requested=articleURL(read.requested_url), url=articleURL(read.final_url);\n  if(!searchURLs.has(requested))fail('not_in_search_sources');\n  if(new URL(requested).hostname!==new URL(url).hostname)fail('cross_source_redirect');\n  const age=instant(now)-instant(read.fetched_at);\n  if(age<0 || age>3600000)fail('read_stale');\n  if(read.date_kind!=='published')fail('publication_date_required');\n  const raw=clean(read.publication_date_raw,150,'publication_date_raw');\n  const context=clean(read.publication_date_context,1200,'publication_date_context');\n  if(!normalizedEvidence(context).includes(normalizedEvidence(raw)))fail('date_context_mismatch');\n  if(/dateModified|дата обновления|последнее обновление/i.test(context))fail('updated_is_not_published');\n  const published=parseDateEvidence(raw);\n  if(published<isoDay(win.from) || published>isoDay(win.to))fail('outside_date_window');\n  return {url,requested_url:requested,published_date:published,article_text:clean(read.article_text,60000,'article_text'),\n    source_name:sourceName(url),fetched_at:read.fetched_at,publication_date_context:context};\n}\nfunction validateCandidate(candidate, read, searchURLs, win, now=new Date()) {\n  const page=validateRead(read,searchURLs,win,now);\n  if(!candidate || candidate.read_id!==read.read_id)fail('read_id_mismatch');\n  if(candidate.relevance!=='direct_personal_finance')fail('not_relevant_to_people');\n  if(!Object.hasOwn(STAGES,candidate.stage))fail('stage_unknown');\n  if(candidate.published_date!==page.published_date)fail('date_mismatch');\n  if(!Array.isArray(candidate.facts) || candidate.facts.length<1 || candidate.facts.length>4)fail('facts_invalid');\n  const facts=candidate.facts.map(f=>({text:clean(f.text,400,'fact'),evidence:quoteIn(f.evidence,page.article_text)}));\n  const stage_evidence=quoteIn(candidate.stage_evidence,page.article_text);\n  let effective_date=null;\n  if(candidate.effective_date!=null){\n    effective_date=isoDay(candidate.effective_date);\n    const evidence=quoteIn(candidate.effective_evidence,page.article_text);\n    const raw=clean(candidate.effective_date_raw,150,'effective_date_raw');\n    if(!normalizedEvidence(evidence).includes(normalizedEvidence(raw)) || parseDateEvidence(raw)!==effective_date)fail('effective_date_mismatch');\n    if(candidate.stage==='in_force' && effective_date>win.to)fail('not_in_force_yet');\n  }\n  // A verified date plus supporting words are mandatory before advertising that a new rule is in force.\n  if(candidate.stage==='in_force' && !effective_date)fail('effective_date_required');\n  return {url:page.url,source_name:page.source_name,published_date:page.published_date,\n    title:clean(candidate.title,150,'title'),summary:clean(candidate.summary,400,'summary'),\n    why:clean(candidate.why,220,'why'),stage:candidate.stage,stage_label:STAGES[candidate.stage],\n    effective_date,stage_evidence,facts,verified_at:page.fetched_at,\n    // Additional event clustering requires verified document IDs in the workflow; do not invent them.\n    dedup_key:page.url,format:'news_fact_pack_v1'};\n}\nfunction chooseTopics(items, excludedURLs=[], limit=CONFIG.max_topics) {\n  if(!Number.isInteger(limit)||limit<1||limit>CONFIG.max_topics)fail('limit_invalid');\n  const seen=new Set(excludedURLs.map(articleURL));\n  const result=[];\n  for(const item of [...items].sort((a,b)=>b.published_date.localeCompare(a.published_date))){\n    const key=articleURL(item.url);\n    if(seen.has(key))continue;\n    seen.add(key);result.push(item);if(result.length===limit)break;\n  }\n  return result;\n}\nfunction callbackData(batchId,itemId){\n  if(!/^[A-Za-z0-9_-]{1,16}$/.test(batchId)||!/^[A-Za-z0-9_-]{1,12}$/.test(itemId))fail('id_invalid');\n  return `news:${batchId}:${itemId}`;\n}\n// Advisory guard only. Perform and persist the state claim atomically AFTER this guard.\nfunction guardSelection({callback,batch,item,now=new Date()}) {\n  if(!callback||!batch||!item)fail('selection_missing');\n  if(String(callback.from_id)!==String(batch.owner_id)||String(callback.chat_id)!==String(batch.owner_id))fail('not_owner');\n  if(String(callback.message_id)!==String(item.message_id))fail('wrong_message');\n  if(callback.data!==callbackData(batch.id,item.id)||item.batch_id!==batch.id)fail('wrong_item');\n  if(instant(now)>=instant(batch.expires_at))fail('selection_expired');\n  if(batch.status!=='ready'||item.status!=='offered')fail('selection_not_available');\n  return {batch_id:batch.id,item_id:item.id,selection_key:`news:${batch.id}:${item.id}`};\n}\nfunction captionBudget(sourceURL) {\n  const suffix='\\n\\nИсточник: '+articleURL(sourceURL);\n  const body_limit=CONFIG.caption_limit_utf16-suffix.length;\n  if(body_limit<250)fail('source_url_too_long_for_caption');\n  return {suffix,body_limit,preferred_body_max:Math.min(800,body_limit)};\n}\nfunction composeNewsCaption(body,sourceURL) {\n  const {suffix,body_limit}=captionBudget(sourceURL);\n  const text=clean(body,body_limit,'caption_body');\n  // Model writes body only. Link is appended deterministically and retained through revision.\n  if(/https?:\\/\\//i.test(text))fail('unexpected_link_in_body');\n  return text+suffix;\n}\nfunction topicMessage(item){\n  const text=`${item.title}\\n\\n${item.summary}\\n\\nПочему это важно: ${item.why}\\n\\n${item.stage_label} · ${item.published_date}\\n${item.source_name}\\n${item.url}`;\n  return clean(text,4096,'topic_message');\n}\nfunction progressText(event,{count=0,days=7,partial=false}={}){\n  switch(event){\n    case 'accepted': return `Взято в работу. Ищу новости за последние ${days} дней.`;\n    case 'verifying': return 'Проверяю даты, источники и связь новостей с личными долгами.';\n    case 'ready': return `Готово. Тем на выбор: ${count}. Нажми «Подготовить пост» под подходящей новостью.`+(partial?' Часть источников проверить не удалось.':'');\n    case 'empty': return `За последние ${days} дней подходящих новостей с проверенным источником не нашлось.`+(days===7?' Можно расширить поиск командой /news 30.':'');\n    case 'partial_empty': return 'Полностью проверить источники не удалось; подтверждённых тем пока нет. Можно повторить поиск позже.';\n    case 'failed': return 'Поиск сейчас не завершился. Можно повторить /news позже. Существующие черновики сохранены.';\n    case 'selected': return 'Тема выбрана. Готовлю пост по источнику — затем пришлю на согласование.';\n    case 'expired': return 'Эта подборка устарела. Отправь /news, чтобы получить свежие темы.';\n    default: fail('progress_event_unknown');\n  }\n}\n\nreturn {CONFIG,STAGES,parseCommand,windowFor,parseDateEvidence,canonicalURL,articleURL,\n  validateRead,validateCandidate,chooseTopics,callbackData,guardSelection,captionBudget,\n  composeNewsCaption,topicMessage,progressText};})();\nconst RESP=(()=>{'use strict';\nconst core=NEWS;\nfunction modelName(model){\n  if(typeof model!=='string'||!model.trim()||model.includes('/')||/\\s/.test(model))throw Error('Configure a real OpenAI REST model ID with web_search support');\n  return model;\n}\nfunction searchRequest({model,instructions,days=7,topic='',now=new Date()}){\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Search instructions required');\n  const win=core.windowFor(days,now);\n  return {model:modelName(model),store:false,\n    instructions,\n    input:JSON.stringify({task:'Find recent official and editorial articles about personal finance, debts and rights in Russia',window:win,topic,max_candidates:core.CONFIG.max_candidates}),\n    tools:[{type:'web_search',external_web_access:true,filters:{allowed_domains:core.CONFIG.sources.filter(s=>s.enabled).map(s=>s.host)}}],\n    tool_choice:'required',include:['web_search_call.action.sources'],max_output_tokens:8000};\n}\nfunction outputJSON(response){\n  if(!response||response.status!=='completed'||response.error)throw Error('responses_not_completed');\n  const chunks=[];\n  for(const output of response.output||[]){\n    if(output.type!=='message')continue;\n    if(output.status && output.status!=='completed')throw Error('message_not_completed');\n    for(const part of output.content||[]){\n      if(part.type==='refusal')throw Error('model_refusal');\n      if(part.type==='output_text')chunks.push(part.text);\n    }\n  }\n  const text=chunks.join('\\n').trim().replace(/^```json\\s*/i,'').replace(/\\s*```$/,'');\n  let value;try{value=JSON.parse(text);}catch{throw Error('invalid_output_json');}\n  if(!value||Array.isArray(value)||typeof value!=='object')throw Error('invalid_output_object');\n  return value;\n}\nfunction parseDiscovery(response){\n  const value=outputJSON(response);\n  if(!(response.output||[]).some(o=>o.type==='web_search_call'&&o.status==='completed'&&o.action?.type==='search'))throw Error('web_search_not_executed');\n  const searchURLs=new Set();\n  function remember(url){try{searchURLs.add(core.articleURL(url));}catch{/* Skip unsupported sources. */}}\n  for(const output of response.output||[]){\n    if(output.type==='web_search_call'&&output.status==='completed'){\n      for(const s of output.action?.sources||[])remember(s.url);\n      if(output.action?.type==='open_page')remember(output.action.url);\n    }\n    if(output.type==='message')for(const p of output.content||[])for(const a of p.annotations||[])if(a.type==='url_citation')remember(a.url);\n  }\n  if(!Array.isArray(value.candidates)||value.candidates.length>core.CONFIG.max_candidates)throw Error('invalid_candidates');\n  const candidates=[],rejected=[];const seen=new Set();\n  for(const candidate of value.candidates){\n    try{\n      const url=core.articleURL(candidate.url);\n      if(!searchURLs.has(url))throw Error('candidate_not_in_search_sources');\n      if(seen.has(url))continue;\n      seen.add(url);candidates.push({url}); // Discard model guesses about dates/facts until the page is read.\n    }catch(error){rejected.push({code:error.code||error.message});}\n  }\n  return {candidates,search_urls:[...searchURLs],rejected,response_id:response.id||null};\n}\nfunction verificationRequest({model,instructions,reads,searchURLs,win,now=new Date()}){\n  if(!Array.isArray(reads)||!reads.length||reads.length>5)throw Error('verification_batch_size');\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Verification instructions required');\n  const ids=new Set();\n  const pages=reads.map(read=>{\n    if(typeof read.read_id!=='string'||!read.read_id||ids.has(read.read_id))throw Error('read_id_invalid');\n    ids.add(read.read_id);\n    const page=core.validateRead(read,searchURLs,win,now);\n    // Reader must extract the article rather than sending the entire site navigation.\n    if(page.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n    return {read_id:read.read_id,published_date:page.published_date,url:page.url,article_text:page.article_text};\n  });\n  return {model:modelName(model),store:false,instructions,input:JSON.stringify({window:win,pages}),max_output_tokens:8000};\n}\nfunction parseVerification(response,reads,searchURLs,win,now=new Date()){\n  const value=outputJSON(response);\n  if(!Array.isArray(value.results)||value.results.length!==reads.length)throw Error('verification_results_incomplete');\n  const remaining=new Map(reads.map(r=>[r.read_id,r]));\n  const accepted=[],rejected=[];\n  for(const result of value.results){\n    const read=remaining.get(result.read_id);\n    if(!read)throw Error('unknown_or_duplicate_read_id');\n    remaining.delete(result.read_id);\n    if(result.accept===false){rejected.push({read_id:read.read_id,code:'editorial_exclusion',reason:String(result.reason||'').slice(0,400)});continue;}\n    if(result.accept!==true)throw Error('verification_decision_missing');\n    try{accepted.push(core.validateCandidate(result,read,searchURLs,win,now));}\n    catch(error){rejected.push({read_id:read.read_id,code:error.code||error.message});}\n  }\n  return {accepted,rejected};\n}\n\nreturn {searchRequest,outputJSON,parseDiscovery,verificationRequest,parseVerification};})();\nfunction snapshotOf(topic){\n if(topic.format!=='news_topic_v2'&&topic.format!=='news_fact_pack_v2')throw Error('news_format_invalid');\n const snap=topic.snapshot;\n if(!snap||snap.url!==NEWS.articleURL(topic.url)||snap.published_date!==topic.published_date)throw Error('snapshot_source_mismatch');\n const blocks=QUALITY.sourceBlocks(snap.article_text,snap.document_key);\n if(JSON.stringify(blocks.blocks)!==JSON.stringify(snap.blocks))throw Error('snapshot_blocks_mismatch');\n return snap;\n}\nfunction strictRequest(instructions,input,schema,name,max=4500){return {model:'gpt-5.6-sol',reasoning:{effort:'low'},store:false,max_output_tokens:max,instructions,input:JSON.stringify(input),text:{format:{type:'json_schema',name,strict:true,schema}}};}\nfunction apiResult(r,prefix){\n if(r.statusCode!==200)throw Error(prefix+(r.statusCode?'_http_'+r.statusCode+(r.statusCode>=500?'_outcome_unknown':''):'_outcome_unknown'));\n if(r.body?.status==='incomplete')throw Error(prefix+'_incomplete');\n if(r.body?.output?.some(x=>x.content?.some(c=>c.type==='refusal')))throw Error(prefix+'_refusal');\n try{return RESP.outputJSON(r.body);}catch{throw Error(prefix+'_malformed_json');}\n}\nfunction verifiedFacts(topic,result){\n const snap=snapshotOf(topic);\n if(result.document_key!==snap.document_key)throw Error('wrong_source_document');\n if(result.status!=='ready')throw Error('selected_needs_source_check');\n if(!['news','analysis','opinion','explanation','statistics','court','bill','adopted','in_force'].includes(result.content_type))throw Error('selected_content_type_invalid');\n // A news report is not itself an enacted legal document. Narrow attributed reporting remains available.\n if(result.content_type==='in_force')throw Error('selected_primary_document_required');\n if(!Array.isArray(result.facts)||!result.facts.length||result.facts.length>4)throw Error('selected_facts_invalid');\n const facts=result.facts.map(f=>{if(typeof f.text!=='string'||!f.text.trim()||f.text.length>700||!Array.isArray(f.qualifiers)||typeof f.attribution!=='string')throw Error('selected_fact_invalid');return {...f,source_blocks:QUALITY.resolveEvidenceRefs(f,snap)};});\n return {...topic,format:'news_fact_pack_v2',verification_state:'facts_selected',facts,content_type:result.content_type,limitations:result.limitations,selected_at:new Date().toISOString()};\n}\nfunction previewInput(s){\n const fact=JSON.parse(s.fact_pack);\n if(NEWS.articleURL(fact.url)!==s.source_url||NEWS.composeNewsCaption(s.body,s.source_url)!==s.caption)throw Error('news_caption_source_mismatch');\n let source;\n if(fact.format==='news_fact_pack_v2'){source=snapshotOf(fact);for(const f of fact.facts)QUALITY.resolveEvidenceRefs(f,source);}\n else if(fact.format==='news_fact_pack_v1')source=QUALITY.sourceBlocks([fact.stage_evidence,...fact.facts.map(f=>f.evidence)].filter(Boolean).join('\\n\\n'),'legacy_saved_evidence');\n else throw Error('news_facts_not_selected');\n return {caption_body:s.body,cover_title:s.cover_title,cover_subtitle:s.cover_subtitle,facts:fact.facts,content_type:fact.content_type||fact.stage,limitations:fact.limitations||[],source,source_url:s.source_url,legacy_evidence_only:fact.format==='news_fact_pack_v1'};\n}\nfunction assessPreview(result){\n if(typeof result.pass!=='boolean'||!Array.isArray(result.issues)||result.issues.length>10)throw Error('preview_malformed_json');\n if(result.pass&&result.issues.length)throw Error('preview_contradictory_result');\n if(!result.pass&&!result.issues.length)throw Error('preview_empty_failure');\n for(const i of result.issues)if(!['caption_body','cover_title','cover_subtitle'].includes(i.field)||typeof i.fragment!=='string'||!i.fragment||typeof i.suggested_replacement!=='string'||typeof i.problem!=='string')throw Error('preview_issue_invalid');\n return result;\n}\nfunction repairPreview(s,issues){\n const out={...s};let exact=null;try{exact=JSON.parse(s.exact_revision_json||'null');}catch{}\n // A precise owner edit may only be corrected inside its replacement span. The saved cover stays byte-identical.\n const bounds=exact&&!exact.validation_error?{start:s.body.indexOf(exact.newText),length:exact.newText?.length||0}:null;\n if(bounds&&(bounds.start<0||s.body.indexOf(exact.newText,bounds.start+1)>=0))throw Error('preview_exact_span_ambiguous');\n const edits={};\n for(const issue of issues){const key=issue.field==='caption_body'?'body':issue.field,old=out[key];if(typeof old!=='string')throw Error('preview_field_missing');const at=old.indexOf(issue.fragment);if(at<0||old.indexOf(issue.fragment,at+1)>=0)throw Error('preview_repair_ambiguous');\n if(bounds&&(key!=='body'||at<bounds.start||at+issue.fragment.length>bounds.start+bounds.length))throw Error('preview_exact_requires_owner_edit');\n (edits[key]||=[]).push({at,end:at+issue.fragment.length,text:issue.suggested_replacement});}\n for(const [key,changes] of Object.entries(edits)){changes.sort((a,b)=>b.at-a.at);let last=Infinity;for(const e of changes){if(e.end>last)throw Error('preview_repair_overlap');out[key]=out[key].slice(0,e.at)+e.text+out[key].slice(e.end);last=e.at;}}\n if(!out.cover_title||out.cover_title.length>70||out.cover_subtitle.length>110)throw Error('preview_cover_bounds');\n out.caption=NEWS.composeNewsCaption(out.body,out.source_url);out.text=out.caption;out.channel_caption=out.caption;out.caption_length=out.caption.length;\n if(bounds){exact.body=out.body;exact.caption=out.caption;out.exact_revision_json=JSON.stringify(exact);}\n return out;\n}\nconst requested=NEWS.articleURL($json.url);return {json:{...$json,requested_url:requested,url:requested.startsWith('https://rbc.ru/')?requested.replace('https://rbc.ru/','https://www.rbc.ru/'):requested,redirects:0,read_id:$json.read_id||('r'+$execution.id),transport:'http'}};\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        220,
        0
      ]
    },
    {
      "id": "ca93fb1c-1c0d-4d9b-a29c-5636a6479b9c",
      "name": "HTTP статья 0",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "text",
              "outputPropertyName": "body"
            }
          }
        }
      },
      "position": [
        440,
        0
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": false
    },
    {
      "id": "da1cdcd4-bc16-4ca7-837f-050ace8a1412",
      "name": "Проверить HTTP 0",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\n// Small, deliberately restrictive URL adapter for the n8n sandbox (global URL is absent).\n// Only the official ASCII hosts used by this workflow are representable.\nclass StrictURL {\n constructor(value){\n  if(typeof value!=='string'||value.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value))throw Error('url_invalid');\n  const m=/^https:\\/\\/((?:www\\.)?(?:nalog\\.gov\\.ru|cbr\\.ru|vsrf\\.ru|pravo\\.ru|probankrotstvo\\.ru|kommersant\\.ru|rbc\\.ru))(\\/[^?#]*)?(\\?[^#]*)?(?:#[^\\s]*)?$/i.exec(value);\n  if(!m)throw Error('url_forbidden');\n  this.protocol='https:';this.hostname=m[1].toLowerCase();this.pathname=m[2]||'/';this.username='';this.password='';this.port='';this.hash='';\n  if(/%|\\/{2}|(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(this.pathname))throw Error('url_path_encoding_forbidden');\n  this.params=[];\n  for(const entry of (m[3]||'').slice(1).split('&').filter(Boolean)){\n   const at=entry.indexOf('=');const key=decodeURIComponent((at<0?entry:entry.slice(0,at)).replace(/\\+/g,' '));const val=decodeURIComponent((at<0?'':entry.slice(at+1)).replace(/\\+/g,' '));\n   if(/[\\u0000-\\u001f\\u007f]/.test(key+val)||this.params.some(x=>x[0]===key))throw Error('url_query_invalid');this.params.push([key,val]);\n  }\n  this.searchParams={keys:()=>this.params.map(x=>x[0]),get:key=>this.params.find(x=>x[0]===key)?.[1]??null,delete:key=>{this.params=this.params.filter(x=>x[0]!==key)},sort:()=>this.params.sort((a,b)=>a[0].localeCompare(b[0]))};\n }\n toString(){return 'https://'+this.hostname+this.pathname+(this.params.length?'?'+this.params.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'):'');}\n}\nfunction resolveOfficialLocation(location,base){\n if(typeof location!=='string'||!location||location.startsWith('//'))throw Error('redirect_forbidden');\n const current=new StrictURL(base);let target;\n if(location.startsWith('https://'))target=location;\n else if(location.startsWith('/'))target='https://'+current.hostname+location;\n else if(location.startsWith('?'))target='https://'+current.hostname+current.pathname+location;\n else throw Error('redirect_relative_path_unsupported');\n const next=new StrictURL(target);if(next.hostname.replace(/^www\\./,'')!==current.hostname.replace(/^www\\./,''))throw Error('cross_source_redirect');return next.toString();\n}\n\nconst QUALITY=(()=>{const URL=StrictURL;\n'use strict';\n// Standalone helpers. These do not fetch pages, classify their meaning, mutate state or publish.\n// Merge into actual deployed nodes. Do not replace the server's modified v0.4 wholesale.\nconst POLICY={\"version\": \"0.4.1\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"max_per_publisher_first_pass\": 2, \"require_exact_quotes_to_offer_topic\": false, \"verify_selected_topic_before_image\": true, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\"}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\"}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\"}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\"}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\"}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\"}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\"}], \"categories\": {\"bankruptcy\": \"Банкротство граждан\", \"credit\": \"Кредиты и МФО\", \"collection\": \"Приставы и взыскание\", \"property\": \"Жильё и имущество\", \"family\": \"Семья, наследство и долги\", \"fraud\": \"Финансовое мошенничество\", \"consumer\": \"Права потребителей финансовых услуг\"}};\nfunction error(code){throw Object.assign(new Error(code),{code});}\nfunction safeText(value,max=20000){\n  if(typeof value!=='string'||!value.trim()||value.length>max)error('text_invalid');\n  if(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u.test(value))error('text_controls');\n  return value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n}\nfunction normalizeExcerpt(value){\n  return safeText(value,60000).replace(/[\\u00ad\\u200b\\ufeff]/gu,'')\n    .replace(/[«»“”„‟]/gu,'\"').replace(/[‘’‚‛]/gu,\"'\")\n    .replace(/[\\u2010-\\u2015\\u2212]/gu,'-').replace(/\\s+/gu,' ').trim();\n}\nfunction checkExcerpt(quote,article){\n  try{\n    const q=normalizeExcerpt(quote),body=normalizeExcerpt(article);\n    // A short factual phrase may be legitimate. No arbitrary 15-character cutoff.\n    if((q.match(/[\\p{L}\\p{N}]+/gu)||[]).length<2)return {matched:false,reason:'fragment_too_short'};\n    const at=body.indexOf(q);\n    return at<0 ? {matched:false,reason:'text_differs'} : {matched:true,reason:'normalized_match',normalized_excerpt:q};\n  }catch(e){return {matched:false,reason:e.code||'evidence_invalid'};}\n}\nfunction partitionEvidence(facts,article){\n  if(!Array.isArray(facts))error('facts_invalid');\n  const matched=[],needs_review=[];\n  facts.forEach((fact,index)=>{\n    const result=checkExcerpt(fact?.evidence,article);\n    const row={index,fact,result};\n    (result.matched?matched:needs_review).push(row);\n  });\n  // Matching is provenance, NOT semantic proof. Caller must review the selected news.\n  return {matched,needs_review,topic_must_not_be_dropped_for_this_alone:true,semantic_review_required:true};\n}\nfunction sourceBlocks(article,documentKey){\n  if(typeof documentKey!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(documentKey))error('document_key_invalid');\n  const paragraphs=safeText(article,60000).split(/\\n\\s*\\n/u).filter(p=>p.trim());\n  const chunks=[];\n  for(let paragraph of paragraphs){\n    while(paragraph.length>1800){\n      let cut=paragraph.lastIndexOf(' ',1800);\n      if(cut<900)cut=1800;\n      chunks.push(paragraph.slice(0,cut));paragraph=paragraph.slice(cut).trimStart();\n    }\n    if(paragraph.trim())chunks.push(paragraph.trim());\n  }\n  return {document_key:documentKey,blocks:chunks.map((text,i)=>({id:'p'+String(i+1).padStart(3,'0'),text}))};\n}\nfunction resolveEvidenceRefs(fact,document){\n  if(!fact||fact.document_key!==document.document_key)error('wrong_source_document');\n  if(!Array.isArray(fact.source_ids)||!fact.source_ids.length||fact.source_ids.length>3||new Set(fact.source_ids).size!==fact.source_ids.length)error('source_ids_invalid');\n  const map=new Map(document.blocks.map(b=>[b.id,b.text]));\n  return fact.source_ids.map(id=>{\n    if(!map.has(id))error('source_block_not_found');\n    return {id,text:map.get(id)};\n  });\n}\nfunction sourceURL(raw){\n  if(typeof raw!=='string'||raw.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(raw))error('url_invalid');\n  let u;try{u=new URL(raw);}catch{error('url_invalid');}\n  if(u.protocol!=='https:'||u.username||u.password||u.port)error('url_forbidden');\n  const source=POLICY.sources.find(s=>u.hostname===s.host||u.hostname==='www.'+s.host);\n  if(!source)error('source_not_allowed');\n  u.hostname=source.host;u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  if(source.host==='pravo.ru')u.pathname=u.pathname.replace(/(\\/(?:news|story)\\/\\d+)\\/amp\\/?$/,'$1/');const p=u.pathname;\n  const article = source.host==='nalog.gov.ru' ? /^\\/rn\\d+\\/(?:news\\/[^/]+|ifns\\/imns\\d+_\\d+\\/info)\\/\\d+\\/?$/.test(p)\n    : source.host==='cbr.ru' ? /^\\/press\\/(?:event|regevent)\\/\\/?$/i.test(p)&&/^\\d+$/.test(u.searchParams.get('id')||'') || /^\\/press\\/pr\\/\\/?$/i.test(p)&&!!u.searchParams.get('file')\n    : source.host==='vsrf.ru' ? /^(?:\\/news|\\/press_center\\/news)\\/\\d+\\/?$/.test(p)\n    : source.host==='pravo.ru' ? /^\\/(?:news|story)\\/\\d+\\/?$/.test(p)\n    : source.host==='probankrotstvo.ru' ? /^\\/(?:news|article|articles)\\/[a-z0-9-]+-\\d+\\/?$/.test(p)\n    : source.host==='kommersant.ru' ? /^\\/doc\\/\\d+\\/?$/.test(p)\n    : source.host==='rbc.ru' ? /^\\/[a-z0-9_-]+\\/\\d{2}\\/\\d{2}\\/\\d{4}\\/[a-f0-9]{24}\\/?$/i.test(p)\n    : false;\n  if(!article)error('article_path_needs_adapter');\n  if(source.host==='vsrf.ru')u.pathname='/news/'+p.match(/\\d+/)[0];\n  else if(source.host==='nalog.gov.ru'||source.host==='pravo.ru')u.pathname=p.replace(/\\/?$/,'/');\n  else if(source.host==='cbr.ru')u.pathname=p.toLowerCase().replace(/\\/?$/,'/');\n  else u.pathname=p.replace(/\\/$/,'');\n  const keep=source.host==='cbr.ru'?(/event/i.test(p)?'id':'file'):null;\n  for(const key of [...u.searchParams.keys()])if(key!==keep)u.searchParams.delete(key);\n  return {url:u.toString(),publisher:source.host,source_name:source.name,source_kind:source.kind};\n}\nfunction dateValue(value){\n  if(typeof value!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))error('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if(!Number.isFinite(d.getTime())||d.toISOString().slice(0,10)!==value)error('invalid_date');\n  return value;\n}\n// read is trusted HTTP/extractor output; judgment is untrusted classifier output.\n// Actual reading, redirects, trace provenance and publication-date extraction stay in n8n.\nfunction offerTopic(read,judgment,window){\n  if(!read||!judgment||judgment.read_id!==read.read_id)error('read_id_mismatch');\n  const src=sourceURL(read.url);\n  if(read.transport!=='http'||read.status_code!==200||read.date_kind!=='published'||read.discovery_bound!==true)error('source_read_required');\n  safeText(read.article_text,60000);\n  const day=dateValue(read.published_date),from=dateValue(window.from),to=dateValue(window.to);\n  if(from>to)error('date_window_invalid');\n  if(day<from||day>to)return {offer:false,reason:'outside_window'};\n  if(judgment.relevance==='unrelated')return {offer:false,reason:'unrelated'};\n  if(!['direct','adjacent','context'].includes(judgment.relevance))error('relevance_invalid');\n  if(!Object.hasOwn(POLICY.categories,judgment.category))error('category_invalid');\n  if(!['news','analysis','opinion','explanation','statistics','advertisement'].includes(judgment.content_type))error('content_type_invalid');\n  if(judgment.content_type==='advertisement')return {offer:false,reason:'advertisement'};\n  return {offer:true,read_id:read.read_id,...src,published_date:day,\n    title:safeText(judgment.title,160),angle:safeText(judgment.angle,350),\n    relevance:judgment.relevance,category:judgment.category,category_label:POLICY.categories[judgment.category],\n    content_type:judgment.content_type,verification_state:'on_selection',\n    score:judgment.relevance==='direct'?30:judgment.relevance==='adjacent'?20:10};\n}\nfunction rankTopics(topics,{excludedURLs=[],limit=POLICY.max_topics}={}){\n  if(!Number.isInteger(limit)||limit<1||limit>POLICY.max_topics)error('limit_invalid');\n  const seen=new Set(excludedURLs.map(url=>sourceURL(url).url)),unique=[];\n  for(const topic of [...topics].filter(t=>t.offer===true).sort((a,b)=>b.score-a.score||b.published_date.localeCompare(a.published_date))){\n    const url=sourceURL(topic.url).url;\n    if(seen.has(url))continue;seen.add(url);unique.push(topic);\n  }\n  const selected=[],deferred=[],counts=new Map();\n  for(const t of unique){\n    if((counts.get(t.publisher)||0)>=POLICY.max_per_publisher_first_pass){deferred.push(t);continue;}\n    selected.push(t);counts.set(t.publisher,(counts.get(t.publisher)||0)+1);\n  }\n  // Diversity is a preference. Do not discard useful topics when only one source has them.\n  return [...selected,...deferred].slice(0,limit);\n}\nfunction topicText(topic){\n  return safeText(`${topic.title}\\n\\nИдея поста: ${topic.angle}\\n\\n${topic.category_label} · ${topic.published_date}\\n${topic.source_name}\\n${topic.url}\\n\\nПосле выбора проверю факты и подготовлю черновик.`,4096);\n}\nreturn {POLICY,normalizeExcerpt,checkExcerpt,partitionEvidence,sourceBlocks,resolveEvidenceRefs,\n  sourceURL,offerTopic,rankTopics,topicText};\n\n})();\nconst NEWS=(()=>{const URL=StrictURL;\n'use strict';\n// Pure helpers. No network, database, paid calls or Telegram mutations.\n// Port to n8n Code nodes against the actual runtime; require is not assumed there.\nconst CONFIG={\"version\": \"0.4.1\", \"timezone\": \"Europe/Moscow\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"selection_ttl_hours\": 24, \"caption_limit_utf16\": 1024, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\", \"enabled\": true}]};\nconst STAGES = Object.freeze({\n  proposal:'Инициатива', bill:'Законопроект', adopted:'Принято, вступление в силу не подтверждено',\n  in_force:'Действует', court:'Судебная практика', explanation:'Разъяснение', statistics:'Статистика'\n});\nconst CONTROLS = /[\\u0000-\\u0008\\u000b-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u;\nfunction fail(code) { throw Object.assign(new Error(code), {code}); }\nfunction clean(value, max, field='text') {\n  if (typeof value !== 'string') fail(field+'_type');\n  const s=value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n  if (!s || s.length>max || CONTROLS.test(s)) fail(field+'_invalid');\n  return s;\n}\nfunction instant(value) {\n  const t=value instanceof Date ? value.getTime() : Date.parse(value);\n  if (!Number.isFinite(t)) fail('invalid_clock');\n  return t;\n}\nfunction isoDay(value) {\n  if (typeof value!=='string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) fail('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if (!Number.isFinite(d.getTime()) || d.toISOString().slice(0,10)!==value) fail('invalid_date');\n  return value;\n}\nfunction parseDateEvidence(value) {\n  const s=clean(value,150,'publication_date_raw').toLowerCase();\n  let m=s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:$|t\\d{2}:)/);\n  if (m) return isoDay(`${m[1]}-${m[2]}-${m[3]}`);\n  m=s.match(/^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:$|\\s)/);\n  if (m) return isoDay(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  const months=['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];\n  m=s.match(/^(\\d{1,2})\\s+([а-я]+)\\s+(\\d{4})(?:$|\\s)/);\n  if (m && months.includes(m[2])) return isoDay(`${m[3]}-${String(months.indexOf(m[2])+1).padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  fail('publication_date_unreadable');\n}\nfunction windowFor(days=7, now=new Date()) {\n  if (!CONFIG.allowed_days.includes(days)) fail('days_not_supported');\n  const parts=new Intl.DateTimeFormat('en-CA',{timeZone:CONFIG.timezone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(instant(now)));\n  const get=t=>parts.find(p=>p.type===t).value;\n  const to=`${get('year')}-${get('month')}-${get('day')}`;\n  const from=new Date(Date.parse(to+'T00:00:00Z')-(days-1)*86400000).toISOString().slice(0,10);\n  return {from,to,days,timezone:CONFIG.timezone};\n}\nfunction parseCommand(text, botUsername='bflcontentbot') {\n  if (typeof text!=='string') return null;\n  const match=text.trim().match(/^\\/news(?:@([a-z0-9_]+))?(?:\\s+([\\s\\S]*))?$/i);\n  if (!match) return null;\n  if (match[1] && match[1].toLowerCase()!==botUsername.toLowerCase()) return null;\n  let tail=(match[2]||'').trim(),days=CONFIG.default_days;\n  const d=tail.match(/^(\\d+)(?:\\s+|$)/);\n  if(d){ days=Number(d[1]); if(!CONFIG.allowed_days.includes(days))fail('days_not_supported'); tail=tail.slice(d[0].length).trim(); }\n  return {days,topic:tail ? clean(tail,200,'topic') : ''};\n}\nfunction canonicalURL(value) {\n  if (typeof value!=='string' || value.length>1500 || /[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value)) fail('url_invalid');\n  let u; try{ u=new URL(value); }catch{ fail('url_invalid'); }\n  if(u.protocol!=='https:' || u.username || u.password || u.port) fail('url_forbidden');\n  const source=CONFIG.sources.find(s=>s.enabled && (u.hostname===s.host || u.hostname==='www.'+s.host));\n  // Exact known hosts; do not follow arbitrary subdomains just because web_search allows them.\n  if(!source)fail('source_not_allowed');\n  if(u.hostname==='www.'+source.host)u.hostname=source.host;\n  u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  return u.toString();\n}\nfunction articleURL(value){return QUALITY.sourceURL(value).url;}\nfunction sourceName(url) { const host=new URL(canonicalURL(url)).hostname; return CONFIG.sources.find(s=>s.host===host).name; }\nfunction normalizedEvidence(text) { return text.normalize('NFC').replace(/\\s+/gu,' ').trim(); }\nfunction quoteIn(quote,body) {\n  const q=clean(quote,800,'evidence');\n  if(q.length<15 || !normalizedEvidence(body).includes(normalizedEvidence(q)))fail('evidence_not_found');\n  return q;\n}\n// read MUST come from an actual HTTP reader/extractor. Never pass a model-produced read object.\nfunction validateRead(read, searchURLs, win, now=new Date()) {\n  if(!read || read.transport!=='http' || read.status_code!==200 || !/^text\\/html(?:;|$)/i.test(read.content_type||''))fail('page_not_read');\n  const requested=articleURL(read.requested_url), url=articleURL(read.final_url);\n  if(!searchURLs.has(requested))fail('not_in_search_sources');\n  if(new URL(requested).hostname!==new URL(url).hostname)fail('cross_source_redirect');\n  const age=instant(now)-instant(read.fetched_at);\n  if(age<0 || age>3600000)fail('read_stale');\n  if(read.date_kind!=='published')fail('publication_date_required');\n  const raw=clean(read.publication_date_raw,150,'publication_date_raw');\n  const context=clean(read.publication_date_context,1200,'publication_date_context');\n  if(!normalizedEvidence(context).includes(normalizedEvidence(raw)))fail('date_context_mismatch');\n  if(/dateModified|дата обновления|последнее обновление/i.test(context))fail('updated_is_not_published');\n  const published=parseDateEvidence(raw);\n  if(published<isoDay(win.from) || published>isoDay(win.to))fail('outside_date_window');\n  return {url,requested_url:requested,published_date:published,article_text:clean(read.article_text,60000,'article_text'),\n    source_name:sourceName(url),fetched_at:read.fetched_at,publication_date_context:context};\n}\nfunction validateCandidate(candidate, read, searchURLs, win, now=new Date()) {\n  const page=validateRead(read,searchURLs,win,now);\n  if(!candidate || candidate.read_id!==read.read_id)fail('read_id_mismatch');\n  if(candidate.relevance!=='direct_personal_finance')fail('not_relevant_to_people');\n  if(!Object.hasOwn(STAGES,candidate.stage))fail('stage_unknown');\n  if(candidate.published_date!==page.published_date)fail('date_mismatch');\n  if(!Array.isArray(candidate.facts) || candidate.facts.length<1 || candidate.facts.length>4)fail('facts_invalid');\n  const facts=candidate.facts.map(f=>({text:clean(f.text,400,'fact'),evidence:quoteIn(f.evidence,page.article_text)}));\n  const stage_evidence=quoteIn(candidate.stage_evidence,page.article_text);\n  let effective_date=null;\n  if(candidate.effective_date!=null){\n    effective_date=isoDay(candidate.effective_date);\n    const evidence=quoteIn(candidate.effective_evidence,page.article_text);\n    const raw=clean(candidate.effective_date_raw,150,'effective_date_raw');\n    if(!normalizedEvidence(evidence).includes(normalizedEvidence(raw)) || parseDateEvidence(raw)!==effective_date)fail('effective_date_mismatch');\n    if(candidate.stage==='in_force' && effective_date>win.to)fail('not_in_force_yet');\n  }\n  // A verified date plus supporting words are mandatory before advertising that a new rule is in force.\n  if(candidate.stage==='in_force' && !effective_date)fail('effective_date_required');\n  return {url:page.url,source_name:page.source_name,published_date:page.published_date,\n    title:clean(candidate.title,150,'title'),summary:clean(candidate.summary,400,'summary'),\n    why:clean(candidate.why,220,'why'),stage:candidate.stage,stage_label:STAGES[candidate.stage],\n    effective_date,stage_evidence,facts,verified_at:page.fetched_at,\n    // Additional event clustering requires verified document IDs in the workflow; do not invent them.\n    dedup_key:page.url,format:'news_fact_pack_v1'};\n}\nfunction chooseTopics(items, excludedURLs=[], limit=CONFIG.max_topics) {\n  if(!Number.isInteger(limit)||limit<1||limit>CONFIG.max_topics)fail('limit_invalid');\n  const seen=new Set(excludedURLs.map(articleURL));\n  const result=[];\n  for(const item of [...items].sort((a,b)=>b.published_date.localeCompare(a.published_date))){\n    const key=articleURL(item.url);\n    if(seen.has(key))continue;\n    seen.add(key);result.push(item);if(result.length===limit)break;\n  }\n  return result;\n}\nfunction callbackData(batchId,itemId){\n  if(!/^[A-Za-z0-9_-]{1,16}$/.test(batchId)||!/^[A-Za-z0-9_-]{1,12}$/.test(itemId))fail('id_invalid');\n  return `news:${batchId}:${itemId}`;\n}\n// Advisory guard only. Perform and persist the state claim atomically AFTER this guard.\nfunction guardSelection({callback,batch,item,now=new Date()}) {\n  if(!callback||!batch||!item)fail('selection_missing');\n  if(String(callback.from_id)!==String(batch.owner_id)||String(callback.chat_id)!==String(batch.owner_id))fail('not_owner');\n  if(String(callback.message_id)!==String(item.message_id))fail('wrong_message');\n  if(callback.data!==callbackData(batch.id,item.id)||item.batch_id!==batch.id)fail('wrong_item');\n  if(instant(now)>=instant(batch.expires_at))fail('selection_expired');\n  if(batch.status!=='ready'||item.status!=='offered')fail('selection_not_available');\n  return {batch_id:batch.id,item_id:item.id,selection_key:`news:${batch.id}:${item.id}`};\n}\nfunction captionBudget(sourceURL) {\n  const suffix='\\n\\nИсточник: '+articleURL(sourceURL);\n  const body_limit=CONFIG.caption_limit_utf16-suffix.length;\n  if(body_limit<250)fail('source_url_too_long_for_caption');\n  return {suffix,body_limit,preferred_body_max:Math.min(800,body_limit)};\n}\nfunction composeNewsCaption(body,sourceURL) {\n  const {suffix,body_limit}=captionBudget(sourceURL);\n  const text=clean(body,body_limit,'caption_body');\n  // Model writes body only. Link is appended deterministically and retained through revision.\n  if(/https?:\\/\\//i.test(text))fail('unexpected_link_in_body');\n  return text+suffix;\n}\nfunction topicMessage(item){\n  const text=`${item.title}\\n\\n${item.summary}\\n\\nПочему это важно: ${item.why}\\n\\n${item.stage_label} · ${item.published_date}\\n${item.source_name}\\n${item.url}`;\n  return clean(text,4096,'topic_message');\n}\nfunction progressText(event,{count=0,days=7,partial=false}={}){\n  switch(event){\n    case 'accepted': return `Взято в работу. Ищу новости за последние ${days} дней.`;\n    case 'verifying': return 'Проверяю даты, источники и связь новостей с личными долгами.';\n    case 'ready': return `Готово. Тем на выбор: ${count}. Нажми «Подготовить пост» под подходящей новостью.`+(partial?' Часть источников проверить не удалось.':'');\n    case 'empty': return `За последние ${days} дней подходящих новостей с проверенным источником не нашлось.`+(days===7?' Можно расширить поиск командой /news 30.':'');\n    case 'partial_empty': return 'Полностью проверить источники не удалось; подтверждённых тем пока нет. Можно повторить поиск позже.';\n    case 'failed': return 'Поиск сейчас не завершился. Можно повторить /news позже. Существующие черновики сохранены.';\n    case 'selected': return 'Тема выбрана. Готовлю пост по источнику — затем пришлю на согласование.';\n    case 'expired': return 'Эта подборка устарела. Отправь /news, чтобы получить свежие темы.';\n    default: fail('progress_event_unknown');\n  }\n}\n\nreturn {CONFIG,STAGES,parseCommand,windowFor,parseDateEvidence,canonicalURL,articleURL,\n  validateRead,validateCandidate,chooseTopics,callbackData,guardSelection,captionBudget,\n  composeNewsCaption,topicMessage,progressText};})();\nconst RESP=(()=>{'use strict';\nconst core=NEWS;\nfunction modelName(model){\n  if(typeof model!=='string'||!model.trim()||model.includes('/')||/\\s/.test(model))throw Error('Configure a real OpenAI REST model ID with web_search support');\n  return model;\n}\nfunction searchRequest({model,instructions,days=7,topic='',now=new Date()}){\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Search instructions required');\n  const win=core.windowFor(days,now);\n  return {model:modelName(model),store:false,\n    instructions,\n    input:JSON.stringify({task:'Find recent official and editorial articles about personal finance, debts and rights in Russia',window:win,topic,max_candidates:core.CONFIG.max_candidates}),\n    tools:[{type:'web_search',external_web_access:true,filters:{allowed_domains:core.CONFIG.sources.filter(s=>s.enabled).map(s=>s.host)}}],\n    tool_choice:'required',include:['web_search_call.action.sources'],max_output_tokens:8000};\n}\nfunction outputJSON(response){\n  if(!response||response.status!=='completed'||response.error)throw Error('responses_not_completed');\n  const chunks=[];\n  for(const output of response.output||[]){\n    if(output.type!=='message')continue;\n    if(output.status && output.status!=='completed')throw Error('message_not_completed');\n    for(const part of output.content||[]){\n      if(part.type==='refusal')throw Error('model_refusal');\n      if(part.type==='output_text')chunks.push(part.text);\n    }\n  }\n  const text=chunks.join('\\n').trim().replace(/^```json\\s*/i,'').replace(/\\s*```$/,'');\n  let value;try{value=JSON.parse(text);}catch{throw Error('invalid_output_json');}\n  if(!value||Array.isArray(value)||typeof value!=='object')throw Error('invalid_output_object');\n  return value;\n}\nfunction parseDiscovery(response){\n  const value=outputJSON(response);\n  if(!(response.output||[]).some(o=>o.type==='web_search_call'&&o.status==='completed'&&o.action?.type==='search'))throw Error('web_search_not_executed');\n  const searchURLs=new Set();\n  function remember(url){try{searchURLs.add(core.articleURL(url));}catch{/* Skip unsupported sources. */}}\n  for(const output of response.output||[]){\n    if(output.type==='web_search_call'&&output.status==='completed'){\n      for(const s of output.action?.sources||[])remember(s.url);\n      if(output.action?.type==='open_page')remember(output.action.url);\n    }\n    if(output.type==='message')for(const p of output.content||[])for(const a of p.annotations||[])if(a.type==='url_citation')remember(a.url);\n  }\n  if(!Array.isArray(value.candidates)||value.candidates.length>core.CONFIG.max_candidates)throw Error('invalid_candidates');\n  const candidates=[],rejected=[];const seen=new Set();\n  for(const candidate of value.candidates){\n    try{\n      const url=core.articleURL(candidate.url);\n      if(!searchURLs.has(url))throw Error('candidate_not_in_search_sources');\n      if(seen.has(url))continue;\n      seen.add(url);candidates.push({url}); // Discard model guesses about dates/facts until the page is read.\n    }catch(error){rejected.push({code:error.code||error.message});}\n  }\n  return {candidates,search_urls:[...searchURLs],rejected,response_id:response.id||null};\n}\nfunction verificationRequest({model,instructions,reads,searchURLs,win,now=new Date()}){\n  if(!Array.isArray(reads)||!reads.length||reads.length>5)throw Error('verification_batch_size');\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Verification instructions required');\n  const ids=new Set();\n  const pages=reads.map(read=>{\n    if(typeof read.read_id!=='string'||!read.read_id||ids.has(read.read_id))throw Error('read_id_invalid');\n    ids.add(read.read_id);\n    const page=core.validateRead(read,searchURLs,win,now);\n    // Reader must extract the article rather than sending the entire site navigation.\n    if(page.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n    return {read_id:read.read_id,published_date:page.published_date,url:page.url,article_text:page.article_text};\n  });\n  return {model:modelName(model),store:false,instructions,input:JSON.stringify({window:win,pages}),max_output_tokens:8000};\n}\nfunction parseVerification(response,reads,searchURLs,win,now=new Date()){\n  const value=outputJSON(response);\n  if(!Array.isArray(value.results)||value.results.length!==reads.length)throw Error('verification_results_incomplete');\n  const remaining=new Map(reads.map(r=>[r.read_id,r]));\n  const accepted=[],rejected=[];\n  for(const result of value.results){\n    const read=remaining.get(result.read_id);\n    if(!read)throw Error('unknown_or_duplicate_read_id');\n    remaining.delete(result.read_id);\n    if(result.accept===false){rejected.push({read_id:read.read_id,code:'editorial_exclusion',reason:String(result.reason||'').slice(0,400)});continue;}\n    if(result.accept!==true)throw Error('verification_decision_missing');\n    try{accepted.push(core.validateCandidate(result,read,searchURLs,win,now));}\n    catch(error){rejected.push({read_id:read.read_id,code:error.code||error.message});}\n  }\n  return {accepted,rejected};\n}\n\nreturn {searchRequest,outputJSON,parseDiscovery,verificationRequest,parseVerification};})();\nfunction snapshotOf(topic){\n if(topic.format!=='news_topic_v2'&&topic.format!=='news_fact_pack_v2')throw Error('news_format_invalid');\n const snap=topic.snapshot;\n if(!snap||snap.url!==NEWS.articleURL(topic.url)||snap.published_date!==topic.published_date)throw Error('snapshot_source_mismatch');\n const blocks=QUALITY.sourceBlocks(snap.article_text,snap.document_key);\n if(JSON.stringify(blocks.blocks)!==JSON.stringify(snap.blocks))throw Error('snapshot_blocks_mismatch');\n return snap;\n}\nfunction strictRequest(instructions,input,schema,name,max=4500){return {model:'gpt-5.6-sol',reasoning:{effort:'low'},store:false,max_output_tokens:max,instructions,input:JSON.stringify(input),text:{format:{type:'json_schema',name,strict:true,schema}}};}\nfunction apiResult(r,prefix){\n if(r.statusCode!==200)throw Error(prefix+(r.statusCode?'_http_'+r.statusCode+(r.statusCode>=500?'_outcome_unknown':''):'_outcome_unknown'));\n if(r.body?.status==='incomplete')throw Error(prefix+'_incomplete');\n if(r.body?.output?.some(x=>x.content?.some(c=>c.type==='refusal')))throw Error(prefix+'_refusal');\n try{return RESP.outputJSON(r.body);}catch{throw Error(prefix+'_malformed_json');}\n}\nfunction verifiedFacts(topic,result){\n const snap=snapshotOf(topic);\n if(result.document_key!==snap.document_key)throw Error('wrong_source_document');\n if(result.status!=='ready')throw Error('selected_needs_source_check');\n if(!['news','analysis','opinion','explanation','statistics','court','bill','adopted','in_force'].includes(result.content_type))throw Error('selected_content_type_invalid');\n // A news report is not itself an enacted legal document. Narrow attributed reporting remains available.\n if(result.content_type==='in_force')throw Error('selected_primary_document_required');\n if(!Array.isArray(result.facts)||!result.facts.length||result.facts.length>4)throw Error('selected_facts_invalid');\n const facts=result.facts.map(f=>{if(typeof f.text!=='string'||!f.text.trim()||f.text.length>700||!Array.isArray(f.qualifiers)||typeof f.attribution!=='string')throw Error('selected_fact_invalid');return {...f,source_blocks:QUALITY.resolveEvidenceRefs(f,snap)};});\n return {...topic,format:'news_fact_pack_v2',verification_state:'facts_selected',facts,content_type:result.content_type,limitations:result.limitations,selected_at:new Date().toISOString()};\n}\nfunction previewInput(s){\n const fact=JSON.parse(s.fact_pack);\n if(NEWS.articleURL(fact.url)!==s.source_url||NEWS.composeNewsCaption(s.body,s.source_url)!==s.caption)throw Error('news_caption_source_mismatch');\n let source;\n if(fact.format==='news_fact_pack_v2'){source=snapshotOf(fact);for(const f of fact.facts)QUALITY.resolveEvidenceRefs(f,source);}\n else if(fact.format==='news_fact_pack_v1')source=QUALITY.sourceBlocks([fact.stage_evidence,...fact.facts.map(f=>f.evidence)].filter(Boolean).join('\\n\\n'),'legacy_saved_evidence');\n else throw Error('news_facts_not_selected');\n return {caption_body:s.body,cover_title:s.cover_title,cover_subtitle:s.cover_subtitle,facts:fact.facts,content_type:fact.content_type||fact.stage,limitations:fact.limitations||[],source,source_url:s.source_url,legacy_evidence_only:fact.format==='news_fact_pack_v1'};\n}\nfunction assessPreview(result){\n if(typeof result.pass!=='boolean'||!Array.isArray(result.issues)||result.issues.length>10)throw Error('preview_malformed_json');\n if(result.pass&&result.issues.length)throw Error('preview_contradictory_result');\n if(!result.pass&&!result.issues.length)throw Error('preview_empty_failure');\n for(const i of result.issues)if(!['caption_body','cover_title','cover_subtitle'].includes(i.field)||typeof i.fragment!=='string'||!i.fragment||typeof i.suggested_replacement!=='string'||typeof i.problem!=='string')throw Error('preview_issue_invalid');\n return result;\n}\nfunction repairPreview(s,issues){\n const out={...s};let exact=null;try{exact=JSON.parse(s.exact_revision_json||'null');}catch{}\n // A precise owner edit may only be corrected inside its replacement span. The saved cover stays byte-identical.\n const bounds=exact&&!exact.validation_error?{start:s.body.indexOf(exact.newText),length:exact.newText?.length||0}:null;\n if(bounds&&(bounds.start<0||s.body.indexOf(exact.newText,bounds.start+1)>=0))throw Error('preview_exact_span_ambiguous');\n const edits={};\n for(const issue of issues){const key=issue.field==='caption_body'?'body':issue.field,old=out[key];if(typeof old!=='string')throw Error('preview_field_missing');const at=old.indexOf(issue.fragment);if(at<0||old.indexOf(issue.fragment,at+1)>=0)throw Error('preview_repair_ambiguous');\n if(bounds&&(key!=='body'||at<bounds.start||at+issue.fragment.length>bounds.start+bounds.length))throw Error('preview_exact_requires_owner_edit');\n (edits[key]||=[]).push({at,end:at+issue.fragment.length,text:issue.suggested_replacement});}\n for(const [key,changes] of Object.entries(edits)){changes.sort((a,b)=>b.at-a.at);let last=Infinity;for(const e of changes){if(e.end>last)throw Error('preview_repair_overlap');out[key]=out[key].slice(0,e.at)+e.text+out[key].slice(e.end);last=e.at;}}\n if(!out.cover_title||out.cover_title.length>70||out.cover_subtitle.length>110)throw Error('preview_cover_bounds');\n out.caption=NEWS.composeNewsCaption(out.body,out.source_url);out.text=out.caption;out.channel_caption=out.caption;out.caption_length=out.caption.length;\n if(bounds){exact.body=out.body;exact.caption=out.caption;out.exact_revision_json=JSON.stringify(exact);}\n return out;\n}\nconst ctx=$('Начать чтение').first().json,r=$json;let out={...ctx,follow:false,status_code:r.statusCode||0,content_type:r.headers?.['content-type']||'',fetched_at:new Date().toISOString(),final_url:ctx.url};\nif(r.statusCode>=300&&r.statusCode<400){try{if(0>=3)throw Error('redirect_limit');const target=resolveOfficialLocation(r.headers?.location,ctx.url);NEWS.articleURL(target);return {json:{...out,url:target,redirects:0+1,follow:true}};}catch(e){return {json:{...out,read_error:e.message}};}}\nif(r.statusCode!==200)return {json:{...out,read_error:r.statusCode?'http_'+r.statusCode:'transport_error'}};\nif(!/^text\\/html(?:;|$)/i.test(out.content_type))return {json:{...out,read_error:'content_type_unsupported'}};\nconst html=r.body;if(typeof html!=='string'||!html)return {json:{...out,read_error:'empty_response'}};\n// Reject oversized/incomplete documents before HTML parsing; no partial text reaches the model.\nif(html.length>1048576||Number(r.headers?.['content-length']||0)>1048576)return {json:{...out,read_error:'response_too_large'}};\nconst host=new StrictURL(ctx.url).hostname.replace(/^www\\./,'');\nconst selectors={\n 'nalog.gov.ru':{body_selector:'.page-content__center',date_selector:'.page-content__center > p.gray',time_selector:'time[data-unavailable]'},\n 'cbr.ru':{body_selector:'.landing-text',date_selector:'.news-info-line_date',time_selector:'time[data-unavailable]'},\n 'pravo.ru':{body_selector:'section.article-content',date_selector:'[data-bfl-none]',time_selector:'time[itemprop=datePublished][datetime]'},\n 'probankrotstvo.ru':{body_selector:'.post-content',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'kommersant.ru':{body_selector:'.doc__text, .doc__intro',date_selector:'[data-bfl-none]',time_selector:'time.doc_header__publish_time[datetime]'},\n 'rbc.ru':{body_selector:'.article__text',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'vsrf.ru':{body_selector:'div.text-text-primary[class*=\"cms-info__icon\"]',date_selector:'time .sr-only',time_selector:'time[datetime]'}\n};\nreturn {json:{...out,...selectors[host],html}};\n\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        660,
        0
      ]
    },
    {
      "id": "d7734e3f-d72f-402e-8a01-eb5f3bfff1d2",
      "name": "Есть проверенный редирект 0",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.follow === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "position": [
        880,
        0
      ]
    },
    {
      "id": "9d391efa-c0b1-40c6-a63d-b48b348525af",
      "name": "HTTP статья 1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "text",
              "outputPropertyName": "body"
            }
          }
        }
      },
      "position": [
        1100,
        0
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": false
    },
    {
      "id": "7419617c-f031-4f9c-ac8e-6147c59bb608",
      "name": "Проверить HTTP 1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\n// Small, deliberately restrictive URL adapter for the n8n sandbox (global URL is absent).\n// Only the official ASCII hosts used by this workflow are representable.\nclass StrictURL {\n constructor(value){\n  if(typeof value!=='string'||value.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value))throw Error('url_invalid');\n  const m=/^https:\\/\\/((?:www\\.)?(?:nalog\\.gov\\.ru|cbr\\.ru|vsrf\\.ru|pravo\\.ru|probankrotstvo\\.ru|kommersant\\.ru|rbc\\.ru))(\\/[^?#]*)?(\\?[^#]*)?(?:#[^\\s]*)?$/i.exec(value);\n  if(!m)throw Error('url_forbidden');\n  this.protocol='https:';this.hostname=m[1].toLowerCase();this.pathname=m[2]||'/';this.username='';this.password='';this.port='';this.hash='';\n  if(/%|\\/{2}|(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(this.pathname))throw Error('url_path_encoding_forbidden');\n  this.params=[];\n  for(const entry of (m[3]||'').slice(1).split('&').filter(Boolean)){\n   const at=entry.indexOf('=');const key=decodeURIComponent((at<0?entry:entry.slice(0,at)).replace(/\\+/g,' '));const val=decodeURIComponent((at<0?'':entry.slice(at+1)).replace(/\\+/g,' '));\n   if(/[\\u0000-\\u001f\\u007f]/.test(key+val)||this.params.some(x=>x[0]===key))throw Error('url_query_invalid');this.params.push([key,val]);\n  }\n  this.searchParams={keys:()=>this.params.map(x=>x[0]),get:key=>this.params.find(x=>x[0]===key)?.[1]??null,delete:key=>{this.params=this.params.filter(x=>x[0]!==key)},sort:()=>this.params.sort((a,b)=>a[0].localeCompare(b[0]))};\n }\n toString(){return 'https://'+this.hostname+this.pathname+(this.params.length?'?'+this.params.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'):'');}\n}\nfunction resolveOfficialLocation(location,base){\n if(typeof location!=='string'||!location||location.startsWith('//'))throw Error('redirect_forbidden');\n const current=new StrictURL(base);let target;\n if(location.startsWith('https://'))target=location;\n else if(location.startsWith('/'))target='https://'+current.hostname+location;\n else if(location.startsWith('?'))target='https://'+current.hostname+current.pathname+location;\n else throw Error('redirect_relative_path_unsupported');\n const next=new StrictURL(target);if(next.hostname.replace(/^www\\./,'')!==current.hostname.replace(/^www\\./,''))throw Error('cross_source_redirect');return next.toString();\n}\n\nconst QUALITY=(()=>{const URL=StrictURL;\n'use strict';\n// Standalone helpers. These do not fetch pages, classify their meaning, mutate state or publish.\n// Merge into actual deployed nodes. Do not replace the server's modified v0.4 wholesale.\nconst POLICY={\"version\": \"0.4.1\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"max_per_publisher_first_pass\": 2, \"require_exact_quotes_to_offer_topic\": false, \"verify_selected_topic_before_image\": true, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\"}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\"}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\"}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\"}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\"}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\"}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\"}], \"categories\": {\"bankruptcy\": \"Банкротство граждан\", \"credit\": \"Кредиты и МФО\", \"collection\": \"Приставы и взыскание\", \"property\": \"Жильё и имущество\", \"family\": \"Семья, наследство и долги\", \"fraud\": \"Финансовое мошенничество\", \"consumer\": \"Права потребителей финансовых услуг\"}};\nfunction error(code){throw Object.assign(new Error(code),{code});}\nfunction safeText(value,max=20000){\n  if(typeof value!=='string'||!value.trim()||value.length>max)error('text_invalid');\n  if(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u.test(value))error('text_controls');\n  return value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n}\nfunction normalizeExcerpt(value){\n  return safeText(value,60000).replace(/[\\u00ad\\u200b\\ufeff]/gu,'')\n    .replace(/[«»“”„‟]/gu,'\"').replace(/[‘’‚‛]/gu,\"'\")\n    .replace(/[\\u2010-\\u2015\\u2212]/gu,'-').replace(/\\s+/gu,' ').trim();\n}\nfunction checkExcerpt(quote,article){\n  try{\n    const q=normalizeExcerpt(quote),body=normalizeExcerpt(article);\n    // A short factual phrase may be legitimate. No arbitrary 15-character cutoff.\n    if((q.match(/[\\p{L}\\p{N}]+/gu)||[]).length<2)return {matched:false,reason:'fragment_too_short'};\n    const at=body.indexOf(q);\n    return at<0 ? {matched:false,reason:'text_differs'} : {matched:true,reason:'normalized_match',normalized_excerpt:q};\n  }catch(e){return {matched:false,reason:e.code||'evidence_invalid'};}\n}\nfunction partitionEvidence(facts,article){\n  if(!Array.isArray(facts))error('facts_invalid');\n  const matched=[],needs_review=[];\n  facts.forEach((fact,index)=>{\n    const result=checkExcerpt(fact?.evidence,article);\n    const row={index,fact,result};\n    (result.matched?matched:needs_review).push(row);\n  });\n  // Matching is provenance, NOT semantic proof. Caller must review the selected news.\n  return {matched,needs_review,topic_must_not_be_dropped_for_this_alone:true,semantic_review_required:true};\n}\nfunction sourceBlocks(article,documentKey){\n  if(typeof documentKey!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(documentKey))error('document_key_invalid');\n  const paragraphs=safeText(article,60000).split(/\\n\\s*\\n/u).filter(p=>p.trim());\n  const chunks=[];\n  for(let paragraph of paragraphs){\n    while(paragraph.length>1800){\n      let cut=paragraph.lastIndexOf(' ',1800);\n      if(cut<900)cut=1800;\n      chunks.push(paragraph.slice(0,cut));paragraph=paragraph.slice(cut).trimStart();\n    }\n    if(paragraph.trim())chunks.push(paragraph.trim());\n  }\n  return {document_key:documentKey,blocks:chunks.map((text,i)=>({id:'p'+String(i+1).padStart(3,'0'),text}))};\n}\nfunction resolveEvidenceRefs(fact,document){\n  if(!fact||fact.document_key!==document.document_key)error('wrong_source_document');\n  if(!Array.isArray(fact.source_ids)||!fact.source_ids.length||fact.source_ids.length>3||new Set(fact.source_ids).size!==fact.source_ids.length)error('source_ids_invalid');\n  const map=new Map(document.blocks.map(b=>[b.id,b.text]));\n  return fact.source_ids.map(id=>{\n    if(!map.has(id))error('source_block_not_found');\n    return {id,text:map.get(id)};\n  });\n}\nfunction sourceURL(raw){\n  if(typeof raw!=='string'||raw.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(raw))error('url_invalid');\n  let u;try{u=new URL(raw);}catch{error('url_invalid');}\n  if(u.protocol!=='https:'||u.username||u.password||u.port)error('url_forbidden');\n  const source=POLICY.sources.find(s=>u.hostname===s.host||u.hostname==='www.'+s.host);\n  if(!source)error('source_not_allowed');\n  u.hostname=source.host;u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  if(source.host==='pravo.ru')u.pathname=u.pathname.replace(/(\\/(?:news|story)\\/\\d+)\\/amp\\/?$/,'$1/');const p=u.pathname;\n  const article = source.host==='nalog.gov.ru' ? /^\\/rn\\d+\\/(?:news\\/[^/]+|ifns\\/imns\\d+_\\d+\\/info)\\/\\d+\\/?$/.test(p)\n    : source.host==='cbr.ru' ? /^\\/press\\/(?:event|regevent)\\/\\/?$/i.test(p)&&/^\\d+$/.test(u.searchParams.get('id')||'') || /^\\/press\\/pr\\/\\/?$/i.test(p)&&!!u.searchParams.get('file')\n    : source.host==='vsrf.ru' ? /^(?:\\/news|\\/press_center\\/news)\\/\\d+\\/?$/.test(p)\n    : source.host==='pravo.ru' ? /^\\/(?:news|story)\\/\\d+\\/?$/.test(p)\n    : source.host==='probankrotstvo.ru' ? /^\\/(?:news|article|articles)\\/[a-z0-9-]+-\\d+\\/?$/.test(p)\n    : source.host==='kommersant.ru' ? /^\\/doc\\/\\d+\\/?$/.test(p)\n    : source.host==='rbc.ru' ? /^\\/[a-z0-9_-]+\\/\\d{2}\\/\\d{2}\\/\\d{4}\\/[a-f0-9]{24}\\/?$/i.test(p)\n    : false;\n  if(!article)error('article_path_needs_adapter');\n  if(source.host==='vsrf.ru')u.pathname='/news/'+p.match(/\\d+/)[0];\n  else if(source.host==='nalog.gov.ru'||source.host==='pravo.ru')u.pathname=p.replace(/\\/?$/,'/');\n  else if(source.host==='cbr.ru')u.pathname=p.toLowerCase().replace(/\\/?$/,'/');\n  else u.pathname=p.replace(/\\/$/,'');\n  const keep=source.host==='cbr.ru'?(/event/i.test(p)?'id':'file'):null;\n  for(const key of [...u.searchParams.keys()])if(key!==keep)u.searchParams.delete(key);\n  return {url:u.toString(),publisher:source.host,source_name:source.name,source_kind:source.kind};\n}\nfunction dateValue(value){\n  if(typeof value!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))error('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if(!Number.isFinite(d.getTime())||d.toISOString().slice(0,10)!==value)error('invalid_date');\n  return value;\n}\n// read is trusted HTTP/extractor output; judgment is untrusted classifier output.\n// Actual reading, redirects, trace provenance and publication-date extraction stay in n8n.\nfunction offerTopic(read,judgment,window){\n  if(!read||!judgment||judgment.read_id!==read.read_id)error('read_id_mismatch');\n  const src=sourceURL(read.url);\n  if(read.transport!=='http'||read.status_code!==200||read.date_kind!=='published'||read.discovery_bound!==true)error('source_read_required');\n  safeText(read.article_text,60000);\n  const day=dateValue(read.published_date),from=dateValue(window.from),to=dateValue(window.to);\n  if(from>to)error('date_window_invalid');\n  if(day<from||day>to)return {offer:false,reason:'outside_window'};\n  if(judgment.relevance==='unrelated')return {offer:false,reason:'unrelated'};\n  if(!['direct','adjacent','context'].includes(judgment.relevance))error('relevance_invalid');\n  if(!Object.hasOwn(POLICY.categories,judgment.category))error('category_invalid');\n  if(!['news','analysis','opinion','explanation','statistics','advertisement'].includes(judgment.content_type))error('content_type_invalid');\n  if(judgment.content_type==='advertisement')return {offer:false,reason:'advertisement'};\n  return {offer:true,read_id:read.read_id,...src,published_date:day,\n    title:safeText(judgment.title,160),angle:safeText(judgment.angle,350),\n    relevance:judgment.relevance,category:judgment.category,category_label:POLICY.categories[judgment.category],\n    content_type:judgment.content_type,verification_state:'on_selection',\n    score:judgment.relevance==='direct'?30:judgment.relevance==='adjacent'?20:10};\n}\nfunction rankTopics(topics,{excludedURLs=[],limit=POLICY.max_topics}={}){\n  if(!Number.isInteger(limit)||limit<1||limit>POLICY.max_topics)error('limit_invalid');\n  const seen=new Set(excludedURLs.map(url=>sourceURL(url).url)),unique=[];\n  for(const topic of [...topics].filter(t=>t.offer===true).sort((a,b)=>b.score-a.score||b.published_date.localeCompare(a.published_date))){\n    const url=sourceURL(topic.url).url;\n    if(seen.has(url))continue;seen.add(url);unique.push(topic);\n  }\n  const selected=[],deferred=[],counts=new Map();\n  for(const t of unique){\n    if((counts.get(t.publisher)||0)>=POLICY.max_per_publisher_first_pass){deferred.push(t);continue;}\n    selected.push(t);counts.set(t.publisher,(counts.get(t.publisher)||0)+1);\n  }\n  // Diversity is a preference. Do not discard useful topics when only one source has them.\n  return [...selected,...deferred].slice(0,limit);\n}\nfunction topicText(topic){\n  return safeText(`${topic.title}\\n\\nИдея поста: ${topic.angle}\\n\\n${topic.category_label} · ${topic.published_date}\\n${topic.source_name}\\n${topic.url}\\n\\nПосле выбора проверю факты и подготовлю черновик.`,4096);\n}\nreturn {POLICY,normalizeExcerpt,checkExcerpt,partitionEvidence,sourceBlocks,resolveEvidenceRefs,\n  sourceURL,offerTopic,rankTopics,topicText};\n\n})();\nconst NEWS=(()=>{const URL=StrictURL;\n'use strict';\n// Pure helpers. No network, database, paid calls or Telegram mutations.\n// Port to n8n Code nodes against the actual runtime; require is not assumed there.\nconst CONFIG={\"version\": \"0.4.1\", \"timezone\": \"Europe/Moscow\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"selection_ttl_hours\": 24, \"caption_limit_utf16\": 1024, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\", \"enabled\": true}]};\nconst STAGES = Object.freeze({\n  proposal:'Инициатива', bill:'Законопроект', adopted:'Принято, вступление в силу не подтверждено',\n  in_force:'Действует', court:'Судебная практика', explanation:'Разъяснение', statistics:'Статистика'\n});\nconst CONTROLS = /[\\u0000-\\u0008\\u000b-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u;\nfunction fail(code) { throw Object.assign(new Error(code), {code}); }\nfunction clean(value, max, field='text') {\n  if (typeof value !== 'string') fail(field+'_type');\n  const s=value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n  if (!s || s.length>max || CONTROLS.test(s)) fail(field+'_invalid');\n  return s;\n}\nfunction instant(value) {\n  const t=value instanceof Date ? value.getTime() : Date.parse(value);\n  if (!Number.isFinite(t)) fail('invalid_clock');\n  return t;\n}\nfunction isoDay(value) {\n  if (typeof value!=='string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) fail('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if (!Number.isFinite(d.getTime()) || d.toISOString().slice(0,10)!==value) fail('invalid_date');\n  return value;\n}\nfunction parseDateEvidence(value) {\n  const s=clean(value,150,'publication_date_raw').toLowerCase();\n  let m=s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:$|t\\d{2}:)/);\n  if (m) return isoDay(`${m[1]}-${m[2]}-${m[3]}`);\n  m=s.match(/^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:$|\\s)/);\n  if (m) return isoDay(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  const months=['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];\n  m=s.match(/^(\\d{1,2})\\s+([а-я]+)\\s+(\\d{4})(?:$|\\s)/);\n  if (m && months.includes(m[2])) return isoDay(`${m[3]}-${String(months.indexOf(m[2])+1).padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  fail('publication_date_unreadable');\n}\nfunction windowFor(days=7, now=new Date()) {\n  if (!CONFIG.allowed_days.includes(days)) fail('days_not_supported');\n  const parts=new Intl.DateTimeFormat('en-CA',{timeZone:CONFIG.timezone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(instant(now)));\n  const get=t=>parts.find(p=>p.type===t).value;\n  const to=`${get('year')}-${get('month')}-${get('day')}`;\n  const from=new Date(Date.parse(to+'T00:00:00Z')-(days-1)*86400000).toISOString().slice(0,10);\n  return {from,to,days,timezone:CONFIG.timezone};\n}\nfunction parseCommand(text, botUsername='bflcontentbot') {\n  if (typeof text!=='string') return null;\n  const match=text.trim().match(/^\\/news(?:@([a-z0-9_]+))?(?:\\s+([\\s\\S]*))?$/i);\n  if (!match) return null;\n  if (match[1] && match[1].toLowerCase()!==botUsername.toLowerCase()) return null;\n  let tail=(match[2]||'').trim(),days=CONFIG.default_days;\n  const d=tail.match(/^(\\d+)(?:\\s+|$)/);\n  if(d){ days=Number(d[1]); if(!CONFIG.allowed_days.includes(days))fail('days_not_supported'); tail=tail.slice(d[0].length).trim(); }\n  return {days,topic:tail ? clean(tail,200,'topic') : ''};\n}\nfunction canonicalURL(value) {\n  if (typeof value!=='string' || value.length>1500 || /[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value)) fail('url_invalid');\n  let u; try{ u=new URL(value); }catch{ fail('url_invalid'); }\n  if(u.protocol!=='https:' || u.username || u.password || u.port) fail('url_forbidden');\n  const source=CONFIG.sources.find(s=>s.enabled && (u.hostname===s.host || u.hostname==='www.'+s.host));\n  // Exact known hosts; do not follow arbitrary subdomains just because web_search allows them.\n  if(!source)fail('source_not_allowed');\n  if(u.hostname==='www.'+source.host)u.hostname=source.host;\n  u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  return u.toString();\n}\nfunction articleURL(value){return QUALITY.sourceURL(value).url;}\nfunction sourceName(url) { const host=new URL(canonicalURL(url)).hostname; return CONFIG.sources.find(s=>s.host===host).name; }\nfunction normalizedEvidence(text) { return text.normalize('NFC').replace(/\\s+/gu,' ').trim(); }\nfunction quoteIn(quote,body) {\n  const q=clean(quote,800,'evidence');\n  if(q.length<15 || !normalizedEvidence(body).includes(normalizedEvidence(q)))fail('evidence_not_found');\n  return q;\n}\n// read MUST come from an actual HTTP reader/extractor. Never pass a model-produced read object.\nfunction validateRead(read, searchURLs, win, now=new Date()) {\n  if(!read || read.transport!=='http' || read.status_code!==200 || !/^text\\/html(?:;|$)/i.test(read.content_type||''))fail('page_not_read');\n  const requested=articleURL(read.requested_url), url=articleURL(read.final_url);\n  if(!searchURLs.has(requested))fail('not_in_search_sources');\n  if(new URL(requested).hostname!==new URL(url).hostname)fail('cross_source_redirect');\n  const age=instant(now)-instant(read.fetched_at);\n  if(age<0 || age>3600000)fail('read_stale');\n  if(read.date_kind!=='published')fail('publication_date_required');\n  const raw=clean(read.publication_date_raw,150,'publication_date_raw');\n  const context=clean(read.publication_date_context,1200,'publication_date_context');\n  if(!normalizedEvidence(context).includes(normalizedEvidence(raw)))fail('date_context_mismatch');\n  if(/dateModified|дата обновления|последнее обновление/i.test(context))fail('updated_is_not_published');\n  const published=parseDateEvidence(raw);\n  if(published<isoDay(win.from) || published>isoDay(win.to))fail('outside_date_window');\n  return {url,requested_url:requested,published_date:published,article_text:clean(read.article_text,60000,'article_text'),\n    source_name:sourceName(url),fetched_at:read.fetched_at,publication_date_context:context};\n}\nfunction validateCandidate(candidate, read, searchURLs, win, now=new Date()) {\n  const page=validateRead(read,searchURLs,win,now);\n  if(!candidate || candidate.read_id!==read.read_id)fail('read_id_mismatch');\n  if(candidate.relevance!=='direct_personal_finance')fail('not_relevant_to_people');\n  if(!Object.hasOwn(STAGES,candidate.stage))fail('stage_unknown');\n  if(candidate.published_date!==page.published_date)fail('date_mismatch');\n  if(!Array.isArray(candidate.facts) || candidate.facts.length<1 || candidate.facts.length>4)fail('facts_invalid');\n  const facts=candidate.facts.map(f=>({text:clean(f.text,400,'fact'),evidence:quoteIn(f.evidence,page.article_text)}));\n  const stage_evidence=quoteIn(candidate.stage_evidence,page.article_text);\n  let effective_date=null;\n  if(candidate.effective_date!=null){\n    effective_date=isoDay(candidate.effective_date);\n    const evidence=quoteIn(candidate.effective_evidence,page.article_text);\n    const raw=clean(candidate.effective_date_raw,150,'effective_date_raw');\n    if(!normalizedEvidence(evidence).includes(normalizedEvidence(raw)) || parseDateEvidence(raw)!==effective_date)fail('effective_date_mismatch');\n    if(candidate.stage==='in_force' && effective_date>win.to)fail('not_in_force_yet');\n  }\n  // A verified date plus supporting words are mandatory before advertising that a new rule is in force.\n  if(candidate.stage==='in_force' && !effective_date)fail('effective_date_required');\n  return {url:page.url,source_name:page.source_name,published_date:page.published_date,\n    title:clean(candidate.title,150,'title'),summary:clean(candidate.summary,400,'summary'),\n    why:clean(candidate.why,220,'why'),stage:candidate.stage,stage_label:STAGES[candidate.stage],\n    effective_date,stage_evidence,facts,verified_at:page.fetched_at,\n    // Additional event clustering requires verified document IDs in the workflow; do not invent them.\n    dedup_key:page.url,format:'news_fact_pack_v1'};\n}\nfunction chooseTopics(items, excludedURLs=[], limit=CONFIG.max_topics) {\n  if(!Number.isInteger(limit)||limit<1||limit>CONFIG.max_topics)fail('limit_invalid');\n  const seen=new Set(excludedURLs.map(articleURL));\n  const result=[];\n  for(const item of [...items].sort((a,b)=>b.published_date.localeCompare(a.published_date))){\n    const key=articleURL(item.url);\n    if(seen.has(key))continue;\n    seen.add(key);result.push(item);if(result.length===limit)break;\n  }\n  return result;\n}\nfunction callbackData(batchId,itemId){\n  if(!/^[A-Za-z0-9_-]{1,16}$/.test(batchId)||!/^[A-Za-z0-9_-]{1,12}$/.test(itemId))fail('id_invalid');\n  return `news:${batchId}:${itemId}`;\n}\n// Advisory guard only. Perform and persist the state claim atomically AFTER this guard.\nfunction guardSelection({callback,batch,item,now=new Date()}) {\n  if(!callback||!batch||!item)fail('selection_missing');\n  if(String(callback.from_id)!==String(batch.owner_id)||String(callback.chat_id)!==String(batch.owner_id))fail('not_owner');\n  if(String(callback.message_id)!==String(item.message_id))fail('wrong_message');\n  if(callback.data!==callbackData(batch.id,item.id)||item.batch_id!==batch.id)fail('wrong_item');\n  if(instant(now)>=instant(batch.expires_at))fail('selection_expired');\n  if(batch.status!=='ready'||item.status!=='offered')fail('selection_not_available');\n  return {batch_id:batch.id,item_id:item.id,selection_key:`news:${batch.id}:${item.id}`};\n}\nfunction captionBudget(sourceURL) {\n  const suffix='\\n\\nИсточник: '+articleURL(sourceURL);\n  const body_limit=CONFIG.caption_limit_utf16-suffix.length;\n  if(body_limit<250)fail('source_url_too_long_for_caption');\n  return {suffix,body_limit,preferred_body_max:Math.min(800,body_limit)};\n}\nfunction composeNewsCaption(body,sourceURL) {\n  const {suffix,body_limit}=captionBudget(sourceURL);\n  const text=clean(body,body_limit,'caption_body');\n  // Model writes body only. Link is appended deterministically and retained through revision.\n  if(/https?:\\/\\//i.test(text))fail('unexpected_link_in_body');\n  return text+suffix;\n}\nfunction topicMessage(item){\n  const text=`${item.title}\\n\\n${item.summary}\\n\\nПочему это важно: ${item.why}\\n\\n${item.stage_label} · ${item.published_date}\\n${item.source_name}\\n${item.url}`;\n  return clean(text,4096,'topic_message');\n}\nfunction progressText(event,{count=0,days=7,partial=false}={}){\n  switch(event){\n    case 'accepted': return `Взято в работу. Ищу новости за последние ${days} дней.`;\n    case 'verifying': return 'Проверяю даты, источники и связь новостей с личными долгами.';\n    case 'ready': return `Готово. Тем на выбор: ${count}. Нажми «Подготовить пост» под подходящей новостью.`+(partial?' Часть источников проверить не удалось.':'');\n    case 'empty': return `За последние ${days} дней подходящих новостей с проверенным источником не нашлось.`+(days===7?' Можно расширить поиск командой /news 30.':'');\n    case 'partial_empty': return 'Полностью проверить источники не удалось; подтверждённых тем пока нет. Можно повторить поиск позже.';\n    case 'failed': return 'Поиск сейчас не завершился. Можно повторить /news позже. Существующие черновики сохранены.';\n    case 'selected': return 'Тема выбрана. Готовлю пост по источнику — затем пришлю на согласование.';\n    case 'expired': return 'Эта подборка устарела. Отправь /news, чтобы получить свежие темы.';\n    default: fail('progress_event_unknown');\n  }\n}\n\nreturn {CONFIG,STAGES,parseCommand,windowFor,parseDateEvidence,canonicalURL,articleURL,\n  validateRead,validateCandidate,chooseTopics,callbackData,guardSelection,captionBudget,\n  composeNewsCaption,topicMessage,progressText};})();\nconst RESP=(()=>{'use strict';\nconst core=NEWS;\nfunction modelName(model){\n  if(typeof model!=='string'||!model.trim()||model.includes('/')||/\\s/.test(model))throw Error('Configure a real OpenAI REST model ID with web_search support');\n  return model;\n}\nfunction searchRequest({model,instructions,days=7,topic='',now=new Date()}){\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Search instructions required');\n  const win=core.windowFor(days,now);\n  return {model:modelName(model),store:false,\n    instructions,\n    input:JSON.stringify({task:'Find recent official and editorial articles about personal finance, debts and rights in Russia',window:win,topic,max_candidates:core.CONFIG.max_candidates}),\n    tools:[{type:'web_search',external_web_access:true,filters:{allowed_domains:core.CONFIG.sources.filter(s=>s.enabled).map(s=>s.host)}}],\n    tool_choice:'required',include:['web_search_call.action.sources'],max_output_tokens:8000};\n}\nfunction outputJSON(response){\n  if(!response||response.status!=='completed'||response.error)throw Error('responses_not_completed');\n  const chunks=[];\n  for(const output of response.output||[]){\n    if(output.type!=='message')continue;\n    if(output.status && output.status!=='completed')throw Error('message_not_completed');\n    for(const part of output.content||[]){\n      if(part.type==='refusal')throw Error('model_refusal');\n      if(part.type==='output_text')chunks.push(part.text);\n    }\n  }\n  const text=chunks.join('\\n').trim().replace(/^```json\\s*/i,'').replace(/\\s*```$/,'');\n  let value;try{value=JSON.parse(text);}catch{throw Error('invalid_output_json');}\n  if(!value||Array.isArray(value)||typeof value!=='object')throw Error('invalid_output_object');\n  return value;\n}\nfunction parseDiscovery(response){\n  const value=outputJSON(response);\n  if(!(response.output||[]).some(o=>o.type==='web_search_call'&&o.status==='completed'&&o.action?.type==='search'))throw Error('web_search_not_executed');\n  const searchURLs=new Set();\n  function remember(url){try{searchURLs.add(core.articleURL(url));}catch{/* Skip unsupported sources. */}}\n  for(const output of response.output||[]){\n    if(output.type==='web_search_call'&&output.status==='completed'){\n      for(const s of output.action?.sources||[])remember(s.url);\n      if(output.action?.type==='open_page')remember(output.action.url);\n    }\n    if(output.type==='message')for(const p of output.content||[])for(const a of p.annotations||[])if(a.type==='url_citation')remember(a.url);\n  }\n  if(!Array.isArray(value.candidates)||value.candidates.length>core.CONFIG.max_candidates)throw Error('invalid_candidates');\n  const candidates=[],rejected=[];const seen=new Set();\n  for(const candidate of value.candidates){\n    try{\n      const url=core.articleURL(candidate.url);\n      if(!searchURLs.has(url))throw Error('candidate_not_in_search_sources');\n      if(seen.has(url))continue;\n      seen.add(url);candidates.push({url}); // Discard model guesses about dates/facts until the page is read.\n    }catch(error){rejected.push({code:error.code||error.message});}\n  }\n  return {candidates,search_urls:[...searchURLs],rejected,response_id:response.id||null};\n}\nfunction verificationRequest({model,instructions,reads,searchURLs,win,now=new Date()}){\n  if(!Array.isArray(reads)||!reads.length||reads.length>5)throw Error('verification_batch_size');\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Verification instructions required');\n  const ids=new Set();\n  const pages=reads.map(read=>{\n    if(typeof read.read_id!=='string'||!read.read_id||ids.has(read.read_id))throw Error('read_id_invalid');\n    ids.add(read.read_id);\n    const page=core.validateRead(read,searchURLs,win,now);\n    // Reader must extract the article rather than sending the entire site navigation.\n    if(page.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n    return {read_id:read.read_id,published_date:page.published_date,url:page.url,article_text:page.article_text};\n  });\n  return {model:modelName(model),store:false,instructions,input:JSON.stringify({window:win,pages}),max_output_tokens:8000};\n}\nfunction parseVerification(response,reads,searchURLs,win,now=new Date()){\n  const value=outputJSON(response);\n  if(!Array.isArray(value.results)||value.results.length!==reads.length)throw Error('verification_results_incomplete');\n  const remaining=new Map(reads.map(r=>[r.read_id,r]));\n  const accepted=[],rejected=[];\n  for(const result of value.results){\n    const read=remaining.get(result.read_id);\n    if(!read)throw Error('unknown_or_duplicate_read_id');\n    remaining.delete(result.read_id);\n    if(result.accept===false){rejected.push({read_id:read.read_id,code:'editorial_exclusion',reason:String(result.reason||'').slice(0,400)});continue;}\n    if(result.accept!==true)throw Error('verification_decision_missing');\n    try{accepted.push(core.validateCandidate(result,read,searchURLs,win,now));}\n    catch(error){rejected.push({read_id:read.read_id,code:error.code||error.message});}\n  }\n  return {accepted,rejected};\n}\n\nreturn {searchRequest,outputJSON,parseDiscovery,verificationRequest,parseVerification};})();\nfunction snapshotOf(topic){\n if(topic.format!=='news_topic_v2'&&topic.format!=='news_fact_pack_v2')throw Error('news_format_invalid');\n const snap=topic.snapshot;\n if(!snap||snap.url!==NEWS.articleURL(topic.url)||snap.published_date!==topic.published_date)throw Error('snapshot_source_mismatch');\n const blocks=QUALITY.sourceBlocks(snap.article_text,snap.document_key);\n if(JSON.stringify(blocks.blocks)!==JSON.stringify(snap.blocks))throw Error('snapshot_blocks_mismatch');\n return snap;\n}\nfunction strictRequest(instructions,input,schema,name,max=4500){return {model:'gpt-5.6-sol',reasoning:{effort:'low'},store:false,max_output_tokens:max,instructions,input:JSON.stringify(input),text:{format:{type:'json_schema',name,strict:true,schema}}};}\nfunction apiResult(r,prefix){\n if(r.statusCode!==200)throw Error(prefix+(r.statusCode?'_http_'+r.statusCode+(r.statusCode>=500?'_outcome_unknown':''):'_outcome_unknown'));\n if(r.body?.status==='incomplete')throw Error(prefix+'_incomplete');\n if(r.body?.output?.some(x=>x.content?.some(c=>c.type==='refusal')))throw Error(prefix+'_refusal');\n try{return RESP.outputJSON(r.body);}catch{throw Error(prefix+'_malformed_json');}\n}\nfunction verifiedFacts(topic,result){\n const snap=snapshotOf(topic);\n if(result.document_key!==snap.document_key)throw Error('wrong_source_document');\n if(result.status!=='ready')throw Error('selected_needs_source_check');\n if(!['news','analysis','opinion','explanation','statistics','court','bill','adopted','in_force'].includes(result.content_type))throw Error('selected_content_type_invalid');\n // A news report is not itself an enacted legal document. Narrow attributed reporting remains available.\n if(result.content_type==='in_force')throw Error('selected_primary_document_required');\n if(!Array.isArray(result.facts)||!result.facts.length||result.facts.length>4)throw Error('selected_facts_invalid');\n const facts=result.facts.map(f=>{if(typeof f.text!=='string'||!f.text.trim()||f.text.length>700||!Array.isArray(f.qualifiers)||typeof f.attribution!=='string')throw Error('selected_fact_invalid');return {...f,source_blocks:QUALITY.resolveEvidenceRefs(f,snap)};});\n return {...topic,format:'news_fact_pack_v2',verification_state:'facts_selected',facts,content_type:result.content_type,limitations:result.limitations,selected_at:new Date().toISOString()};\n}\nfunction previewInput(s){\n const fact=JSON.parse(s.fact_pack);\n if(NEWS.articleURL(fact.url)!==s.source_url||NEWS.composeNewsCaption(s.body,s.source_url)!==s.caption)throw Error('news_caption_source_mismatch');\n let source;\n if(fact.format==='news_fact_pack_v2'){source=snapshotOf(fact);for(const f of fact.facts)QUALITY.resolveEvidenceRefs(f,source);}\n else if(fact.format==='news_fact_pack_v1')source=QUALITY.sourceBlocks([fact.stage_evidence,...fact.facts.map(f=>f.evidence)].filter(Boolean).join('\\n\\n'),'legacy_saved_evidence');\n else throw Error('news_facts_not_selected');\n return {caption_body:s.body,cover_title:s.cover_title,cover_subtitle:s.cover_subtitle,facts:fact.facts,content_type:fact.content_type||fact.stage,limitations:fact.limitations||[],source,source_url:s.source_url,legacy_evidence_only:fact.format==='news_fact_pack_v1'};\n}\nfunction assessPreview(result){\n if(typeof result.pass!=='boolean'||!Array.isArray(result.issues)||result.issues.length>10)throw Error('preview_malformed_json');\n if(result.pass&&result.issues.length)throw Error('preview_contradictory_result');\n if(!result.pass&&!result.issues.length)throw Error('preview_empty_failure');\n for(const i of result.issues)if(!['caption_body','cover_title','cover_subtitle'].includes(i.field)||typeof i.fragment!=='string'||!i.fragment||typeof i.suggested_replacement!=='string'||typeof i.problem!=='string')throw Error('preview_issue_invalid');\n return result;\n}\nfunction repairPreview(s,issues){\n const out={...s};let exact=null;try{exact=JSON.parse(s.exact_revision_json||'null');}catch{}\n // A precise owner edit may only be corrected inside its replacement span. The saved cover stays byte-identical.\n const bounds=exact&&!exact.validation_error?{start:s.body.indexOf(exact.newText),length:exact.newText?.length||0}:null;\n if(bounds&&(bounds.start<0||s.body.indexOf(exact.newText,bounds.start+1)>=0))throw Error('preview_exact_span_ambiguous');\n const edits={};\n for(const issue of issues){const key=issue.field==='caption_body'?'body':issue.field,old=out[key];if(typeof old!=='string')throw Error('preview_field_missing');const at=old.indexOf(issue.fragment);if(at<0||old.indexOf(issue.fragment,at+1)>=0)throw Error('preview_repair_ambiguous');\n if(bounds&&(key!=='body'||at<bounds.start||at+issue.fragment.length>bounds.start+bounds.length))throw Error('preview_exact_requires_owner_edit');\n (edits[key]||=[]).push({at,end:at+issue.fragment.length,text:issue.suggested_replacement});}\n for(const [key,changes] of Object.entries(edits)){changes.sort((a,b)=>b.at-a.at);let last=Infinity;for(const e of changes){if(e.end>last)throw Error('preview_repair_overlap');out[key]=out[key].slice(0,e.at)+e.text+out[key].slice(e.end);last=e.at;}}\n if(!out.cover_title||out.cover_title.length>70||out.cover_subtitle.length>110)throw Error('preview_cover_bounds');\n out.caption=NEWS.composeNewsCaption(out.body,out.source_url);out.text=out.caption;out.channel_caption=out.caption;out.caption_length=out.caption.length;\n if(bounds){exact.body=out.body;exact.caption=out.caption;out.exact_revision_json=JSON.stringify(exact);}\n return out;\n}\nconst ctx=$('Проверить HTTP 0').first().json,r=$json;let out={...ctx,follow:false,status_code:r.statusCode||0,content_type:r.headers?.['content-type']||'',fetched_at:new Date().toISOString(),final_url:ctx.url};\nif(r.statusCode>=300&&r.statusCode<400){try{if(1>=3)throw Error('redirect_limit');const target=resolveOfficialLocation(r.headers?.location,ctx.url);NEWS.articleURL(target);return {json:{...out,url:target,redirects:1+1,follow:true}};}catch(e){return {json:{...out,read_error:e.message}};}}\nif(r.statusCode!==200)return {json:{...out,read_error:r.statusCode?'http_'+r.statusCode:'transport_error'}};\nif(!/^text\\/html(?:;|$)/i.test(out.content_type))return {json:{...out,read_error:'content_type_unsupported'}};\nconst html=r.body;if(typeof html!=='string'||!html)return {json:{...out,read_error:'empty_response'}};\n// Reject oversized/incomplete documents before HTML parsing; no partial text reaches the model.\nif(html.length>1048576||Number(r.headers?.['content-length']||0)>1048576)return {json:{...out,read_error:'response_too_large'}};\nconst host=new StrictURL(ctx.url).hostname.replace(/^www\\./,'');\nconst selectors={\n 'nalog.gov.ru':{body_selector:'.page-content__center',date_selector:'.page-content__center > p.gray',time_selector:'time[data-unavailable]'},\n 'cbr.ru':{body_selector:'.landing-text',date_selector:'.news-info-line_date',time_selector:'time[data-unavailable]'},\n 'pravo.ru':{body_selector:'section.article-content',date_selector:'[data-bfl-none]',time_selector:'time[itemprop=datePublished][datetime]'},\n 'probankrotstvo.ru':{body_selector:'.post-content',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'kommersant.ru':{body_selector:'.doc__text, .doc__intro',date_selector:'[data-bfl-none]',time_selector:'time.doc_header__publish_time[datetime]'},\n 'rbc.ru':{body_selector:'.article__text',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'vsrf.ru':{body_selector:'div.text-text-primary[class*=\"cms-info__icon\"]',date_selector:'time .sr-only',time_selector:'time[datetime]'}\n};\nreturn {json:{...out,...selectors[host],html}};\n\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        1320,
        0
      ]
    },
    {
      "id": "13de7e0f-5492-432b-8b88-21cb456758ee",
      "name": "Есть проверенный редирект 1",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.follow === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "position": [
        1540,
        0
      ]
    },
    {
      "id": "1a050c2f-f5cf-421e-a84e-7f53a42c8db3",
      "name": "HTTP статья 2",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "text",
              "outputPropertyName": "body"
            }
          }
        }
      },
      "position": [
        1760,
        0
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": false
    },
    {
      "id": "6175b067-d75f-40d9-b472-c57f10e34e2a",
      "name": "Проверить HTTP 2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\n// Small, deliberately restrictive URL adapter for the n8n sandbox (global URL is absent).\n// Only the official ASCII hosts used by this workflow are representable.\nclass StrictURL {\n constructor(value){\n  if(typeof value!=='string'||value.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value))throw Error('url_invalid');\n  const m=/^https:\\/\\/((?:www\\.)?(?:nalog\\.gov\\.ru|cbr\\.ru|vsrf\\.ru|pravo\\.ru|probankrotstvo\\.ru|kommersant\\.ru|rbc\\.ru))(\\/[^?#]*)?(\\?[^#]*)?(?:#[^\\s]*)?$/i.exec(value);\n  if(!m)throw Error('url_forbidden');\n  this.protocol='https:';this.hostname=m[1].toLowerCase();this.pathname=m[2]||'/';this.username='';this.password='';this.port='';this.hash='';\n  if(/%|\\/{2}|(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(this.pathname))throw Error('url_path_encoding_forbidden');\n  this.params=[];\n  for(const entry of (m[3]||'').slice(1).split('&').filter(Boolean)){\n   const at=entry.indexOf('=');const key=decodeURIComponent((at<0?entry:entry.slice(0,at)).replace(/\\+/g,' '));const val=decodeURIComponent((at<0?'':entry.slice(at+1)).replace(/\\+/g,' '));\n   if(/[\\u0000-\\u001f\\u007f]/.test(key+val)||this.params.some(x=>x[0]===key))throw Error('url_query_invalid');this.params.push([key,val]);\n  }\n  this.searchParams={keys:()=>this.params.map(x=>x[0]),get:key=>this.params.find(x=>x[0]===key)?.[1]??null,delete:key=>{this.params=this.params.filter(x=>x[0]!==key)},sort:()=>this.params.sort((a,b)=>a[0].localeCompare(b[0]))};\n }\n toString(){return 'https://'+this.hostname+this.pathname+(this.params.length?'?'+this.params.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'):'');}\n}\nfunction resolveOfficialLocation(location,base){\n if(typeof location!=='string'||!location||location.startsWith('//'))throw Error('redirect_forbidden');\n const current=new StrictURL(base);let target;\n if(location.startsWith('https://'))target=location;\n else if(location.startsWith('/'))target='https://'+current.hostname+location;\n else if(location.startsWith('?'))target='https://'+current.hostname+current.pathname+location;\n else throw Error('redirect_relative_path_unsupported');\n const next=new StrictURL(target);if(next.hostname.replace(/^www\\./,'')!==current.hostname.replace(/^www\\./,''))throw Error('cross_source_redirect');return next.toString();\n}\n\nconst QUALITY=(()=>{const URL=StrictURL;\n'use strict';\n// Standalone helpers. These do not fetch pages, classify their meaning, mutate state or publish.\n// Merge into actual deployed nodes. Do not replace the server's modified v0.4 wholesale.\nconst POLICY={\"version\": \"0.4.1\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"max_per_publisher_first_pass\": 2, \"require_exact_quotes_to_offer_topic\": false, \"verify_selected_topic_before_image\": true, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\"}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\"}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\"}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\"}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\"}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\"}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\"}], \"categories\": {\"bankruptcy\": \"Банкротство граждан\", \"credit\": \"Кредиты и МФО\", \"collection\": \"Приставы и взыскание\", \"property\": \"Жильё и имущество\", \"family\": \"Семья, наследство и долги\", \"fraud\": \"Финансовое мошенничество\", \"consumer\": \"Права потребителей финансовых услуг\"}};\nfunction error(code){throw Object.assign(new Error(code),{code});}\nfunction safeText(value,max=20000){\n  if(typeof value!=='string'||!value.trim()||value.length>max)error('text_invalid');\n  if(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u.test(value))error('text_controls');\n  return value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n}\nfunction normalizeExcerpt(value){\n  return safeText(value,60000).replace(/[\\u00ad\\u200b\\ufeff]/gu,'')\n    .replace(/[«»“”„‟]/gu,'\"').replace(/[‘’‚‛]/gu,\"'\")\n    .replace(/[\\u2010-\\u2015\\u2212]/gu,'-').replace(/\\s+/gu,' ').trim();\n}\nfunction checkExcerpt(quote,article){\n  try{\n    const q=normalizeExcerpt(quote),body=normalizeExcerpt(article);\n    // A short factual phrase may be legitimate. No arbitrary 15-character cutoff.\n    if((q.match(/[\\p{L}\\p{N}]+/gu)||[]).length<2)return {matched:false,reason:'fragment_too_short'};\n    const at=body.indexOf(q);\n    return at<0 ? {matched:false,reason:'text_differs'} : {matched:true,reason:'normalized_match',normalized_excerpt:q};\n  }catch(e){return {matched:false,reason:e.code||'evidence_invalid'};}\n}\nfunction partitionEvidence(facts,article){\n  if(!Array.isArray(facts))error('facts_invalid');\n  const matched=[],needs_review=[];\n  facts.forEach((fact,index)=>{\n    const result=checkExcerpt(fact?.evidence,article);\n    const row={index,fact,result};\n    (result.matched?matched:needs_review).push(row);\n  });\n  // Matching is provenance, NOT semantic proof. Caller must review the selected news.\n  return {matched,needs_review,topic_must_not_be_dropped_for_this_alone:true,semantic_review_required:true};\n}\nfunction sourceBlocks(article,documentKey){\n  if(typeof documentKey!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(documentKey))error('document_key_invalid');\n  const paragraphs=safeText(article,60000).split(/\\n\\s*\\n/u).filter(p=>p.trim());\n  const chunks=[];\n  for(let paragraph of paragraphs){\n    while(paragraph.length>1800){\n      let cut=paragraph.lastIndexOf(' ',1800);\n      if(cut<900)cut=1800;\n      chunks.push(paragraph.slice(0,cut));paragraph=paragraph.slice(cut).trimStart();\n    }\n    if(paragraph.trim())chunks.push(paragraph.trim());\n  }\n  return {document_key:documentKey,blocks:chunks.map((text,i)=>({id:'p'+String(i+1).padStart(3,'0'),text}))};\n}\nfunction resolveEvidenceRefs(fact,document){\n  if(!fact||fact.document_key!==document.document_key)error('wrong_source_document');\n  if(!Array.isArray(fact.source_ids)||!fact.source_ids.length||fact.source_ids.length>3||new Set(fact.source_ids).size!==fact.source_ids.length)error('source_ids_invalid');\n  const map=new Map(document.blocks.map(b=>[b.id,b.text]));\n  return fact.source_ids.map(id=>{\n    if(!map.has(id))error('source_block_not_found');\n    return {id,text:map.get(id)};\n  });\n}\nfunction sourceURL(raw){\n  if(typeof raw!=='string'||raw.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(raw))error('url_invalid');\n  let u;try{u=new URL(raw);}catch{error('url_invalid');}\n  if(u.protocol!=='https:'||u.username||u.password||u.port)error('url_forbidden');\n  const source=POLICY.sources.find(s=>u.hostname===s.host||u.hostname==='www.'+s.host);\n  if(!source)error('source_not_allowed');\n  u.hostname=source.host;u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  if(source.host==='pravo.ru')u.pathname=u.pathname.replace(/(\\/(?:news|story)\\/\\d+)\\/amp\\/?$/,'$1/');const p=u.pathname;\n  const article = source.host==='nalog.gov.ru' ? /^\\/rn\\d+\\/(?:news\\/[^/]+|ifns\\/imns\\d+_\\d+\\/info)\\/\\d+\\/?$/.test(p)\n    : source.host==='cbr.ru' ? /^\\/press\\/(?:event|regevent)\\/\\/?$/i.test(p)&&/^\\d+$/.test(u.searchParams.get('id')||'') || /^\\/press\\/pr\\/\\/?$/i.test(p)&&!!u.searchParams.get('file')\n    : source.host==='vsrf.ru' ? /^(?:\\/news|\\/press_center\\/news)\\/\\d+\\/?$/.test(p)\n    : source.host==='pravo.ru' ? /^\\/(?:news|story)\\/\\d+\\/?$/.test(p)\n    : source.host==='probankrotstvo.ru' ? /^\\/(?:news|article|articles)\\/[a-z0-9-]+-\\d+\\/?$/.test(p)\n    : source.host==='kommersant.ru' ? /^\\/doc\\/\\d+\\/?$/.test(p)\n    : source.host==='rbc.ru' ? /^\\/[a-z0-9_-]+\\/\\d{2}\\/\\d{2}\\/\\d{4}\\/[a-f0-9]{24}\\/?$/i.test(p)\n    : false;\n  if(!article)error('article_path_needs_adapter');\n  if(source.host==='vsrf.ru')u.pathname='/news/'+p.match(/\\d+/)[0];\n  else if(source.host==='nalog.gov.ru'||source.host==='pravo.ru')u.pathname=p.replace(/\\/?$/,'/');\n  else if(source.host==='cbr.ru')u.pathname=p.toLowerCase().replace(/\\/?$/,'/');\n  else u.pathname=p.replace(/\\/$/,'');\n  const keep=source.host==='cbr.ru'?(/event/i.test(p)?'id':'file'):null;\n  for(const key of [...u.searchParams.keys()])if(key!==keep)u.searchParams.delete(key);\n  return {url:u.toString(),publisher:source.host,source_name:source.name,source_kind:source.kind};\n}\nfunction dateValue(value){\n  if(typeof value!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))error('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if(!Number.isFinite(d.getTime())||d.toISOString().slice(0,10)!==value)error('invalid_date');\n  return value;\n}\n// read is trusted HTTP/extractor output; judgment is untrusted classifier output.\n// Actual reading, redirects, trace provenance and publication-date extraction stay in n8n.\nfunction offerTopic(read,judgment,window){\n  if(!read||!judgment||judgment.read_id!==read.read_id)error('read_id_mismatch');\n  const src=sourceURL(read.url);\n  if(read.transport!=='http'||read.status_code!==200||read.date_kind!=='published'||read.discovery_bound!==true)error('source_read_required');\n  safeText(read.article_text,60000);\n  const day=dateValue(read.published_date),from=dateValue(window.from),to=dateValue(window.to);\n  if(from>to)error('date_window_invalid');\n  if(day<from||day>to)return {offer:false,reason:'outside_window'};\n  if(judgment.relevance==='unrelated')return {offer:false,reason:'unrelated'};\n  if(!['direct','adjacent','context'].includes(judgment.relevance))error('relevance_invalid');\n  if(!Object.hasOwn(POLICY.categories,judgment.category))error('category_invalid');\n  if(!['news','analysis','opinion','explanation','statistics','advertisement'].includes(judgment.content_type))error('content_type_invalid');\n  if(judgment.content_type==='advertisement')return {offer:false,reason:'advertisement'};\n  return {offer:true,read_id:read.read_id,...src,published_date:day,\n    title:safeText(judgment.title,160),angle:safeText(judgment.angle,350),\n    relevance:judgment.relevance,category:judgment.category,category_label:POLICY.categories[judgment.category],\n    content_type:judgment.content_type,verification_state:'on_selection',\n    score:judgment.relevance==='direct'?30:judgment.relevance==='adjacent'?20:10};\n}\nfunction rankTopics(topics,{excludedURLs=[],limit=POLICY.max_topics}={}){\n  if(!Number.isInteger(limit)||limit<1||limit>POLICY.max_topics)error('limit_invalid');\n  const seen=new Set(excludedURLs.map(url=>sourceURL(url).url)),unique=[];\n  for(const topic of [...topics].filter(t=>t.offer===true).sort((a,b)=>b.score-a.score||b.published_date.localeCompare(a.published_date))){\n    const url=sourceURL(topic.url).url;\n    if(seen.has(url))continue;seen.add(url);unique.push(topic);\n  }\n  const selected=[],deferred=[],counts=new Map();\n  for(const t of unique){\n    if((counts.get(t.publisher)||0)>=POLICY.max_per_publisher_first_pass){deferred.push(t);continue;}\n    selected.push(t);counts.set(t.publisher,(counts.get(t.publisher)||0)+1);\n  }\n  // Diversity is a preference. Do not discard useful topics when only one source has them.\n  return [...selected,...deferred].slice(0,limit);\n}\nfunction topicText(topic){\n  return safeText(`${topic.title}\\n\\nИдея поста: ${topic.angle}\\n\\n${topic.category_label} · ${topic.published_date}\\n${topic.source_name}\\n${topic.url}\\n\\nПосле выбора проверю факты и подготовлю черновик.`,4096);\n}\nreturn {POLICY,normalizeExcerpt,checkExcerpt,partitionEvidence,sourceBlocks,resolveEvidenceRefs,\n  sourceURL,offerTopic,rankTopics,topicText};\n\n})();\nconst NEWS=(()=>{const URL=StrictURL;\n'use strict';\n// Pure helpers. No network, database, paid calls or Telegram mutations.\n// Port to n8n Code nodes against the actual runtime; require is not assumed there.\nconst CONFIG={\"version\": \"0.4.1\", \"timezone\": \"Europe/Moscow\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"selection_ttl_hours\": 24, \"caption_limit_utf16\": 1024, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\", \"enabled\": true}]};\nconst STAGES = Object.freeze({\n  proposal:'Инициатива', bill:'Законопроект', adopted:'Принято, вступление в силу не подтверждено',\n  in_force:'Действует', court:'Судебная практика', explanation:'Разъяснение', statistics:'Статистика'\n});\nconst CONTROLS = /[\\u0000-\\u0008\\u000b-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u;\nfunction fail(code) { throw Object.assign(new Error(code), {code}); }\nfunction clean(value, max, field='text') {\n  if (typeof value !== 'string') fail(field+'_type');\n  const s=value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n  if (!s || s.length>max || CONTROLS.test(s)) fail(field+'_invalid');\n  return s;\n}\nfunction instant(value) {\n  const t=value instanceof Date ? value.getTime() : Date.parse(value);\n  if (!Number.isFinite(t)) fail('invalid_clock');\n  return t;\n}\nfunction isoDay(value) {\n  if (typeof value!=='string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) fail('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if (!Number.isFinite(d.getTime()) || d.toISOString().slice(0,10)!==value) fail('invalid_date');\n  return value;\n}\nfunction parseDateEvidence(value) {\n  const s=clean(value,150,'publication_date_raw').toLowerCase();\n  let m=s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:$|t\\d{2}:)/);\n  if (m) return isoDay(`${m[1]}-${m[2]}-${m[3]}`);\n  m=s.match(/^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:$|\\s)/);\n  if (m) return isoDay(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  const months=['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];\n  m=s.match(/^(\\d{1,2})\\s+([а-я]+)\\s+(\\d{4})(?:$|\\s)/);\n  if (m && months.includes(m[2])) return isoDay(`${m[3]}-${String(months.indexOf(m[2])+1).padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  fail('publication_date_unreadable');\n}\nfunction windowFor(days=7, now=new Date()) {\n  if (!CONFIG.allowed_days.includes(days)) fail('days_not_supported');\n  const parts=new Intl.DateTimeFormat('en-CA',{timeZone:CONFIG.timezone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(instant(now)));\n  const get=t=>parts.find(p=>p.type===t).value;\n  const to=`${get('year')}-${get('month')}-${get('day')}`;\n  const from=new Date(Date.parse(to+'T00:00:00Z')-(days-1)*86400000).toISOString().slice(0,10);\n  return {from,to,days,timezone:CONFIG.timezone};\n}\nfunction parseCommand(text, botUsername='bflcontentbot') {\n  if (typeof text!=='string') return null;\n  const match=text.trim().match(/^\\/news(?:@([a-z0-9_]+))?(?:\\s+([\\s\\S]*))?$/i);\n  if (!match) return null;\n  if (match[1] && match[1].toLowerCase()!==botUsername.toLowerCase()) return null;\n  let tail=(match[2]||'').trim(),days=CONFIG.default_days;\n  const d=tail.match(/^(\\d+)(?:\\s+|$)/);\n  if(d){ days=Number(d[1]); if(!CONFIG.allowed_days.includes(days))fail('days_not_supported'); tail=tail.slice(d[0].length).trim(); }\n  return {days,topic:tail ? clean(tail,200,'topic') : ''};\n}\nfunction canonicalURL(value) {\n  if (typeof value!=='string' || value.length>1500 || /[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value)) fail('url_invalid');\n  let u; try{ u=new URL(value); }catch{ fail('url_invalid'); }\n  if(u.protocol!=='https:' || u.username || u.password || u.port) fail('url_forbidden');\n  const source=CONFIG.sources.find(s=>s.enabled && (u.hostname===s.host || u.hostname==='www.'+s.host));\n  // Exact known hosts; do not follow arbitrary subdomains just because web_search allows them.\n  if(!source)fail('source_not_allowed');\n  if(u.hostname==='www.'+source.host)u.hostname=source.host;\n  u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  return u.toString();\n}\nfunction articleURL(value){return QUALITY.sourceURL(value).url;}\nfunction sourceName(url) { const host=new URL(canonicalURL(url)).hostname; return CONFIG.sources.find(s=>s.host===host).name; }\nfunction normalizedEvidence(text) { return text.normalize('NFC').replace(/\\s+/gu,' ').trim(); }\nfunction quoteIn(quote,body) {\n  const q=clean(quote,800,'evidence');\n  if(q.length<15 || !normalizedEvidence(body).includes(normalizedEvidence(q)))fail('evidence_not_found');\n  return q;\n}\n// read MUST come from an actual HTTP reader/extractor. Never pass a model-produced read object.\nfunction validateRead(read, searchURLs, win, now=new Date()) {\n  if(!read || read.transport!=='http' || read.status_code!==200 || !/^text\\/html(?:;|$)/i.test(read.content_type||''))fail('page_not_read');\n  const requested=articleURL(read.requested_url), url=articleURL(read.final_url);\n  if(!searchURLs.has(requested))fail('not_in_search_sources');\n  if(new URL(requested).hostname!==new URL(url).hostname)fail('cross_source_redirect');\n  const age=instant(now)-instant(read.fetched_at);\n  if(age<0 || age>3600000)fail('read_stale');\n  if(read.date_kind!=='published')fail('publication_date_required');\n  const raw=clean(read.publication_date_raw,150,'publication_date_raw');\n  const context=clean(read.publication_date_context,1200,'publication_date_context');\n  if(!normalizedEvidence(context).includes(normalizedEvidence(raw)))fail('date_context_mismatch');\n  if(/dateModified|дата обновления|последнее обновление/i.test(context))fail('updated_is_not_published');\n  const published=parseDateEvidence(raw);\n  if(published<isoDay(win.from) || published>isoDay(win.to))fail('outside_date_window');\n  return {url,requested_url:requested,published_date:published,article_text:clean(read.article_text,60000,'article_text'),\n    source_name:sourceName(url),fetched_at:read.fetched_at,publication_date_context:context};\n}\nfunction validateCandidate(candidate, read, searchURLs, win, now=new Date()) {\n  const page=validateRead(read,searchURLs,win,now);\n  if(!candidate || candidate.read_id!==read.read_id)fail('read_id_mismatch');\n  if(candidate.relevance!=='direct_personal_finance')fail('not_relevant_to_people');\n  if(!Object.hasOwn(STAGES,candidate.stage))fail('stage_unknown');\n  if(candidate.published_date!==page.published_date)fail('date_mismatch');\n  if(!Array.isArray(candidate.facts) || candidate.facts.length<1 || candidate.facts.length>4)fail('facts_invalid');\n  const facts=candidate.facts.map(f=>({text:clean(f.text,400,'fact'),evidence:quoteIn(f.evidence,page.article_text)}));\n  const stage_evidence=quoteIn(candidate.stage_evidence,page.article_text);\n  let effective_date=null;\n  if(candidate.effective_date!=null){\n    effective_date=isoDay(candidate.effective_date);\n    const evidence=quoteIn(candidate.effective_evidence,page.article_text);\n    const raw=clean(candidate.effective_date_raw,150,'effective_date_raw');\n    if(!normalizedEvidence(evidence).includes(normalizedEvidence(raw)) || parseDateEvidence(raw)!==effective_date)fail('effective_date_mismatch');\n    if(candidate.stage==='in_force' && effective_date>win.to)fail('not_in_force_yet');\n  }\n  // A verified date plus supporting words are mandatory before advertising that a new rule is in force.\n  if(candidate.stage==='in_force' && !effective_date)fail('effective_date_required');\n  return {url:page.url,source_name:page.source_name,published_date:page.published_date,\n    title:clean(candidate.title,150,'title'),summary:clean(candidate.summary,400,'summary'),\n    why:clean(candidate.why,220,'why'),stage:candidate.stage,stage_label:STAGES[candidate.stage],\n    effective_date,stage_evidence,facts,verified_at:page.fetched_at,\n    // Additional event clustering requires verified document IDs in the workflow; do not invent them.\n    dedup_key:page.url,format:'news_fact_pack_v1'};\n}\nfunction chooseTopics(items, excludedURLs=[], limit=CONFIG.max_topics) {\n  if(!Number.isInteger(limit)||limit<1||limit>CONFIG.max_topics)fail('limit_invalid');\n  const seen=new Set(excludedURLs.map(articleURL));\n  const result=[];\n  for(const item of [...items].sort((a,b)=>b.published_date.localeCompare(a.published_date))){\n    const key=articleURL(item.url);\n    if(seen.has(key))continue;\n    seen.add(key);result.push(item);if(result.length===limit)break;\n  }\n  return result;\n}\nfunction callbackData(batchId,itemId){\n  if(!/^[A-Za-z0-9_-]{1,16}$/.test(batchId)||!/^[A-Za-z0-9_-]{1,12}$/.test(itemId))fail('id_invalid');\n  return `news:${batchId}:${itemId}`;\n}\n// Advisory guard only. Perform and persist the state claim atomically AFTER this guard.\nfunction guardSelection({callback,batch,item,now=new Date()}) {\n  if(!callback||!batch||!item)fail('selection_missing');\n  if(String(callback.from_id)!==String(batch.owner_id)||String(callback.chat_id)!==String(batch.owner_id))fail('not_owner');\n  if(String(callback.message_id)!==String(item.message_id))fail('wrong_message');\n  if(callback.data!==callbackData(batch.id,item.id)||item.batch_id!==batch.id)fail('wrong_item');\n  if(instant(now)>=instant(batch.expires_at))fail('selection_expired');\n  if(batch.status!=='ready'||item.status!=='offered')fail('selection_not_available');\n  return {batch_id:batch.id,item_id:item.id,selection_key:`news:${batch.id}:${item.id}`};\n}\nfunction captionBudget(sourceURL) {\n  const suffix='\\n\\nИсточник: '+articleURL(sourceURL);\n  const body_limit=CONFIG.caption_limit_utf16-suffix.length;\n  if(body_limit<250)fail('source_url_too_long_for_caption');\n  return {suffix,body_limit,preferred_body_max:Math.min(800,body_limit)};\n}\nfunction composeNewsCaption(body,sourceURL) {\n  const {suffix,body_limit}=captionBudget(sourceURL);\n  const text=clean(body,body_limit,'caption_body');\n  // Model writes body only. Link is appended deterministically and retained through revision.\n  if(/https?:\\/\\//i.test(text))fail('unexpected_link_in_body');\n  return text+suffix;\n}\nfunction topicMessage(item){\n  const text=`${item.title}\\n\\n${item.summary}\\n\\nПочему это важно: ${item.why}\\n\\n${item.stage_label} · ${item.published_date}\\n${item.source_name}\\n${item.url}`;\n  return clean(text,4096,'topic_message');\n}\nfunction progressText(event,{count=0,days=7,partial=false}={}){\n  switch(event){\n    case 'accepted': return `Взято в работу. Ищу новости за последние ${days} дней.`;\n    case 'verifying': return 'Проверяю даты, источники и связь новостей с личными долгами.';\n    case 'ready': return `Готово. Тем на выбор: ${count}. Нажми «Подготовить пост» под подходящей новостью.`+(partial?' Часть источников проверить не удалось.':'');\n    case 'empty': return `За последние ${days} дней подходящих новостей с проверенным источником не нашлось.`+(days===7?' Можно расширить поиск командой /news 30.':'');\n    case 'partial_empty': return 'Полностью проверить источники не удалось; подтверждённых тем пока нет. Можно повторить поиск позже.';\n    case 'failed': return 'Поиск сейчас не завершился. Можно повторить /news позже. Существующие черновики сохранены.';\n    case 'selected': return 'Тема выбрана. Готовлю пост по источнику — затем пришлю на согласование.';\n    case 'expired': return 'Эта подборка устарела. Отправь /news, чтобы получить свежие темы.';\n    default: fail('progress_event_unknown');\n  }\n}\n\nreturn {CONFIG,STAGES,parseCommand,windowFor,parseDateEvidence,canonicalURL,articleURL,\n  validateRead,validateCandidate,chooseTopics,callbackData,guardSelection,captionBudget,\n  composeNewsCaption,topicMessage,progressText};})();\nconst RESP=(()=>{'use strict';\nconst core=NEWS;\nfunction modelName(model){\n  if(typeof model!=='string'||!model.trim()||model.includes('/')||/\\s/.test(model))throw Error('Configure a real OpenAI REST model ID with web_search support');\n  return model;\n}\nfunction searchRequest({model,instructions,days=7,topic='',now=new Date()}){\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Search instructions required');\n  const win=core.windowFor(days,now);\n  return {model:modelName(model),store:false,\n    instructions,\n    input:JSON.stringify({task:'Find recent official and editorial articles about personal finance, debts and rights in Russia',window:win,topic,max_candidates:core.CONFIG.max_candidates}),\n    tools:[{type:'web_search',external_web_access:true,filters:{allowed_domains:core.CONFIG.sources.filter(s=>s.enabled).map(s=>s.host)}}],\n    tool_choice:'required',include:['web_search_call.action.sources'],max_output_tokens:8000};\n}\nfunction outputJSON(response){\n  if(!response||response.status!=='completed'||response.error)throw Error('responses_not_completed');\n  const chunks=[];\n  for(const output of response.output||[]){\n    if(output.type!=='message')continue;\n    if(output.status && output.status!=='completed')throw Error('message_not_completed');\n    for(const part of output.content||[]){\n      if(part.type==='refusal')throw Error('model_refusal');\n      if(part.type==='output_text')chunks.push(part.text);\n    }\n  }\n  const text=chunks.join('\\n').trim().replace(/^```json\\s*/i,'').replace(/\\s*```$/,'');\n  let value;try{value=JSON.parse(text);}catch{throw Error('invalid_output_json');}\n  if(!value||Array.isArray(value)||typeof value!=='object')throw Error('invalid_output_object');\n  return value;\n}\nfunction parseDiscovery(response){\n  const value=outputJSON(response);\n  if(!(response.output||[]).some(o=>o.type==='web_search_call'&&o.status==='completed'&&o.action?.type==='search'))throw Error('web_search_not_executed');\n  const searchURLs=new Set();\n  function remember(url){try{searchURLs.add(core.articleURL(url));}catch{/* Skip unsupported sources. */}}\n  for(const output of response.output||[]){\n    if(output.type==='web_search_call'&&output.status==='completed'){\n      for(const s of output.action?.sources||[])remember(s.url);\n      if(output.action?.type==='open_page')remember(output.action.url);\n    }\n    if(output.type==='message')for(const p of output.content||[])for(const a of p.annotations||[])if(a.type==='url_citation')remember(a.url);\n  }\n  if(!Array.isArray(value.candidates)||value.candidates.length>core.CONFIG.max_candidates)throw Error('invalid_candidates');\n  const candidates=[],rejected=[];const seen=new Set();\n  for(const candidate of value.candidates){\n    try{\n      const url=core.articleURL(candidate.url);\n      if(!searchURLs.has(url))throw Error('candidate_not_in_search_sources');\n      if(seen.has(url))continue;\n      seen.add(url);candidates.push({url}); // Discard model guesses about dates/facts until the page is read.\n    }catch(error){rejected.push({code:error.code||error.message});}\n  }\n  return {candidates,search_urls:[...searchURLs],rejected,response_id:response.id||null};\n}\nfunction verificationRequest({model,instructions,reads,searchURLs,win,now=new Date()}){\n  if(!Array.isArray(reads)||!reads.length||reads.length>5)throw Error('verification_batch_size');\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Verification instructions required');\n  const ids=new Set();\n  const pages=reads.map(read=>{\n    if(typeof read.read_id!=='string'||!read.read_id||ids.has(read.read_id))throw Error('read_id_invalid');\n    ids.add(read.read_id);\n    const page=core.validateRead(read,searchURLs,win,now);\n    // Reader must extract the article rather than sending the entire site navigation.\n    if(page.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n    return {read_id:read.read_id,published_date:page.published_date,url:page.url,article_text:page.article_text};\n  });\n  return {model:modelName(model),store:false,instructions,input:JSON.stringify({window:win,pages}),max_output_tokens:8000};\n}\nfunction parseVerification(response,reads,searchURLs,win,now=new Date()){\n  const value=outputJSON(response);\n  if(!Array.isArray(value.results)||value.results.length!==reads.length)throw Error('verification_results_incomplete');\n  const remaining=new Map(reads.map(r=>[r.read_id,r]));\n  const accepted=[],rejected=[];\n  for(const result of value.results){\n    const read=remaining.get(result.read_id);\n    if(!read)throw Error('unknown_or_duplicate_read_id');\n    remaining.delete(result.read_id);\n    if(result.accept===false){rejected.push({read_id:read.read_id,code:'editorial_exclusion',reason:String(result.reason||'').slice(0,400)});continue;}\n    if(result.accept!==true)throw Error('verification_decision_missing');\n    try{accepted.push(core.validateCandidate(result,read,searchURLs,win,now));}\n    catch(error){rejected.push({read_id:read.read_id,code:error.code||error.message});}\n  }\n  return {accepted,rejected};\n}\n\nreturn {searchRequest,outputJSON,parseDiscovery,verificationRequest,parseVerification};})();\nfunction snapshotOf(topic){\n if(topic.format!=='news_topic_v2'&&topic.format!=='news_fact_pack_v2')throw Error('news_format_invalid');\n const snap=topic.snapshot;\n if(!snap||snap.url!==NEWS.articleURL(topic.url)||snap.published_date!==topic.published_date)throw Error('snapshot_source_mismatch');\n const blocks=QUALITY.sourceBlocks(snap.article_text,snap.document_key);\n if(JSON.stringify(blocks.blocks)!==JSON.stringify(snap.blocks))throw Error('snapshot_blocks_mismatch');\n return snap;\n}\nfunction strictRequest(instructions,input,schema,name,max=4500){return {model:'gpt-5.6-sol',reasoning:{effort:'low'},store:false,max_output_tokens:max,instructions,input:JSON.stringify(input),text:{format:{type:'json_schema',name,strict:true,schema}}};}\nfunction apiResult(r,prefix){\n if(r.statusCode!==200)throw Error(prefix+(r.statusCode?'_http_'+r.statusCode+(r.statusCode>=500?'_outcome_unknown':''):'_outcome_unknown'));\n if(r.body?.status==='incomplete')throw Error(prefix+'_incomplete');\n if(r.body?.output?.some(x=>x.content?.some(c=>c.type==='refusal')))throw Error(prefix+'_refusal');\n try{return RESP.outputJSON(r.body);}catch{throw Error(prefix+'_malformed_json');}\n}\nfunction verifiedFacts(topic,result){\n const snap=snapshotOf(topic);\n if(result.document_key!==snap.document_key)throw Error('wrong_source_document');\n if(result.status!=='ready')throw Error('selected_needs_source_check');\n if(!['news','analysis','opinion','explanation','statistics','court','bill','adopted','in_force'].includes(result.content_type))throw Error('selected_content_type_invalid');\n // A news report is not itself an enacted legal document. Narrow attributed reporting remains available.\n if(result.content_type==='in_force')throw Error('selected_primary_document_required');\n if(!Array.isArray(result.facts)||!result.facts.length||result.facts.length>4)throw Error('selected_facts_invalid');\n const facts=result.facts.map(f=>{if(typeof f.text!=='string'||!f.text.trim()||f.text.length>700||!Array.isArray(f.qualifiers)||typeof f.attribution!=='string')throw Error('selected_fact_invalid');return {...f,source_blocks:QUALITY.resolveEvidenceRefs(f,snap)};});\n return {...topic,format:'news_fact_pack_v2',verification_state:'facts_selected',facts,content_type:result.content_type,limitations:result.limitations,selected_at:new Date().toISOString()};\n}\nfunction previewInput(s){\n const fact=JSON.parse(s.fact_pack);\n if(NEWS.articleURL(fact.url)!==s.source_url||NEWS.composeNewsCaption(s.body,s.source_url)!==s.caption)throw Error('news_caption_source_mismatch');\n let source;\n if(fact.format==='news_fact_pack_v2'){source=snapshotOf(fact);for(const f of fact.facts)QUALITY.resolveEvidenceRefs(f,source);}\n else if(fact.format==='news_fact_pack_v1')source=QUALITY.sourceBlocks([fact.stage_evidence,...fact.facts.map(f=>f.evidence)].filter(Boolean).join('\\n\\n'),'legacy_saved_evidence');\n else throw Error('news_facts_not_selected');\n return {caption_body:s.body,cover_title:s.cover_title,cover_subtitle:s.cover_subtitle,facts:fact.facts,content_type:fact.content_type||fact.stage,limitations:fact.limitations||[],source,source_url:s.source_url,legacy_evidence_only:fact.format==='news_fact_pack_v1'};\n}\nfunction assessPreview(result){\n if(typeof result.pass!=='boolean'||!Array.isArray(result.issues)||result.issues.length>10)throw Error('preview_malformed_json');\n if(result.pass&&result.issues.length)throw Error('preview_contradictory_result');\n if(!result.pass&&!result.issues.length)throw Error('preview_empty_failure');\n for(const i of result.issues)if(!['caption_body','cover_title','cover_subtitle'].includes(i.field)||typeof i.fragment!=='string'||!i.fragment||typeof i.suggested_replacement!=='string'||typeof i.problem!=='string')throw Error('preview_issue_invalid');\n return result;\n}\nfunction repairPreview(s,issues){\n const out={...s};let exact=null;try{exact=JSON.parse(s.exact_revision_json||'null');}catch{}\n // A precise owner edit may only be corrected inside its replacement span. The saved cover stays byte-identical.\n const bounds=exact&&!exact.validation_error?{start:s.body.indexOf(exact.newText),length:exact.newText?.length||0}:null;\n if(bounds&&(bounds.start<0||s.body.indexOf(exact.newText,bounds.start+1)>=0))throw Error('preview_exact_span_ambiguous');\n const edits={};\n for(const issue of issues){const key=issue.field==='caption_body'?'body':issue.field,old=out[key];if(typeof old!=='string')throw Error('preview_field_missing');const at=old.indexOf(issue.fragment);if(at<0||old.indexOf(issue.fragment,at+1)>=0)throw Error('preview_repair_ambiguous');\n if(bounds&&(key!=='body'||at<bounds.start||at+issue.fragment.length>bounds.start+bounds.length))throw Error('preview_exact_requires_owner_edit');\n (edits[key]||=[]).push({at,end:at+issue.fragment.length,text:issue.suggested_replacement});}\n for(const [key,changes] of Object.entries(edits)){changes.sort((a,b)=>b.at-a.at);let last=Infinity;for(const e of changes){if(e.end>last)throw Error('preview_repair_overlap');out[key]=out[key].slice(0,e.at)+e.text+out[key].slice(e.end);last=e.at;}}\n if(!out.cover_title||out.cover_title.length>70||out.cover_subtitle.length>110)throw Error('preview_cover_bounds');\n out.caption=NEWS.composeNewsCaption(out.body,out.source_url);out.text=out.caption;out.channel_caption=out.caption;out.caption_length=out.caption.length;\n if(bounds){exact.body=out.body;exact.caption=out.caption;out.exact_revision_json=JSON.stringify(exact);}\n return out;\n}\nconst ctx=$('Проверить HTTP 1').first().json,r=$json;let out={...ctx,follow:false,status_code:r.statusCode||0,content_type:r.headers?.['content-type']||'',fetched_at:new Date().toISOString(),final_url:ctx.url};\nif(r.statusCode>=300&&r.statusCode<400){try{if(2>=3)throw Error('redirect_limit');const target=resolveOfficialLocation(r.headers?.location,ctx.url);NEWS.articleURL(target);return {json:{...out,url:target,redirects:2+1,follow:true}};}catch(e){return {json:{...out,read_error:e.message}};}}\nif(r.statusCode!==200)return {json:{...out,read_error:r.statusCode?'http_'+r.statusCode:'transport_error'}};\nif(!/^text\\/html(?:;|$)/i.test(out.content_type))return {json:{...out,read_error:'content_type_unsupported'}};\nconst html=r.body;if(typeof html!=='string'||!html)return {json:{...out,read_error:'empty_response'}};\n// Reject oversized/incomplete documents before HTML parsing; no partial text reaches the model.\nif(html.length>1048576||Number(r.headers?.['content-length']||0)>1048576)return {json:{...out,read_error:'response_too_large'}};\nconst host=new StrictURL(ctx.url).hostname.replace(/^www\\./,'');\nconst selectors={\n 'nalog.gov.ru':{body_selector:'.page-content__center',date_selector:'.page-content__center > p.gray',time_selector:'time[data-unavailable]'},\n 'cbr.ru':{body_selector:'.landing-text',date_selector:'.news-info-line_date',time_selector:'time[data-unavailable]'},\n 'pravo.ru':{body_selector:'section.article-content',date_selector:'[data-bfl-none]',time_selector:'time[itemprop=datePublished][datetime]'},\n 'probankrotstvo.ru':{body_selector:'.post-content',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'kommersant.ru':{body_selector:'.doc__text, .doc__intro',date_selector:'[data-bfl-none]',time_selector:'time.doc_header__publish_time[datetime]'},\n 'rbc.ru':{body_selector:'.article__text',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'vsrf.ru':{body_selector:'div.text-text-primary[class*=\"cms-info__icon\"]',date_selector:'time .sr-only',time_selector:'time[datetime]'}\n};\nreturn {json:{...out,...selectors[host],html}};\n\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        0,
        240
      ]
    },
    {
      "id": "5015b5db-c555-4678-97f3-660335cbb786",
      "name": "Есть проверенный редирект 2",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.follow === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "position": [
        220,
        240
      ]
    },
    {
      "id": "b5d7b198-010d-45d0-a6c8-acdfef902ab1",
      "name": "HTTP статья 3",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "text",
              "outputPropertyName": "body"
            }
          }
        }
      },
      "position": [
        440,
        240
      ],
      "onError": "continueRegularOutput",
      "retryOnFail": false
    },
    {
      "id": "7871f332-8923-4d78-8d5e-689be89bd130",
      "name": "Проверить HTTP 3",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\n// Small, deliberately restrictive URL adapter for the n8n sandbox (global URL is absent).\n// Only the official ASCII hosts used by this workflow are representable.\nclass StrictURL {\n constructor(value){\n  if(typeof value!=='string'||value.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value))throw Error('url_invalid');\n  const m=/^https:\\/\\/((?:www\\.)?(?:nalog\\.gov\\.ru|cbr\\.ru|vsrf\\.ru|pravo\\.ru|probankrotstvo\\.ru|kommersant\\.ru|rbc\\.ru))(\\/[^?#]*)?(\\?[^#]*)?(?:#[^\\s]*)?$/i.exec(value);\n  if(!m)throw Error('url_forbidden');\n  this.protocol='https:';this.hostname=m[1].toLowerCase();this.pathname=m[2]||'/';this.username='';this.password='';this.port='';this.hash='';\n  if(/%|\\/{2}|(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(this.pathname))throw Error('url_path_encoding_forbidden');\n  this.params=[];\n  for(const entry of (m[3]||'').slice(1).split('&').filter(Boolean)){\n   const at=entry.indexOf('=');const key=decodeURIComponent((at<0?entry:entry.slice(0,at)).replace(/\\+/g,' '));const val=decodeURIComponent((at<0?'':entry.slice(at+1)).replace(/\\+/g,' '));\n   if(/[\\u0000-\\u001f\\u007f]/.test(key+val)||this.params.some(x=>x[0]===key))throw Error('url_query_invalid');this.params.push([key,val]);\n  }\n  this.searchParams={keys:()=>this.params.map(x=>x[0]),get:key=>this.params.find(x=>x[0]===key)?.[1]??null,delete:key=>{this.params=this.params.filter(x=>x[0]!==key)},sort:()=>this.params.sort((a,b)=>a[0].localeCompare(b[0]))};\n }\n toString(){return 'https://'+this.hostname+this.pathname+(this.params.length?'?'+this.params.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'):'');}\n}\nfunction resolveOfficialLocation(location,base){\n if(typeof location!=='string'||!location||location.startsWith('//'))throw Error('redirect_forbidden');\n const current=new StrictURL(base);let target;\n if(location.startsWith('https://'))target=location;\n else if(location.startsWith('/'))target='https://'+current.hostname+location;\n else if(location.startsWith('?'))target='https://'+current.hostname+current.pathname+location;\n else throw Error('redirect_relative_path_unsupported');\n const next=new StrictURL(target);if(next.hostname.replace(/^www\\./,'')!==current.hostname.replace(/^www\\./,''))throw Error('cross_source_redirect');return next.toString();\n}\n\nconst QUALITY=(()=>{const URL=StrictURL;\n'use strict';\n// Standalone helpers. These do not fetch pages, classify their meaning, mutate state or publish.\n// Merge into actual deployed nodes. Do not replace the server's modified v0.4 wholesale.\nconst POLICY={\"version\": \"0.4.1\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"max_per_publisher_first_pass\": 2, \"require_exact_quotes_to_offer_topic\": false, \"verify_selected_topic_before_image\": true, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\"}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\"}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\"}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\"}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\"}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\"}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\"}], \"categories\": {\"bankruptcy\": \"Банкротство граждан\", \"credit\": \"Кредиты и МФО\", \"collection\": \"Приставы и взыскание\", \"property\": \"Жильё и имущество\", \"family\": \"Семья, наследство и долги\", \"fraud\": \"Финансовое мошенничество\", \"consumer\": \"Права потребителей финансовых услуг\"}};\nfunction error(code){throw Object.assign(new Error(code),{code});}\nfunction safeText(value,max=20000){\n  if(typeof value!=='string'||!value.trim()||value.length>max)error('text_invalid');\n  if(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u.test(value))error('text_controls');\n  return value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n}\nfunction normalizeExcerpt(value){\n  return safeText(value,60000).replace(/[\\u00ad\\u200b\\ufeff]/gu,'')\n    .replace(/[«»“”„‟]/gu,'\"').replace(/[‘’‚‛]/gu,\"'\")\n    .replace(/[\\u2010-\\u2015\\u2212]/gu,'-').replace(/\\s+/gu,' ').trim();\n}\nfunction checkExcerpt(quote,article){\n  try{\n    const q=normalizeExcerpt(quote),body=normalizeExcerpt(article);\n    // A short factual phrase may be legitimate. No arbitrary 15-character cutoff.\n    if((q.match(/[\\p{L}\\p{N}]+/gu)||[]).length<2)return {matched:false,reason:'fragment_too_short'};\n    const at=body.indexOf(q);\n    return at<0 ? {matched:false,reason:'text_differs'} : {matched:true,reason:'normalized_match',normalized_excerpt:q};\n  }catch(e){return {matched:false,reason:e.code||'evidence_invalid'};}\n}\nfunction partitionEvidence(facts,article){\n  if(!Array.isArray(facts))error('facts_invalid');\n  const matched=[],needs_review=[];\n  facts.forEach((fact,index)=>{\n    const result=checkExcerpt(fact?.evidence,article);\n    const row={index,fact,result};\n    (result.matched?matched:needs_review).push(row);\n  });\n  // Matching is provenance, NOT semantic proof. Caller must review the selected news.\n  return {matched,needs_review,topic_must_not_be_dropped_for_this_alone:true,semantic_review_required:true};\n}\nfunction sourceBlocks(article,documentKey){\n  if(typeof documentKey!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(documentKey))error('document_key_invalid');\n  const paragraphs=safeText(article,60000).split(/\\n\\s*\\n/u).filter(p=>p.trim());\n  const chunks=[];\n  for(let paragraph of paragraphs){\n    while(paragraph.length>1800){\n      let cut=paragraph.lastIndexOf(' ',1800);\n      if(cut<900)cut=1800;\n      chunks.push(paragraph.slice(0,cut));paragraph=paragraph.slice(cut).trimStart();\n    }\n    if(paragraph.trim())chunks.push(paragraph.trim());\n  }\n  return {document_key:documentKey,blocks:chunks.map((text,i)=>({id:'p'+String(i+1).padStart(3,'0'),text}))};\n}\nfunction resolveEvidenceRefs(fact,document){\n  if(!fact||fact.document_key!==document.document_key)error('wrong_source_document');\n  if(!Array.isArray(fact.source_ids)||!fact.source_ids.length||fact.source_ids.length>3||new Set(fact.source_ids).size!==fact.source_ids.length)error('source_ids_invalid');\n  const map=new Map(document.blocks.map(b=>[b.id,b.text]));\n  return fact.source_ids.map(id=>{\n    if(!map.has(id))error('source_block_not_found');\n    return {id,text:map.get(id)};\n  });\n}\nfunction sourceURL(raw){\n  if(typeof raw!=='string'||raw.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(raw))error('url_invalid');\n  let u;try{u=new URL(raw);}catch{error('url_invalid');}\n  if(u.protocol!=='https:'||u.username||u.password||u.port)error('url_forbidden');\n  const source=POLICY.sources.find(s=>u.hostname===s.host||u.hostname==='www.'+s.host);\n  if(!source)error('source_not_allowed');\n  u.hostname=source.host;u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  if(source.host==='pravo.ru')u.pathname=u.pathname.replace(/(\\/(?:news|story)\\/\\d+)\\/amp\\/?$/,'$1/');const p=u.pathname;\n  const article = source.host==='nalog.gov.ru' ? /^\\/rn\\d+\\/(?:news\\/[^/]+|ifns\\/imns\\d+_\\d+\\/info)\\/\\d+\\/?$/.test(p)\n    : source.host==='cbr.ru' ? /^\\/press\\/(?:event|regevent)\\/\\/?$/i.test(p)&&/^\\d+$/.test(u.searchParams.get('id')||'') || /^\\/press\\/pr\\/\\/?$/i.test(p)&&!!u.searchParams.get('file')\n    : source.host==='vsrf.ru' ? /^(?:\\/news|\\/press_center\\/news)\\/\\d+\\/?$/.test(p)\n    : source.host==='pravo.ru' ? /^\\/(?:news|story)\\/\\d+\\/?$/.test(p)\n    : source.host==='probankrotstvo.ru' ? /^\\/(?:news|article|articles)\\/[a-z0-9-]+-\\d+\\/?$/.test(p)\n    : source.host==='kommersant.ru' ? /^\\/doc\\/\\d+\\/?$/.test(p)\n    : source.host==='rbc.ru' ? /^\\/[a-z0-9_-]+\\/\\d{2}\\/\\d{2}\\/\\d{4}\\/[a-f0-9]{24}\\/?$/i.test(p)\n    : false;\n  if(!article)error('article_path_needs_adapter');\n  if(source.host==='vsrf.ru')u.pathname='/news/'+p.match(/\\d+/)[0];\n  else if(source.host==='nalog.gov.ru'||source.host==='pravo.ru')u.pathname=p.replace(/\\/?$/,'/');\n  else if(source.host==='cbr.ru')u.pathname=p.toLowerCase().replace(/\\/?$/,'/');\n  else u.pathname=p.replace(/\\/$/,'');\n  const keep=source.host==='cbr.ru'?(/event/i.test(p)?'id':'file'):null;\n  for(const key of [...u.searchParams.keys()])if(key!==keep)u.searchParams.delete(key);\n  return {url:u.toString(),publisher:source.host,source_name:source.name,source_kind:source.kind};\n}\nfunction dateValue(value){\n  if(typeof value!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))error('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if(!Number.isFinite(d.getTime())||d.toISOString().slice(0,10)!==value)error('invalid_date');\n  return value;\n}\n// read is trusted HTTP/extractor output; judgment is untrusted classifier output.\n// Actual reading, redirects, trace provenance and publication-date extraction stay in n8n.\nfunction offerTopic(read,judgment,window){\n  if(!read||!judgment||judgment.read_id!==read.read_id)error('read_id_mismatch');\n  const src=sourceURL(read.url);\n  if(read.transport!=='http'||read.status_code!==200||read.date_kind!=='published'||read.discovery_bound!==true)error('source_read_required');\n  safeText(read.article_text,60000);\n  const day=dateValue(read.published_date),from=dateValue(window.from),to=dateValue(window.to);\n  if(from>to)error('date_window_invalid');\n  if(day<from||day>to)return {offer:false,reason:'outside_window'};\n  if(judgment.relevance==='unrelated')return {offer:false,reason:'unrelated'};\n  if(!['direct','adjacent','context'].includes(judgment.relevance))error('relevance_invalid');\n  if(!Object.hasOwn(POLICY.categories,judgment.category))error('category_invalid');\n  if(!['news','analysis','opinion','explanation','statistics','advertisement'].includes(judgment.content_type))error('content_type_invalid');\n  if(judgment.content_type==='advertisement')return {offer:false,reason:'advertisement'};\n  return {offer:true,read_id:read.read_id,...src,published_date:day,\n    title:safeText(judgment.title,160),angle:safeText(judgment.angle,350),\n    relevance:judgment.relevance,category:judgment.category,category_label:POLICY.categories[judgment.category],\n    content_type:judgment.content_type,verification_state:'on_selection',\n    score:judgment.relevance==='direct'?30:judgment.relevance==='adjacent'?20:10};\n}\nfunction rankTopics(topics,{excludedURLs=[],limit=POLICY.max_topics}={}){\n  if(!Number.isInteger(limit)||limit<1||limit>POLICY.max_topics)error('limit_invalid');\n  const seen=new Set(excludedURLs.map(url=>sourceURL(url).url)),unique=[];\n  for(const topic of [...topics].filter(t=>t.offer===true).sort((a,b)=>b.score-a.score||b.published_date.localeCompare(a.published_date))){\n    const url=sourceURL(topic.url).url;\n    if(seen.has(url))continue;seen.add(url);unique.push(topic);\n  }\n  const selected=[],deferred=[],counts=new Map();\n  for(const t of unique){\n    if((counts.get(t.publisher)||0)>=POLICY.max_per_publisher_first_pass){deferred.push(t);continue;}\n    selected.push(t);counts.set(t.publisher,(counts.get(t.publisher)||0)+1);\n  }\n  // Diversity is a preference. Do not discard useful topics when only one source has them.\n  return [...selected,...deferred].slice(0,limit);\n}\nfunction topicText(topic){\n  return safeText(`${topic.title}\\n\\nИдея поста: ${topic.angle}\\n\\n${topic.category_label} · ${topic.published_date}\\n${topic.source_name}\\n${topic.url}\\n\\nПосле выбора проверю факты и подготовлю черновик.`,4096);\n}\nreturn {POLICY,normalizeExcerpt,checkExcerpt,partitionEvidence,sourceBlocks,resolveEvidenceRefs,\n  sourceURL,offerTopic,rankTopics,topicText};\n\n})();\nconst NEWS=(()=>{const URL=StrictURL;\n'use strict';\n// Pure helpers. No network, database, paid calls or Telegram mutations.\n// Port to n8n Code nodes against the actual runtime; require is not assumed there.\nconst CONFIG={\"version\": \"0.4.1\", \"timezone\": \"Europe/Moscow\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"selection_ttl_hours\": 24, \"caption_limit_utf16\": 1024, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\", \"enabled\": true}]};\nconst STAGES = Object.freeze({\n  proposal:'Инициатива', bill:'Законопроект', adopted:'Принято, вступление в силу не подтверждено',\n  in_force:'Действует', court:'Судебная практика', explanation:'Разъяснение', statistics:'Статистика'\n});\nconst CONTROLS = /[\\u0000-\\u0008\\u000b-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u;\nfunction fail(code) { throw Object.assign(new Error(code), {code}); }\nfunction clean(value, max, field='text') {\n  if (typeof value !== 'string') fail(field+'_type');\n  const s=value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n  if (!s || s.length>max || CONTROLS.test(s)) fail(field+'_invalid');\n  return s;\n}\nfunction instant(value) {\n  const t=value instanceof Date ? value.getTime() : Date.parse(value);\n  if (!Number.isFinite(t)) fail('invalid_clock');\n  return t;\n}\nfunction isoDay(value) {\n  if (typeof value!=='string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) fail('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if (!Number.isFinite(d.getTime()) || d.toISOString().slice(0,10)!==value) fail('invalid_date');\n  return value;\n}\nfunction parseDateEvidence(value) {\n  const s=clean(value,150,'publication_date_raw').toLowerCase();\n  let m=s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:$|t\\d{2}:)/);\n  if (m) return isoDay(`${m[1]}-${m[2]}-${m[3]}`);\n  m=s.match(/^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:$|\\s)/);\n  if (m) return isoDay(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  const months=['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];\n  m=s.match(/^(\\d{1,2})\\s+([а-я]+)\\s+(\\d{4})(?:$|\\s)/);\n  if (m && months.includes(m[2])) return isoDay(`${m[3]}-${String(months.indexOf(m[2])+1).padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  fail('publication_date_unreadable');\n}\nfunction windowFor(days=7, now=new Date()) {\n  if (!CONFIG.allowed_days.includes(days)) fail('days_not_supported');\n  const parts=new Intl.DateTimeFormat('en-CA',{timeZone:CONFIG.timezone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(instant(now)));\n  const get=t=>parts.find(p=>p.type===t).value;\n  const to=`${get('year')}-${get('month')}-${get('day')}`;\n  const from=new Date(Date.parse(to+'T00:00:00Z')-(days-1)*86400000).toISOString().slice(0,10);\n  return {from,to,days,timezone:CONFIG.timezone};\n}\nfunction parseCommand(text, botUsername='bflcontentbot') {\n  if (typeof text!=='string') return null;\n  const match=text.trim().match(/^\\/news(?:@([a-z0-9_]+))?(?:\\s+([\\s\\S]*))?$/i);\n  if (!match) return null;\n  if (match[1] && match[1].toLowerCase()!==botUsername.toLowerCase()) return null;\n  let tail=(match[2]||'').trim(),days=CONFIG.default_days;\n  const d=tail.match(/^(\\d+)(?:\\s+|$)/);\n  if(d){ days=Number(d[1]); if(!CONFIG.allowed_days.includes(days))fail('days_not_supported'); tail=tail.slice(d[0].length).trim(); }\n  return {days,topic:tail ? clean(tail,200,'topic') : ''};\n}\nfunction canonicalURL(value) {\n  if (typeof value!=='string' || value.length>1500 || /[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value)) fail('url_invalid');\n  let u; try{ u=new URL(value); }catch{ fail('url_invalid'); }\n  if(u.protocol!=='https:' || u.username || u.password || u.port) fail('url_forbidden');\n  const source=CONFIG.sources.find(s=>s.enabled && (u.hostname===s.host || u.hostname==='www.'+s.host));\n  // Exact known hosts; do not follow arbitrary subdomains just because web_search allows them.\n  if(!source)fail('source_not_allowed');\n  if(u.hostname==='www.'+source.host)u.hostname=source.host;\n  u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  return u.toString();\n}\nfunction articleURL(value){return QUALITY.sourceURL(value).url;}\nfunction sourceName(url) { const host=new URL(canonicalURL(url)).hostname; return CONFIG.sources.find(s=>s.host===host).name; }\nfunction normalizedEvidence(text) { return text.normalize('NFC').replace(/\\s+/gu,' ').trim(); }\nfunction quoteIn(quote,body) {\n  const q=clean(quote,800,'evidence');\n  if(q.length<15 || !normalizedEvidence(body).includes(normalizedEvidence(q)))fail('evidence_not_found');\n  return q;\n}\n// read MUST come from an actual HTTP reader/extractor. Never pass a model-produced read object.\nfunction validateRead(read, searchURLs, win, now=new Date()) {\n  if(!read || read.transport!=='http' || read.status_code!==200 || !/^text\\/html(?:;|$)/i.test(read.content_type||''))fail('page_not_read');\n  const requested=articleURL(read.requested_url), url=articleURL(read.final_url);\n  if(!searchURLs.has(requested))fail('not_in_search_sources');\n  if(new URL(requested).hostname!==new URL(url).hostname)fail('cross_source_redirect');\n  const age=instant(now)-instant(read.fetched_at);\n  if(age<0 || age>3600000)fail('read_stale');\n  if(read.date_kind!=='published')fail('publication_date_required');\n  const raw=clean(read.publication_date_raw,150,'publication_date_raw');\n  const context=clean(read.publication_date_context,1200,'publication_date_context');\n  if(!normalizedEvidence(context).includes(normalizedEvidence(raw)))fail('date_context_mismatch');\n  if(/dateModified|дата обновления|последнее обновление/i.test(context))fail('updated_is_not_published');\n  const published=parseDateEvidence(raw);\n  if(published<isoDay(win.from) || published>isoDay(win.to))fail('outside_date_window');\n  return {url,requested_url:requested,published_date:published,article_text:clean(read.article_text,60000,'article_text'),\n    source_name:sourceName(url),fetched_at:read.fetched_at,publication_date_context:context};\n}\nfunction validateCandidate(candidate, read, searchURLs, win, now=new Date()) {\n  const page=validateRead(read,searchURLs,win,now);\n  if(!candidate || candidate.read_id!==read.read_id)fail('read_id_mismatch');\n  if(candidate.relevance!=='direct_personal_finance')fail('not_relevant_to_people');\n  if(!Object.hasOwn(STAGES,candidate.stage))fail('stage_unknown');\n  if(candidate.published_date!==page.published_date)fail('date_mismatch');\n  if(!Array.isArray(candidate.facts) || candidate.facts.length<1 || candidate.facts.length>4)fail('facts_invalid');\n  const facts=candidate.facts.map(f=>({text:clean(f.text,400,'fact'),evidence:quoteIn(f.evidence,page.article_text)}));\n  const stage_evidence=quoteIn(candidate.stage_evidence,page.article_text);\n  let effective_date=null;\n  if(candidate.effective_date!=null){\n    effective_date=isoDay(candidate.effective_date);\n    const evidence=quoteIn(candidate.effective_evidence,page.article_text);\n    const raw=clean(candidate.effective_date_raw,150,'effective_date_raw');\n    if(!normalizedEvidence(evidence).includes(normalizedEvidence(raw)) || parseDateEvidence(raw)!==effective_date)fail('effective_date_mismatch');\n    if(candidate.stage==='in_force' && effective_date>win.to)fail('not_in_force_yet');\n  }\n  // A verified date plus supporting words are mandatory before advertising that a new rule is in force.\n  if(candidate.stage==='in_force' && !effective_date)fail('effective_date_required');\n  return {url:page.url,source_name:page.source_name,published_date:page.published_date,\n    title:clean(candidate.title,150,'title'),summary:clean(candidate.summary,400,'summary'),\n    why:clean(candidate.why,220,'why'),stage:candidate.stage,stage_label:STAGES[candidate.stage],\n    effective_date,stage_evidence,facts,verified_at:page.fetched_at,\n    // Additional event clustering requires verified document IDs in the workflow; do not invent them.\n    dedup_key:page.url,format:'news_fact_pack_v1'};\n}\nfunction chooseTopics(items, excludedURLs=[], limit=CONFIG.max_topics) {\n  if(!Number.isInteger(limit)||limit<1||limit>CONFIG.max_topics)fail('limit_invalid');\n  const seen=new Set(excludedURLs.map(articleURL));\n  const result=[];\n  for(const item of [...items].sort((a,b)=>b.published_date.localeCompare(a.published_date))){\n    const key=articleURL(item.url);\n    if(seen.has(key))continue;\n    seen.add(key);result.push(item);if(result.length===limit)break;\n  }\n  return result;\n}\nfunction callbackData(batchId,itemId){\n  if(!/^[A-Za-z0-9_-]{1,16}$/.test(batchId)||!/^[A-Za-z0-9_-]{1,12}$/.test(itemId))fail('id_invalid');\n  return `news:${batchId}:${itemId}`;\n}\n// Advisory guard only. Perform and persist the state claim atomically AFTER this guard.\nfunction guardSelection({callback,batch,item,now=new Date()}) {\n  if(!callback||!batch||!item)fail('selection_missing');\n  if(String(callback.from_id)!==String(batch.owner_id)||String(callback.chat_id)!==String(batch.owner_id))fail('not_owner');\n  if(String(callback.message_id)!==String(item.message_id))fail('wrong_message');\n  if(callback.data!==callbackData(batch.id,item.id)||item.batch_id!==batch.id)fail('wrong_item');\n  if(instant(now)>=instant(batch.expires_at))fail('selection_expired');\n  if(batch.status!=='ready'||item.status!=='offered')fail('selection_not_available');\n  return {batch_id:batch.id,item_id:item.id,selection_key:`news:${batch.id}:${item.id}`};\n}\nfunction captionBudget(sourceURL) {\n  const suffix='\\n\\nИсточник: '+articleURL(sourceURL);\n  const body_limit=CONFIG.caption_limit_utf16-suffix.length;\n  if(body_limit<250)fail('source_url_too_long_for_caption');\n  return {suffix,body_limit,preferred_body_max:Math.min(800,body_limit)};\n}\nfunction composeNewsCaption(body,sourceURL) {\n  const {suffix,body_limit}=captionBudget(sourceURL);\n  const text=clean(body,body_limit,'caption_body');\n  // Model writes body only. Link is appended deterministically and retained through revision.\n  if(/https?:\\/\\//i.test(text))fail('unexpected_link_in_body');\n  return text+suffix;\n}\nfunction topicMessage(item){\n  const text=`${item.title}\\n\\n${item.summary}\\n\\nПочему это важно: ${item.why}\\n\\n${item.stage_label} · ${item.published_date}\\n${item.source_name}\\n${item.url}`;\n  return clean(text,4096,'topic_message');\n}\nfunction progressText(event,{count=0,days=7,partial=false}={}){\n  switch(event){\n    case 'accepted': return `Взято в работу. Ищу новости за последние ${days} дней.`;\n    case 'verifying': return 'Проверяю даты, источники и связь новостей с личными долгами.';\n    case 'ready': return `Готово. Тем на выбор: ${count}. Нажми «Подготовить пост» под подходящей новостью.`+(partial?' Часть источников проверить не удалось.':'');\n    case 'empty': return `За последние ${days} дней подходящих новостей с проверенным источником не нашлось.`+(days===7?' Можно расширить поиск командой /news 30.':'');\n    case 'partial_empty': return 'Полностью проверить источники не удалось; подтверждённых тем пока нет. Можно повторить поиск позже.';\n    case 'failed': return 'Поиск сейчас не завершился. Можно повторить /news позже. Существующие черновики сохранены.';\n    case 'selected': return 'Тема выбрана. Готовлю пост по источнику — затем пришлю на согласование.';\n    case 'expired': return 'Эта подборка устарела. Отправь /news, чтобы получить свежие темы.';\n    default: fail('progress_event_unknown');\n  }\n}\n\nreturn {CONFIG,STAGES,parseCommand,windowFor,parseDateEvidence,canonicalURL,articleURL,\n  validateRead,validateCandidate,chooseTopics,callbackData,guardSelection,captionBudget,\n  composeNewsCaption,topicMessage,progressText};})();\nconst RESP=(()=>{'use strict';\nconst core=NEWS;\nfunction modelName(model){\n  if(typeof model!=='string'||!model.trim()||model.includes('/')||/\\s/.test(model))throw Error('Configure a real OpenAI REST model ID with web_search support');\n  return model;\n}\nfunction searchRequest({model,instructions,days=7,topic='',now=new Date()}){\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Search instructions required');\n  const win=core.windowFor(days,now);\n  return {model:modelName(model),store:false,\n    instructions,\n    input:JSON.stringify({task:'Find recent official and editorial articles about personal finance, debts and rights in Russia',window:win,topic,max_candidates:core.CONFIG.max_candidates}),\n    tools:[{type:'web_search',external_web_access:true,filters:{allowed_domains:core.CONFIG.sources.filter(s=>s.enabled).map(s=>s.host)}}],\n    tool_choice:'required',include:['web_search_call.action.sources'],max_output_tokens:8000};\n}\nfunction outputJSON(response){\n  if(!response||response.status!=='completed'||response.error)throw Error('responses_not_completed');\n  const chunks=[];\n  for(const output of response.output||[]){\n    if(output.type!=='message')continue;\n    if(output.status && output.status!=='completed')throw Error('message_not_completed');\n    for(const part of output.content||[]){\n      if(part.type==='refusal')throw Error('model_refusal');\n      if(part.type==='output_text')chunks.push(part.text);\n    }\n  }\n  const text=chunks.join('\\n').trim().replace(/^```json\\s*/i,'').replace(/\\s*```$/,'');\n  let value;try{value=JSON.parse(text);}catch{throw Error('invalid_output_json');}\n  if(!value||Array.isArray(value)||typeof value!=='object')throw Error('invalid_output_object');\n  return value;\n}\nfunction parseDiscovery(response){\n  const value=outputJSON(response);\n  if(!(response.output||[]).some(o=>o.type==='web_search_call'&&o.status==='completed'&&o.action?.type==='search'))throw Error('web_search_not_executed');\n  const searchURLs=new Set();\n  function remember(url){try{searchURLs.add(core.articleURL(url));}catch{/* Skip unsupported sources. */}}\n  for(const output of response.output||[]){\n    if(output.type==='web_search_call'&&output.status==='completed'){\n      for(const s of output.action?.sources||[])remember(s.url);\n      if(output.action?.type==='open_page')remember(output.action.url);\n    }\n    if(output.type==='message')for(const p of output.content||[])for(const a of p.annotations||[])if(a.type==='url_citation')remember(a.url);\n  }\n  if(!Array.isArray(value.candidates)||value.candidates.length>core.CONFIG.max_candidates)throw Error('invalid_candidates');\n  const candidates=[],rejected=[];const seen=new Set();\n  for(const candidate of value.candidates){\n    try{\n      const url=core.articleURL(candidate.url);\n      if(!searchURLs.has(url))throw Error('candidate_not_in_search_sources');\n      if(seen.has(url))continue;\n      seen.add(url);candidates.push({url}); // Discard model guesses about dates/facts until the page is read.\n    }catch(error){rejected.push({code:error.code||error.message});}\n  }\n  return {candidates,search_urls:[...searchURLs],rejected,response_id:response.id||null};\n}\nfunction verificationRequest({model,instructions,reads,searchURLs,win,now=new Date()}){\n  if(!Array.isArray(reads)||!reads.length||reads.length>5)throw Error('verification_batch_size');\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Verification instructions required');\n  const ids=new Set();\n  const pages=reads.map(read=>{\n    if(typeof read.read_id!=='string'||!read.read_id||ids.has(read.read_id))throw Error('read_id_invalid');\n    ids.add(read.read_id);\n    const page=core.validateRead(read,searchURLs,win,now);\n    // Reader must extract the article rather than sending the entire site navigation.\n    if(page.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n    return {read_id:read.read_id,published_date:page.published_date,url:page.url,article_text:page.article_text};\n  });\n  return {model:modelName(model),store:false,instructions,input:JSON.stringify({window:win,pages}),max_output_tokens:8000};\n}\nfunction parseVerification(response,reads,searchURLs,win,now=new Date()){\n  const value=outputJSON(response);\n  if(!Array.isArray(value.results)||value.results.length!==reads.length)throw Error('verification_results_incomplete');\n  const remaining=new Map(reads.map(r=>[r.read_id,r]));\n  const accepted=[],rejected=[];\n  for(const result of value.results){\n    const read=remaining.get(result.read_id);\n    if(!read)throw Error('unknown_or_duplicate_read_id');\n    remaining.delete(result.read_id);\n    if(result.accept===false){rejected.push({read_id:read.read_id,code:'editorial_exclusion',reason:String(result.reason||'').slice(0,400)});continue;}\n    if(result.accept!==true)throw Error('verification_decision_missing');\n    try{accepted.push(core.validateCandidate(result,read,searchURLs,win,now));}\n    catch(error){rejected.push({read_id:read.read_id,code:error.code||error.message});}\n  }\n  return {accepted,rejected};\n}\n\nreturn {searchRequest,outputJSON,parseDiscovery,verificationRequest,parseVerification};})();\nfunction snapshotOf(topic){\n if(topic.format!=='news_topic_v2'&&topic.format!=='news_fact_pack_v2')throw Error('news_format_invalid');\n const snap=topic.snapshot;\n if(!snap||snap.url!==NEWS.articleURL(topic.url)||snap.published_date!==topic.published_date)throw Error('snapshot_source_mismatch');\n const blocks=QUALITY.sourceBlocks(snap.article_text,snap.document_key);\n if(JSON.stringify(blocks.blocks)!==JSON.stringify(snap.blocks))throw Error('snapshot_blocks_mismatch');\n return snap;\n}\nfunction strictRequest(instructions,input,schema,name,max=4500){return {model:'gpt-5.6-sol',reasoning:{effort:'low'},store:false,max_output_tokens:max,instructions,input:JSON.stringify(input),text:{format:{type:'json_schema',name,strict:true,schema}}};}\nfunction apiResult(r,prefix){\n if(r.statusCode!==200)throw Error(prefix+(r.statusCode?'_http_'+r.statusCode+(r.statusCode>=500?'_outcome_unknown':''):'_outcome_unknown'));\n if(r.body?.status==='incomplete')throw Error(prefix+'_incomplete');\n if(r.body?.output?.some(x=>x.content?.some(c=>c.type==='refusal')))throw Error(prefix+'_refusal');\n try{return RESP.outputJSON(r.body);}catch{throw Error(prefix+'_malformed_json');}\n}\nfunction verifiedFacts(topic,result){\n const snap=snapshotOf(topic);\n if(result.document_key!==snap.document_key)throw Error('wrong_source_document');\n if(result.status!=='ready')throw Error('selected_needs_source_check');\n if(!['news','analysis','opinion','explanation','statistics','court','bill','adopted','in_force'].includes(result.content_type))throw Error('selected_content_type_invalid');\n // A news report is not itself an enacted legal document. Narrow attributed reporting remains available.\n if(result.content_type==='in_force')throw Error('selected_primary_document_required');\n if(!Array.isArray(result.facts)||!result.facts.length||result.facts.length>4)throw Error('selected_facts_invalid');\n const facts=result.facts.map(f=>{if(typeof f.text!=='string'||!f.text.trim()||f.text.length>700||!Array.isArray(f.qualifiers)||typeof f.attribution!=='string')throw Error('selected_fact_invalid');return {...f,source_blocks:QUALITY.resolveEvidenceRefs(f,snap)};});\n return {...topic,format:'news_fact_pack_v2',verification_state:'facts_selected',facts,content_type:result.content_type,limitations:result.limitations,selected_at:new Date().toISOString()};\n}\nfunction previewInput(s){\n const fact=JSON.parse(s.fact_pack);\n if(NEWS.articleURL(fact.url)!==s.source_url||NEWS.composeNewsCaption(s.body,s.source_url)!==s.caption)throw Error('news_caption_source_mismatch');\n let source;\n if(fact.format==='news_fact_pack_v2'){source=snapshotOf(fact);for(const f of fact.facts)QUALITY.resolveEvidenceRefs(f,source);}\n else if(fact.format==='news_fact_pack_v1')source=QUALITY.sourceBlocks([fact.stage_evidence,...fact.facts.map(f=>f.evidence)].filter(Boolean).join('\\n\\n'),'legacy_saved_evidence');\n else throw Error('news_facts_not_selected');\n return {caption_body:s.body,cover_title:s.cover_title,cover_subtitle:s.cover_subtitle,facts:fact.facts,content_type:fact.content_type||fact.stage,limitations:fact.limitations||[],source,source_url:s.source_url,legacy_evidence_only:fact.format==='news_fact_pack_v1'};\n}\nfunction assessPreview(result){\n if(typeof result.pass!=='boolean'||!Array.isArray(result.issues)||result.issues.length>10)throw Error('preview_malformed_json');\n if(result.pass&&result.issues.length)throw Error('preview_contradictory_result');\n if(!result.pass&&!result.issues.length)throw Error('preview_empty_failure');\n for(const i of result.issues)if(!['caption_body','cover_title','cover_subtitle'].includes(i.field)||typeof i.fragment!=='string'||!i.fragment||typeof i.suggested_replacement!=='string'||typeof i.problem!=='string')throw Error('preview_issue_invalid');\n return result;\n}\nfunction repairPreview(s,issues){\n const out={...s};let exact=null;try{exact=JSON.parse(s.exact_revision_json||'null');}catch{}\n // A precise owner edit may only be corrected inside its replacement span. The saved cover stays byte-identical.\n const bounds=exact&&!exact.validation_error?{start:s.body.indexOf(exact.newText),length:exact.newText?.length||0}:null;\n if(bounds&&(bounds.start<0||s.body.indexOf(exact.newText,bounds.start+1)>=0))throw Error('preview_exact_span_ambiguous');\n const edits={};\n for(const issue of issues){const key=issue.field==='caption_body'?'body':issue.field,old=out[key];if(typeof old!=='string')throw Error('preview_field_missing');const at=old.indexOf(issue.fragment);if(at<0||old.indexOf(issue.fragment,at+1)>=0)throw Error('preview_repair_ambiguous');\n if(bounds&&(key!=='body'||at<bounds.start||at+issue.fragment.length>bounds.start+bounds.length))throw Error('preview_exact_requires_owner_edit');\n (edits[key]||=[]).push({at,end:at+issue.fragment.length,text:issue.suggested_replacement});}\n for(const [key,changes] of Object.entries(edits)){changes.sort((a,b)=>b.at-a.at);let last=Infinity;for(const e of changes){if(e.end>last)throw Error('preview_repair_overlap');out[key]=out[key].slice(0,e.at)+e.text+out[key].slice(e.end);last=e.at;}}\n if(!out.cover_title||out.cover_title.length>70||out.cover_subtitle.length>110)throw Error('preview_cover_bounds');\n out.caption=NEWS.composeNewsCaption(out.body,out.source_url);out.text=out.caption;out.channel_caption=out.caption;out.caption_length=out.caption.length;\n if(bounds){exact.body=out.body;exact.caption=out.caption;out.exact_revision_json=JSON.stringify(exact);}\n return out;\n}\nconst ctx=$('Проверить HTTP 2').first().json,r=$json;let out={...ctx,follow:false,status_code:r.statusCode||0,content_type:r.headers?.['content-type']||'',fetched_at:new Date().toISOString(),final_url:ctx.url};\nif(r.statusCode>=300&&r.statusCode<400){try{if(3>=3)throw Error('redirect_limit');const target=resolveOfficialLocation(r.headers?.location,ctx.url);NEWS.articleURL(target);return {json:{...out,url:target,redirects:3+1,follow:true}};}catch(e){return {json:{...out,read_error:e.message}};}}\nif(r.statusCode!==200)return {json:{...out,read_error:r.statusCode?'http_'+r.statusCode:'transport_error'}};\nif(!/^text\\/html(?:;|$)/i.test(out.content_type))return {json:{...out,read_error:'content_type_unsupported'}};\nconst html=r.body;if(typeof html!=='string'||!html)return {json:{...out,read_error:'empty_response'}};\n// Reject oversized/incomplete documents before HTML parsing; no partial text reaches the model.\nif(html.length>1048576||Number(r.headers?.['content-length']||0)>1048576)return {json:{...out,read_error:'response_too_large'}};\nconst host=new StrictURL(ctx.url).hostname.replace(/^www\\./,'');\nconst selectors={\n 'nalog.gov.ru':{body_selector:'.page-content__center',date_selector:'.page-content__center > p.gray',time_selector:'time[data-unavailable]'},\n 'cbr.ru':{body_selector:'.landing-text',date_selector:'.news-info-line_date',time_selector:'time[data-unavailable]'},\n 'pravo.ru':{body_selector:'section.article-content',date_selector:'[data-bfl-none]',time_selector:'time[itemprop=datePublished][datetime]'},\n 'probankrotstvo.ru':{body_selector:'.post-content',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'kommersant.ru':{body_selector:'.doc__text, .doc__intro',date_selector:'[data-bfl-none]',time_selector:'time.doc_header__publish_time[datetime]'},\n 'rbc.ru':{body_selector:'.article__text',date_selector:'[data-bfl-none]',time_selector:'[data-bfl-none]'},\n 'vsrf.ru':{body_selector:'div.text-text-primary[class*=\"cms-info__icon\"]',date_selector:'time .sr-only',time_selector:'time[datetime]'}\n};\nreturn {json:{...out,...selectors[host],html}};\n\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        660,
        240
      ]
    },
    {
      "id": "ce364d3f-f20a-4787-b553-f41cacb39937",
      "name": "Граница чтения",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\nreturn {json:$json};\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        880,
        240
      ]
    },
    {
      "id": "13260c90-9410-46f3-a1b9-e2736d6638cf",
      "name": "HTML доступен?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ !$json.read_error }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "position": [
        1100,
        240
      ]
    },
    {
      "id": "c3a4a37f-3fa6-47a6-95ab-bd69499fc119",
      "name": "Извлечь статью и дату",
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "parameters": {
        "operation": "extractHtmlContent",
        "sourceData": "json",
        "dataPropertyName": "html",
        "extractionValues": {
          "values": [
            {
              "key": "article_text",
              "cssSelector": "={{ $json.body_selector }}",
              "returnValue": "text",
              "skipSelectors": "script,style,nav,footer,aside,form,button,.item_region,.page-info,.print,.social,.share,.news-info-line,.print-hidden,.sr-only,.post-content__banner,.banner,.subscribe,.paywall,.read-more,.related",
              "returnArray": true
            },
            {
              "key": "date_blocks",
              "cssSelector": "={{ $json.date_selector }}",
              "returnValue": "text",
              "returnArray": true
            },
            {
              "key": "time_attrs",
              "cssSelector": "={{ $json.time_selector }}",
              "returnValue": "attribute",
              "attribute": "datetime",
              "returnArray": true
            },
            {
              "key": "meta_dates",
              "cssSelector": "meta[property=\"article:published_time\"],meta[itemprop=\"datePublished\"]",
              "returnValue": "attribute",
              "attribute": "content",
              "returnArray": true
            },
            {
              "key": "lead_text",
              "cssSelector": "={{ $json.body_selector === \".landing-text\" ? \".lead-text\" : \"[data-bfl-no-lead]\" }}",
              "returnValue": "text",
              "returnArray": false
            },
            {
              "key": "meta_title",
              "cssSelector": "meta[property=\"og:title\"]",
              "returnValue": "attribute",
              "attribute": "content",
              "returnArray": false
            },
            {
              "key": "article_title",
              "cssSelector": "h1",
              "returnValue": "text",
              "returnArray": false
            }
          ]
        },
        "options": {
          "trimValues": true,
          "cleanUpText": false
        }
      },
      "position": [
        1320,
        240
      ],
      "onError": "continueRegularOutput"
    },
    {
      "id": "c71b1f1f-5fb4-454f-973d-c6d4b8744922",
      "name": "Проверить извлечённую статью",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\n// Small, deliberately restrictive URL adapter for the n8n sandbox (global URL is absent).\n// Only the official ASCII hosts used by this workflow are representable.\nclass StrictURL {\n constructor(value){\n  if(typeof value!=='string'||value.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value))throw Error('url_invalid');\n  const m=/^https:\\/\\/((?:www\\.)?(?:nalog\\.gov\\.ru|cbr\\.ru|vsrf\\.ru|pravo\\.ru|probankrotstvo\\.ru|kommersant\\.ru|rbc\\.ru))(\\/[^?#]*)?(\\?[^#]*)?(?:#[^\\s]*)?$/i.exec(value);\n  if(!m)throw Error('url_forbidden');\n  this.protocol='https:';this.hostname=m[1].toLowerCase();this.pathname=m[2]||'/';this.username='';this.password='';this.port='';this.hash='';\n  if(/%|\\/{2}|(?:^|\\/)\\.{1,2}(?:\\/|$)/.test(this.pathname))throw Error('url_path_encoding_forbidden');\n  this.params=[];\n  for(const entry of (m[3]||'').slice(1).split('&').filter(Boolean)){\n   const at=entry.indexOf('=');const key=decodeURIComponent((at<0?entry:entry.slice(0,at)).replace(/\\+/g,' '));const val=decodeURIComponent((at<0?'':entry.slice(at+1)).replace(/\\+/g,' '));\n   if(/[\\u0000-\\u001f\\u007f]/.test(key+val)||this.params.some(x=>x[0]===key))throw Error('url_query_invalid');this.params.push([key,val]);\n  }\n  this.searchParams={keys:()=>this.params.map(x=>x[0]),get:key=>this.params.find(x=>x[0]===key)?.[1]??null,delete:key=>{this.params=this.params.filter(x=>x[0]!==key)},sort:()=>this.params.sort((a,b)=>a[0].localeCompare(b[0]))};\n }\n toString(){return 'https://'+this.hostname+this.pathname+(this.params.length?'?'+this.params.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&'):'');}\n}\nfunction resolveOfficialLocation(location,base){\n if(typeof location!=='string'||!location||location.startsWith('//'))throw Error('redirect_forbidden');\n const current=new StrictURL(base);let target;\n if(location.startsWith('https://'))target=location;\n else if(location.startsWith('/'))target='https://'+current.hostname+location;\n else if(location.startsWith('?'))target='https://'+current.hostname+current.pathname+location;\n else throw Error('redirect_relative_path_unsupported');\n const next=new StrictURL(target);if(next.hostname.replace(/^www\\./,'')!==current.hostname.replace(/^www\\./,''))throw Error('cross_source_redirect');return next.toString();\n}\n\nconst QUALITY=(()=>{const URL=StrictURL;\n'use strict';\n// Standalone helpers. These do not fetch pages, classify their meaning, mutate state or publish.\n// Merge into actual deployed nodes. Do not replace the server's modified v0.4 wholesale.\nconst POLICY={\"version\": \"0.4.1\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"max_per_publisher_first_pass\": 2, \"require_exact_quotes_to_offer_topic\": false, \"verify_selected_topic_before_image\": true, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\"}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\"}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\"}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\"}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\"}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\"}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\"}], \"categories\": {\"bankruptcy\": \"Банкротство граждан\", \"credit\": \"Кредиты и МФО\", \"collection\": \"Приставы и взыскание\", \"property\": \"Жильё и имущество\", \"family\": \"Семья, наследство и долги\", \"fraud\": \"Финансовое мошенничество\", \"consumer\": \"Права потребителей финансовых услуг\"}};\nfunction error(code){throw Object.assign(new Error(code),{code});}\nfunction safeText(value,max=20000){\n  if(typeof value!=='string'||!value.trim()||value.length>max)error('text_invalid');\n  if(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u.test(value))error('text_controls');\n  return value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n}\nfunction normalizeExcerpt(value){\n  return safeText(value,60000).replace(/[\\u00ad\\u200b\\ufeff]/gu,'')\n    .replace(/[«»“”„‟]/gu,'\"').replace(/[‘’‚‛]/gu,\"'\")\n    .replace(/[\\u2010-\\u2015\\u2212]/gu,'-').replace(/\\s+/gu,' ').trim();\n}\nfunction checkExcerpt(quote,article){\n  try{\n    const q=normalizeExcerpt(quote),body=normalizeExcerpt(article);\n    // A short factual phrase may be legitimate. No arbitrary 15-character cutoff.\n    if((q.match(/[\\p{L}\\p{N}]+/gu)||[]).length<2)return {matched:false,reason:'fragment_too_short'};\n    const at=body.indexOf(q);\n    return at<0 ? {matched:false,reason:'text_differs'} : {matched:true,reason:'normalized_match',normalized_excerpt:q};\n  }catch(e){return {matched:false,reason:e.code||'evidence_invalid'};}\n}\nfunction partitionEvidence(facts,article){\n  if(!Array.isArray(facts))error('facts_invalid');\n  const matched=[],needs_review=[];\n  facts.forEach((fact,index)=>{\n    const result=checkExcerpt(fact?.evidence,article);\n    const row={index,fact,result};\n    (result.matched?matched:needs_review).push(row);\n  });\n  // Matching is provenance, NOT semantic proof. Caller must review the selected news.\n  return {matched,needs_review,topic_must_not_be_dropped_for_this_alone:true,semantic_review_required:true};\n}\nfunction sourceBlocks(article,documentKey){\n  if(typeof documentKey!=='string'||!/^[A-Za-z0-9_-]{1,80}$/.test(documentKey))error('document_key_invalid');\n  const paragraphs=safeText(article,60000).split(/\\n\\s*\\n/u).filter(p=>p.trim());\n  const chunks=[];\n  for(let paragraph of paragraphs){\n    while(paragraph.length>1800){\n      let cut=paragraph.lastIndexOf(' ',1800);\n      if(cut<900)cut=1800;\n      chunks.push(paragraph.slice(0,cut));paragraph=paragraph.slice(cut).trimStart();\n    }\n    if(paragraph.trim())chunks.push(paragraph.trim());\n  }\n  return {document_key:documentKey,blocks:chunks.map((text,i)=>({id:'p'+String(i+1).padStart(3,'0'),text}))};\n}\nfunction resolveEvidenceRefs(fact,document){\n  if(!fact||fact.document_key!==document.document_key)error('wrong_source_document');\n  if(!Array.isArray(fact.source_ids)||!fact.source_ids.length||fact.source_ids.length>3||new Set(fact.source_ids).size!==fact.source_ids.length)error('source_ids_invalid');\n  const map=new Map(document.blocks.map(b=>[b.id,b.text]));\n  return fact.source_ids.map(id=>{\n    if(!map.has(id))error('source_block_not_found');\n    return {id,text:map.get(id)};\n  });\n}\nfunction sourceURL(raw){\n  if(typeof raw!=='string'||raw.length>1500||/[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(raw))error('url_invalid');\n  let u;try{u=new URL(raw);}catch{error('url_invalid');}\n  if(u.protocol!=='https:'||u.username||u.password||u.port)error('url_forbidden');\n  const source=POLICY.sources.find(s=>u.hostname===s.host||u.hostname==='www.'+s.host);\n  if(!source)error('source_not_allowed');\n  u.hostname=source.host;u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  if(source.host==='pravo.ru')u.pathname=u.pathname.replace(/(\\/(?:news|story)\\/\\d+)\\/amp\\/?$/,'$1/');const p=u.pathname;\n  const article = source.host==='nalog.gov.ru' ? /^\\/rn\\d+\\/(?:news\\/[^/]+|ifns\\/imns\\d+_\\d+\\/info)\\/\\d+\\/?$/.test(p)\n    : source.host==='cbr.ru' ? /^\\/press\\/(?:event|regevent)\\/\\/?$/i.test(p)&&/^\\d+$/.test(u.searchParams.get('id')||'') || /^\\/press\\/pr\\/\\/?$/i.test(p)&&!!u.searchParams.get('file')\n    : source.host==='vsrf.ru' ? /^(?:\\/news|\\/press_center\\/news)\\/\\d+\\/?$/.test(p)\n    : source.host==='pravo.ru' ? /^\\/(?:news|story)\\/\\d+\\/?$/.test(p)\n    : source.host==='probankrotstvo.ru' ? /^\\/(?:news|article|articles)\\/[a-z0-9-]+-\\d+\\/?$/.test(p)\n    : source.host==='kommersant.ru' ? /^\\/doc\\/\\d+\\/?$/.test(p)\n    : source.host==='rbc.ru' ? /^\\/[a-z0-9_-]+\\/\\d{2}\\/\\d{2}\\/\\d{4}\\/[a-f0-9]{24}\\/?$/i.test(p)\n    : false;\n  if(!article)error('article_path_needs_adapter');\n  if(source.host==='vsrf.ru')u.pathname='/news/'+p.match(/\\d+/)[0];\n  else if(source.host==='nalog.gov.ru'||source.host==='pravo.ru')u.pathname=p.replace(/\\/?$/,'/');\n  else if(source.host==='cbr.ru')u.pathname=p.toLowerCase().replace(/\\/?$/,'/');\n  else u.pathname=p.replace(/\\/$/,'');\n  const keep=source.host==='cbr.ru'?(/event/i.test(p)?'id':'file'):null;\n  for(const key of [...u.searchParams.keys()])if(key!==keep)u.searchParams.delete(key);\n  return {url:u.toString(),publisher:source.host,source_name:source.name,source_kind:source.kind};\n}\nfunction dateValue(value){\n  if(typeof value!=='string'||!/^\\d{4}-\\d{2}-\\d{2}$/.test(value))error('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if(!Number.isFinite(d.getTime())||d.toISOString().slice(0,10)!==value)error('invalid_date');\n  return value;\n}\n// read is trusted HTTP/extractor output; judgment is untrusted classifier output.\n// Actual reading, redirects, trace provenance and publication-date extraction stay in n8n.\nfunction offerTopic(read,judgment,window){\n  if(!read||!judgment||judgment.read_id!==read.read_id)error('read_id_mismatch');\n  const src=sourceURL(read.url);\n  if(read.transport!=='http'||read.status_code!==200||read.date_kind!=='published'||read.discovery_bound!==true)error('source_read_required');\n  safeText(read.article_text,60000);\n  const day=dateValue(read.published_date),from=dateValue(window.from),to=dateValue(window.to);\n  if(from>to)error('date_window_invalid');\n  if(day<from||day>to)return {offer:false,reason:'outside_window'};\n  if(judgment.relevance==='unrelated')return {offer:false,reason:'unrelated'};\n  if(!['direct','adjacent','context'].includes(judgment.relevance))error('relevance_invalid');\n  if(!Object.hasOwn(POLICY.categories,judgment.category))error('category_invalid');\n  if(!['news','analysis','opinion','explanation','statistics','advertisement'].includes(judgment.content_type))error('content_type_invalid');\n  if(judgment.content_type==='advertisement')return {offer:false,reason:'advertisement'};\n  return {offer:true,read_id:read.read_id,...src,published_date:day,\n    title:safeText(judgment.title,160),angle:safeText(judgment.angle,350),\n    relevance:judgment.relevance,category:judgment.category,category_label:POLICY.categories[judgment.category],\n    content_type:judgment.content_type,verification_state:'on_selection',\n    score:judgment.relevance==='direct'?30:judgment.relevance==='adjacent'?20:10};\n}\nfunction rankTopics(topics,{excludedURLs=[],limit=POLICY.max_topics}={}){\n  if(!Number.isInteger(limit)||limit<1||limit>POLICY.max_topics)error('limit_invalid');\n  const seen=new Set(excludedURLs.map(url=>sourceURL(url).url)),unique=[];\n  for(const topic of [...topics].filter(t=>t.offer===true).sort((a,b)=>b.score-a.score||b.published_date.localeCompare(a.published_date))){\n    const url=sourceURL(topic.url).url;\n    if(seen.has(url))continue;seen.add(url);unique.push(topic);\n  }\n  const selected=[],deferred=[],counts=new Map();\n  for(const t of unique){\n    if((counts.get(t.publisher)||0)>=POLICY.max_per_publisher_first_pass){deferred.push(t);continue;}\n    selected.push(t);counts.set(t.publisher,(counts.get(t.publisher)||0)+1);\n  }\n  // Diversity is a preference. Do not discard useful topics when only one source has them.\n  return [...selected,...deferred].slice(0,limit);\n}\nfunction topicText(topic){\n  return safeText(`${topic.title}\\n\\nИдея поста: ${topic.angle}\\n\\n${topic.category_label} · ${topic.published_date}\\n${topic.source_name}\\n${topic.url}\\n\\nПосле выбора проверю факты и подготовлю черновик.`,4096);\n}\nreturn {POLICY,normalizeExcerpt,checkExcerpt,partitionEvidence,sourceBlocks,resolveEvidenceRefs,\n  sourceURL,offerTopic,rankTopics,topicText};\n\n})();\nconst NEWS=(()=>{const URL=StrictURL;\n'use strict';\n// Pure helpers. No network, database, paid calls or Telegram mutations.\n// Port to n8n Code nodes against the actual runtime; require is not assumed there.\nconst CONFIG={\"version\": \"0.4.1\", \"timezone\": \"Europe/Moscow\", \"default_days\": 7, \"allowed_days\": [7, 30], \"max_candidates\": 20, \"max_topics\": 5, \"selection_ttl_hours\": 24, \"caption_limit_utf16\": 1024, \"sources\": [{\"host\": \"nalog.gov.ru\", \"name\": \"ФНС России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"cbr.ru\", \"name\": \"Банк России\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"vsrf.ru\", \"name\": \"Верховный Суд РФ\", \"kind\": \"official\", \"enabled\": true}, {\"host\": \"pravo.ru\", \"name\": \"Право.ru\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"probankrotstvo.ru\", \"name\": \"PROбанкротство\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"kommersant.ru\", \"name\": \"Коммерсантъ\", \"kind\": \"editorial\", \"enabled\": true}, {\"host\": \"rbc.ru\", \"name\": \"РБК\", \"kind\": \"editorial\", \"enabled\": true}]};\nconst STAGES = Object.freeze({\n  proposal:'Инициатива', bill:'Законопроект', adopted:'Принято, вступление в силу не подтверждено',\n  in_force:'Действует', court:'Судебная практика', explanation:'Разъяснение', statistics:'Статистика'\n});\nconst CONTROLS = /[\\u0000-\\u0008\\u000b-\\u001f\\u007f\\u202a-\\u202e\\u2066-\\u2069]/u;\nfunction fail(code) { throw Object.assign(new Error(code), {code}); }\nfunction clean(value, max, field='text') {\n  if (typeof value !== 'string') fail(field+'_type');\n  const s=value.normalize('NFC').replace(/\\r\\n?/g,'\\n').trim();\n  if (!s || s.length>max || CONTROLS.test(s)) fail(field+'_invalid');\n  return s;\n}\nfunction instant(value) {\n  const t=value instanceof Date ? value.getTime() : Date.parse(value);\n  if (!Number.isFinite(t)) fail('invalid_clock');\n  return t;\n}\nfunction isoDay(value) {\n  if (typeof value!=='string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) fail('invalid_date');\n  const d=new Date(value+'T00:00:00Z');\n  if (!Number.isFinite(d.getTime()) || d.toISOString().slice(0,10)!==value) fail('invalid_date');\n  return value;\n}\nfunction parseDateEvidence(value) {\n  const s=clean(value,150,'publication_date_raw').toLowerCase();\n  let m=s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:$|t\\d{2}:)/);\n  if (m) return isoDay(`${m[1]}-${m[2]}-${m[3]}`);\n  m=s.match(/^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})(?:$|\\s)/);\n  if (m) return isoDay(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  const months=['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];\n  m=s.match(/^(\\d{1,2})\\s+([а-я]+)\\s+(\\d{4})(?:$|\\s)/);\n  if (m && months.includes(m[2])) return isoDay(`${m[3]}-${String(months.indexOf(m[2])+1).padStart(2,'0')}-${m[1].padStart(2,'0')}`);\n  fail('publication_date_unreadable');\n}\nfunction windowFor(days=7, now=new Date()) {\n  if (!CONFIG.allowed_days.includes(days)) fail('days_not_supported');\n  const parts=new Intl.DateTimeFormat('en-CA',{timeZone:CONFIG.timezone,year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date(instant(now)));\n  const get=t=>parts.find(p=>p.type===t).value;\n  const to=`${get('year')}-${get('month')}-${get('day')}`;\n  const from=new Date(Date.parse(to+'T00:00:00Z')-(days-1)*86400000).toISOString().slice(0,10);\n  return {from,to,days,timezone:CONFIG.timezone};\n}\nfunction parseCommand(text, botUsername='bflcontentbot') {\n  if (typeof text!=='string') return null;\n  const match=text.trim().match(/^\\/news(?:@([a-z0-9_]+))?(?:\\s+([\\s\\S]*))?$/i);\n  if (!match) return null;\n  if (match[1] && match[1].toLowerCase()!==botUsername.toLowerCase()) return null;\n  let tail=(match[2]||'').trim(),days=CONFIG.default_days;\n  const d=tail.match(/^(\\d+)(?:\\s+|$)/);\n  if(d){ days=Number(d[1]); if(!CONFIG.allowed_days.includes(days))fail('days_not_supported'); tail=tail.slice(d[0].length).trim(); }\n  return {days,topic:tail ? clean(tail,200,'topic') : ''};\n}\nfunction canonicalURL(value) {\n  if (typeof value!=='string' || value.length>1500 || /[\\s\\\\\\u0000-\\u001f\\u007f]/u.test(value)) fail('url_invalid');\n  let u; try{ u=new URL(value); }catch{ fail('url_invalid'); }\n  if(u.protocol!=='https:' || u.username || u.password || u.port) fail('url_forbidden');\n  const source=CONFIG.sources.find(s=>s.enabled && (u.hostname===s.host || u.hostname==='www.'+s.host));\n  // Exact known hosts; do not follow arbitrary subdomains just because web_search allows them.\n  if(!source)fail('source_not_allowed');\n  if(u.hostname==='www.'+source.host)u.hostname=source.host;\n  u.hash='';\n  for(const key of [...u.searchParams.keys()])if(/^(utm_|yclid$|gclid$|fbclid$)/i.test(key))u.searchParams.delete(key);\n  u.searchParams.sort();\n  return u.toString();\n}\nfunction articleURL(value){return QUALITY.sourceURL(value).url;}\nfunction sourceName(url) { const host=new URL(canonicalURL(url)).hostname; return CONFIG.sources.find(s=>s.host===host).name; }\nfunction normalizedEvidence(text) { return text.normalize('NFC').replace(/\\s+/gu,' ').trim(); }\nfunction quoteIn(quote,body) {\n  const q=clean(quote,800,'evidence');\n  if(q.length<15 || !normalizedEvidence(body).includes(normalizedEvidence(q)))fail('evidence_not_found');\n  return q;\n}\n// read MUST come from an actual HTTP reader/extractor. Never pass a model-produced read object.\nfunction validateRead(read, searchURLs, win, now=new Date()) {\n  if(!read || read.transport!=='http' || read.status_code!==200 || !/^text\\/html(?:;|$)/i.test(read.content_type||''))fail('page_not_read');\n  const requested=articleURL(read.requested_url), url=articleURL(read.final_url);\n  if(!searchURLs.has(requested))fail('not_in_search_sources');\n  if(new URL(requested).hostname!==new URL(url).hostname)fail('cross_source_redirect');\n  const age=instant(now)-instant(read.fetched_at);\n  if(age<0 || age>3600000)fail('read_stale');\n  if(read.date_kind!=='published')fail('publication_date_required');\n  const raw=clean(read.publication_date_raw,150,'publication_date_raw');\n  const context=clean(read.publication_date_context,1200,'publication_date_context');\n  if(!normalizedEvidence(context).includes(normalizedEvidence(raw)))fail('date_context_mismatch');\n  if(/dateModified|дата обновления|последнее обновление/i.test(context))fail('updated_is_not_published');\n  const published=parseDateEvidence(raw);\n  if(published<isoDay(win.from) || published>isoDay(win.to))fail('outside_date_window');\n  return {url,requested_url:requested,published_date:published,article_text:clean(read.article_text,60000,'article_text'),\n    source_name:sourceName(url),fetched_at:read.fetched_at,publication_date_context:context};\n}\nfunction validateCandidate(candidate, read, searchURLs, win, now=new Date()) {\n  const page=validateRead(read,searchURLs,win,now);\n  if(!candidate || candidate.read_id!==read.read_id)fail('read_id_mismatch');\n  if(candidate.relevance!=='direct_personal_finance')fail('not_relevant_to_people');\n  if(!Object.hasOwn(STAGES,candidate.stage))fail('stage_unknown');\n  if(candidate.published_date!==page.published_date)fail('date_mismatch');\n  if(!Array.isArray(candidate.facts) || candidate.facts.length<1 || candidate.facts.length>4)fail('facts_invalid');\n  const facts=candidate.facts.map(f=>({text:clean(f.text,400,'fact'),evidence:quoteIn(f.evidence,page.article_text)}));\n  const stage_evidence=quoteIn(candidate.stage_evidence,page.article_text);\n  let effective_date=null;\n  if(candidate.effective_date!=null){\n    effective_date=isoDay(candidate.effective_date);\n    const evidence=quoteIn(candidate.effective_evidence,page.article_text);\n    const raw=clean(candidate.effective_date_raw,150,'effective_date_raw');\n    if(!normalizedEvidence(evidence).includes(normalizedEvidence(raw)) || parseDateEvidence(raw)!==effective_date)fail('effective_date_mismatch');\n    if(candidate.stage==='in_force' && effective_date>win.to)fail('not_in_force_yet');\n  }\n  // A verified date plus supporting words are mandatory before advertising that a new rule is in force.\n  if(candidate.stage==='in_force' && !effective_date)fail('effective_date_required');\n  return {url:page.url,source_name:page.source_name,published_date:page.published_date,\n    title:clean(candidate.title,150,'title'),summary:clean(candidate.summary,400,'summary'),\n    why:clean(candidate.why,220,'why'),stage:candidate.stage,stage_label:STAGES[candidate.stage],\n    effective_date,stage_evidence,facts,verified_at:page.fetched_at,\n    // Additional event clustering requires verified document IDs in the workflow; do not invent them.\n    dedup_key:page.url,format:'news_fact_pack_v1'};\n}\nfunction chooseTopics(items, excludedURLs=[], limit=CONFIG.max_topics) {\n  if(!Number.isInteger(limit)||limit<1||limit>CONFIG.max_topics)fail('limit_invalid');\n  const seen=new Set(excludedURLs.map(articleURL));\n  const result=[];\n  for(const item of [...items].sort((a,b)=>b.published_date.localeCompare(a.published_date))){\n    const key=articleURL(item.url);\n    if(seen.has(key))continue;\n    seen.add(key);result.push(item);if(result.length===limit)break;\n  }\n  return result;\n}\nfunction callbackData(batchId,itemId){\n  if(!/^[A-Za-z0-9_-]{1,16}$/.test(batchId)||!/^[A-Za-z0-9_-]{1,12}$/.test(itemId))fail('id_invalid');\n  return `news:${batchId}:${itemId}`;\n}\n// Advisory guard only. Perform and persist the state claim atomically AFTER this guard.\nfunction guardSelection({callback,batch,item,now=new Date()}) {\n  if(!callback||!batch||!item)fail('selection_missing');\n  if(String(callback.from_id)!==String(batch.owner_id)||String(callback.chat_id)!==String(batch.owner_id))fail('not_owner');\n  if(String(callback.message_id)!==String(item.message_id))fail('wrong_message');\n  if(callback.data!==callbackData(batch.id,item.id)||item.batch_id!==batch.id)fail('wrong_item');\n  if(instant(now)>=instant(batch.expires_at))fail('selection_expired');\n  if(batch.status!=='ready'||item.status!=='offered')fail('selection_not_available');\n  return {batch_id:batch.id,item_id:item.id,selection_key:`news:${batch.id}:${item.id}`};\n}\nfunction captionBudget(sourceURL) {\n  const suffix='\\n\\nИсточник: '+articleURL(sourceURL);\n  const body_limit=CONFIG.caption_limit_utf16-suffix.length;\n  if(body_limit<250)fail('source_url_too_long_for_caption');\n  return {suffix,body_limit,preferred_body_max:Math.min(800,body_limit)};\n}\nfunction composeNewsCaption(body,sourceURL) {\n  const {suffix,body_limit}=captionBudget(sourceURL);\n  const text=clean(body,body_limit,'caption_body');\n  // Model writes body only. Link is appended deterministically and retained through revision.\n  if(/https?:\\/\\//i.test(text))fail('unexpected_link_in_body');\n  return text+suffix;\n}\nfunction topicMessage(item){\n  const text=`${item.title}\\n\\n${item.summary}\\n\\nПочему это важно: ${item.why}\\n\\n${item.stage_label} · ${item.published_date}\\n${item.source_name}\\n${item.url}`;\n  return clean(text,4096,'topic_message');\n}\nfunction progressText(event,{count=0,days=7,partial=false}={}){\n  switch(event){\n    case 'accepted': return `Взято в работу. Ищу новости за последние ${days} дней.`;\n    case 'verifying': return 'Проверяю даты, источники и связь новостей с личными долгами.';\n    case 'ready': return `Готово. Тем на выбор: ${count}. Нажми «Подготовить пост» под подходящей новостью.`+(partial?' Часть источников проверить не удалось.':'');\n    case 'empty': return `За последние ${days} дней подходящих новостей с проверенным источником не нашлось.`+(days===7?' Можно расширить поиск командой /news 30.':'');\n    case 'partial_empty': return 'Полностью проверить источники не удалось; подтверждённых тем пока нет. Можно повторить поиск позже.';\n    case 'failed': return 'Поиск сейчас не завершился. Можно повторить /news позже. Существующие черновики сохранены.';\n    case 'selected': return 'Тема выбрана. Готовлю пост по источнику — затем пришлю на согласование.';\n    case 'expired': return 'Эта подборка устарела. Отправь /news, чтобы получить свежие темы.';\n    default: fail('progress_event_unknown');\n  }\n}\n\nreturn {CONFIG,STAGES,parseCommand,windowFor,parseDateEvidence,canonicalURL,articleURL,\n  validateRead,validateCandidate,chooseTopics,callbackData,guardSelection,captionBudget,\n  composeNewsCaption,topicMessage,progressText};})();\nconst RESP=(()=>{'use strict';\nconst core=NEWS;\nfunction modelName(model){\n  if(typeof model!=='string'||!model.trim()||model.includes('/')||/\\s/.test(model))throw Error('Configure a real OpenAI REST model ID with web_search support');\n  return model;\n}\nfunction searchRequest({model,instructions,days=7,topic='',now=new Date()}){\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Search instructions required');\n  const win=core.windowFor(days,now);\n  return {model:modelName(model),store:false,\n    instructions,\n    input:JSON.stringify({task:'Find recent official and editorial articles about personal finance, debts and rights in Russia',window:win,topic,max_candidates:core.CONFIG.max_candidates}),\n    tools:[{type:'web_search',external_web_access:true,filters:{allowed_domains:core.CONFIG.sources.filter(s=>s.enabled).map(s=>s.host)}}],\n    tool_choice:'required',include:['web_search_call.action.sources'],max_output_tokens:8000};\n}\nfunction outputJSON(response){\n  if(!response||response.status!=='completed'||response.error)throw Error('responses_not_completed');\n  const chunks=[];\n  for(const output of response.output||[]){\n    if(output.type!=='message')continue;\n    if(output.status && output.status!=='completed')throw Error('message_not_completed');\n    for(const part of output.content||[]){\n      if(part.type==='refusal')throw Error('model_refusal');\n      if(part.type==='output_text')chunks.push(part.text);\n    }\n  }\n  const text=chunks.join('\\n').trim().replace(/^```json\\s*/i,'').replace(/\\s*```$/,'');\n  let value;try{value=JSON.parse(text);}catch{throw Error('invalid_output_json');}\n  if(!value||Array.isArray(value)||typeof value!=='object')throw Error('invalid_output_object');\n  return value;\n}\nfunction parseDiscovery(response){\n  const value=outputJSON(response);\n  if(!(response.output||[]).some(o=>o.type==='web_search_call'&&o.status==='completed'&&o.action?.type==='search'))throw Error('web_search_not_executed');\n  const searchURLs=new Set();\n  function remember(url){try{searchURLs.add(core.articleURL(url));}catch{/* Skip unsupported sources. */}}\n  for(const output of response.output||[]){\n    if(output.type==='web_search_call'&&output.status==='completed'){\n      for(const s of output.action?.sources||[])remember(s.url);\n      if(output.action?.type==='open_page')remember(output.action.url);\n    }\n    if(output.type==='message')for(const p of output.content||[])for(const a of p.annotations||[])if(a.type==='url_citation')remember(a.url);\n  }\n  if(!Array.isArray(value.candidates)||value.candidates.length>core.CONFIG.max_candidates)throw Error('invalid_candidates');\n  const candidates=[],rejected=[];const seen=new Set();\n  for(const candidate of value.candidates){\n    try{\n      const url=core.articleURL(candidate.url);\n      if(!searchURLs.has(url))throw Error('candidate_not_in_search_sources');\n      if(seen.has(url))continue;\n      seen.add(url);candidates.push({url}); // Discard model guesses about dates/facts until the page is read.\n    }catch(error){rejected.push({code:error.code||error.message});}\n  }\n  return {candidates,search_urls:[...searchURLs],rejected,response_id:response.id||null};\n}\nfunction verificationRequest({model,instructions,reads,searchURLs,win,now=new Date()}){\n  if(!Array.isArray(reads)||!reads.length||reads.length>5)throw Error('verification_batch_size');\n  if(typeof instructions!=='string'||!instructions.trim())throw Error('Verification instructions required');\n  const ids=new Set();\n  const pages=reads.map(read=>{\n    if(typeof read.read_id!=='string'||!read.read_id||ids.has(read.read_id))throw Error('read_id_invalid');\n    ids.add(read.read_id);\n    const page=core.validateRead(read,searchURLs,win,now);\n    // Reader must extract the article rather than sending the entire site navigation.\n    if(page.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n    return {read_id:read.read_id,published_date:page.published_date,url:page.url,article_text:page.article_text};\n  });\n  return {model:modelName(model),store:false,instructions,input:JSON.stringify({window:win,pages}),max_output_tokens:8000};\n}\nfunction parseVerification(response,reads,searchURLs,win,now=new Date()){\n  const value=outputJSON(response);\n  if(!Array.isArray(value.results)||value.results.length!==reads.length)throw Error('verification_results_incomplete');\n  const remaining=new Map(reads.map(r=>[r.read_id,r]));\n  const accepted=[],rejected=[];\n  for(const result of value.results){\n    const read=remaining.get(result.read_id);\n    if(!read)throw Error('unknown_or_duplicate_read_id');\n    remaining.delete(result.read_id);\n    if(result.accept===false){rejected.push({read_id:read.read_id,code:'editorial_exclusion',reason:String(result.reason||'').slice(0,400)});continue;}\n    if(result.accept!==true)throw Error('verification_decision_missing');\n    try{accepted.push(core.validateCandidate(result,read,searchURLs,win,now));}\n    catch(error){rejected.push({read_id:read.read_id,code:error.code||error.message});}\n  }\n  return {accepted,rejected};\n}\n\nreturn {searchRequest,outputJSON,parseDiscovery,verificationRequest,parseVerification};})();\nfunction snapshotOf(topic){\n if(topic.format!=='news_topic_v2'&&topic.format!=='news_fact_pack_v2')throw Error('news_format_invalid');\n const snap=topic.snapshot;\n if(!snap||snap.url!==NEWS.articleURL(topic.url)||snap.published_date!==topic.published_date)throw Error('snapshot_source_mismatch');\n const blocks=QUALITY.sourceBlocks(snap.article_text,snap.document_key);\n if(JSON.stringify(blocks.blocks)!==JSON.stringify(snap.blocks))throw Error('snapshot_blocks_mismatch');\n return snap;\n}\nfunction strictRequest(instructions,input,schema,name,max=4500){return {model:'gpt-5.6-sol',reasoning:{effort:'low'},store:false,max_output_tokens:max,instructions,input:JSON.stringify(input),text:{format:{type:'json_schema',name,strict:true,schema}}};}\nfunction apiResult(r,prefix){\n if(r.statusCode!==200)throw Error(prefix+(r.statusCode?'_http_'+r.statusCode+(r.statusCode>=500?'_outcome_unknown':''):'_outcome_unknown'));\n if(r.body?.status==='incomplete')throw Error(prefix+'_incomplete');\n if(r.body?.output?.some(x=>x.content?.some(c=>c.type==='refusal')))throw Error(prefix+'_refusal');\n try{return RESP.outputJSON(r.body);}catch{throw Error(prefix+'_malformed_json');}\n}\nfunction verifiedFacts(topic,result){\n const snap=snapshotOf(topic);\n if(result.document_key!==snap.document_key)throw Error('wrong_source_document');\n if(result.status!=='ready')throw Error('selected_needs_source_check');\n if(!['news','analysis','opinion','explanation','statistics','court','bill','adopted','in_force'].includes(result.content_type))throw Error('selected_content_type_invalid');\n // A news report is not itself an enacted legal document. Narrow attributed reporting remains available.\n if(result.content_type==='in_force')throw Error('selected_primary_document_required');\n if(!Array.isArray(result.facts)||!result.facts.length||result.facts.length>4)throw Error('selected_facts_invalid');\n const facts=result.facts.map(f=>{if(typeof f.text!=='string'||!f.text.trim()||f.text.length>700||!Array.isArray(f.qualifiers)||typeof f.attribution!=='string')throw Error('selected_fact_invalid');return {...f,source_blocks:QUALITY.resolveEvidenceRefs(f,snap)};});\n return {...topic,format:'news_fact_pack_v2',verification_state:'facts_selected',facts,content_type:result.content_type,limitations:result.limitations,selected_at:new Date().toISOString()};\n}\nfunction previewInput(s){\n const fact=JSON.parse(s.fact_pack);\n if(NEWS.articleURL(fact.url)!==s.source_url||NEWS.composeNewsCaption(s.body,s.source_url)!==s.caption)throw Error('news_caption_source_mismatch');\n let source;\n if(fact.format==='news_fact_pack_v2'){source=snapshotOf(fact);for(const f of fact.facts)QUALITY.resolveEvidenceRefs(f,source);}\n else if(fact.format==='news_fact_pack_v1')source=QUALITY.sourceBlocks([fact.stage_evidence,...fact.facts.map(f=>f.evidence)].filter(Boolean).join('\\n\\n'),'legacy_saved_evidence');\n else throw Error('news_facts_not_selected');\n return {caption_body:s.body,cover_title:s.cover_title,cover_subtitle:s.cover_subtitle,facts:fact.facts,content_type:fact.content_type||fact.stage,limitations:fact.limitations||[],source,source_url:s.source_url,legacy_evidence_only:fact.format==='news_fact_pack_v1'};\n}\nfunction assessPreview(result){\n if(typeof result.pass!=='boolean'||!Array.isArray(result.issues)||result.issues.length>10)throw Error('preview_malformed_json');\n if(result.pass&&result.issues.length)throw Error('preview_contradictory_result');\n if(!result.pass&&!result.issues.length)throw Error('preview_empty_failure');\n for(const i of result.issues)if(!['caption_body','cover_title','cover_subtitle'].includes(i.field)||typeof i.fragment!=='string'||!i.fragment||typeof i.suggested_replacement!=='string'||typeof i.problem!=='string')throw Error('preview_issue_invalid');\n return result;\n}\nfunction repairPreview(s,issues){\n const out={...s};let exact=null;try{exact=JSON.parse(s.exact_revision_json||'null');}catch{}\n // A precise owner edit may only be corrected inside its replacement span. The saved cover stays byte-identical.\n const bounds=exact&&!exact.validation_error?{start:s.body.indexOf(exact.newText),length:exact.newText?.length||0}:null;\n if(bounds&&(bounds.start<0||s.body.indexOf(exact.newText,bounds.start+1)>=0))throw Error('preview_exact_span_ambiguous');\n const edits={};\n for(const issue of issues){const key=issue.field==='caption_body'?'body':issue.field,old=out[key];if(typeof old!=='string')throw Error('preview_field_missing');const at=old.indexOf(issue.fragment);if(at<0||old.indexOf(issue.fragment,at+1)>=0)throw Error('preview_repair_ambiguous');\n if(bounds&&(key!=='body'||at<bounds.start||at+issue.fragment.length>bounds.start+bounds.length))throw Error('preview_exact_requires_owner_edit');\n (edits[key]||=[]).push({at,end:at+issue.fragment.length,text:issue.suggested_replacement});}\n for(const [key,changes] of Object.entries(edits)){changes.sort((a,b)=>b.at-a.at);let last=Infinity;for(const e of changes){if(e.end>last)throw Error('preview_repair_overlap');out[key]=out[key].slice(0,e.at)+e.text+out[key].slice(e.end);last=e.at;}}\n if(!out.cover_title||out.cover_title.length>70||out.cover_subtitle.length>110)throw Error('preview_cover_bounds');\n out.caption=NEWS.composeNewsCaption(out.body,out.source_url);out.text=out.caption;out.channel_caption=out.caption;out.caption_length=out.caption.length;\n if(bounds){exact.body=out.body;exact.caption=out.caption;out.exact_revision_json=JSON.stringify(exact);}\n return out;\n}\nconst ctx=$('Граница чтения').first().json;const {html,...base}=ctx;let read={...base,article_text:Array.isArray($json.article_text)?$json.article_text.map(x=>String(x).replace(/\\s+/gu,' ').trim()).filter(Boolean).join('\\n\\n'):typeof $json.article_text==='string'?$json.article_text.replace(/\\s+/gu,' ').trim():''};\ntry{\n if($json.error)throw Error('html_extraction_failed');\n if(typeof read.article_text!=='string'||read.article_text.trim().length<80)throw Error('article_body_missing');\n if(typeof $json.lead_text==='string'&&$json.lead_text.trim())read.article_text=$json.lead_text.replace(/\\s+/gu,' ').trim()+'\\n\\n'+read.article_text;\n if(read.article_text.length>22000)throw Error('article_needs_bounded_extraction');\n const candidates=[];\n for(const value of $json.date_blocks||[]){const t=String(value).trim();if(new StrictURL(ctx.url).hostname.includes('nalog.gov.ru')){const m=/^Дата публикации:\\s*(.+)$/.exec(t);if(m)candidates.push({raw:m[1].trim(),context:t});}else if(t)candidates.push({raw:t,context:t});}\n for(const raw of $json.time_attrs||[])if(raw)candidates.push({raw,context:'time datetime=\"'+raw+'\"'});\n for(const raw of $json.meta_dates||[])if(raw)candidates.push({raw,context:'article:published_time / datePublished content=\"'+raw+'\"'});\n if(!candidates.length&&ctx.url.includes('nalog.gov.ru')){const m=/Дата публикации:\\s*(\\d{2}\\.\\d{2}\\.\\d{4}(?:\\s+\\d{2}:\\d{2})?)/.exec(read.article_text.slice(0,250));if(m)candidates.push({raw:m[1],context:m[0]});}if(!candidates.length)throw Error('publication_date_required');\n const valid=candidates.map(x=>({...x,date:NEWS.parseDateEvidence(x.raw)}));if(new Set(valid.map(x=>x.date)).size!==1)throw Error('publication_date_ambiguous');\n read={...read,date_kind:'published',publication_date_raw:valid[0].raw,publication_date_context:valid[0].context};\n const validated=NEWS.validateRead(read,new Set(ctx.search_urls),ctx.win);read={...read,...validated,discovery_bound:true,title:String(ctx.url.includes('vsrf.ru')?($json.meta_title||$json.article_title||''):($json.article_title||$json.meta_title||'')).replace(/\\s+/gu,' ').trim(),url:validated.url};const snapshot={...QUALITY.sourceBlocks(read.article_text,'s'+$execution.id+'_'+read.read_id),article_text:read.article_text,url:read.url,published_date:read.published_date,read_id:read.read_id,fetched_at:read.fetched_at};return {json:{...read,snapshot,read_ok:true}};\n}catch(e){return {json:{...read,read_ok:false,read_error:e.message}};}\n\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        1540,
        240
      ]
    },
    {
      "id": "89361303-f8bf-4963-bc06-688c47d1d803",
      "name": "Вернуть ошибку чтения",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function bflAccess(){return $('Вход').first().json._bfl;}\nconst __result = await (async()=>{\nconst {html,...r}=$json;return {json:{...r,read_ok:false}};\n})();\nif(__result===null||__result===undefined)return __result;\nconst __a=bflAccess();\nconst __attach=(item)=>({...item,json:{...item.json,_bfl:__a,operation_actor_user_id:__a.user_id,operation_actor_chat_id:__a.chat_id,created_by_user_id:item.json.created_by_user_id||$json.created_by_user_id||__a.user_id}});\nreturn Array.isArray(__result)?__result.map(__attach):__attach(__result);"
      },
      "position": [
        1760,
        240
      ]
    },
    {
      "id": "cfe149f8-26e6-53f5-9e43-be520efc9242",
      "name": "Контекст → HTTP статья 0",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2200,
        1500
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "b438f632-802c-5e09-ba5a-8691c51abdaa",
      "name": "Допуск → HTTP статья 0",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -1780,
        1500
      ],
      "parameters": {
        "method": "POST",
        "url": "http://bfl-collaboration:8084/authorize",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {ticket:$('Вход').first().json._bfl.ticket,stage:\"BFLNewsReader04:HTTP статья 0\",execution_id:String($execution.id),context:{draft_key:$json.draft_key,version:$json.version,batch_key:$json.batch_key,operation_actor_user_id:$('Вход').first().json._bfl.user_id} } }}",
        "options": {
          "timeout": 150000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "CREDENTIAL_01",
          "name": "REDACTED httpHeaderAuth"
        }
      },
      "retryOnFail": false
    },
    {
      "id": "b876a223-183a-59ac-8982-1e0c74f3bb9d",
      "name": "Вернуть → HTTP статья 0",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1360,
        1500
      ],
      "parameters": {
        "jsCode": "const i=$(\"Контекст → HTTP статья 0\").item;return {json:i.json,...(i.binary?{binary:i.binary}:{})};"
      }
    },
    {
      "id": "62029269-3e6d-5293-a5ba-55c7bfc59b18",
      "name": "Контекст → HTTP статья 1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -940,
        1500
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "eb1143a4-f6e2-54a8-a276-a0c37d3db3fd",
      "name": "Допуск → HTTP статья 1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -520,
        1500
      ],
      "parameters": {
        "method": "POST",
        "url": "http://bfl-collaboration:8084/authorize",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {ticket:$('Вход').first().json._bfl.ticket,stage:\"BFLNewsReader04:HTTP статья 1\",execution_id:String($execution.id),context:{draft_key:$json.draft_key,version:$json.version,batch_key:$json.batch_key,operation_actor_user_id:$('Вход').first().json._bfl.user_id} } }}",
        "options": {
          "timeout": 150000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "CREDENTIAL_01",
          "name": "REDACTED httpHeaderAuth"
        }
      },
      "retryOnFail": false
    },
    {
      "id": "48016f3e-69ec-5aa9-b029-1d08ec690c00",
      "name": "Вернуть → HTTP статья 1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2200,
        1720
      ],
      "parameters": {
        "jsCode": "const i=$(\"Контекст → HTTP статья 1\").item;return {json:i.json,...(i.binary?{binary:i.binary}:{})};"
      }
    },
    {
      "id": "7a79f8ce-df52-536d-936a-6514fef5d7e0",
      "name": "Контекст → HTTP статья 2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1780,
        1720
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "f5b7df08-ac86-5fbd-b87f-dd56422382aa",
      "name": "Допуск → HTTP статья 2",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -1360,
        1720
      ],
      "parameters": {
        "method": "POST",
        "url": "http://bfl-collaboration:8084/authorize",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {ticket:$('Вход').first().json._bfl.ticket,stage:\"BFLNewsReader04:HTTP статья 2\",execution_id:String($execution.id),context:{draft_key:$json.draft_key,version:$json.version,batch_key:$json.batch_key,operation_actor_user_id:$('Вход').first().json._bfl.user_id} } }}",
        "options": {
          "timeout": 150000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "CREDENTIAL_01",
          "name": "REDACTED httpHeaderAuth"
        }
      },
      "retryOnFail": false
    },
    {
      "id": "5e34f4b3-9141-5c06-92ac-6e8883b54221",
      "name": "Вернуть → HTTP статья 2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -940,
        1720
      ],
      "parameters": {
        "jsCode": "const i=$(\"Контекст → HTTP статья 2\").item;return {json:i.json,...(i.binary?{binary:i.binary}:{})};"
      }
    },
    {
      "id": "29adae0b-6817-5ebd-9121-f7f0b3a533a9",
      "name": "Контекст → HTTP статья 3",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -520,
        1720
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "bd0e7cd2-8b33-5403-ab30-aad4133d8b78",
      "name": "Допуск → HTTP статья 3",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -2200,
        1940
      ],
      "parameters": {
        "method": "POST",
        "url": "http://bfl-collaboration:8084/authorize",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {ticket:$('Вход').first().json._bfl.ticket,stage:\"BFLNewsReader04:HTTP статья 3\",execution_id:String($execution.id),context:{draft_key:$json.draft_key,version:$json.version,batch_key:$json.batch_key,operation_actor_user_id:$('Вход').first().json._bfl.user_id} } }}",
        "options": {
          "timeout": 150000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "CREDENTIAL_01",
          "name": "REDACTED httpHeaderAuth"
        }
      },
      "retryOnFail": false
    },
    {
      "id": "623198d2-d019-568a-85ab-7d41db5b4928",
      "name": "Вернуть → HTTP статья 3",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1780,
        1940
      ],
      "parameters": {
        "jsCode": "const i=$(\"Контекст → HTTP статья 3\").item;return {json:i.json,...(i.binary?{binary:i.binary}:{})};"
      }
    },
    {
      "id": "5994a7cc-626d-5d68-beb3-40bc5a70dba2",
      "name": "Записать контекст выполнения",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -2300,
        1800
      ],
      "parameters": {
        "method": "POST",
        "url": "http://bfl-collaboration:8084/execution-context",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {ticket:$json._bfl.ticket,context:$json,execution_id:String($execution.id)} }}",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "CREDENTIAL_01",
          "name": "REDACTED httpHeaderAuth"
        }
      },
      "retryOnFail": false
    }
  ],
  "connections": {
    "Вход": {
      "main": [
        [
          {
            "node": "Записать контекст выполнения",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Начать чтение": {
      "main": [
        [
          {
            "node": "Контекст → HTTP статья 0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP статья 0": {
      "main": [
        [
          {
            "node": "Проверить HTTP 0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Проверить HTTP 0": {
      "main": [
        [
          {
            "node": "Есть проверенный редирект 0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Есть проверенный редирект 0": {
      "main": [
        [
          {
            "node": "Контекст → HTTP статья 1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Граница чтения",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP статья 1": {
      "main": [
        [
          {
            "node": "Проверить HTTP 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Проверить HTTP 1": {
      "main": [
        [
          {
            "node": "Есть проверенный редирект 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Есть проверенный редирект 1": {
      "main": [
        [
          {
            "node": "Контекст → HTTP статья 2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Граница чтения",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP статья 2": {
      "main": [
        [
          {
            "node": "Проверить HTTP 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Проверить HTTP 2": {
      "main": [
        [
          {
            "node": "Есть проверенный редирект 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Есть проверенный редирект 2": {
      "main": [
        [
          {
            "node": "Контекст → HTTP статья 3",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Граница чтения",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP статья 3": {
      "main": [
        [
          {
            "node": "Проверить HTTP 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Проверить HTTP 3": {
      "main": [
        [
          {
            "node": "Граница чтения",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Граница чтения": {
      "main": [
        [
          {
            "node": "HTML доступен?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTML доступен?": {
      "main": [
        [
          {
            "node": "Извлечь статью и дату",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Вернуть ошибку чтения",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Извлечь статью и дату": {
      "main": [
        [
          {
            "node": "Проверить извлечённую статью",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Контекст → HTTP статья 0": {
      "main": [
        [
          {
            "node": "Допуск → HTTP статья 0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Допуск → HTTP статья 0": {
      "main": [
        [
          {
            "node": "Вернуть → HTTP статья 0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Вернуть → HTTP статья 0": {
      "main": [
        [
          {
            "node": "HTTP статья 0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Контекст → HTTP статья 1": {
      "main": [
        [
          {
            "node": "Допуск → HTTP статья 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Допуск → HTTP статья 1": {
      "main": [
        [
          {
            "node": "Вернуть → HTTP статья 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Вернуть → HTTP статья 1": {
      "main": [
        [
          {
            "node": "HTTP статья 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Контекст → HTTP статья 2": {
      "main": [
        [
          {
            "node": "Допуск → HTTP статья 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Допуск → HTTP статья 2": {
      "main": [
        [
          {
            "node": "Вернуть → HTTP статья 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Вернуть → HTTP статья 2": {
      "main": [
        [
          {
            "node": "HTTP статья 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Контекст → HTTP статья 3": {
      "main": [
        [
          {
            "node": "Допуск → HTTP статья 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Допуск → HTTP статья 3": {
      "main": [
        [
          {
            "node": "Вернуть → HTTP статья 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Вернуть → HTTP статья 3": {
      "main": [
        [
          {
            "node": "HTTP статья 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Записать контекст выполнения": {
      "main": [
        [
          {
            "node": "Начать чтение",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 90,
    "saveDataSuccessExecution": "all",
    "saveDataErrorExecution": "all",
    "errorWorkflow": "BFLEditorError20260916"
  },
  "activeVersionId": "b6723070-4210-43f4-be9a-872c535864be"
}