O.G.A.S. English

Программа: граф и запуски

Программа изнутри: граф, события, переменные, узлы и данные, которые по ним идут, рамки, запуск, контрольная точка, группы и вызовы. Здесь правила, по которым программа работает; что делает каждый вид узла — Виды узлов.

Введение

Документ описывает устройство и выполнение рабочего процесса. Хозяин рабочего процесса — функция 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 Программа

3 Правила графа

  1. Данные идут только по рёбрам между именованными портами.
  2. Прогон начинается с события (раздел 5); других начал у прогона нет.
  3. Повторяется только тело рамки (раздел 11). Стрелка назад в обход рамки не принимается.
  4. Несколько источников на одном горячем входе требуют явно выбранного правила, которое берёт один из пакетов: правила входов узла any (10.3) или слияния first (10.4). При all второй пакет того же круга никогда не был бы взят, поэтому проверка и сохранение такой граф отклоняют (graph_invalid) и называют каждый такой вход. Узел-модуль без своего правила судится по правилу манифеста. Холодные рёбра и возвраты тела в рамку источниками здесь не считаются.
  5. Сквозной объект создаётся один раз: смысл хранит граф, пульт только показывает (раздел 16).
  6. Повтор шага с внешним действием не считается безопасным, если модуль не объявил шаг идемпотентным или человек не подтвердил повтор.
  7. Новый прогон не получает результат прошлого прогона сам собой. Всё, что сохраняется между прогонами, хранится в переменных (раздел 7).
  8. Вид узла добавляется, только если он примитив: его нельзя собрать из остальных видов или нужна неделимость, которой у составной конструкции не будет. Остальное человек собирает подграфом (раздел 18) без изменения платформы. Сокращение ради читаемости допускается, если оно не вводит нового смысла; такой вид назван сокращением в разделе 4.
  9. Элемент, который уводит работу за границу графа, — узел-модуль, вопрос человеку, событие по часам, пауза, файл, вызов процесса, элемент пульта и всякий новый такой элемент — отвечает на пять вопросов: каков у него срок, что бывает при отказе, повторяется ли он, как он отменяется и чьё то, что он оставил. Новую границу без ответа на любой из пяти в платформу не добавляют.

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). Сокращения собираются из примитивов, и оставлены они намеренно:

Название узла — слова на его шапке (поле 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 по местному времени машины, где работает платформа. Часовой пояс и летнее время платформа не хранит, а получает от системы. Действуют правила:

Дни срабатывания. По умолчанию событие срабатывает ежедневно. Дни ограничиваются двумя необязательными параметрами (таблица 3).

Таблица 3 — Дни срабатывания события по часам

Параметр Что значит
weekdays дни недели числами: 1 — понедельник … 7 — воскресенье (ISO 8601). [1,2,3,4,5] — по будням
month_days числа месяца, 1–31, и last — последнее число месяца. [1,15] — первого и пятнадцатого, ["last"] — в последний день месяца

Событие по часам не проходит документ (graph_invalid), если at не разбирается, если день недели вне 1–7, если число месяца вне 1–31 и не last или если названы оба списка сразу.

Время таймера отсчитывается, пока работает программа. Интервал отсчитывается по работе платформы, а не по стенным часам: пока экземпляр остановлен, Windows спит или WSL приостановлена, интервал не накапливается, и после пробуждения таймер продолжает отсчёт с места остановки. Поэтому пропущенных сроков не бывает: таймер задаёт интервал, а не срок. Расписание по часам задаётся другим источником — schedule — и отсчитывается по реальному времени.

Модуль-служба также может начинать прогон, самостоятельно выдавая пакет на свой выход — например, «в папке появился файл» (Модули, раздел 4). Такой пакет идёт по рёбрам выхода как начало нового прогона.

Правило наложения — параметр overlap события: поведение при поступлении события, когда прошлый прогон от него ещё выполняется.

У модуля-службы, который сам выдаёт пакеты, правило наложения — поле overlap его узла, как retry и memory, а не параметр модуля.

Событие изменения (change) называет переменную параметром variable — переменную этого процесса; без неё документ не проходит (graph_invalid). Каждая запись человека начинает прогон каждого события, которое следит за этой переменной, по его правилу наложения — так программа откликается на ввод без кнопки. Запись узла событие не вызывает: ветка, которая пишет то, за чем следит, иначе запускала бы себя без конца, а то, что записал граф, граф и так знает. Запущенное рукой без набранных данных, событие получает значение, которое переменная держит сейчас.

Событие по сообщению (message) называет своё имя параметром name — идентификатор, под которым сообщение отправляют. Сообщение начинает прогон каждого события программы с этим именем, каждого по его правилу наложения; программа без такого события сообщения не берёт. Выключенная программа сообщений не принимает и не копит: отправитель узнаёт об этом сразу (раздел 23).

6 Горячие и холодные входы

Пример: узел генерации запускается новым сообщением из чата (горячий вход), а стиль берёт тот, что сейчас выбран на пульте (холодный вход от переменной «стиль»).

7 Переменные

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"
}

