Galatine Связаться с нами

galatine / ru / format

CATK

Формат файла расследования

Одно расследование в одном файле: граф, материалы, написанные по нему, и задачи проверки, которыми его вели. Файл создаёт человек кнопкой «Экспорт → Рабочее пространство», и он же решает, кому его отдать.

Этот документ описывает формат словами. Машинная половина — schema/counterattack-workspace-1.schema.json, JSON Schema черновика 2020-12. Схема написана по-английски: её читают программы и люди, не знающие русского. Оба описания сверяются тестами (tests/format-schema.test.mjs), поэтому разойтись молча не могут.

Зачем формат опубликован. Чтобы чужая редакция могла проверить наш файл своими средствами, не ставя наш продукт. Это же и обязательство: опубликованный формат нельзя менять произвольно, и поэтому он публикуется только теперь — после того, как журнал ревизий и перенос устоялись.

Две формы одного файла

Форма Что внутри Когда
.json расследование, документы только названы вложений нет, или их решили не класть
.catk то же самое плюс сами документы обычный ZIP: workspace.json и папка files/

.catk — обычный ZIP-архив: получатель откроет его любым архиватором и прочитает документы даже без CATK. Записывается без сжатия, потому что PDF и сканы уже сжаты.

Любую из двух форм можно зашифровать парольной фразой. Тогда файл начинается строкой CATKENC1, дальше идёт заголовок обычным текстом (алгоритм, число повторов, соль, вектор) и шифротекст. Заголовок читается чем угодно, поэтому нашедший файл поймёт, что это, а не решит, что файл испорчен. Запасного входа нет: забытая фраза означает, что файл не открыть.

Публикация — тот же файл, в котором меньше записей

Опубликованный материал — файл, а не страница: то же поле format, та же версия, та же схема, тот же разбор. Внутри один материал и только то, на чём он стоит: процитированные и связанные записи, связи между ними, их доказательства, документы с хешами, подписи и закрытые задачи проверки. Ни одной чужой записи расследования. Связь уезжает только тогда, когда уехали оба её конца: иначе в файле осталась бы линия, ведущая в ничто.

Узнаётся публикация по полю publication:

  • articleId и title — какой материал опубликован;
  • standsOn — записи, на которых он стоит, и у каждой признак quoted: процитирована в тексте или только связана с материалом;
  • omitted — чего в файле нет и почему, словами. Пропуск, о котором не сказали, читатель принимает за отсутствие поля.

Почему координаты публикуются, хотя это место на карте. latitude, longitude, areaRadiusKm и область area входят в отпечаток подписи. Без них запись перестаёт быть той, под которой стоит подпись: все подписи стали бы «под прежней версией», четвёртый шаг проверенности погас бы, и файл показал бы шагов меньше, чем есть, то есть соврал бы в пользу сомнения. Решение «не показывать место» принимается до подписи: при публикации его уже не принять.

Байты документов по умолчанию не уезжают (attachments.included: false). Имена и хеши позволяют сверить цепочку. Отдать материалы дела целиком — отдельное решение на каждую публикацию, и сказать «да» значит выбрать .catk, ту же вторую форму файла, что и у расследования.

Реестр исправлений лежит в файле, а не только на странице

Исправленная публикация несёт publication.corrections — список записей, у каждой три поля: date (гггг-мм-дд), what (что именно исправлено) и who (кто потребовал). Запись без любого из трёх продукт не пишет: он отказывается собрать файл и называет недостающее поле. «Исправлено 14 сентября» без того, что исправлено, выглядит отчётом и ничего не сообщает.

Список накопительный и старые первыми: каждая следующая версия несёт все прежние записи. Читатель, которому прислали только последний файл, обязан видеть всю историю правок, а не последнюю из них.

Полей нет, пока исправлений не было. Пустой список утверждал бы, что исправлений не было. Отсутствующее поле не утверждает ничего, и прежние публикации, собранные до появления реестра, остаются файлами той же формы. Так же в записи нет eventDate, если даты никто не ставил.

Новая версия сменяет прежний файл, и прежний остаётся на месте

