from pathlib import Path
R=Path('/mnt/data/work4100/site');p=R/'ресурсы/сценарии/исполнитель-контура.js';s=p.read_text()
s=s.replace('let pageLease=null; // Один активный исполнитель этого контекста; не межвкладочный замок.', 'let pageLease=null; // Допуск удерживается до окончания наблюдаемого завершения; только этот контекст.')
s=s.replace('constructor({factory=null,timeout=120000}={})', 'constructor({factory=null,timeout=120000,cleanupTimeout=5000}={})')
s=s.replace('this.factory=factory;this.timeout=timeout;this.serial=0;this.active=null;', "if(!Number.isFinite(cleanupTimeout)||cleanupTimeout<1||cleanupTimeout>30000)throw Error('Предел ожидания завершения должен быть от 1 до 30000 мс.');\n      this.factory=factory;this.timeout=timeout;this.cleanupTimeout=cleanupTimeout;this.serial=0;this.active=null;")
s=s.replace("if(this.active||pageLease)return Promise.reject(Error('Другая операция ещё выполняется в этой странице.'));", "if(this.active||pageLease)return Promise.reject(Error(pageLease?.blocked?'Завершение прежнего потока не подтверждено. Новый запуск заблокирован; сохраните работу и перезагрузите страницу.':'Другая операция ещё выполняется или завершает очистку в этой странице.'));\n      if(!Number.isSafeInteger(this.serial)||this.serial<0||this.serial>=Number.MAX_SAFE_INTEGER)return Promise.reject(Error('Исчерпан безопасный счётчик операций. Создайте новый исполнитель.'));")
s=s.replace('K.resourceCheck(data,{envelope});if(!isCurrent())', 'K.operationBudget(command,data);if(!isCurrent())')
old='''        let worker,timer,done=false,next=1;
        const finish=(error,value)=>{
          if(done)return;done=true;clearTimeout(timer);signal?.removeEventListener('abort',abort);this.active=null;
          if(pageLease===lease)pageLease=null;
          try{const end=worker?.terminate();end?.catch?.(()=>{});}catch{}
          if(error)reject(error);else resolve(value);
        };'''
new='''        let worker,timer,done=false,next=1,exited=false,cleanupTimer,settled=false;
        const release=()=>{clearTimeout(cleanupTimer);if(pageLease===lease)pageLease=null;this.active=null;};
        const finish=(error,value)=>{
          if(done)return;done=true;clearTimeout(timer);signal?.removeEventListener('abort',abort);
          if(this.active)this.active.phase='очистка';
          const cleanupFailure=cause=>{
            if(settled)return;settled=true;lease.blocked=true;
            if(this.active)this.active.phase='завершение не подтверждено';
            const e=Error('Завершение прежнего потока не подтверждено; результат не принят, новый запуск заблокирован. '+String(cause?.message||cause||''));e.name='CleanupError';reject(e);
          };
          const complete=()=>{
            release();if(settled)return;settled=true;
            // Данные могли измениться, пока terminate ожидал exit.
            try{if(!error&&signal?.aborted)error=new DOMException('Обработка отменена до принятия результата.','AbortError');
              if(!error&&!isCurrent())error=Error('Данные изменились во время завершения; результат не принят.');
            }catch(e){error=e;}
            if(error)reject(error);else resolve(value);
          };
          try{
            if(!worker||exited){complete();return;}
            const end=worker.terminate();
            if(end&&typeof end.then==='function'){
              cleanupTimer=setTimeout(()=>cleanupFailure('Истекло время ожидания остановки.'),this.cleanupTimeout);
              Promise.resolve(end).then(complete,cleanupFailure);
            }else{
              // Браузерный API не сообщает об освобождении физической памяти.
              // Возврат terminate — граница API, а не измерение числа потоков ОС.
              complete();
            }
          }catch(e){cleanupFailure(e);}
        };'''
assert old in s;s=s.replace(old,new)
s=s.replace('this.active={id,fail:e=>finish(e)};', "this.active={id,phase:'работа',fail:e=>finish(e)};")
s=s.replace("worker.on('exit',code=>{if(!done)finish", "worker.on('exit',code=>{exited=true;if(done&&lease.blocked){release();return;}if(!done)finish")
p.write_text(s)
# Optional early conflict ceiling in the SAME merge generator.
p=R/'ресурсы/сценарии/доказательный-контур.js';s=p.read_text()
s=s.replace('function* mergeBaseSteps(sources, schema)', 'function* mergeBaseSteps(sources, schema, budget=null)')
s=s.replace('const add = (kind, message, hashes, block = true) => { conflicts.push', "const add = (kind, message, hashes, block = true) => { if(budget&&conflicts.length>=budget.conflicts)throw Error('Ограничительный профиль: более 2048 предметных конфликтов; расчёт прекращён до следующей записи.');conflicts.push")
s=s.replace('function* mergeSteps(sources,schema,resolutions=[],revocations=[],migrations=[])', 'function* mergeSteps(sources,schema,resolutions=[],revocations=[],migrations=[],budget=null)')
s=s.replace('yield* mergeBaseSteps(sources,schema)', 'yield* mergeBaseSteps(sources,schema,budget)')
s=s.replace('mergeBaseSteps(sources.filter(s=>!excluded.includes(s.отпечаток_исходного_файла)),schema)', 'mergeBaseSteps(sources.filter(s=>!excluded.includes(s.отпечаток_исходного_файла)),schema,budget)')
s=s.replace('revocations.map(r=>copy(r,revocationTrust)),migrations.slice());', 'revocations.map(r=>copy(r,revocationTrust)),migrations.slice(),options.jsonBudget||null);')
p.write_text(s)