Субъект развилки. По умолчанию развилка проверяет пакет. Необязательное поле 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 Вопрос человеку

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).

При 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

Узел слияния явно выбирает правило:

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)

Пример: генерация, проверка результата, условие «совпало?»: «нет» — модуль уточнения запроса и again; «да» — finish; предел — три круга.

11.2 «Для каждого» (for_each)

12 Прогон

12.1 Начало

Прогон начинается с события (раздел 5) включённой программы. Запуск всего процесса использует названное событие пульта, а без имени — единственное событие, из которого идут рёбра: событие без рёбер работы не начинает. Для проверки части графа допускается запустить один узел или группу (target = group | node) с тем, что набрано в полях событий (24.5); такой запуск тоже прогон и тоже требует включённой программы.

Запуск сразу возвращает run_id, attempt_id и trace_id; работа продолжается, о ходе сообщают события run.changed и node.changed. Прогон знает, какое событие его начало (event_node).

12.2 Одновременные прогоны и ветки

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 после исчерпания повторов: неповторяемая ошибка — сразу, повторяемая — когда исчерпан 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. Без уборки обе папки неограниченно растут.

Файлы удаляет ядро по просьбе функции графа (core.document.remove): папку прогона и папку его результатов, но никогда всю runs или results одним вызовом. Методы — Методы графа.

13 Модули в работе и выгрузка

Время существования OS-процесса модуля объявляет манифест (Модули, раздел 4):

Выгрузка по ресурсам. Память и видеопамять ограничены. Модуль объявляет, сколько видеопамяти ему нужно (resources.gpu_mb). Функция модулей держит поднятыми модули всех включённых программ, пока их сумма укладывается в настройку modules.gpu_budget_mb; при 0 видеокартой одновременно пользуется один модуль. Когда для запуска модуля не хватает места, выгружается модуль, который дольше всех не работал. Модуль во время шага не выгружается: новый модуль ожидает освобождения места.

Память узла. Модуль в запуске один на все свои узлы, а память у каждого узла своя: state/<node_id>/ процесса. Как модуль её сохраняет и получает — Модули, раздел 4. Со стороны графа:

Части результата. Модуль может выдавать результат шага частями — например, текст ответа по мере генерации. Части идут по рёбрам его выхода, но действуют только на узлы 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):

Шаг, который модуль отклонил без повтора, имеет известный исход — отказ, и продолжением не повторяется ни с подтверждением, ни без него: причина в контрольной точке — 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 включена ли программа показывает; включает и выключает

Состояние на экране берётся из контрольных точек прогонов (process.snapshot) и из переменных программы. Цвет узла графа определяется последней записью executions или queued с его node_id. Просмотр состояния модули не вызывает.

17 Группа

Группа — запись в groups[]: group_id, имя, список узлов, место на холсте, признак свёрнутости, родительская группа и лицо (17.1). У запущенной группы свои очереди. Остановка группы останавливает её узлы и узлы групп внутри неё; соседние ветки продолжают работать.

Метка вложения. Собранное состоит из узлов любой глубины вперемешку, и число, сколько раз собирали, говорит о нём не то; поэтому у узла нет уровня, а у того, внутри чего что-то лежит, — у группы и у рамки с телом, — есть метка, что в нём есть вложения. У узла, внутри которого ничего нет, метки нет.

Состояние группы определяется состояниями её узлов: все узлы не запущены — «стоит»; хотя бы один работает — «работает»; есть ошибка — «ошибка»; часть завершена, часть не запущена, ошибок нет — «частично»; все завершены — «готово»; группа остановлена — «остановлена».

17.1 Свёрнутая группа

Группу можно свернуть: тогда она рисуется одним узлом, а её содержимое спрятано. Развёрнутая группа — область холста с телом внутри, как рамка (раздел 11).

Лицо — то, что видно у свёрнутой группы. Его выбирает человек, отмечая нужное у узлов внутри:

Правила:

