Программа: граф и запуски
Программа изнутри: граф, события, переменные, узлы и данные, которые по ним идут, рамки, запуск, контрольная точка, группы и вызовы. Здесь правила, по которым программа работает; что делает каждый вид узла — Виды узлов.
Введение
Документ описывает устройство и выполнение рабочего процесса. Хозяин рабочего процесса — функция graph. Место в платформе — Устройство платформы, форматы файлов — Форматы файлов, разделы 5, 6, 9, 11, методы — Методы графа.
Рабочий процесс является живой программой. Программу включают, и включённая программа ожидает событий, хранит состояние в переменных и на каждое событие отвечает прогоном. Модель «запустить процесс — получить результат» — частный случай: событие пульта «Запуск» и прогон от него.
1 Состав рабочего процесса
Рабочий процесс хранится в папке data/processes/<process_id>/ (таблица 1).
Таблица 1 — Состав рабочего процесса
| Файл | Что внутри | Хозяин |
|---|---|---|
process.json |
граф: узлы, рёбра, группы, переменные, сквозные объекты | graph |
workspace.json |
пульт | canvas |
pulpit/ |
рисунки пульта и свои виды его элементов (Форматы файлов, 7.2) | canvas |
settings.json |
настройки процесса | settings |
state.json |
значения переменных с отметкой «помнить», кроме списков, и поколения запомненных списков | graph |
kept/<id>/ |
элементы запомненного списка: целое одним документом и каждый добавленный или изменённый элемент своим (Форматы файлов, 5.2) | graph |
modules.lock.json |
точные версии модулей процесса | modules |
generation.json |
номер поколения документов графа | graph |
.venv |
окружение Python процесса; рядом сведённый requirements.txt |
modules |
state/<имя>/ |
память узла — своя, названная его идентификатором, или общая нескольких узлов одного модуля: промежуточное состояние, которое модуль узла сохраняет перед выгрузкой (раздел 13) | модуль узла |
inputs/ |
файлы, которые человек дал процессу; путь к файлу вне папки процесса тоже допустим, и платформа его не копирует | человек |
files/ |
файлы, которые держат постоянные и переменные: картинка, звук, видео, файл, данные в окне (Форматы файлов, 16.3) | graph |
runs/<run_id>/ |
прогон: контрольная точка, журнал, рабочая папка (12.6) | graph, observe, modules |
results/<run_id>/ |
файлы результатов прогона под именами, которые им дали модули (12.6) | modules |
У процесса одно окружение Python на все модули (Модули, раздел 5). Всё, что относится к процессу, лежит в этой папке, и удаление папки уносит процесс целиком (Устройство платформы, 3.8.1).
process_id получается из имени процесса, и им названа папка процесса: латинские буквы и цифры переводятся в нижний регистр, русские буквы пишутся латиницей — так, как имя пишут в паспорте, без знаков над буквами (ж — zh, х — kh, ц — ts, щ — shch, ю — yu, я — ya; ъ и ь не пишутся), — всё остальное — пробелы, знаки, буквы других алфавитов — становится одним дефисом, длина обрезается до 64 символов: «Раскладка на лист» лежит в raskladka-na-list. Если букв и цифр в имени нет, id — process. Занятый id получает суффикс -2, -3 и дальше. Id дают процессу один раз, при создании, приёме и передаче: новое имя программы папку не переносит — по id программу называют вызовы других программ, панели библиотеки и раскладка окон.
2 Программа
- Включение. Функция графа проверяет граф, поколение документов и версии модулей по
modules.lock.json, как перед прогоном. Переменные получают начальные значения, а переменные с отметкой «помнить» — последнее значение изstate.json(раздел 7). Поднимаются модули-службы (раздел 13). Срабатывают события «при включении», таймеры начинают отсчёт. - Включённая программа ждёт событий. Каждое событие вызывает прогон (раздел 12).
- Выключение. Новые события не принимаются, идущие прогоны останавливаются (12.3), открытые вопросы закрываются без ответа, модули программы выгружаются (раздел 13). Значения «помнить» уже записаны: они пишутся при каждом изменении.
- Число программ. Число одновременно включённых программ не ограничено; одна программа включается не более одного раза.
- Изменение. Включённая программа не изменяется: её сохранение отклоняется (
process_running). Программу выключают, изменяют и включают снова. Другие программы изменяются без ограничений (С чего начать, 7). - После перезапуска платформы все программы выключены и включаются человеком. Запуск платформы ярлыком программы — действие человека: он включает эту программу (Мастерская, 6.6).
- Долгий случай — заказ, заявка, согласование, которые тянутся днями, — запись, а не прогон. Случай лежит записью со своим этапом в запомненном списке (раздел 7, запись по ключу), а прогоны короткие — «пришла оплата», «станок освободился», «вышел срок»: каждый читает запись, делает шаг и записывает новый этап. Прогон — для работы на минуты и часы. Правка программы и перезапуск платформы случаев не теряют: случаи лежат в переменной, а не в прогонах. Версий графа для идущих прогонов нет.
3 Правила графа
- Данные идут только по рёбрам между именованными портами.
- Прогон начинается с события (раздел 5); других начал у прогона нет.
- Повторяется только тело рамки (раздел 11). Стрелка назад в обход рамки не принимается.
- Несколько источников на одном горячем входе требуют явно выбранного правила, которое берёт один из пакетов: правила входов узла
any(10.3) или слиянияfirst(10.4). Приallвторой пакет того же круга никогда не был бы взят, поэтому проверка и сохранение такой граф отклоняют (graph_invalid) и называют каждый такой вход. Узел-модуль без своего правила судится по правилу манифеста. Холодные рёбра и возвраты тела в рамку источниками здесь не считаются. - Сквозной объект создаётся один раз: смысл хранит граф, пульт только показывает (раздел 16).
- Повтор шага с внешним действием не считается безопасным, если модуль не объявил шаг идемпотентным или человек не подтвердил повтор.
- Новый прогон не получает результат прошлого прогона сам собой. Всё, что сохраняется между прогонами, хранится в переменных (раздел 7).
- Вид узла добавляется, только если он примитив: его нельзя собрать из остальных видов или нужна неделимость, которой у составной конструкции не будет. Остальное человек собирает подграфом (раздел 18) без изменения платформы. Сокращение ради читаемости допускается, если оно не вводит нового смысла; такой вид назван сокращением в разделе 4.
- Элемент, который уводит работу за границу графа, — узел-модуль, вопрос человеку, событие по часам, пауза, файл, вызов процесса, элемент пульта и всякий новый такой элемент — отвечает на пять вопросов: каков у него срок, что бывает при отказе, повторяется ли он, как он отменяется и чьё то, что он оставил. Новую границу без ответа на любой из пяти в платформу не добавляют.
4 Узлы
Какие виды узлов есть, какие из них примитивы, а какие сокращения, их порты и сколько их, — каталог узловой системы (Виды узлов, разделы 2, 3, 8 и приложение А); каталог отсылает к разделам этого документа, где описан смысл каждого вида.
Порты узлов платформы задаёт их вид; ребро может идти только через них. Узел, чьё правило входов — any или слияние first, может отдавать невыбранные пакеты через выход overflow (10.3). Ребро через порт, которого у узла нет, пакета не понесёт никогда, поэтому process.validate и process.save его отклоняют (graph_invalid) и называют все такие рёбра сразу; у узла-модуля порты сверяются с манифестом. Порты в документ узла не копируются.
Порты узла-модуля — входы и выходы его манифеста и, сверх них, выход failed, который даёт граф (12.4); параметры — по схеме манифеста (Модули, раздел 10). Узел хранит только свои значения, описание модуля в него не копируется (Форматы файлов, раздел 5).
Типы портов. У портов модуля в манифесте есть тип — вид данных из единого перечня (раздел 24; имена — Форматы файлов, 16.1). Ребро соединяет порты, только если типы подходят друг другу: они одинаковые, один из них json или картинка, звук и видео идут на порт file — картинка тоже файл; файл на порт картинки не идёт: что в нём, неизвестно. json принимает и отдаёт что угодно. У узлов платформы манифеста нет, их порты считаются json. Порт, тип которого манифест не объявил, не проверяется. Неподходящие типы — ошибка graph_invalid при проверке и при сохранении процесса, с перечнем всех таких рёбер.
Узлы внутри рамки отмечены полем region — id рамки. Рамки вкладываются друг в друга. Через границу рамки ребро идёт только так: снаружи — на вход рамки и из выхода рамки; внутри — из внутреннего выхода рамки к её узлам и от её узлов на внутренние входы рамки. Холодное ребро (раздел 6) от переменной или постоянной может идти через границу куда угодно. Событие внутри рамки не бывает.
Что здесь примитив, а что сокращение (правило 8 раздела 3). Вид-примитив несёт операцию, которую не собрать из остальных видов или которой нужна неделимость; так, «Впервые?» проверяет и запоминает одним действием, а слияние объединяет два разных примитива — схождение-И и схождение-ИЛИ (10.4). Сокращения собираются из примитивов, и оставлены они намеренно:
- «Выбор» — цепочка проверок на равенство с именованными выходами. Нового смысла не вводит и оставлен для читаемости: цепочка из пяти условий на холсте читается хуже, чем один узел с пятью выходами;
- вызов процесса — макрос: разворачивается в граф до прогона и в движке не существует (раздел 18); вызов процесса также служит механизмом сборки всего, что не является примитивом;
- «Постоянная» — значение параметра, поданное ребром; нужна там, где значение подают холодным входом;
- «Результат» — вторая половина вызова: вместе с узлом-процессом исчезает при разворачивании (раздел 18);
- «Переключатель» — выбор, у которого субъект сравнения не пакет, а переменная (раздел 8). Отдельный вид оставлен потому, что по холсту должно быть видно: развилка здесь по состоянию, а не по данным.
Название узла — слова на его шапке (поле name, Форматы файлов, 5.1). Пока человек своего не дал, узел называется по умолчанию: вид словами, у узла-модуля — название модуля, у вызова процесса — название вызываемого процесса. Название — обычный текст, как имя группы (раздел 17): перевода у него нет, и идентификатором оно не служит — в связях, журнале и событиях узел называется по id.
Описание узла — слова человека о том, что узел делает и зачем он здесь (поле description, Форматы файлов, 5.1). Такое же описание есть у группы: собранный узел второго или третьего уровня без него — коробка, о назначении которой нужно догадываться. Описание — обычный текст в несколько строк; на прогон оно не влияет, и ничто в платформе его не читает — оно сказано тому, кто смотрит на граф. Когда группу делают процессом (17.1), её описание переходит узлу вызова, который встал на её место.
Группа — не вид узла, а надстройка над узлами (раздел 17).
5 События
Событие начинает прогон. Источник события задаёт параметр source узла event (таблица 2).
Таблица 2 — Источники событий
source |
Когда срабатывает | Пакет |
|---|---|---|
start |
один раз, когда программу включили | {} |
pult |
кнопка пульта, привязанная к событию (раздел 16) | значения переменных, которые кнопка несёт с собой, каждое под именем поля, названным в кнопке: { "<имя поля>": значение, … }; у кнопки без переменных — {} |
timer |
каждые interval_ms, пока программа включена; не чаще, чем раз в graph.timer_min_ms |
{ "time": "<ISO 8601>" } |
schedule |
в at — время вида 08:00 по часам этой машины; каждый день, если не сужено до дней недели или чисел месяца |
{ "time": "<ISO 8601>" } |
call |
процесс вызвали узлом process из другого процесса (раздел 18) |
пакет, пришедший на вход этого узла |
message |
другая программа — или эта же — отправила сообщение с именем name узлом «Отправить» (раздел 23) или его отправил человек методом process.message.send |
отправленные данные как есть |
change |
человек записал переменную, названную параметром variable: элементом пульта, полем на узле, методом process.variable.set или вернул её к начальному значению (process.variable.reset, кнопка «Вернуть как было» пульта); запись узлом «Запись» его не вызывает |
{ "value": новое значение } |
Событие по часам. Источник schedule срабатывает в названный час: параметр at — время суток вида 08:00 по местному времени машины, где работает платформа. Часовой пояс и летнее время платформа не хранит, а получает от системы. Действуют правила:
- при включении программы расписание наводится на ближайший будущий момент; момент, прошедший сегодня до включения, не выполняется: включение программы не является основанием выполнять пропущенное;
- момент, прошедший, пока программа была включена, а машина спала или WSL была приостановлена, выполняется один раз при первом же пробуждении;
- несколько пропущенных подряд дают один прогон, а не по одному на каждый: работа выполняется один раз, а не по числу пропусков;
- системные часы, переведённые вперёд за названный момент, дают один прогон, как пропуск; переведённые назад за момент, который уже выполнен, его не повторяют: следующий момент считается от последнего срабатывания. Смену часового пояса и летнего времени расписание принимает сразу: момент считается по местному времени того дня, в который он наступает.
Дни срабатывания. По умолчанию событие срабатывает ежедневно. Дни ограничиваются двумя необязательными параметрами (таблица 3).
Таблица 3 — Дни срабатывания события по часам
| Параметр | Что значит |
|---|---|
weekdays |
дни недели числами: 1 — понедельник … 7 — воскресенье (ISO 8601). [1,2,3,4,5] — по будням |
month_days |
числа месяца, 1–31, и last — последнее число месяца. [1,15] — первого и пятнадцатого, ["last"] — в последний день месяца |
- пустой список и отсутствующий параметр значат одно: каждый день;
- оба параметра сразу документ не проходит. Условие «по понедельникам и первого числа» допускает два прочтения («и», «или»); в расписаниях других продуктов это известный источник ошибок. Для обоих смыслов используются два события;
- числа, которого в месяце нет, не бывает:
31в апреле не наступает и никуда не переносится. Последнее число месяца называют словомlast: в апреле это 30-е, в феврале — 28-е или 29-е;lastи30в месяце из тридцати дней дают один прогон, а не два; - день недели и число месяца считаются по тем же местным часам, что и
at.
Событие по часам не проходит документ (graph_invalid), если at не разбирается, если день недели вне 1–7, если число месяца вне 1–31 и не last или если названы оба списка сразу.
Время таймера отсчитывается, пока работает программа. Интервал отсчитывается по работе платформы, а не по стенным часам: пока экземпляр остановлен, Windows спит или WSL приостановлена, интервал не накапливается, и после пробуждения таймер продолжает отсчёт с места остановки. Поэтому пропущенных сроков не бывает: таймер задаёт интервал, а не срок. Расписание по часам задаётся другим источником — schedule — и отсчитывается по реальному времени.
Модуль-служба также может начинать прогон, самостоятельно выдавая пакет на свой выход — например, «в папке появился файл» (Модули, раздел 4). Такой пакет идёт по рёбрам выхода как начало нового прогона.
Правило наложения — параметр overlap события: поведение при поступлении события, когда прошлый прогон от него ещё выполняется.
queue— по умолчанию: новый прогон ожидает завершения прошлого. Ожидать может не болееgraph.queue_depthсобытий; лишнее отклоняется с записьюevent.droppedв журнале (Обмен с ядром, раздел 7), а не теряется без сообщения. Ожидающие начинаются по одному и в том порядке, в каком пришли: событие занято, пока идёт его прогон, и конец прогона сразу отдаёт очередь следующему ожидающему; пришедшее в этот момент ждёт за ними, а не начинается раньше. Занятость и очередь — одно состояние программы.skip— новый прогон не начинается вовсе, пока идёт прошлый; отказ записываетсяevent.droppedс причинойskip(Обмен с ядром, раздел 7). В отличие от переполнения очереди, где событие ожидало и не поместилось, здесь событие намеренно не ставится в очередь.replace— прошлый прогон останавливается (12.3), начинается новый.parallel— прогоны выполняются одновременно.
У модуля-службы, который сам выдаёт пакеты, правило наложения — поле overlap его узла, как retry и memory, а не параметр модуля.
Событие изменения (change) называет переменную параметром variable — переменную этого процесса; без неё документ не проходит (graph_invalid). Каждая запись человека начинает прогон каждого события, которое следит за этой переменной, по его правилу наложения — так программа откликается на ввод без кнопки. Запись узла событие не вызывает: ветка, которая пишет то, за чем следит, иначе запускала бы себя без конца, а то, что записал граф, граф и так знает. Запущенное рукой без набранных данных, событие получает значение, которое переменная держит сейчас.
Событие по сообщению (message) называет своё имя параметром name — идентификатор, под которым сообщение отправляют. Сообщение начинает прогон каждого события программы с этим именем, каждого по его правилу наложения; программа без такого события сообщения не берёт. Выключенная программа сообщений не принимает и не копит: отправитель узнаёт об этом сразу (раздел 23).
6 Горячие и холодные входы
- Горячий — пакет по обычному ребру. Он встаёт в очередь входа и может запустить узел.
- Холодный — ребро от узла
variableилиconstant. Узел его не ждёт: в момент работы он берёт текущее значение переменной или постоянной. Отбор частей на ребре (parts, 10.1) действует и здесь: узел получает только названные поля значения. - Узел с холодным входом получает объект по именам входов: горячий пакет под именем своего входа, значения — под именами своих.
- На одном входе — либо горячие рёбра, либо одно холодное; смешивать не допускается (
graph_invalid). - Узел без единого горячего входа никогда не сработает. Документ из-за него не отклоняется — узел не выполняется, — но
process.validateназывает такой узел предупреждениемno_hot_input, и мастерская его помечает. Исключения —event,variableиconstant: у них входов нет. - Параметр узла из переменной. Значение параметра
{ "variable": "<id>" }читается в момент шага. Так пульт изменяет параметр модуля: элемент пульта пишет переменную, узел берёт её значение.
Пример: узел генерации запускается новым сообщением из чата (горячий вход), а стиль берёт тот, что сейчас выбран на пульте (холодный вход от переменной «стиль»).
7 Переменные
- Объявление — массив
variablesвprocess.json:id,type— вид данных из единого перечня (раздел 24):text,number,boolean,time,list,image,audio,video,fileилиjson, начальное значениеinitialи отметкаremember. Переменная картинки, звука, видео и файла начинает без файла (null) и получает его в работе — с пульта, от модуля, записью; набирать её начальное значение не нужно. - Время существования. Переменные программы существуют, пока программа включена. При включении каждая получает
initial, а переменная сremember: true— последнее записанное значение изstate.json. Записанное в такую переменную сразу попадает на диск: элемент запомненного списка — своим документом, остальное — вstate.json(Форматы файлов, 5.2). - Переменная прогона — отметка
scope: "run"в объявлении. У каждого прогона своё значение такой переменной: прогон начинает его сinitial, записанное одним прогоном другие прогоны не видят, и с прогоном значение уходит. Так два прогона, идущие одновременно, не затирают друг другу то, на что ссылаются, а свёртка списка копит значение по кругам рамки в своём прогоне. Отметки «помнить» у переменной прогона нет. Переменная без отметки — переменная программы, общая для всех прогонов. Как это устроено: что переменные прогона держат, пишется в каждую контрольную точку прогона, и продолжение после сбоя начинает с этого (Форматы файлов, раздел 11); круги рамки одного прогона делят одно значение, поэтому приparallelбольше единицы записи кругов идут в том порядке, в каком пришли; одного значения на программу у переменной прогона нет — на пульте её нет, человек её не пишет (process.variable.setотказываетgraph_invalid), событиеchangeеё не слушает, а узел переменной на холсте показывает её начальное значение. - Источники записи: узел
writeи пульт — элемент ввода, привязанный к переменной (раздел 16). - Записываемое значение задаёт параметр
path: поле пакета по правилам раздела 8, напримерtext; безpathзаписывается весь пакет. Отсутствие поля в пакете является ошибкой узла. - Способ записи задаёт параметр
mode:replaceзаменяет значение;appendу списка дописывает элемент, у текста дописывает строку; у переменной другого типаappendотклоняет проверка графа;resetвозвращает переменной её начальное значение —initial, а без него пустое значение её типа, как при включении: пакет не читается,pathне нужен. Так переменную очищают в начале прогона без постоянной, которая держала бы пустое значение.spreadпишет сразу несколько переменных, названных спискомvariables: каждая получает поле пакета со своим именем — или поле записиpath, когда он задан, — так, как записал быreplace. Все поля находятся до записи: если какого-то нет, не пишется ни одна переменная, и это ошибка узла. Так запись из файла или от модуля ложится в десятки переменных одним узлом, а не цепочкой записей; переменные списка проверка графа сверяет с объявленными, а переименование переменной доходит и до него. - Запись в список по ключу:
putиremove— у списка записей, с параметромkey— именем поля записи.putнаходит запись, у которой это поле равно полю записываемой, и сливает их: поля записываемой заменяют прежние, остальные остаются; такой записи нет — записываемая дописывается в конец.removeубирает все записи с тем же значением поля. Записываемое без поляkeyили не запись — ошибка узла; у переменной не спискаputиremove, у узла безkey— ошибка проверки графа. Так меняют и убирают одну запись списка — сообщение, чат, — не переписывая список целиком. Записываемое может быть и списком записей — пачкой: каждая запись пачки сливается со своей или дописывается по порядку пачки,removeубирает все названные; одна запись пачки без поляkey— ошибка узла, и не записывается ничего. Так папка из тысяч снимков ложится в список одной записью, а не тысячей шагов «Для каждого». - Сохранение типа. Значение другого типа в переменную не записывается — ни от узла, ни от пульта (
graph_invalid). Переменная для значений любого вида объявляется типомjson. - Порядок. Записи одной переменной выполняются по очереди; сохраняется последнее записанное значение. Каждая запись публикует событие
variable.changed, и пульт показывает новое значение сразу; значение больше 16 КиБ событие не несёт, а только говорит, что оно большое, и окно читает его само — один раз на серию таких событий: иначе список из тысяч записей, записываемый по одной, шёл бы целиком в шину и в журнал на каждой записи. Записи окна в одну переменную уходят к функции графа в том порядке, в каком их сделал человек: следующая — когда на предыдущую ответили. Пока они в пути,variable.changedоб этой переменной — эхо прежних, и окно не показывает его вместо набранного; если за это время переменную записала программа, окно показывает её значение, когда ответят на последнюю запись. - Имя.
idпеременной — имя, по которому её находят узлы, лица групп, пульт и запомненное значение. Переименование — одно действие, а не правка поля: функция графа меняет имя в объявлении и везде, где документ его называет, — в узле, вид которого называет переменную, в параметре, взятом из неё ({ "variable": … }), в списках переменных на лицах групп — и записывает в документ, чем стал объект пульта (renamed_objects, Форматы файлов, раздел 5). По этой записи пульт находит переменную под новым именем, а программа при включении — значение, запомненное под прежним. Имя, которое не идентификатор или уже есть у другой переменной, не даётся. Вызов в другом процессе, дающий значение переменной под прежним именем (раздел 18), за именем пойти не может — это другой документ: его называют, а прогон такого вызова отказывает с названием переменной. - Части результата. Пока модуль выдаёт результат частями (раздел 13),
writeпоказывает в переменной накопленное: текст склеивается, элементы списка накапливаются. Окончательный пакет ставит окончательное значение; в режимеappendнедописанный элемент заменяется окончательным. - Контрольная точка переменные не откатывает: записанное прогоном остаётся, даже если прогон затем завершился отказом.
8 Условие, выбор и переключатель
Условие не выполняет произвольный код. Проверка выбирается из закрытого набора:
{
"op": "exists | not_empty | equals | not_equals | greater | less | contains | has_part",
"path": "optional.dotted.path",
"value": "для equals, not_equals, greater, less, before, after",
"part": "имя части для has_part"
}
path— шаги через точку, без фильтров и выражений. Шаг в объекте — имя поля, идентификатор[A-Za-z_][A-Za-z0-9_]*. Шаг в списке — номер элемента, начиная с 1, как «Для каждого» нумерует круги (11.2), илиlast— последний элемент:defects.last.box,rows.2. В объектеlast— обычное имя поля, а число — не имя, и такой путь ничего не находит. Номер за концом списка,0иlastпустого списка тоже ничего не находят;- значение берётся из данных пакета; если в пакете есть
parts, путь ищется сначала вparts, потом в корне; exists— путь есть и значение неnull;not_empty— значение есть и это неnull,"",[],{};equals/not_equals— равенство JSON-значений;greater/less— только числа; иначе ошибка узлаgraph_condition_type;before/after— раньше и позже: оба значения — время ISO 8601 (видtime, Форматы файлов, 16.1), время без пояса — по часам этой машины; поле, в котором времени нет, — срок не задан — не раньше и не позже ничего; сравнить не со временем — ошибка узлаgraph_condition_type;contains— текст содержит текстvalue, большие и малые буквы не различаются; список содержит элемент, равныйvalue. У значения другого вида проверка не проходит — выходfalse, а не ошибка;- значение проверки из пакета —
"value": { "packet": "<путь>" }: поле пакета, пришедшего на узел. У проверки самого пакета это то же, чтоother_path; у проверки переменной и у «Отбора» (раздел 26) — сравнение с тем, что пришло; has_part— вpartsесть ключpart;other_path— путь ко второму полю того же субъекта: сравнивается поле с полем. Вместе сvalueне задаётся (graph_invalid);- выходы — только
trueиfalse; данные пакета не меняются.
Субъект развилки. По умолчанию развилка проверяет пакет. Необязательное поле variable узла меняет субъект: если поле задано, условие и выбор проверяют значение этой переменной, а path ищет путь уже внутри неё. Пакет при этом не изменяется и передаётся дальше в исходном виде: развилка отвечает о пакете, а не изменяет его. При отсутствии переменной с таким id документ не проходит проверку (graph_invalid).
Развилка является одним примитивом с двумя осями: субъект проверки (пакет или переменная) и число выходов (два или много). Условие — два выхода, выбор — много, переключатель — то же, что выбор по переменной, оставленный отдельным видом ради читаемости (раздел 4).
Запись логических знаков. Набор проверок закрыт намеренно; часть знаков в нём отсутствует, так как они выражаются устройством графа, а не новой проверкой (таблица 4).
Таблица 4 — Запись логических знаков
| Знак | Способ записи |
|---|---|
«не» (¬) |
второй выход того же узла: у проверки всегда два конца, true и false |
«больше или равно» (≥) |
выход false у проверки «меньше»; «меньше или равно» — false у «больше» |
«и» (∧) |
цепочка: выход true первой проверки идёт на вход второй |
«или» (∨) |
цепочка по false, а оба выхода true сходятся в слияние с правилом first; сработает ровно одна ветка, потому что обе идут от одной проверки разными выходами |
| сравнение со значением, заданным на пульте | значением проверки может быть переменная: "value": { "variable": "<id>" }. Она читается в момент шага (раздел 6), поэтому порог изменяется на пульте, а документ не меняется |
| сравнение двух полей одного пакета | other_path у проверки |
Выбор — развилка на много выходов. Параметры: path — по тем же правилам, cases — список { "port": "<имя выхода>", "value": <JSON> }. Пакет уходит на выход первого варианта, чьё значение равно значению по path, а если такого нет — на other. Имя выхода — идентификатор; два варианта с одним значением или одним именем проверка отклоняет. Данные пакета не меняются.
Переключатель — та же развилка, что и выбор, но значение берётся не из пакета, а из переменной: variable — её id, cases — те же { "port": …, "value": … }, что у выбора, и те же правила для имён и значений. Пакет уходит на выход первого совпавшего варианта, иначе на other, и данные пакета не меняются.
Отдельный вид при наличии выбора обоснован тем, что значение переключателя задаёт человек на пульте до прогона, и по графу должно быть видно, что развилка выполняется по состоянию, а не по данным. Условие и выбор читают только пакет, а переключатель читает переменную и явно показывает это своим видом.
То же выражается и без переключателя (переменная холодным входом в слияние, затем условие; 6, 10.4), но слияние вкладывает пакет в объект: далее по ветке передаётся { "<вход>": пакет, "<вход>": значение }, и все пути ниже смещаются. Переключатель пакет не вкладывает.
Значение переменной читается в момент шага, как у холодного входа (раздел 6). Путь внутрь значения задаёт path: для переменной типа json развилка допускается по её полю. При отсутствии переменной с таким id документ не проходит проверку (graph_invalid).
Добавление проверки в набор означает новую версию формата ogas.process/*.
9 Вопрос человеку
- Пакет на входе
questionостанавливает эту ветку прогона и открывает вопрос. Пульт и сам узел на холсте показывают данные пакета и ждут ответа (ask:<node_id>, раздел 16; 24.2). Остальные ветки прогона работают дальше. - Параметр
kind:text— ответ строкой; у него параметрfill— путь к полю данных вопроса (раздел 8), текст которого уже стоит в поле ответа: человек правит показанный текст, а не набирает его заново;choice— один изchoices;confirm— да или нет;number— числом;area— областью, которую человек обводит рукой на картинке вопроса:{ "x", "y", "width", "height" }долями стороны картинки, область внутри неё, ширина и высота больше нуля; картинка — та, что в данных вопроса, в том числе в записи внутри записи;points— точками, которые человек ставит на той же картинке: список{ "x", "y", "keep" },xиyдолями стороны,keep— брать то, что под точкой, или не брать; хотя бы одна точка «брать», не больше двухсот точек;mask— кистью: человек закрашивает нужное на той же картинке, ответ — ссылка на картинку того же размера, белую там, где закрашено, и чёрную в остальном;image,audio,video,file— ссылкой на файл этого вида (Форматы файлов, 16.2). Ответ другого вида отклоняется. - Ответ уходит на
answerвместе с вопросом:{ "question": <данные пакета>, "answer": <ответ> }. «Нет» уconfirmи отказ отвечать уходят наdeclined. По истечении срокаtimeout_ms(0— без срока) пакет уходит наtimeout. - Срок ответа — параметр
timeout_ms. По истечении срока пакет уходит на выходtimeout, а вопрос закрывается. Значение0означает ожидание без срока. Таких мест в платформе два — вопрос и ожидание сигнала (раздел 22), — и это единственные два места, где прогон может не завершиться: у всех прочих ожиданий срок есть по устройству. - Открытый вопрос записан в контрольной точке: после сбоя и продолжения он открыт снова, и срок ответа отсчитывается заново — пока платформа не работала, человек вопроса не видел. Вопрос, который открыть снова нельзя, делает продолжение отказом, а не теряет свою ветку; так же и ожидание сигнала (раздел 22).
- Остановка прогона и выключение программы закрывают вопрос без ответа. Вопрос, проигравший другой ветке у слияния
first, тоже закрывается (10.3).
10 Пакеты и готовность узла
10.1 Пакеты и очереди
Пакет содержит packet_id, run_id, iteration_id, источник, порт, parts и artifacts. Большие данные передаются файлами результатов (артефактами): в пакете только ссылка artifact_handle — имя файла, сам файл лежит в результатах прогона results/<run_id>/ папки процесса.
Ссылки на артефакты идут дальше вместе с пакетом: узлы платформы передают их как есть, а модуль получает их в artifact_handles вместе с путями к их файлам (Модули, раздел 6) и сам решает, какие вернуть на выход. Узел-модуль называет модулю и вход, на который пришли данные.
У каждого входного порта очередь ограниченной длины graph.queue_depth: пакеты обрабатываются в порядке поступления. Пакет уходит из очереди только после того, как шаг успешно записан. При ошибке пакет остаётся для разрешённого повтора. Переполнение очереди — ошибка graph_queue_overflow, а не потеря без сообщения.
Очередь ограничена сознательно: при неограниченной очереди нельзя ответить, хватит ли программе памяти. Ограничивать её безопасно потому, что пакет не может циркулировать, наращивая очередь: стрелка назад в обход рамки не принимается (раздел 3, правило 3), а круги внутри рамки ограничены max_iterations и max_loop_ms (11.1). Эти два правила взаимосвязаны: разрешение колец вне рамки потребовало бы либо снять предел очереди, либо допустить взаимную блокировку.
10.2 Разветвление (fan-out)
Одно ребро передаёт пакет одному получателю. Несколько рёбер с одного выхода — разветвление: все получатели видят один и тот же packet_id, у каждой доставки свой delivery_id. Общих изменяемых данных у получателей нет. Пакет на выходе без рёбер не передаётся: контрольная точка прогона записывает его в outputs с кодом packet_absorbed, а функция графа публикует событие packet.absorbed — так журнал прогона говорит, почему результат никуда не пришёл.
10.3 Готовность узла
Готовность узла к работе определяет правило входов — поле input_policy узла, а у узла-модуля без своего правила — input_policy его манифеста. Правило касается только горячих входов; холодные узел не ждёт (раздел 6).
all— по одному подходящему пакету на каждом обязательном горячем входе;any— первый пакет с любого горячего входа.
При any узел берёт пакет, доставленный первым, с какого бы входа он ни пришёл. Остальные пакеты того же круга — того же происхождения (packet_id) и того же круга рамки — не выбраны, независимо от того, ожидали ли они в очереди или поступили позже. Если у узла есть ребро из выхода overflow, невыбранные пакеты уходят по нему; иначе они отбрасываются, и контрольная точка записывает каждый в dropped с причиной unselected (Форматы файлов, раздел 11). В очереди они не накапливаются.
Проигравшие ожидания снимаются. Пакет того же круга может ещё не дойти до узла, а ждать на пути к нему — вопроса человеку (раздел 9), сигнала (раздел 22) или момента (раздел 19). Когда узел с правилом any или слияние first взяли пакет круга, такое ожидание снимается, если все дороги из него ведут только к этому узлу: вопрос закрывается и уходит с пульта, ожидание сигнала и пауза снимаются. Контрольная точка записывает каждое снятое ожидание в dropped с причиной withdrawn и узлом, который решил (decided_by). Так выражается отложенный выбор — «что раньше: ответит человек, придёт сигнал или выйдет срок» — и прогон не висит на ожидании, которое уже не нужно. Ожидание, из которого есть дорога и в другое место, остаётся: у его ветки своя работа.
При all пакеты подбираются в набор по join_key, если он задан на узле или рёбрах. Без join_key берётся по одному пакету с каждого входа в порядке прихода, и при источниках, работающих одновременно, такой результат не считается воспроизводимым.
10.4 Слияние (fan-in) и join_key
Узел слияния явно выбирает правило:
all— ждёт по пакету на каждом входе и отдаёт объект{ "<вход>": данные, ... }— и при одном входе тоже, поэтому слияние с одним входом даёт пакету имя; пакеты подбираются поjoin_key; файлы всех собранных пакетов идут дальше вместе, по порядку входов, каждый один раз;first— первый доставленный пакет; какой придёт первым, заранее неизвестно. Остальные пакеты того же круга — как у правилаany(10.3). Слияниеfirstвыбирает либо между ветками развилки (раздел 8), либо между ожиданиями — кто первым ответит, дождётся сигнала или времени; если ни того, ни другого на путях к его входам нет, первым придёт тот, кого раньше поставили в очередь, и это случайность, а не смысл программы. О таком слиянии проверка графа предупреждает —first_by_chance, предупреждением, а не отказом. Ожиданием здесь считаются вопрос человеку, ожидание сигнала, пауза, шаг модуля, вызов процесса и начала разных прогонов — события; общий путь до того места, где входы расходятся, в счёт не идёт: после общего шага модуля два мгновенных пути приходят сразу.shape: "record"у правилаall— одной записью: данные входов, которые являются записью, складываются полями в один объект, данные не запись ложатся под имя своего входа. Поле, пришедшее с двух входов, — ошибка узлаgraph_invalidс именами входов: какое из двух брать, решает человек, переименовав поле узлом «Взять поле». Так из набранного, отметки и постоянной собирается запись сообщения{ id, time, from, text }, а не вложенный объект по входам.
join_key — путь через точку по правилам раздела 8; значения сравниваются как JSON. Пакет без ключа остаётся в очереди, при переполнении — graph_queue_overflow. Пакеты с разными ключами в один набор не попадают.
Правило «последнее значение» у слияния отсутствует: для этого служит холодный вход от переменной (раздел 6).
Узел, который никогда не сработает. Два пути из одной развилки (раздел 8) не выполняются вместе, поэтому узел, ожидающий пакетов с обоих, не сработает: пакет останется в очереди до конца прогона. Проверка графа называет такой узел предупреждением never_fires — до прогона. Прогон, завершившийся с пакетами в очередях, записывает каждый из них в контрольную точку с причиной stalled (Форматы файлов, раздел 11): итог прогона — completed, так как выполнять больше нечего, но незавершённая работа отражена.
11 Рамки
Рамка — узел, внутри которого на холсте расположены другие узлы (тело рамки) (узлы с region = id рамки). Пакеты разных кругов не смешиваются: очереди тела разделены по (узел, порт, iteration_id).
Рамки вкладываются друг в друга. Пакет, вошедший в рамку из круга другой рамки, в теле носит свой packet_id: прежний и номер того круга через «/», например packet-1/2; выходя через done, пакет получает прежние packet_id и номер круга. Без этого шаг внутренней рамки во втором круге внешней носил бы тот же operation_id, что в первом, и считался бы уже выполненным, а результат внутренней рамки вернулся бы не в тот круг.
11.1 «Повторять» (repeat)
- Пакет на
inputначинает первый круг:iteration_id = 1, пакет уходит в тело черезbody. - Тело возвращает пакет на
again— начинается следующий круг с этим пакетом. Так на следующий круг переносится уточнённое значение: например, исправленный запрос к модулю генерации. - Тело возвращает пакет на
finish— рамка выходит черезdoneс этим пакетом. - Параметры:
max_iterations,max_loop_ms,on_limit(fail— по умолчанию, илиstop_with_result). Превышение числа кругов или времени — ошибкаgraph_iteration_limit; с"on_limit": "stop_with_result"рамка, исчерпавшаяmax_iterations, выходит черезdoneс последним пакетом. Время считается от первого входа пакета в тело. Действует меньший из пределов узла и настроекgraph.max_iterations/graph.max_loop_ms. - Рамка без узлов тела или без ребра на
againлибоfinishне принимается. - Время
max_loop_msпроверяется, когда тело возвращает пакет наagain: тело, вернувшее пакет наfinishпозже предела, выводит рамку без ошибки, а таймера, который прервал бы ожидающее тело, нет. - Круг, пакет которого кончился в теле и не вернулся ни на
again, ни наfinish— например, ушёл в выход без рёбер, — останавливает прогон ошибкойgraph_iteration_limitс узлом рамки, как только прогону больше нечего делать: ждать такой круг некому. Проверка графа заранее предупреждаетframe_may_not_returnо выходе узла тела без рёбер, если пакет доходит до этого узла без разветвления; после разветвления другая копия пакета может вернуться, и ветка, кончающаяся намеренно, предупреждением не считается.
Пример: генерация, проверка результата, условие «совпало?»: «нет» — модуль уточнения запроса и again; «да» — finish; предел — три круга.
11.2 «Для каждого» (for_each)
- Пакет на
listнесёт список: по параметруpath(по правилам раздела 8) или сами данные пакета. Каждый элемент уходит в тело черезitemотдельным пакетом сiteration_id= номер элемента, начиная с 1. - Тело возвращает результат элемента на
result. Когда вернулись результаты всех элементов, рамка отдаёт черезdoneсписок результатов в порядке элементов. Ветка элемента, завершившаяся доresult, даёт на его местеnull. Результат одного элемента берут из списка путём: по номеру элемента или последний — шагомlast(раздел 8). - Файлы идут с элементами. Каждый элемент уходит в тело с файлами результатов, пришедшими со списком (10.1): модуль в теле находит среди них свой. Список результатов выходит через
doneс файлами всех результатов — по порядку элементов, каждый один раз. - Параметр
parallel— число одновременно обрабатываемых элементов, по умолчанию 1. Число элементов ограниченоgraph.max_iterations: более длинный список рамка не берёт — ошибкаgraph_iteration_limit, и ни один элемент не уходит в тело. - Пустой список сразу даёт на
doneпустой список. - Рамка без узлов тела или без ребра на
resultне принимается (graph_invalid), как и рамка «Повторять» (11.1). Ветка элемента может кончиться раньшеresult, но дорога к нему у тела есть: рамка, в которую ничто не возвращается, — почти всегда забытое ребро.
12 Прогон
12.1 Начало
Прогон начинается с события (раздел 5) включённой программы. Запуск всего процесса использует названное событие пульта, а без имени — единственное событие, из которого идут рёбра: событие без рёбер работы не начинает. Для проверки части графа допускается запустить один узел или группу (target = group | node) с тем, что набрано в полях событий (24.5); такой запуск тоже прогон и тоже требует включённой программы.
Запуск сразу возвращает run_id, attempt_id и trace_id; работа продолжается, о ходе сообщают события run.changed и node.changed. Прогон знает, какое событие его начало (event_node).
12.2 Одновременные прогоны и ветки
- Прогоны разных программ выполняются одновременно.
- Прогоны одной программы выполняются по правилам наложения своих событий (раздел 5).
- Ветки одного прогона идут одновременно. Шаг модуля выполняется рядом с прогоном: пока модуль работает, прогон делает шаги других веток, заканчивает паузы, принимает ответы и сигналы. Поэтому ветки разветвления (10.2) не ждут друг друга, а элементы «Для каждого» с
parallelбольше единицы обрабатываются действительно одновременно (11.2). - Что пришло раньше, то и идёт дальше раньше. Прежде чем взять следующий узел, прогон принимает то, что пришло, пока он был занят: конец шага, ответ, конец паузы, сигнал. Узел, который берёт первый пакет круга (10.3), берёт пакет, который действительно пришёл первым: пауза в 100 мс опережает шаг модуля в 800 мс в соседней ветке.
- Вызовы одного модуля идут по очереди — из разных его узлов и из разных прогонов программы, — если манифест не разрешает больше полем
parallelism(Модули, раздел 4). Долгий модуль поднят одной копией на прогон, служба — на программу, и два шага одновременно говорили бы с одной копией наперебой. Поэтому и один узел не выполняется одновременно дважды.
12.3 Остановка
Остановка прогона отправляет cancel каждому шагу модуля, идущему в нём, и дожидается их конца. Шаговый и долгий модуль функция модулей затем останавливает как OS-процесс (core.process.stop), ждёт core.process_stop_grace_ms и, если процесс не завершился, завершает его принудительно. Модуль-служба продолжает работу: у него отменяется только шаг этого прогона. Уже сделанные внешние действия не откатываются.
Шаг, оборванный остановкой, — не отказ: что бы модуль ни ответил в этот момент, шаг не повторяется и на failed не уходит. Он записывается в контрольную точку оборванным — со своими входными данными, как отказавший (Форматы файлов, раздел 11). Остановленный прогон получает итог cancelled; остановленная группа заканчивает только свою ветку, и прогон идёт дальше. Ветки прогона, ожидающие вопроса, сигнала или конца паузы, остаются в контрольной точке, а вопрос человеку закрывается: продолжение остановленного прогона задаёт его снова. Продолжение выполняет оборванный шаг заново; исход оборванного шага неизвестен, поэтому для модуля non_idempotent или unknown продолжение требует подтверждения, как после сбоя (раздел 14).
Остановленная группа снимает свои ожидания. Шаги модулей её узлов обрываются; ветки, ожидающие в ней вопроса, сигнала или конца паузы, снимаются — вопрос уходит с пульта; пакеты, пришедшие к её узлам, дальше не идут. Контрольная точка записывает каждое снятое ожидание и каждый такой пакет в dropped с причиной stopped (Форматы файлов, раздел 11). Иначе прогон ждал бы того, чего группа уже не сделает, а вопрос без срока в остановленной группе не дал бы прогону кончиться никогда.
12.4 Ошибки и повтор
Автоматически повторяется только шаг с ошибкой, которую модуль пометил повторяемой, и не больше max_attempts раз. Правила повтора — поле retry узла, а если его нет — retry манифеста модуля; параметры модуля граф для этого не читает. Пауза между попытками — delay_ms, а при exponential: true она удваивается после каждой попытки. Остальные ошибки ожидают решения человека. Ветка после отказавшего узла результата не получает. Отказавший прогон программу не выключает.
Выход failed. У каждого узла-модуля, кроме портов манифеста, есть выход failed: его даёт граф, а не модуль. Узел модуля, чей манифест объявляет такой выход или overflow, который граф тоже даёт сам (10.3), проверку графа не проходит: модуль при этом исправен, но свой выход скрыл бы выход графа. Пакет по нему передаётся, только если человек провёл из него ребро:
- ребро есть — отказ уходит на
failed, и прогон идёт дальше по этой ветке. Узел при этом отмечается отказавшим, и запись об отказе в журнале остаётся: ветка обработала отказ, а не отменила его; - ребра нет — прогон завершается отказом, как описано выше.
Отказ уходит на failed после исчерпания повторов: неповторяемая ошибка — сразу, повторяемая — когда исчерпан max_attempts.
Отказ, который завершает прогон, обрывает шаги других веток, идущие в это время (12.2): модулям отправляется cancel, новые шаги не начинаются. Каждый оборванный шаг записывается в контрольную точку со своими входными данными, и продолжение выполнит его заново; шаг, успевший кончиться, записывается выполненным, а его результат ждёт продолжения в очереди.
Пакет отказа: { "code": "<код ошибки>", "message": "<текст>", "input": <пакет, пришедший на вход> }. Отказ может не иметь кода — тогда code пустой. input нужен, чтобы ветка знала, какой из ста файлов вызвал отказ.
В контрольной точке пойманный шаг записан выполненным (status: completed, port: failed, а сам отказ — в поле caught), поэтому продолжение прерванного прогона его не повторяет (Форматы файлов, раздел 11).
Узел-вызов (раздел 18) своего failed не имеет: отказ обрабатывается там, где возник, — внутри вызванного процесса.
12.5 Итог прогона
Итог — completed (готово), failed (ошибка), cancelled (остановлен человеком, правилом replace или выключением программы) или interrupted (прерван сбоем и найден при следующем старте). Во время выполнения прогона итог — running, в том числе при ожидании ответа человека.
Прогон с итогом failed называет причину: код, сообщение и узел, на котором он остановился, — и в последней контрольной точке, и в событии run.changed (Форматы файлов, раздел 11). Прогон с итогом cancelled называет, кто его остановил (stopped_by): person — человек, replace — правило наложения своего события, switched_off — выключение программы. Если остановок было несколько, называется первая. Причину называет и отказ вне шага — переполнение очереди, предел рамки, ошибка подстановки: его узел тот, на котором остановился прогон, а у переполнения — узел, чья очередь полна.
12.6 Сохраняемые результаты прогона
Прошлые прогоны не накапливаются без ограничения. Прогон лежит в папке своего процесса: контрольная точка, журнал и рабочая папка — в runs/<run_id>/, файлы результатов под их собственными именами — в results/<run_id>/. Имя прогона — местные дата и время его начала, 2026-10-08_07-29-12; второй прогон той же секунды — -2. Без уборки обе папки неограниченно растут.
- Уборка после прогона. По завершении прогона функция графа оставляет последние
graph.runs_keepпрогонов этого процесса (по умолчанию 20) и удаляет более старые целиком: папку прогона и папку его результатов. Не удаляются: прогон, который человек отметил «Хранить», идущий прогон и прерванный прогон, который ещё можно продолжить. Остановленный человеком прогон продолжить тоже можно, но он остановлен намеренно и удаляется на общих основаниях.0выключает уборку. - Хранить. Прогон можно отметить, чтобы уборка его не трогала, и снять отметку. Отметка — отдельный документ
keep.jsonрядом с контрольной точкой; порядок прогонов берётся из времени контрольной точки, поэтому отметка старого прогона не делает его новым. - Очистить. Человек удаляет прошлые прогоны процесса или всех процессов сразу; отмеченные и идущий остаются.
- Занимаемое место. Список прогонов показывает размер каждого вместе с его результатами и сумму, чтобы решение об очистке принималось с учётом объёма.
Файлы удаляет ядро по просьбе функции графа (core.document.remove): папку прогона и папку его результатов, но никогда всю runs или results одним вызовом. Методы — Методы графа.
13 Модули в работе и выгрузка
Время существования OS-процесса модуля объявляет манифест (Модули, раздел 4):
- шаговый (
step) — один вызов; - долгий (
long) — в пределах прогона; после прогона выгружается; - служба (
service) — поднимается при включении программы и работает до выключения.
Выгрузка по ресурсам. Память и видеопамять ограничены. Модуль объявляет, сколько видеопамяти ему нужно (resources.gpu_mb). Функция модулей держит поднятыми модули всех включённых программ, пока их сумма укладывается в настройку modules.gpu_budget_mb; при 0 видеокартой одновременно пользуется один модуль. Когда для запуска модуля не хватает места, выгружается модуль, который дольше всех не работал. Модуль во время шага не выгружается: новый модуль ожидает освобождения места.
Память узла. Модуль в запуске один на все свои узлы, а память у каждого узла своя: state/<node_id>/ процесса. Как модуль её сохраняет и получает — Модули, раздел 4. Со стороны графа:
- общая память — несколько узлов одного модуля называют одну память полем
memoryузла, например роль «рисование», разнесённая по узлам разных рамок (узел принадлежит одной рамке). Память без названия названа идентификатором узла, поэтому узел может назвать и собственную память другого узла; - память называют только узлы одного модуля: модуль не прочтёт того, что сохранил другой (
graph_invalid); - память живёт, пока её называет хотя бы один узел: после сохранения процесса функция графа просит очистить память, которую не называет ни один узел-модуль, — у удалённого узла и у узлов, которые называют другую;
- вызов одного процесса из двух мест и вставка дают узлы с новыми идентификаторами и новые имена общей памяти (разделы 16, 18), поэтому и памяти у них свои.
Части результата. Модуль может выдавать результат шага частями — например, текст ответа по мере генерации. Части идут по рёбрам его выхода, но действуют только на узлы write (раздел 7): так ответ отображается на пульте по мере формирования. Остальные узлы ожидают окончательного пакета шага.
14 Контрольная точка и продолжение
Контрольная точка — runs/<run_id>/checkpoint.json в папке процесса, своя у каждого прогона. Первая записывается до первого шага, со статусом running и очередями начала: сбой на первом же шаге оставляет прогон, который следующий старт находит прерванным. Порядок: результат шага и исходящие доставки готовятся, потом контрольная точка записывается целиком, и только после этого публикуется событие о завершении шага. Итог прогона записывается в контрольную точку при любом завершении, если запись возможна. Переменные в контрольную точку не входят: они хранятся в программе и в state.json (раздел 7).
Продолжают прогон, который не идёт. Контрольная точка идущего прогона разрешает продолжение — она написана на случай сбоя, — но пока прогон идёт, продолжать нечего: начатый с неё второй раз, рядом с самим собой, он выполнил бы шаги дважды. Функция графа такой запрос отклоняет (run_active, Коды ошибок), а мастерская не предлагает «Продолжить», пока знает, что прогон идёт.
Шаг модуля, идущий рядом с прогоном (12.2), стоит в контрольной точке в очереди своего узла, пока не кончится: прогон, прерванный сбоем в это время, выполнит его заново, а исход шага считается неизвестным, как у всякого шага из queued (ниже).
Контрольная точка хранит и состояние рамок: у рамки «Для каждого» — её список, уже собранные результаты и то, какие элементы отправлены в тело (Форматы файлов, раздел 11). Без этого продолжение рамки было бы невозможно: список поступает одним пакетом и в очередях не сохраняется.
После сбоя run_id остаётся прежним, создаётся новый attempt_id; у повторяемого шага operation_id прежний, execution_id новый. Шаг, результат которого не успели записать, может выполниться ещё раз. Поэтому допустимость продолжения определяется тем, что модуль объявил о своих шагах (execution_semantics):
idempotent— шаг безопасно повторить, продолжение разрешено обычной командой;non_idempotentиunknown— после шага с неизвестным исходом автоматически не продолжаются. После сбоя неизвестным считается исход всех шагов модулей изqueuedпоследней контрольной точки, после остановки — исход оборванного шага, пока он не выполнен заново (12.3), а после отказа — исход шага, который кончился без ответа модуля: его программа закрылась или ответила не по протоколу (module_process_lost) либо шаг не уложился в срок манифеста (module_timeout). Такой шаг мог успеть сделать своё, в том числе внешнее действие. Прогон с таким шагом продолжается только с явным подтверждением человекаconfirm_non_idempotent; контрольная точка остановленного или отказавшего прогона говорит об этом сама —resume.allowed: falseи причинаnon_idempotentилиunknown.
Шаг, который модуль отклонил без повтора, имеет известный исход — отказ, и продолжением не повторяется ни с подтверждением, ни без него: причина в контрольной точке — refused, ответ на продолжение — resume_not_allowed. Повторить отказавший шаг — команда «Повтор» его события (таблица 5). Шаг без ответа модуля повторяется той же командой; для модуля non_idempotent и unknown контрольная точка говорит resume.allowed: false с причиной non_idempotent или unknown, и «Повтор» и продолжение требуют confirm_non_idempotent, а для idempotent шаг повторяется обычной командой.
Эти сведения функция графа берёт из манифеста модуля (module.list). Продолжение прогона допускается, пока его программа включена; после перезапуска платформы прерванный прогон продолжают командой человека, когда программа снова включена.
15 Подстановка параметров
В параметрах узла можно подставить значение настройки процесса: вся строка параметра имеет вид ${имя}, где имя — [A-Za-z_][A-Za-z0-9_]* и такой ключ есть в настройках процесса. Вложенных подстановок, фильтров и выражений нет. Неизвестный ключ — ошибка проверки графа, а не пустая строка.
Значение параметра из переменной — { "variable": "<id>" } (раздел 6); неизвестная переменная — тоже ошибка проверки.
16 Сквозные объекты
Сквозной объект — то, что граф отдаёт пульту. Смысл объекта принадлежит графу; пульт только ссылается на object_id, показывает объект, изменяет и вызывает его (Мастерская, раздел 6). Объекты не хранятся отдельным списком: их задают узлы и переменные графа.
Таблица 5 — Сквозные объекты
| Объект | object_id |
Что это | Что делает пульт |
|---|---|---|---|
| событие | event:<node_id> |
узел event с source: pult |
вызывает событие и передаёт значения переменных, которые несёт кнопка (раздел 5) |
| остановка | cancel:<node_id> |
идущие прогоны этого события | останавливает их |
| повтор | retry:<node_id> |
последний упавший прогон этого события | повторяет упавший шаг и продолжает тот же прогон |
| состояние | status:<node_id> |
последний прогон этого события: идёт, итог, ошибка, чем сейчас занят | показывает |
| переменная | var:<variable_id> |
переменная программы | показывает; элемент ввода её пишет; вместилище по ней показано или спрятано, по ней открыта вкладка, и переключённая рукой вкладка её пишет (Мастерская, 6.8) |
| вопрос | ask:<node_id> |
открытый вопрос узла ask |
показывает вопрос и отправляет ответ |
| сигнал | signal:<node_id> |
сигнал, которого ждёт узел signal |
кнопкой посылает этот сигнал и передаёт значения переменных, которые несёт кнопка; какую из ждущих веток он будит, узел читает из них по своему ключу (раздел 22) |
| показ | show:<node_id> |
последнее, что показал узел «Показать» (Форматы файлов, 16.5) | показывает так же, как узел на холсте |
| выход | out:<node_id>/<выход> |
последнее, что дал выход узла, — у каждого выхода каждого узла | показывает так же, как узел «Показать» |
| вход | in:<node_id>/<вход> |
последнее, что пришло на вход узла по любой из его связей, а у входа, который даёт переменная, — её значение | показывает так же, как узел «Показать» |
| связь | edge:<edge_id> |
последнее, что прошло по связи: что дал её источник, а у связи с отбором частей (10.1) — только эти части | показывает так же, как узел «Показать» |
| ход шага | progress:<node_id> |
насколько продвинулся шаг узла модуля, как сообщил модуль (Модули, раздел 7) | показывает долей полосы |
| программа | program |
включена ли программа | показывает; включает и выключает |
- Параметр модуля на пульте — это переменная, которую узел берёт в параметр (раздел 6). Мастерская заводит такую переменную, когда параметр берут в интерфейс (Мастерская, 6.4). Выход и вход узла отдают интерфейсу то, что дали и что пришло, сами: для них переменная не нужна.
- Между прогонами элемент показа показывает значение своей переменной: последнее записанное, а после включения программы — начальное или запомненное (раздел 7).
- Что дал выход узла, программа помнит, пока включена: последнее значение каждого выхода каждого узла, урезанное так же, как показанное узлом «Показать» (настройка
graph.show_max_bytes). Выключенная программа забывает. Так просмотр интерфейса прикрепляют к выходу любого узла, не ставя узла «Показать» (Мастерская, 6.8). - Событию пульта данные даёт кнопка: значения переменных, названных в её привязке, берутся в момент нажатия и уходят пакетом события. Имя поля пакета называет кнопка, по умолчанию оно совпадает с именем переменной; переименование переменной имён полей не меняет, и узлы, которые читают поле пакета, продолжают его находить. Элемент ввода только записывает свою переменную и события не вызывает.
- Пульт ссылается на
object_idи не копирует смысл объекта. Те же объекты видны в окне процесса. process.validateиprocess.saveпроверяют привязки пульта к графу: объект, которого в графе нет, — пометка на пульте, а не отказ сохранения (С чего начать, 4).- Вставка другого процесса в текущий добавляет узлы, рёбра, группы и переменные, на которые ссылаются вставленные узлы, с новыми идентификаторами. События и пульт источника не копируются: они у программы свои. Делает это
process.insert(Методы графа).
Состояние на экране берётся из контрольных точек прогонов (process.snapshot) и из переменных программы. Цвет узла графа определяется последней записью executions или queued с его node_id. Просмотр состояния модули не вызывает.
17 Группа
Группа — запись в groups[]: group_id, имя, список узлов, место на холсте, признак свёрнутости, родительская группа и лицо (17.1). У запущенной группы свои очереди. Остановка группы останавливает её узлы и узлы групп внутри неё; соседние ветки продолжают работать.
Метка вложения. Собранное состоит из узлов любой глубины вперемешку, и число, сколько раз собирали, говорит о нём не то; поэтому у узла нет уровня, а у того, внутри чего что-то лежит, — у группы и у рамки с телом, — есть метка, что в нём есть вложения. У узла, внутри которого ничего нет, метки нет.
Состояние группы определяется состояниями её узлов: все узлы не запущены — «стоит»; хотя бы один работает — «работает»; есть ошибка — «ошибка»; часть завершена, часть не запущена, ошибок нет — «частично»; все завершены — «готово»; группа остановлена — «остановлена».
17.1 Свёрнутая группа
Группу можно свернуть: тогда она рисуется одним узлом, а её содержимое спрятано. Развёрнутая группа — область холста с телом внутри, как рамка (раздел 11).
Лицо — то, что видно у свёрнутой группы. Его выбирает человек, отмечая нужное у узлов внутри:
- порт внутреннего узла — становится строкой лица со своим именем; ребро снаружи приходит на эту строку;
- переменная, которой пользуются узлы внутри, — показывается на лице и правится в панели выбранного. На узле и на лице она стоит строкой с отметкой о том, что с ней делают: читают — переменная входа, записывают — переменная выхода, или и то и другое. Что делает с переменной узел вида, говорит вид (Методы графа, раздел 1,
variable_use); параметр, взятый из переменной, её читает.
Правила:
- Лицо объявляют, а не выводят. Отметка ставится человеком и не меняется сама, пока он работает внутри: иначе лицо блока изменялось бы при каждом новом ребре.
- Отметка заранее. У узла и у группы верхнего уровня лица вокруг нет, и отметку ставят заранее — на лицо группы, которую из них соберут: узел отмечает свои порты и переменные, с которыми работает, группа — строки своего лица и переменные, которые оно показывает. Когда отмеченное оказывается в группе — его обернули или положили в неё, — отметки становятся её лицом, а у самого узла или группы их больше нет.
- Через границу группы ребро идёт только через отмеченный порт. Ребро в неотмеченный порт узла внутри у свёрнутой группы не имело бы видимого конца; проверка графа отклоняет такой документ (
graph_invalid) и называет все такие рёбра сразу. Ребро в узел группы, лежащей внутри другой, пересекает обе границы, и порт отмечают на лице каждой. - Лицо показывает то, что есть. Строка лица — порт узла этой группы или группы внутри неё, и у узла такой порт есть; переменная лица объявлена в программе. Иначе —
graph_invalid. - Группа переменными не владеет, а показывает их. Переменная принадлежит программе (раздел 7); лицо — это короткий путь к ней, а не своя область видимости.
- Вложенность без предела. Группа может находиться в группе, та — в третьей. Свёрнутая группа ведёт себя как узел, и рабочий процесс может состоять из узлов разного уровня.
- Группы — дерево. Узел лежит прямо в одной группе: её называет поле узла
group_idили списокnode_idsгруппы, а если названы оба — одна и та же. Группа лежит в одной группе (group_id_parent) и ни прямо, ни через другие не лежит сама в себе. - Группа лежит в одном месте холста — на верхнем уровне или в теле одной рамки — и берёт рамку вместе с телом. Свёрнутая группа рисуется одним узлом, а у узла одно место: группе поперёк границы рамки места нет. Группа в теле рамки лежит внутри групп, которые держат эту рамку.
-
Свёртка ничего не меняет в прогоне. Для движка группы нет: узлы, рёбра и очереди те же самые, свёрнута группа или нет. Это способ показа, а не устройство (правило 8 раздела 3).
-
Имя строки лица — своё. По умолчанию оно переносится с внутреннего порта, но человек его меняет: внутри «Вход 1», снаружи «Картинка». Два одинаковых имени на лице платформа нумерует, пока человек не задал свои.
Собранное можно обернуть: человек отмечает узлы и складывает их в группу одним действием, а не собирает её заранее пустой. Обернуть можно узлы и группы одного уровня. Новая группа ложится туда, где они лежали, и сразу свёрнута: собранное — прежде всего один компактный узел с нужным на лице, а внутрь заглядывают, развернув его. Её лицо сразу показывает каждый порт, через который ребро пересекает её границу, и то, что человек отметил заранее на обёрнутом, — порты и переменные; переменная, которую никто не отметил, на лицо сама не встаёт. Дальше лицо меняет человек (Методы графа, раздел 1, process.group.wrap).
Узел и группу кладут в готовую группу и вынимают из неё: меняется только то, в чём они лежат, на холсте — переносом через границу (Мастерская, 5.3). Лицо следует за рёбрами, как при обёртке: с лица группы, из которой вынули, уходят строки вынутого, а порт, через который ребро теперь пересекает границу какой-либо группы, встаёт на её лицо со своим именем; остальное, что лицо показывало, остаётся — его объявил человек. Отметки заранее того, что положили, встают на лицо группы, в которую положили. Положить можно только туда, где лежит то, что кладут: в теле той же рамки или на верхнем уровне; группу — не в саму себя (process.group.move).
Группу можно разобрать: то, что в ней лежало, остаётся на месте и лежит там, где лежала группа, — в группе вокруг неё или на верхнем уровне, — а сама группа исчезает. Лица групп вокруг не меняются: узлы по-прежнему внутри них (process.group.unwrap). Лицо разобранной группы верхнего уровня становится отметками заранее того, что в ней лежало: порт узла — отметкой на узле, порт узла из группы внутри — отметкой на строке лица этой группы, если она его показывает, переменная — отметкой на узлах, которые с ней работают, и на группах внутри, которые её показывают; собранное снова показывает то же. Разобрать — действие, обратное «обернуть»: без него группу, собранную по ошибке, можно было бы убрать только вместе с узлами.
Отметку на лице ставит и снимает человек (process.group.face); отметку заранее — тем же запросом без группы. Порт, через который ребро пересекает границу группы, с лица не снимается, пока ребро есть: у ребра не осталось бы видимого конца; отказ — graph_face_port_used (Коды ошибок). Строка порта на лице внешней группы отмечается у свёрнутой внутренней: свёрнутая группа — узел группы, в которой лежит.
Копия. Узел и группу копируют: копия ложится рядом с оригиналом — в ту же группу и в тело той же рамки. Группа копируется со всем, что в ней лежит, рамка — с телом и группами в теле; рёбра между скопированным копируются, рёбра наружу — нет. Идентификаторы у копий новые (process.node.copy). Имя общей памяти копия сохраняет: копия узла, который делит память, делит её тоже; память, названная идентификатором скопированного узла, называется идентификатором его копии.
Вынести в отдельный процесс. Группу одним действием превращают в отдельный процесс, а на её месте остаётся узел, который этот процесс вызывает (раздел 18): группа живёт в одном процессе, а вызов идёт по ссылке, и собранным пользуются многие процессы. Что чем становится:
- то, что лежало в группе, — узлы, рёбра между ними, группы внутри — становится графом нового процесса;
- строка входа на лице — событием
callнового процесса, строка выхода — узлом «Результат»; имя у них то же, что у строки, а у строки без имени — имя её порта. Одинаковые имена нумеруются; - переменные, с которыми работают только узлы группы, уходят в новый процесс вместе с начальными значениями;
- узел-вызов встаёт на место группы — в ту же группу и в тело той же рамки — и соединяется со всем, с чем была соединена группа, через входы и выходы с теми же именами. Лица групп вокруг показывают эти входы и выходы там, где показывали порты узлов группы.
Новый процесс записывается сразу и дальше живёт как любой процесс; документ, из которого ушла группа, остаётся правкой, которую сохраняют как обычно, и шаг назад возвращает группу на место. Имя нового процесса — имя группы.
Группа остаётся на месте, и платформа называет причину словами, когда отдельным процессом она работала бы иначе:
- в группе стоит событие или узел «Результат» этого процесса: они принадлежат своему процессу;
- с переменной группы работают и узлы вне её, или переменная стоит на пульте: у вызываемого процесса переменная была бы своя;
- через границу группы идёт холодное ребро — от переменной или постоянной: в отдельном процессе оно стало бы входом, которого узел ждёт;
- узлы группы берут значения из настроек процесса (раздел 15): у нового процесса этих настроек нет;
- на лице группы нет ни одного входа или ни одного выхода: процессу нужен вход и выход.
Модули нового процесса работают в его окружении Python (Модули, раздел 5); если им нужны свои библиотеки, окружение нового процесса ставит человек, и окно говорит об этом сразу.
Место свёрнутой группы — место её первого узла: левый верхний угол того, что в ней лежит. Свёрнутую группу переносят со всем, что в ней, поэтому она разворачивается там, где стоит, а рёбра приходят к ней так же, как приходили к её узлам. Развёрнутой группе нужно больше места, чем свёрнутой, и место ей дают соседи: при разворачивании они отодвигаются вправо и вниз, при сворачивании возвращаются (Мастерская, 5.3). Так собранное держат компактным узлом и открывают там же, где оно стоит, — без отдельного холста на каждый уровень.
Удаление группы убирает её со всем, что в ней лежит: свёрнутая группа — один узел, и удаляют её как узел.
Запустить часть (12.1) работает на любом уровне: запускают ту группу, которую хотят проверить, вместе со всем, что внутри неё, включая вложенные группы и тела её рамок. В таком запуске группа стоит отдельно: группы вокруг неё и её лицо снаружи в прогон не входят. Какую часть проверять отдельно, решает тот, кто её собрал.
Имя группы — обычный текст, как имя процесса, а не ключ словаря: это слово человека, и перевода оно не имеет. Ключ и свой словарь появляются у элемента, который поставляют готовым, — так устроен модуль (Модули, раздел 2). Поэтому блок, который человек передаёт другим, перестаёт быть записью внутри процесса и становится элементом со своей папкой и своими текстами; пока он существует в одном процессе, у него есть только имя.
18 Процесс как узел
Готовый процесс вызывается из другого процесса узлом process — вместо того чтобы копировать его граф вставкой (раздел 16). Узел называет процесс параметром process_id. Готовые процессы для вызова и вставки предлагает библиотека; она же — набор собранных узлов (Мастерская, 5.6).
Вызов — кирпич, как любой узел: у него столько входов и выходов, сколько объявил вызываемый процесс, каждый под своим именем, и свои значения параметров. Собранное из узлов тогда ничем не беднее модуля рядом с ним, и писать модуль ради удобства незачем.
Что чем становится. Вызываемый процесс объявляет, где у него входы и что он отдаёт:
- вход — узел
eventсsource: "call". Таких узлов может быть несколько; имя входа — параметрnameсобытия, без имени вход называетсяinput. Пакет, пришедший на вход узлаprocessс этим именем, выходит из события, как из обычного; - выход — узел
result. Таких узлов может быть несколько; имя выхода — параметрnameузла, без имени выход называетсяsuccess. Пакет, дошедший до входаvalueузлаresult, выходит из узлаprocessчерез выход с этим именем; - параметры — переменные вызываемого процесса. Узел
processзадаёт им значения параметромvalues: с этим значением переменная начинает в этом месте вызова. У каждого места вызова переменные вызываемого свои, поэтому два вызова одного процесса с разными значениями друг другу не мешают. Переменная без значения начинает с того, что задал сам процесс.
Имена входов различны, и имена выходов различны: у порта один путь за ним. Два входа или два выхода с одним именем — и два без имени — graph_invalid и при проверке самого процесса, и при проверке того, кто его вызывает.
Процесс без входа или без выхода узлом не вызывается: проверка графа отклоняет вызывающий документ (graph_invalid) и называет, чего не хватает. Так же отклоняется ребро к входу или от выхода, которого у вызываемого процесса нет, — отказ перечисляет те, что есть, — и значение для переменной, которой у процесса нет или тип которой другой.
Вызываемый процесс работает и сам по себе. Рядом со входом call у него могут быть свои события — пульт, таймер, расписание; при вызове они не переносятся, а без вызова начинают прогон, как у любого процесса. Узел result в таком прогоне заканчивает свою ветку: узел отмечается выполненным, и пакет дальше не идёт. Чтобы и событие, и вход вызова питали один узел, они сходятся в слияние first (10.4): при разворачивании событие отпадает, и у слияния остаётся один источник.
Вызов — это не отдельный прогон. Перед проверкой и перед прогоном граф вызываемого процесса разворачивается внутрь вызывающего: его узлы, рёбра, группы и нужные им переменные попадают в граф прогона с новыми идентификаторами вида <узел>-<идентификатор>, ровно как при вставке (раздел 16). Поэтому у вызова нет своей контрольной точки, своей остановки и своего наложения: всё это — вызывающего прогона, и продолжение после сбоя не требует дополнительных механизмов.
Из этого следует:
- Свои события вызываемого не переносятся. События
pult,timer,scheduleиstartпринадлежат своей программе; в граф вызывающего идёт только входcall. Включать вызываемый процесс для вызова не нужно. - Правки видны при следующем прогоне. Вызов идёт по ссылке, а не по копии: изменение вызываемого процесса вызывающий получит при следующем прогоне. В этом состоит отличие вызова от вставки. Поэтому каждый процесс отвечает за свои модули: при включении вызывающего модули вызываемого сверяются по замку вызываемого, и правка вызываемого не требует сохранять вызывающего заново; работают они в окружении Python вызываемого (Модули, раздел 5). Работают они при этом в прогоне и в программе вызывающего: его рабочая папка прогона, его службы, его папки состояния.
- Кольца запрещены. Процесс не может вызвать сам себя ни прямо, ни через цепочку: разворачивание не завершилось бы. Кольцо вызовов —
graph_invalidс перечнем процессов в кольце. - Пульт вызываемого не переносится. Пульт принадлежит своему процессу (раздел 16).
- Экспорт несёт вызываемые. Процесс уходит в экспорт вместе с процессами, которые он вызывает, и при импорте они становятся процессами нового экземпляра, а вызовы — ссылками на них (Форматы файлов, 10.1).
- Развёрнутый граф лежит там, где лежал вызов. Узлы вызываемого попадают в рамку и группу узла
process, его группы — внутрь этой группы. Строка лица, показывавшая вход вызова, показывает узлы, которые питал входcallс этим именем, а строка выхода — узлы, которые питалиresultс этим именем. Поэтому вызов в теле рамки или в группе проходит проверку, как любой другой узел.
19 Пауза
Узел wait останавливает свою ветку на заданное время. Остальные ветки прогона работают дальше — так же, как при вопросе человеку (раздел 9). Пакет, пришедший на вход input, выходит из done неизменным: пауза ничего не добавляет к данным и ничего из них не берёт.
Длительность паузы задаёт параметр wait_ms — целое число миллисекунд. Параметр обязателен: без него пауза имела бы нулевую длительность и узел ничего бы не делал, поэтому документ без параметра не проходит проверку (graph_invalid). Значение может прийти подстановкой настройки или переменной (разделы 15, 7).
Пауза отсчитывается по реальному времени, а не по времени работы платформы. В контрольную точку записан момент, до которого ветка ждёт, — поэтому:
- прогон, прерванный сбоем и продолженный позже, выдерживает остаток паузы, а не начинает её заново;
- момент, прошедший, пока экземпляр был остановлен или машина спала, освобождает ветку сразу при продолжении.
Этим пауза отличается от таймера события, который считает время работы платформы (раздел 5): у таймера нет срока, есть интервал, а у паузы есть именно момент.
Остановка прогона (12.3) снимает паузу вместе с прогоном: в остановленном прогоне ветка паузу не выдерживает.
Отличие паузы от других узлов. Пауза не является повторением до успеха — для этого есть рамка «Повторять» (11.1) и повторы шага модуля (12.4); и не является расписанием: для работы по часам служит событие schedule (раздел 5).
20 «Впервые?»
Узел first_time определяет, встречалось ли значение раньше. Если не встречалось, пакет уходит на выход true, и значение немедленно записывается в список; если встречалось, пакет уходит на false. Сам пакет не меняется ни на том выходе, ни на другом: узел отвечает о пакете, а не изменяет его.
Проверяемое значение задаёт параметр path — путь через точку по правилам раздела 8. Без пути значением считается весь пакет. Место хранения задаёт параметр variable: переменная типа list. Переменная другого типа документ не проходит (graph_invalid), и это видно в мастерской, а не в середине прогона.
Проверить и запомнить — одно действие. Две ветки, встретившие одно и то же значение в один момент, получают разные ответы: ответ «впервые» получит ровно одна. Поэтому это узел, а не пара «условие плюс запись»: между проверкой и записью не должно быть промежутка.
Обоснование отдельного узла. Условие читает пакет и ничего кроме (раздел 8). Проверка, которая читает переменную и её же изменяет, сделала бы условие чем-то другим — и дедупликация перестала бы быть видна на холсте.
Список не ограничивается платформой. Это обычная переменная программы: она видна в мастерской, сохраняется после выключения при remember и очищается человеком или узлом write. Усечение списка платформой привело бы к тому, что забытое значение вернулось бы как новое, что противоречит назначению узла. Объём хранения определяет автор процесса.
21 Взять поле
Узел take берёт одно поле пакета и делает его пакетом. Остальное отбрасывается.
path— путь через точку по правилам раздела 8; поле обязательно, поля нет в пакете — ошибка узлаgraph_invalid;as— как назвать значение в новом пакете. Пусто — по последней части пути: изuser.nameполучаетсяname. Имя должно быть идентификатором, иначе обращение к нему по пути невозможно.
Пакет остаётся объектом: { "<имя>": <значение> }, а не значением без объекта. Так его читают и узлы, и модули, и пульт; значение без объекта потребовало бы разворачивания во всех местах.
Обоснование отдельного узла. Слияние умеет собрать запись из нескольких входов (10.4), а разобрать запись без этого узла нечем: поле пакета можно только проверить — условием, выбором, путём у write. Это единственная операция над данными, которая ничем не собирается из остальных, поэтому она и есть вид узла, а не составная конструкция (правило 8 раздела 3).
Узел не выполняет вычислений и преобразований, а только выделяет поле. Сумма, разность, длина — работа модуля, а не графа.
Близкая возможность — отбор частей на ребре (10.1, поле parts): он берёт несколько полей верхнего уровня и оставляет им их имена. take берёт одно поле по пути любой глубины и может его переименовать.
22 Сигнал извне
Узел signal останавливает свою ветку, пока снаружи не придёт сигнал. Остальные ветки работают дальше — как при вопросе человеку (раздел 9) и при паузе (раздел 19). Это третья форма одного примитива ожидания: ожидание времени, человека и сигнала.
Назначение узла — сценарий «выполнить действие, дождаться файла или ответа, продолжить». Событие (раздел 5) может только начать прогон, в том числе от модуля-службы; без этого узла продолжить ожидающую ветку нельзя, и работу пришлось бы разделять на две программы с переменной между ними.
Ожидаемый сигнал задаёт параметр name — идентификатор, с которым сигнал посылается. Адресата задаёт необязательный key: путь по правилам раздела 8 внутри пакета, который пришёл на вход. Его значение отличает одну стоящую ветку от другой, когда одного имени недостаточно: «файл пришёл» для задания 42, а не для задания 43.
Отправка сигнала: модуль — уведомлением signal своей программе (Модули, раздел 7); другой процесс — узлом «Отправить» (раздел 23); человек — методом process.signal.send (Методы графа) с пульта или из мастерской. Человек шлёт сигнал узлу, а не имени: имя называет узел. С пульта — кнопкой объекта signal:<node_id> (раздел 16): данные сигнала — переменные, которые несёт кнопка, и ключ узел читает из них по своему пути key, как узел «Отправить» читает его из своих данных. Из мастерской — полем на узле (24.5): ветку выбирают по ключу, с которым она ждёт, — его называет контрольная точка, — а набранное уходит данными сигнала.
Правила:
- сигнал будит одну ветку: ту, которая ожидает этого имени с этим ключом дольше всех;
- сигнал не хранится. Если ожидающей ветки нет, сигнал не откладывается: метод явно отвечает
delivered: false, и запись остаётся в журнале. Ветка должна уже ожидать на узле; - пакет на выходе
signal—{ "waited": <пакет, с которым ждали>, "signal": <данные сигнала> }, подобно тому как ответ передаётся вместе с вопросом (раздел 9); timeout_ms— срок ожидания;0означает ожидание без срока, и вместе с вопросом без срока это единственный случай, когда прогон может не завершиться (раздел 9). По истечении срока пакет уходит наtimeoutнеизменным;- остановка прогона и выключение программы снимают ожидание без сигнала; так же снимается ожидание, проигравшее другой ветке у слияния
first(10.3); - ожидающая ветка записана в контрольную точку: после сбоя и продолжения она снова ожидает сигнала, параметры которого читаются из узла.
Отличие сигнала от события и вопроса. Событие начинает прогон, а сигнал продолжает начатый. Вопрос отображается человеку на пульте, а сигнал поступает от любого отправителя и ничего не отображает.
23 Отправить
Узел send передаёт данные другой программе — или этой же — и не ждёт её работы. Это второе отношение между процессами рядом с вызовом: вызов (раздел 18) значит «сделай и верни мне», и граф вызываемого становится частью прогона вызывающего; отправка значит «вот тебе, живи с этим сам», и получатель работает своей программой — в своей папке, по своему правилу наложения, со своей остановкой. Поэтому процесс, который живёт своими событиями, — мессенджер, датчик, наблюдатель папки — вызвать нельзя (его события не переносятся), а написать ему можно.
Параметры:
process_id— программа-получатель;to— вместоprocess_id: поле пакета, которое называет получателя в момент шага. Так одна программа пишет многим — мессенджер в каждый чат. Поля нет или оно не текст — данные уходят наunsentс причинойprogram_off. Задают одно из двух;mode—messageилиsignal. Сообщение начинает прогоны событий получателяsource: "message"с тем же именем (раздел 5). Сигнал будит ветку получателя, которая ждёт его на узлеsignalдольше всех (раздел 22);name— имя, которое знает получатель: идентификатор;key— только для сигнала: путь к полю данных, значение которого находит нужную из ждущих веток, как у узлаsignal.
Выходы. sent — получатель принял: данные идут дальше такими, как были. unsent — не принял: { "reason": <причина>, "input": <данные> }. Причины: program_off — программа выключена или такого процесса нет; no_receiver — нет события сообщения с этим именем; dropped — событие не взяло новый прогон по своему правилу наложения (skip, переполненная очередь); nobody_waits — сигнала никто не ждёт. Ребро из unsent необязательно: без него данные записываются в контрольную точку как ушедшие в никуда, а прогон не отказывает — не принятое сообщение не ошибка отправителя.
Файлы переходят с данными. Файл, который называют данные, — из files/ отправителя или из результатов его прогона — функция графа копирует в files/ получателя, и ссылка в данных называет копию: получатель читает файлы только в своей папке (Форматы файлов, 16.3). Файл, который скопировать не удалось, — причина того, что данные не переданы. В своей программе ссылка на файл из files/ остаётся как есть.
Что не переходит. Сообщение не хранится сверх правила наложения получателя, сигнал не хранится вовсе (раздел 22). Экспорт отправителя получателей не несёт: отправитель работает и без них, а не принятое сообщение называет причину.
Почему это вид узла. Ни один узел не выходит за пределы своей программы: вызов разворачивается внутрь, модулю доступ к платформе закрыт (Защита). Поэтому отправка не собирается из других видов и по правилу 8 раздела 3 является примитивом — УП-15 каталога (Виды узлов, раздел 2).
Программы могут отправлять друг другу без конца — как таймер начинает прогон без конца; каждый прогон при этом конечен (ЗС-05). Сообщение самой себе — это новый прогон своей программы, а не кольцо внутри прогона.
24 Граница с человеком: виды данных, ввод и показ
Готовая программа берёт данные у человека и показывает ему данные через узлы графа; к этим узлам привязаны графические узлы её интерфейса (раздел 16; Мастерская, 6.8). Те же узлы на холсте служат проверке: собранное проверяют там, где его собирают (Мастерская, 5.5). Устройство под них — имена видов данных, ссылка на файл, где лежат файлы, узел «Показать», поле на узле — описано в Форматы файлов, раздел 16.
24.1 Виды данных
Перечень видов данных один — на порты, переменные, пульт и холст. Видов девять (таблица 6).
Таблица 6 — Виды данных
| Вид | Что это |
|---|---|
| текст | строка любой длины; короткий и длинный текст — один вид, разница в том, как его показывают |
| число | целое или дробное число |
| да/нет | одно из двух значений |
| список | упорядоченный набор значений; таблица и лента сообщений — списки записей |
| картинка | изображение |
| звук | запись звука |
| видео | запись со звуком или без него |
| файл | любой другой файл |
| что угодно | данные со своим устройством |
Отдельный вид — то, что человеку показывают по-своему: картинку смотрят, звук слушают, файл открывают. Поэтому звук и видео стоят отдельно от файла, хотя внутри платформы картинка, звук, видео и файл передаются одинаково — файлом (24.4). Дата и время, выбор из списка и область на картинке — не виды данных, а способы ввода: дату набирают календарём, область отмечают на картинке. Они появляются элементами пульта и полями на узле.
Порт модуля и переменная называют любой из девяти видов и дату со временем: перечень один, поэтому картинку кладут в переменную, принимают на пульте и показывают на нём так же, как текст.
24.2 Ввод
Отдельного вида узла для ввода нет. Человек даёт программе данные в трёх местах, и каждое из них — вид узла, который уже есть:
- событие (раздел 5) — ввод начинает прогон;
- вопрос (раздел 9) — прогон дошёл до места и ждёт человека;
- переменная и постоянная (разделы 6 и 7) — значение лежит, и узел берёт его, когда до него доходит очередь.
Ввод не обязан начинать прогон: он бывает промежуточным. Пример: программа делает видео, и с некоторого кадра в нём появляется второй персонаж — его картинку дают в этом месте графа, а берёт её следующий кадр. Это второе или третье место, но не первое.
Каждое из трёх мест принимает любой вид данных (24.1), а его узел на холсте несёт поле, в котором данные задают прямо на узле (Мастерская, 5.5). Причина: четвёртый узел повторял бы три имеющихся и в большинстве графов был бы лишним.
24.3 Показ
Данные человеку показывает узел отображения: то, что пришло на его вход, — текст, картинку, звук, видео, файл, список — он показывает на холсте, а вынесенный на пульт — на листе пульта. Узел отображения — часть программы, а не инструмент мастерской: показывать данные нужно и готовой программе, и тому, кто её собирает, и это одно умение, а не два. Узел, который на пульт не вынесли, человеку, пользующемуся программой, не виден.
По правилу 8 раздела 3 это примитив: показ человеку из остальных видов узлов не собирается: переменная с элементом «Значение» показывает одну строку текста, а не картинку, звук или галерею. Вид узла — «Показать» (show): вход value, выход success с тем же пакетом, параметр path — какое поле показать; поле, которого в пакете нет, показано отсутствующим, и прогон идёт дальше (Форматы файлов, 16.5).
24.4 Файл, который дал человек
Картинка, звук, видео и файл, которые человек дал программе, копируются в папку процесса. Программа не зависит от места, где лежал оригинал: его можно переместить или удалить, а процесс уходит к другому человеку целым (Форматы файлов, 10.1). Цена этого — большой файл занимает место дважды.
Сколько живёт копия, решает место, куда файл дали:
- постоянная — пока она стоит в графе: файл — часть программы, как рисунок пульта;
- переменная — пока переменная его держит, а с отметкой «помнить» и между включениями программы;
- событие и вопрос — вместе с прогоном: копия убирается с ним (12.6).
Файл после появления не изменяется. Правило относится к любому файлу, названному в пакете, — данному человеком и созданному модулем: кому нужен изменённый файл, создаёт новый. Разветвление копирует пакет вместе со ссылками на файлы, и две ветки получают два имени одного файла; изменяемый файл одна ветка портила бы другой. Платформа держит правило сама: файл, данный человеком, принятый с программой, перенесённый из прогона в файлы процесса и файл, который модуль отдал в результаты прогона, с появления только для чтения — модуль, который по ошибке пишет в него, получает отказ системы и кончает шаг сбоем, а не меняет файл другой ветке. Убирают файл вместе с тем, что его держит: это право его папки. Копия в пакете для другого человека — его: её можно писать, и следующий пакет на то же место её заменяет.
24.5 Поле на узле и отдельно запущенная часть
На холсте данные задают прямо на узлах: у постоянной поле правит её значение в документе, у переменной — пишет значение включённой программы, у события — набирает то, с чем его запускают, у вопроса — принимает ответ, у ожидания сигнала — посылает сигнал ждущей ветке (раздел 22). Событие объявляет, что принимает, параметром takes, вопрос — видом ответа kind; постоянная — видом значения type (Форматы файлов, 16.6). С холста событие запускают для проверки при любом источнике, а не только от пульта.
Узел или группа, запущенные отдельно (12.1), получают на входы извне значения постоянных и переменных и набранное в полях событий. Вход, к которому снаружи ведёт ребро от другого узла, не получит ничего, и запуск отклоняется со словами, какой это вход; так же отклоняется запуск, если в событии, которое ведёт в часть, не набрано то, что оно принимает (Форматы файлов, 16.7). Общего ввода у окна нет.
24.6 Уведомление
Программа зовёт человека уведомлением. Узел «Уведомить» (notify) кладёт уведомление поверх окна — мастерской или лица программы, — на какой бы вкладке и в каком окне человек ни был, а когда человека в окне нет — окно спрятано у часов, свёрнуто или он в другой программе, — в угол экрана (Мастерская, 2.2): напоминание о деле, новое письмо, конец долгой работы. Уведомление держится несколько секунд и уходит само или по щелчку; одновременно видны четыре последних. Заголовок — параметр title, без него имя программы; текст — поле, названное параметром path, без него поле text, а без него всё, что пришло, — не длиннее 500 знаков. Пакет идёт дальше без изменений, как у «Показать».
Это примитив по правилу 8 раздела 3: «Показать» показывает тому, кто смотрит, а уведомление зовёт того, кто не смотрит, — из остальных видов оно не собирается. Уведомление не хранится: его не видел, кто был не у компьютера, а звать некому, пока платформа не работает; закрытое окно платформу не останавливает (Мастерская, 2.1). Сама мастерская зовёт уведомлением о вопросе и о сбое запуска (Мастерская, 2.2).
25 Отметка
Узел stamp — источник, как постоянная: входов у него нет, ребро от него холодное (раздел 6), и узел на конце ребра читает отметку в момент своего шага. Отметка — { "time": "<начало прогона>", "id": "<номер>" }:
time— местные дата и время начала прогона со смещением от UTC,2026-10-14T13:21:23+03:00: момент, когда событие начало работу, — когда человек нажал «Отправить» или пришло сообщение;id— номер записи, который не повторяется ни в этой программе, ни в другой: имя процесса, прогон, узел, который читает отметку, и данные, с которыми идёт его шаг, —process-2.2026-10-14_13-21-23.letter.packet-3; в рамке к нему добавляется круг. Имя данных идёт за пакетом от узла к узлу, поэтому шаги различает узел. Два шага одного прогона получают разные номера, два чтения в одном шаге — один.
Отметка повторяется при продолжении прогона (ЗС-06): время — начало того же прогона, номер — из тех же данных; поэтому она не часы и не случайное число. Время в середине долгого прогона — работа модуля.
Почему это вид узла. Время прогона и имя данных шага знает только движок: ни событие — время у него есть лишь у таймера и только в его пакете, — ни переменная, ни модуль их не видят. Без отметки у записи нет ни времени, ни имени, по которому на неё ссылаются: ответ, правка, удаление, «прочитано» (ЗС-10).
26 Отбор
Узел pick берёт из списка записи, которые проходят проверку, и считает их.
- Список — переменная
variable, прочитанная в момент шага, или поле пакетаlist; одно из двух. - Проверка — параметр
testиз набора раздела 8. Еёpathищется внутри записи списка;value— значение, переменная{ "variable": "<id>" }или поле пришедшего пакета{ "packet": "<путь>" }. - Выход
success— пришедший пакет, к которому добавлено полеas(без него —found):{ "items": [записи по порядку списка], "count": <сколько> }. Пакет не запись — ложится подvalue. Списка нет или он пуст —itemsпуст,count0, а не ошибка.
Так собираются поиск (contains по тексту сообщения), запись по ключу (equals, первая — found.items.1) и счёт: «непрочитанных в чате» — два отбора подряд, по чату, затем по отметке «прочитано», и второй берёт список из found.items первого.
Почему это вид узла. «Для каждого» с условием внутри даёт подходящие записи по одной, но не их список и не их число: собрать их обратно граф может только записью в переменную на каждом круге, а это уже не запрос, а изменение. Отбор ничего не вычисляет: проверка из закрытого набора и счёт найденного (ЗС-10). Арифметика и выражения остаются модулю (ВГ-01).
27 Время
Узел moment — «Время» — считает момент по календарю: от даты через столько-то дней, в такой-то час. «Сегодня в 09:00», «через неделю», «завтра в тот же час».
- От какой даты — параметр
from: поле пакета с датой и временем (вид «время», 24.1, Форматы файлов, 16.1). Пусто — от начала прогона, как у отметки (раздел 25). - Через сколько дней —
days: целое число от −36 500 до 36 500 или поле пришедшего пакета{ "packet": "<путь>" }, в котором лежит такое число; без него 0. Дни — дни календаря на часах этой машины, а не по 24 часа: сдвиг через конец месяца, года и 29 февраля даёт следующую дату, а не дату плюс часы. - В котором часу —
at:ЧЧ:ММ; пусто — час исходной даты. - Выход
success— пришедший пакет, к которому добавлено полеas(без него —time): дата и время по часам этой машины без пояса,2026-10-17T09:00, секунды — только если они есть. Пакет не запись — ложится подvalue. Дата в пакете не задана (пустая строка или поля нет) — момент тоже не задан, пустая строка, и прогон идёт дальше.
Поле, в котором нет даты, или поле дней, в котором нет целого числа, — отказ шага graph_invalid с названием поля: момент не выдумывается. Дата с поясом сначала переводится на часы этой машины, поэтому «сегодня» — сегодня у человека.
Как отметка, «Время» — не часы: от начала прогона оно считает то же самое и при продолжении прогона после сбоя (ЗС-06). Так собираются срок повторяемого дела — поле срока и его повтор в днях — и напоминание «сегодня в 09:00».
Почему это вид узла. Счёт с датами из остальных видов не собирается: в графе нет арифметики, а сравнения «раньше» и «позже» (раздел 8) только сравнивают моменты. Произвольные выражения остаются модулю (ВГ-01), но календарь у всех один: «через неделю» не зависит от программы, и без узла каждый автор писал бы один и тот же модуль. Узел ничего не вычисляет, кроме этого одного: сдвиг по календарю и час (ЗС-10).
28 Шаблон
Узел template — «Шаблон» — подставляет поля пакета в текст: «{found.count} дел не сделано», «{author}: {text}».
- Текст — параметр
text. Поле стоит в фигурных скобках — путь по правилам раздела 8 любой глубины, пробелы вокруг него не считаются;{}— весь пришедший пакет;{{и}}— сами скобки. Скобка, которая открыта и не закрыта, — ошибка документаgraph_invalid: проверка графа называет место. - Подстановка — значение как оно есть: текст — текстом, число и «да/нет» — как записаны, запись и список — данными. Поля нет или оно пусто (
null) — пусто, и прогон идёт дальше. - Выход
success— пришедший пакет, к которому добавлено полеas(без него —text: это поле показывает «Уведомить», 24.6). Пакет не запись — ложится подvalue.
Шаблон подставляет и не считает: в скобках поле, а не выражение. Число и «да/нет» он не склоняет («3 дел»), дату не переводит в слова — она стоит так, как лежит в пакете.
Почему это вид узла. Без шаблона сказать словами то, что лежит в данных, граф не может: уведомление, сообщение, строка журнала берут поле целиком, и утренняя сводка не скажет «3 дела не сделано». Модулем это решалось бы у каждого автора своим, а подстановка одна и та же у всех и не вычисляет: счёт, условие внутри текста, формат чисел и дат остаются модулю (ВГ-01, ЗС-10).