publication.revision несёт номер версии, а publication.supersedes называет версию, которую эта сменила: {revision, file, sha256}. Прежний файл остаётся по своему адресу и своими байтами; новый называет его имя и хеш. Скачанный файл отозвать нельзя, поэтому подмена по тому же адресу здесь не предусмотрена вовсе, и хеш — то, чем читатель проверяет, что прежнюю версию не переписали молча.

Цепочка версий читается из самих файлов: каждый называет предшественника. Второго списка версий нет нигде, и разойтись ему не с чем.

Исправление законно понижает проверенность. Поправив имя, доказательство, источники, даты, место или документ, мы получаем запись, под которой прежняя подпись не стоит: подписи становятся «под прежней версией», четвёртый шаг гаснет. Файл при этом исправен и говорит правду: исправленную запись второй человек подписывает заново. Редакционное правило целиком лежит в CORRECTIONS.md.

Что файл обязан иметь

Три поля, без которых читать нечего:

  • format — всегда "counterattack-workspace". Файл называет себя сам, читатель не должен угадывать по форме.
  • version — целое число, сейчас 1. Это версия формата, а не продукта.
  • graph.nodes — список записей. Может быть пустым, но обязан быть списком.

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

Версия старше вашей — это отказ, а не догадка

Читатель, знающий версию 1, обязан отказаться от файла с версией 2, а не читать его оптимистично. Файл заменяет рабочее пространство целиком, и незнакомое поле здесь означает потерю, а не украшение.

Обратное неверно: файл версии 1 читатель версии 2 обязан открывать по-прежнему.

Схема говорит, что файл читается, а не что расследование хорошее

Это главное решение формата, и оно объясняет почти все остальные.

Отказ бывает только для того, что прочитать нельзя: не наш формат, неизвестная версия, граф не список, запись без id, связь без конца. Всё остальное — строки отчёта, а не отказ.

Поэтому в схеме почти нет enum. Уровень риска записан строкой, хотя продукт знает шесть значений и рисует каждому свой цвет. Причина простая: файл может прийти из более новой версии или из чужой системы через мост, и незнакомое значение продукт открывает и показывает цветом «неизвестно», а редактор сохраняет его отдельной опцией с пометкой. Схема, отвергающая такой файл, расходилась бы с продуктом, и посторонний человек получил бы отказ там, где мы сами файл открываем.

Известные значения названы в описании каждого поля. Это разница между «мы этого не ждём» и «так нельзя».

Что означают поля, которые легко понять неправильно

evidence и sourceIds стоят рядом. Первое поле содержит строку, написанную аналитиком: ссылку, номер документа, заметку. Второе содержит указания на записи типа «Источник». Указать на запись сильнее, чем написать строку: на один источник могут сослаться несколько записей. Превращать чужую строку в запись догадкой продукт не станет: это было бы выдуманным доказательством.

confidence — это мнение аналитика. В интерфейсе поле подписано «Оценка аналитика». Проверенность считается из самой записи (источник назван, документ приложен, задача проверки закрыта, подписал второй человек) и в файле не хранится: она вычисляется заново, поэтому не может протухнуть.

Поля confidence может не быть, и это значит, что оценки нет. Мнение, которого никто не высказывал, — такой же выдуманный показатель, как число, посчитанное неизвестно из чего. Читателю файла: не подставляйте на это место значение по умолчанию. Ноль при этом отсутствием не считается: его поставил человек.

place, latitude и longitude независимы. Название пишет аналитик, координаты ставит он же. CATK не превращает название в координаты и координаты в название: любое из направлений — выдуманное доказательство. Отсюда законное состояние: место названо, координат нет. Координаты нужны обе или ни одной.

areaRadiusKm отличает точку от области. Пусто и ноль значат одно: точку. Точка утверждает «ровно здесь», чего источник обычно не говорит: «порт Русе» и «в пределах города» — это области. Своего свойства для радиуса у FollowTheMoney нет, и он идёт в блоке counterattack, как ссылки записи и связи на записи-источники (sourceIds) и оценка аналитика у связи.

