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