Собранное можно обернуть: человек отмечает узлы и складывает их в группу одним действием, а не собирает её заранее пустой. Обернуть можно узлы и группы одного уровня. Новая группа ложится туда, где они лежали, и сразу свёрнута: собранное — прежде всего один компактный узел с нужным на лице, а внутрь заглядывают, развернув его. Её лицо сразу показывает каждый порт, через который ребро пересекает её границу, и то, что человек отметил заранее на обёрнутом, — порты и переменные; переменная, которую никто не отметил, на лицо сама не встаёт. Дальше лицо меняет человек (Методы графа, раздел 1, process.group.wrap).

Узел и группу кладут в готовую группу и вынимают из неё: меняется только то, в чём они лежат, на холсте — переносом через границу (Мастерская, 5.3). Лицо следует за рёбрами, как при обёртке: с лица группы, из которой вынули, уходят строки вынутого, а порт, через который ребро теперь пересекает границу какой-либо группы, встаёт на её лицо со своим именем; остальное, что лицо показывало, остаётся — его объявил человек. Отметки заранее того, что положили, встают на лицо группы, в которую положили. Положить можно только туда, где лежит то, что кладут: в теле той же рамки или на верхнем уровне; группу — не в саму себя (process.group.move).

Группу можно разобрать: то, что в ней лежало, остаётся на месте и лежит там, где лежала группа, — в группе вокруг неё или на верхнем уровне, — а сама группа исчезает. Лица групп вокруг не меняются: узлы по-прежнему внутри них (process.group.unwrap). Лицо разобранной группы верхнего уровня становится отметками заранее того, что в ней лежало: порт узла — отметкой на узле, порт узла из группы внутри — отметкой на строке лица этой группы, если она его показывает, переменная — отметкой на узлах, которые с ней работают, и на группах внутри, которые её показывают; собранное снова показывает то же. Разобрать — действие, обратное «обернуть»: без него группу, собранную по ошибке, можно было бы убрать только вместе с узлами.

Отметку на лице ставит и снимает человек (process.group.face); отметку заранее — тем же запросом без группы. Порт, через который ребро пересекает границу группы, с лица не снимается, пока ребро есть: у ребра не осталось бы видимого конца; отказ — graph_face_port_used (Коды ошибок). Строка порта на лице внешней группы отмечается у свёрнутой внутренней: свёрнутая группа — узел группы, в которой лежит.

Копия. Узел и группу копируют: копия ложится рядом с оригиналом — в ту же группу и в тело той же рамки. Группа копируется со всем, что в ней лежит, рамка — с телом и группами в теле; рёбра между скопированным копируются, рёбра наружу — нет. Идентификаторы у копий новые (process.node.copy). Имя общей памяти копия сохраняет: копия узла, который делит память, делит её тоже; память, названная идентификатором скопированного узла, называется идентификатором его копии.

Вынести в отдельный процесс. Группу одним действием превращают в отдельный процесс, а на её месте остаётся узел, который этот процесс вызывает (раздел 18): группа живёт в одном процессе, а вызов идёт по ссылке, и собранным пользуются многие процессы. Что чем становится:

Новый процесс записывается сразу и дальше живёт как любой процесс; документ, из которого ушла группа, остаётся правкой, которую сохраняют как обычно, и шаг назад возвращает группу на место. Имя нового процесса — имя группы.

Группа остаётся на месте, и платформа называет причину словами, когда отдельным процессом она работала бы иначе:

Модули нового процесса работают в его окружении Python (Модули, раздел 5); если им нужны свои библиотеки, окружение нового процесса ставит человек, и окно говорит об этом сразу.

Место свёрнутой группы — место её первого узла: левый верхний угол того, что в ней лежит. Свёрнутую группу переносят со всем, что в ней, поэтому она разворачивается там, где стоит, а рёбра приходят к ней так же, как приходили к её узлам. Развёрнутой группе нужно больше места, чем свёрнутой, и место ей дают соседи: при разворачивании они отодвигаются вправо и вниз, при сворачивании возвращаются (Мастерская, 5.3). Так собранное держат компактным узлом и открывают там же, где оно стоит, — без отдельного холста на каждый уровень.

Удаление группы убирает её со всем, что в ней лежит: свёрнутая группа — один узел, и удаляют её как узел.

Запустить часть (12.1) работает на любом уровне: запускают ту группу, которую хотят проверить, вместе со всем, что внутри неё, включая вложенные группы и тела её рамок. В таком запуске группа стоит отдельно: группы вокруг неё и её лицо снаружи в прогон не входят. Какую часть проверять отдельно, решает тот, кто её собрал.

Имя группы — обычный текст, как имя процесса, а не ключ словаря: это слово человека, и перевода оно не имеет. Ключ и свой словарь появляются у элемента, который поставляют готовым, — так устроен модуль (Модули, раздел 2). Поэтому блок, который человек передаёт другим, перестаёт быть записью внутри процесса и становится элементом со своей папкой и своими текстами; пока он существует в одном процессе, у него есть только имя.