area у записи и route у связи описывают форму места, которую нарисовал или вписал аналитик. Область задаёт геометрия GeoJSON Polygon с одним замкнутым кольцом, маршрут задаёт LineString, и линия связи на карте идёт по нему вместо прямой. Порядок в GeoJSON — долгота, широта, обратный полям latitude и longitude: чужой инструмент, прочитавший пару наоборот, поставит Дунай в Индийский океан. Форму пишет аналитик: из названия места, типа записи или справочника она не выводится. У записи типа Person области нет, у связи с такой записью маршрута нет: продукт не строит траекторию человека (SCOPE.md). Форма, пришедшая с ними в файле, хранится и не рисуется. Так же хранится и не рисуется форма, которую нельзя прочитать: граница пересекает сама себя, у области меньше трёх точек, больше 500 точек. Область покрывает подпись второго аналитика, маршрут лежит на связи и подписью не покрывается. Через FollowTheMoney формы идут в блоке counterattack. CSV форм не несёт, как не несёт вложений.

Кольцо области обходится против часовой стрелки, как требует RFC 7946: обведённое по часовой продукт разворачивает, оставляя первую вершину первой. Форма из ГИС часто приходит обёрнутой в Feature, с bbox или высотой третьим числом пары. Такая форма хранится как пришла, ничего из неё не снимается, и карта читает из неё геометрию. Пределы первой версии. У области одно кольцо, без дыр. Форма через 180-й меридиан рисуется длинным путём через всю карту. Маршрут состоит из прямых отрезков в проекции карты, дуг по кривизне земли в нём нет.

eventDate — дата события. Время создания и правки записи никогда не идёт вместо неё.

evidenceStatus у связи — отметка, поставленная рукой, и она отдельна от того, приложено ли что-нибудь. Продукт эту разницу называет, а не скрывает.

hash вложения — SHA-256 содержимого, строчными шестнадцатеричными. Отвечает на вопрос, на который имя и размер ответить не могут: тот ли это файл. Пустое значение означает «не проверялся»: документ приложили до появления этой проверки, а поставить хеш задним числом нельзя, потому что содержимое могло измениться как раз в этом промежутке.

origin есть только у заимствованной записи. Запись, пришедшая из чужой системы (Aleph, OpenAleph, любой другой), несёт system, licence и, если они известны, dataset и url. Лицензия обязательна: заимствование без неё — путь, которым набор данных «только для некоммерческого использования» попадает в платный продукт.

То же самое написано и в поле доказательства, обычной фразой. Аналитик читает поле доказательства и не читает служебные поля, поэтому иначе заимствованное утверждение выглядело бы как своя проверка. У записи, сделанной здесь, поля origin нет вовсе. Сама эта фраза доказательством не считается: запись или связь, у которой в поле доказательства больше ничего нет, читается как запись без доказательства. Шаг проверенности «источник назван» она проходит.

wikidataId — элемент Wikidata, с которым аналитик связал запись: Q и цифры. Его ставит нажатие аналитика в досье, сохранение документа элемента или добавление связей из него; по совпадению имени продукт запись с элементом не связывает никогда. Поле есть только у связанной записи. Значение, которое не номер элемента, хранится как есть и ничего не значит. Когда связь есть, её покрывает подпись второго аналитика. Через FollowTheMoney она идёт одноимённым свойством этой модели.

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

Подпись значит ровно одно: кто-то, у кого есть этот ключ, видел эту запись в этом виде. Она не значит, что запись верна, и не доказывает, кто этот человек — имя рядом с ключом пишет тот же, кто ключом владеет. Удостоверяющего центра здесь нет, и мы его не изображаем.

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

position попадает в файл, но не входит в отпечаток подписи. Положение на холсте принадлежит расследованию, поэтому оно хранится вместе с ним. В отпечаток оно не входит: иначе подпись слетала бы от перетаскивания.

dueDate и due у операции — не одно и то же

dueDate — это день, к которому задача должна быть сделана, датой в ISO 8601 (ГГГГ-ММ-ДД). С ним и считают: очередь на сегодня, плитка «срок сегодня или прошёл».

due — прежняя свободная строка: «Срок 27 авг.», «Legal review», «Срок сегодня». Она осталась в формате и хранится как написана. Продукт её не разбирает и не переписывает: у «27 авг.» нет года, «Legal review» вовсе не дата, «Срок сегодня» значит «сегодня по состоянию на день, которого никто не записал». Превратить такое в дату можно только догадкой, а догадка в поле даты становится выдуманным фактом.

Открывая старый файл, продукт показывает эту строку рядом с пустым полем даты и помечает её пришедшей из другой версии. Дату ставит человек.

blocks у материала главные, body — производная копия

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

Известные виды блоков: prose, node, relation, evidence, operation, gaps. Список открытый. Читатель, встретивший незнакомый вид, обязан сохранить блок как есть, а не выбросить: файл может прийти из более новой версии, и выброшенный блок — это молча испорченный чужой материал.

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

Это не то же самое, что пара dueDate и due выше. Там второе поле нельзя вывести из первого, и оба остаются настоящими. Здесь body выводится из blocks целиком, и потому объявлен копией.

gapsHidden — согласие автора скрыть неустановленное, а не тишина

В конце выведенного документа стоит раздел «что не установлено» по всем упомянутым записям. Автор вправе его выключить: черновик для юриста и выжимка на одну страницу — законные документы.

Но выключенный раздел не исчезает молча. На его месте в самом документе остаётся строка о том, что неустановленное скрыто автором. Настройка живёт у того, кто печатает, и до читателя не доходит; строка доходит. Поле gapsHidden — след этого решения в файле.

modelAnswers — что ответила модель, а не что установлено

Поле лежит в графе, и его нет, пока модель ни о чём не спрашивали. У записи хранится последний ответ: когда спросили (askedAt), какую модель просили (requested) и какая ответила (servedBy), чем кончилось (outcome: ответ, отказ классификатора, обрезанный или нечитаемый ответ), что ушло (passages: поля записи и её связей тем текстом, который получила модель), что прошло проверку (proposals) и сколько отброшено (dropped).

Предложение попадает в proposals, только если каждая его цитата стоит слово в слово в том поле passages, которое оно называет. Это проверка цитаты, а не утверждения: верно ли предложение, решает аналитик. Принятое предложение становится правкой записи и дальше живёт как она.

В публикацию поле не попадает: ответ модели — предложение, а не доказательство. Это названо в omitted.

Опись документов

Поле attachments говорит, что случилось с файлами:

  • included — приложены ли байты.
  • files — что уехало, с путём внутри архива.
  • missing — что названо, но не приложено.

Документ, которого не оказалось в хранилище, попадает в missing, а не выдаётся за приехавший. Молчание здесь — худший исход: получатель, уверенный, что документы у него, сошлётся на то, чего не видел.

Чего в формате нет намеренно

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

Как проверить файл

npx -p ajv-cli@5 -p ajv-formats@3 ajv validate -s schema/counterattack-workspace-1.schema.json -d ваш-файл.json --spec=draft2020 -c ajv-formats

Плагин форматов здесь нужен только ajv: в строгом режиме он отказывается компилировать схему, встретив date-time, хотя черновик 2020-12 разрешает считать format пометкой и не проверять её. Любая другая реализация JSON Schema 2020-12 подойдёт так же, и часть из них — без плагинов. В этом и смысл публикации: файл проверяется чужими средствами, а не нашими.

Или страницей «Проверить файл расследования» (/validator в самом продукте). Она читает файл в вашей вкладке, ничего никуда не отправляет и отвечает по существу: что это за файл, сходится ли он со схемой, сколько шагов проверенности прошла каждая запись, какие документы не приехали, настоящие ли подписи и какие пробелы в нём видны. Страница есть на двух языках: ?lang=ru в адресе открывает её по-русски, ?lang=en по-английски.

Подписи она проверяет криптографией, а сравнение строк отвечает на другой вопрос. «Подпись настоящая» и «подпись стоит под нынешним видом записи» — разные утверждения, и файл, где сходится второе, но не первое, подделан.