18 Процесс как узел

Готовый процесс вызывается из другого процесса узлом process — вместо того чтобы копировать его граф вставкой (раздел 16). Узел называет процесс параметром process_id. Готовые процессы для вызова и вставки предлагает библиотека; она же — набор собранных узлов (Мастерская, 5.6).

Вызов — кирпич, как любой узел: у него столько входов и выходов, сколько объявил вызываемый процесс, каждый под своим именем, и свои значения параметров. Собранное из узлов тогда ничем не беднее модуля рядом с ним, и писать модуль ради удобства незачем.

Что чем становится. Вызываемый процесс объявляет, где у него входы и что он отдаёт:

Имена входов различны, и имена выходов различны: у порта один путь за ним. Два входа или два выхода с одним именем — и два без имени — graph_invalid и при проверке самого процесса, и при проверке того, кто его вызывает.

Процесс без входа или без выхода узлом не вызывается: проверка графа отклоняет вызывающий документ (graph_invalid) и называет, чего не хватает. Так же отклоняется ребро к входу или от выхода, которого у вызываемого процесса нет, — отказ перечисляет те, что есть, — и значение для переменной, которой у процесса нет или тип которой другой.

Вызываемый процесс работает и сам по себе. Рядом со входом call у него могут быть свои события — пульт, таймер, расписание; при вызове они не переносятся, а без вызова начинают прогон, как у любого процесса. Узел result в таком прогоне заканчивает свою ветку: узел отмечается выполненным, и пакет дальше не идёт. Чтобы и событие, и вход вызова питали один узел, они сходятся в слияние first (10.4): при разворачивании событие отпадает, и у слияния остаётся один источник.

Вызов — это не отдельный прогон. Перед проверкой и перед прогоном граф вызываемого процесса разворачивается внутрь вызывающего: его узлы, рёбра, группы и нужные им переменные попадают в граф прогона с новыми идентификаторами вида <узел>-<идентификатор>, ровно как при вставке (раздел 16). Поэтому у вызова нет своей контрольной точки, своей остановки и своего наложения: всё это — вызывающего прогона, и продолжение после сбоя не требует дополнительных механизмов.

Из этого следует:

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 берёт одно поле пакета и делает его пакетом. Остальное отбрасывается.

Пакет остаётся объектом: { "<имя>": <значение> }, а не значением без объекта. Так его читают и узлы, и модули, и пульт; значение без объекта потребовало бы разворачивания во всех местах.

Обоснование отдельного узла. Слияние умеет собрать запись из нескольких входов (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): ветку выбирают по ключу, с которым она ждёт, — его называет контрольная точка, — а набранное уходит данными сигнала.

Правила:

Отличие сигнала от события и вопроса. Событие начинает прогон, а сигнал продолжает начатый. Вопрос отображается человеку на пульте, а сигнал поступает от любого отправителя и ничего не отображает.

23 Отправить

Узел send передаёт данные другой программе — или этой же — и не ждёт её работы. Это второе отношение между процессами рядом с вызовом: вызов (раздел 18) значит «сделай и верни мне», и граф вызываемого становится частью прогона вызывающего; отправка значит «вот тебе, живи с этим сам», и получатель работает своей программой — в своей папке, по своему правилу наложения, со своей остановкой. Поэтому процесс, который живёт своими событиями, — мессенджер, датчик, наблюдатель папки — вызвать нельзя (его события не переносятся), а написать ему можно.

Параметры:

Выходы. 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 Ввод

Отдельного вида узла для ввода нет. Человек даёт программе данные в трёх местах, и каждое из них — вид узла, который уже есть:

Ввод не обязан начинать прогон: он бывает промежуточным. Пример: программа делает видео, и с некоторого кадра в нём появляется второй персонаж — его картинку дают в этом месте графа, а берёт её следующий кадр. Это второе или третье место, но не первое.

Каждое из трёх мест принимает любой вид данных (24.1), а его узел на холсте несёт поле, в котором данные задают прямо на узле (Мастерская, 5.5). Причина: четвёртый узел повторял бы три имеющихся и в большинстве графов был бы лишним.

24.3 Показ

Данные человеку показывает узел отображения: то, что пришло на его вход, — текст, картинку, звук, видео, файл, список — он показывает на холсте, а вынесенный на пульт — на листе пульта. Узел отображения — часть программы, а не инструмент мастерской: показывать данные нужно и готовой программе, и тому, кто её собирает, и это одно умение, а не два. Узел, который на пульт не вынесли, человеку, пользующемуся программой, не виден.

По правилу 8 раздела 3 это примитив: показ человеку из остальных видов узлов не собирается: переменная с элементом «Значение» показывает одну строку текста, а не картинку, звук или галерею. Вид узла — «Показать» (show): вход value, выход success с тем же пакетом, параметр path — какое поле показать; поле, которого в пакете нет, показано отсутствующим, и прогон идёт дальше (Форматы файлов, 16.5).

24.4 Файл, который дал человек

Картинка, звук, видео и файл, которые человек дал программе, копируются в папку процесса. Программа не зависит от места, где лежал оригинал: его можно переместить или удалить, а процесс уходит к другому человеку целым (Форматы файлов, 10.1). Цена этого — большой файл занимает место дважды.

Сколько живёт копия, решает место, куда файл дали:

Файл после появления не изменяется. Правило относится к любому файлу, названному в пакете, — данному человеком и созданному модулем: кому нужен изменённый файл, создаёт новый. Разветвление копирует пакет вместе со ссылками на файлы, и две ветки получают два имени одного файла; изменяемый файл одна ветка портила бы другой. Платформа держит правило сама: файл, данный человеком, принятый с программой, перенесённый из прогона в файлы процесса и файл, который модуль отдал в результаты прогона, с появления только для чтения — модуль, который по ошибке пишет в него, получает отказ системы и кончает шаг сбоем, а не меняет файл другой ветке. Убирают файл вместе с тем, что его держит: это право его папки. Копия в пакете для другого человека — его: её можно писать, и следующий пакет на то же место её заменяет.

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": "<номер>" }:

Отметка повторяется при продолжении прогона (ЗС-06): время — начало того же прогона, номер — из тех же данных; поэтому она не часы и не случайное число. Время в середине долгого прогона — работа модуля.

Почему это вид узла. Время прогона и имя данных шага знает только движок: ни событие — время у него есть лишь у таймера и только в его пакете, — ни переменная, ни модуль их не видят. Без отметки у записи нет ни времени, ни имени, по которому на неё ссылаются: ответ, правка, удаление, «прочитано» (ЗС-10).

26 Отбор

Узел pick берёт из списка записи, которые проходят проверку, и считает их.

Так собираются поиск (contains по тексту сообщения), запись по ключу (equals, первая — found.items.1) и счёт: «непрочитанных в чате» — два отбора подряд, по чату, затем по отметке «прочитано», и второй берёт список из found.items первого.

Почему это вид узла. «Для каждого» с условием внутри даёт подходящие записи по одной, но не их список и не их число: собрать их обратно граф может только записью в переменную на каждом круге, а это уже не запрос, а изменение. Отбор ничего не вычисляет: проверка из закрытого набора и счёт найденного (ЗС-10). Арифметика и выражения остаются модулю (ВГ-01).

27 Время

Узел moment — «Время» — считает момент по календарю: от даты через столько-то дней, в такой-то час. «Сегодня в 09:00», «через неделю», «завтра в тот же час».

Поле, в котором нет даты, или поле дней, в котором нет целого числа, — отказ шага graph_invalid с названием поля: момент не выдумывается. Дата с поясом сначала переводится на часы этой машины, поэтому «сегодня» — сегодня у человека.

Как отметка, «Время» — не часы: от начала прогона оно считает то же самое и при продолжении прогона после сбоя (ЗС-06). Так собираются срок повторяемого дела — поле срока и его повтор в днях — и напоминание «сегодня в 09:00».

Почему это вид узла. Счёт с датами из остальных видов не собирается: в графе нет арифметики, а сравнения «раньше» и «позже» (раздел 8) только сравнивают моменты. Произвольные выражения остаются модулю (ВГ-01), но календарь у всех один: «через неделю» не зависит от программы, и без узла каждый автор писал бы один и тот же модуль. Узел ничего не вычисляет, кроме этого одного: сдвиг по календарю и час (ЗС-10).

28 Шаблон

Узел template — «Шаблон» — подставляет поля пакета в текст: «{found.count} дел не сделано», «{author}: {text}».

Шаблон подставляет и не считает: в скобках поле, а не выражение. Число и «да/нет» он не склоняет («3 дел»), дату не переводит в слова — она стоит так, как лежит в пакете.

Почему это вид узла. Без шаблона сказать словами то, что лежит в данных, граф не может: уведомление, сообщение, строка журнала берут поле целиком, и утренняя сводка не скажет «3 дела не сделано». Модулем это решалось бы у каждого автора своим, а подстановка одна и та же у всех и не вычисляет: счёт, условие внутри текста, формат чисел и дат остаются модулю (ВГ-01, ЗС-10).