Системные требования

CPU

x86-64. Для больших проектов рекомендуется не менее 8 ядер

RAM

От 4 ГБ. При обновлении между версиями лучше не меньше 8 ГБ. Для больших проектов — 16+ ГБ

HDD

От 1 ГБ до нескольких ТБ при хранении множества снимков больших проектов

OS

  • Ubuntu 18 и выше
  • Debian 10 и выше
  • RHEL/CentOS 8
  • Rocky Linux 9 и выше
  • Astra Linux Special Edition 1.7, базовый уровень защищенности
  • Alt Linux p8 и выше
  • Microsoft Windows 10

Функциональность сервера при запуске на Windows ограничена:

  • Не поддерживается запуск сервера с параметром --svace: не будет работать импорт .svace-dir через WebUI и REST API
  • Не поддерживается генерация PDF на основе HTML

Browser

Для работы с веб-интерфейсом

  • Chrome 103 и выше
  • Firefox 100 и выше
  • Opera 89 и выше
  • Safari 16 и выше
  • Edge 103 и выше

Создание кейсов в системах отслеживания ошибок

Gitlab

На сервере, где будет выполняться хук, должен быть установлен python3.

  • Скачайте и распакуйте архив со скриптом и конфигом хука: 
Gitlab-hooks.zipZIPСкачать


  • Сделайте файл gitlab-create-issue.py исполняемым
chmod u+x gitlab-create-issue.py
  • Поменяйте в файле gitlab-create-issue.py путь к интерпретатору python3, если в вашей системе он не такой, как указано там в первой строчке
  • В файле hooks.json поменяйте URL гитлаба в args
  • В качестве второго аргумента args впишите ID проекта гитлаба. Его можно найти на главной странице проекта, под именем
  • В гитлабе создайте Access Token для проекта в Settings > Access Tokens. Выберите роль 'Reporter', scope — 'api'


  • Созданный токен впишите как третий аргумент в args
  • В аргументе cmd этого же файла укажите полный путь к скрипту gitlab-create-issue.py
  • Если вы настраиваете хук на Windows, то вместо этого укажите в cmd путь к интерпретатору python (или просто имя, если он есть в PATH), а в качестве первого аргумента в args добавьте путь к скрипту хука
  "cmd": "python",
  "args": [
    "C:\\svacer-hooks\\gitlab-create-issue.py",
    "https://gitlab.example.com",
    ...
  • Запустите svacer с указанием пути к файлу hooks.json в аргументе hooks
 svacer server --hooks=/home/user/svacer/hooks.json
  • Хук будет доступен в UI Svacer, на правой панели, в Details > Hooks > Run


  • После нажатия на хук он выполнится и в указанном проекте гитлаба будет создан новый кейс с информацией из предупреждения в заголовке и описании, в качестве лейбла (тега) будет задан severity чекера


Jira

На сервере, где будет выполняться хук, должен быть установлен python3.

  • Скачайте и распакуйте архив со скриптом и конфигом хука:
Jira-hooks.zipZIPСкачать


  • Сделайте файл jira-create-issue.py исполняемым
 chmod u+x jira-create-issue.py
  • Поменяйте в файле jira-create-issue.py путь к интерпретатору python3, если в вашей системе он не такой, как указано там в первой строчке
  • В файле hooks.json поменяйте URL джиры в args
  • В качестве второго аргумента args впишите ID проекта джиры. Его можно найти на главной странице проекта в графе Key, или взять из URL проекта
  • Создайте Personal Access Token для аккаунта, от которого будут создаваться кейсы в Profile > Personal Access Tokens > Create Token. У этого аккаунта должны быть права на создание кейсов в соответствующем проекте
  • Созданный токен впишите как третий аргумент в args
  • В качестве четвертого аргумента в args укажите тип кейса, который будет создаваться. Посмотреть какие типы доступны в вашем проекте можно при создании кейса в поле issue type
  • В аргументе cmd этого же файла укажите полный путь к скрипту jira-create-issue.py
  • Если вы настраиваете хук на Windows, то вместо этого укажите в cmd путь к интерпретатору python (или просто имя, если он есть в PATH), а в качестве первого аргумента в args добавьте путь к скрипту хука
  "cmd": "python",
  "args": [
    "C:\\svacer-hooks\\jira-create-issue.py",
    "https://jira.example.com",
    ...
  • Запустите svacer с указанием пути к файлу hooks.json в аргументе hooks
 svacer server --hooks=/home/user/svacer/hooks.json
  • Хук будет доступен в UI Svacer, на правой панели, в Details > Hooks > Run

  • После нажатия на хук он выполнится и в указанном проекте джиры будет создан новый кейс с информацией из предупреждения в заголовке и описании, в качестве лейбла (тега) будет задан severity чекера


Redmine

На сервере, где будет выполняться хук, должен быть установлен python3.

  • Скачайте и распакуйте архив со скриптом и конфигом хука: 
Redmine-hooks.zipZIPСкачать


  • Сделайте файл redmine-create-issue.py исполняемым
 chmod u+x redmine-create-issue.py
  • Поменяйте в файле redmine-create-issue.py путь к интерпретатору python3, если в вашей системе он не такой, как указано там в первой строчке
  • В файле hooks.json поменяйте URL редмайна в args
  • В качестве второго аргумента args впишите ID проекта редмайна. Его можно найти в настройках проекта, или взять из URL
  • В Redmine должен быть включен REST API в Administration > Settings > API
  • Когда REST API включен, вы найдете свой API-ключ на странице аккаунта (/my/account), в правой панели
  • Впишите этот ключ как третий аргумент в args
  • В аргументе cmd этого же файла укажите полный путь к скрипту redmine-create-issue.py
  • Если вы настраиваете хук на Windows, то вместо этого укажите в cmd путь к интерпретатору python (или просто имя, если он есть в PATH), а в качестве первого аргумента в args добавьте путь к скрипту хука
  "cmd": "python",
  "args": [
    "C:\\svacer-hooks\\redmine-create-issue.py",
    "https://redmine.example.com",
    ...
  • Запустите svacer с указанием пути к файлу hooks.json в аргументе hooks
 svacer server --hooks=/home/user/svacer/hooks.json
  • Хук будет доступен в UI Svacer, на правой панели, в Details > Hooks > Run
  • После нажатия на хук он выполнится и в указанном проекте редмайна будет создан новый кейс с информацией из предупреждения в заголовке и описании

Примеры использования public REST API

Получение URL загруженного снимка

Этот пример работает только с python3 и не работает при аутентификации в Svacer через LDAP.

Скачайте и распакуйте 

Get_link.zipZIPСкачать


Запрос:

python get_link.py --url http://svacer.example.com:8080 --user admin --password admin --project bash

Ответ:

http://svacer.example.com:8080/mode/review/project/0fd645aa-8e70-4a4f-b68b-766c4f337bf2/branch/8925df5a-7a98-4f07-bc88-ee4ea5b43813/snapshot/e3367efa-a804-4c05-9a7a-a7cb052bef1d

Примеры использования фильтров в public api

Данные фильтры служат только для ограничения вывода некоторых методов в public api. Они являются отдельной сущностью, наравне с глобальными фильтрами и пользовательскими фильтрами. Основное отличие в том, что время их жизни соответствует одному запросу, в котором они заданы.

Вспомогательные модули

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

import requests
from requests import auth
from requests.models import HTTPBasicAuth
import json
from dataclasses import dataclass
import sys
import base64

def printerr(text):
    #print in stderr and exit
    print(f"{text}", file=sys.stderr, flush=True)
    exit()

@dataclass
class ProjectFilter:
    id: str
    name: str

class ProjectFilterEncoder(json.JSONEncoder):
    def default(self, o):
        return o.__dict__

@dataclass
class BranchFilter:
    id: str
    name: str

class BranchFilterEncoder(json.JSONEncoder):
    def default(self, o):
        return o.__dict__

@dataclass
class SnapshotFilter:
    id: str
    name: str

class SnapshotFilterEncoder(json.JSONEncoder):
    def default(self, o):
        return o.__dict__

@dataclass
class MarkerFilter:
    severity: str
    file: str
    checker: str
    review: str

class MarkerFilterEncoder(json.JSONEncoder):
    def default(self, o):
        return o.__dict__

@dataclass
class Filter:
    project: ProjectFilter
    branch: BranchFilter
    snapshot: SnapshotFilter
    marker: MarkerFilter

class FilterEncoder(json.JSONEncoder):
    def default(self, o):
        return o.__dict__

def CreateFilterParam(filter: Filter):
    jsoned = FilterEncoder().encode(filter)
    b64ed = base64.b64encode(jsoned.encode("utf-8"))
    return b64ed.decode("utf-8")

Принцип использования классов следующий:

  1. Создается экземпляр нужного класса XXXXFilter
  2. Создается base64 строка на основе объекта пункта 1, которая в дальнейшем передается в url запросе. Для этого используется метод CreateFilterParam()

Например. Для создания фильтра, ограничивающего множество проектов выражением darpa*, можно использовать следующие строчки кода:

f: Filter = Filter(ProjectFilter(id="", name="darpa*"), None, None, None)
param = CreateFilterParam(f)

Для проекта, ветки и снимка можно указать как имя так и ID. Имя можно указать с использованием регулярного выражения. ID необходимо указать полностью.

Метод /api/public/login

Данный класс может быть использован для соединения с сервером Svacer. В данном модуле демонстрируется взаимодействие с методом /api/public/login.

import requests

class SvacerClient:
    def __init__(self, host, user, password):
        """
        Initialize the Svacer client        
        """
        self.host = host
        self.user = user
        self.password = password
        self.token = None
        self.auth_header = None
        self.session = requests.Session()  # Use session for connection pooling
        self.set_auth_token()
    
    def set_auth_token(self):
        """
        Get authorization token using /api/public/login and setup in session
        """
        try:
            response = requests.post(
                url=f"{self.host}/api/public/login",
                json={
                    "login":self.user,
                    "password":self.password,
                },
                verify=False
            )
            response.raise_for_status()  # Raise an exception for bad status codes            
            body = response.json()
            self.token = body["token"]
            self.auth_header = {'Authorization': f'Bearer {self.token}'}
            
            # Set the authorization header in the session
            self.session.headers.update(self.auth_header)
            
            return self.token
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"Failed to get authentication token: {e}")
        except KeyError as e:
            raise Exception(f"Token not found in response: {e}")
    
    def post(self, endpoint, data=None, json=None, **kwargs):
        """
        Make a POST request to the API

        """
        if self.auth_header is None:
            raise Exception("No authentication token available. Call get_auth_token() first.")
        
        url = f"{self.host}{endpoint}"
        
        try:
            response = self.session.post(
                url, 
                data=data, 
                json=json, 
                verify=False,
                **kwargs
            )
            response.raise_for_status()
            return response
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"POST request failed: {e}")
    
    def get(self, endpoint, params=None, **kwargs):
        """
        Make a GET request to the API
  
        """
        if self.auth_header is None:
            raise Exception("No authentication token available. Call get_auth_token() first.")
        
        url = f"{self.host}{endpoint}"
        
        try:
            response = self.session.get(
                url, 
                params=params, 
                verify=False,
                **kwargs
            )
            response.raise_for_status()
            return response
            
        except requests.exceptions.RequestException as e:
            raise Exception(f"GET request failed: {e}")
Метод /api/public/projects

В данном примере выводится в формате json список проектов, имя которых соответствует регулярному выражению darpa*

from filters import *
from svacer import *

cl = SvacerClient(host="http://swarm-mgr:10142",user="admin",password="admin")
f: Filter = Filter(ProjectFilter(id="", name="darpa*"), None, None, None)
param = CreateFilterParam(f)
resp=cl.get(f"/api/public/projects?filters={param}")
print(f"Response: {resp.json()}")

Результат может быть следующим

[
    {
        "project":
        {
            "id": "019abb61-8cd3-7437-957d-aed0c87e603a",
            "name": "darpa-clone",
            "time": "2025-11-25T14:18:43.79526Z",
            "created_by": "admin",
            "created_by_id": "00000000-0000-0000-0000-000000000000"
        },
        "branches":
        [
            {
                "id": "019abb61-83b1-7d80-8d23-ce849387bd6e",
                "name": "master",
                "time": "2025-11-25T14:18:41.457887Z",
                "created_by": "admin",
                "created_by_id": "00000000-0000-0000-0000-000000000000"
            }
        ]
    },
    {
        "project":
        {
            "id": "e6abca1b-23a0-4107-9120-4d8d3596030b",
            "name": "darpa",
            "time": "2020-04-03T04:54:13.754864Z",
            "created_by": "importer",
            "created_by_id": null
        },
        "branches":
        [
            {
                "id": "ab179fad-c96a-498d-8407-2a3c82719385",
                "name": "master",
                "time": "2020-04-03T04:54:13.754864Z",
                "created_by": "importer",
                "created_by_id": null
            }
        ]
    }
]
Метод /api/public/projects/{project_id}/branch/{branch_id}/snapshots

С помощью данного метода можно получить список снимков указанной ветки. Пример кода для получения снимков с именем ver. 2.7.0*

from filters import *
from svacer import *
import json

def get_snapshots(pr_id, br_id):
    cl = SvacerClient(host="http://swarm-mgr:10142",user="admin",password="admin")
    f: Filter = Filter(None,None,SnapshotFilter(id="",name="ver. 2.7.0*"), None)
    param = CreateFilterParam(f)
    resp=cl.get(f"/api/public/projects/{pr_id}/branch/{br_id}/snapshots?filters={param}")
    fmt = json.dumps(resp.json())
    print(f"Response: {fmt}")

get_snapshots("e6abca1b-23a0-4107-9120-4d8d3596030b","ab179fad-c96a-498d-8407-2a3c82719385")

Возможный вывод команды:

[
    {
        "id": "4242b43a-da44-44ca-9b28-770295b98493",
        "name": " ver. 2.7.1 200419 05:55",
        "time": "2020-04-03T04:56:49.006218Z",
        "link": "http://swarm-mgr.home:10142/mode/review/project/e6abca1b-23a0-4107-9120-4d8d3596030b/branch/ab179fad-c96a-498d-8407-2a3c82719385/snapshot/4242b43a-da44-44ca-9b28-770295b98493",
        "created_by": "importer",
        "created_by_id": "00000000-0000-0000-0000-000000000002",
        "import_time": "2019-04-20T04:26:52Z",
        "details":
        {
            "branch": "master",
            "branchIDHint": "4d1f1413-c638-4a8b-aa4d-d9fccd8df3e9",
            "buildObject": "cf43335308de8b7b3ae03735a7f738a6e9246f52",
            "dxrIncluded": true,
            "host":
            {
                "hostname": "seroshki",
                "user": "sergey"
            },
            "id": "4242b43a-da44-44ca-9b28-770295b98493",
            "importTime": "2019-04-20T04:26:52Z",
            "migration-data":
            {
                "DXR": "ce03cd6d1bd54eac92e227d4184356a90b64b7c1",
                "SOURCE_TREE": "0949114563f48d5a2df6e94816b7b1095b6c6540",
                "date": "2019-04-20T04:26:52Z",
                "description": " ver. 2.7.1 200419 05:55",
                "id": "2af625b5faad4083be881135ad1daf6b304e4c99",
                "owner": "7a831745d52244db2a4f1692f0c473e61f0f0157",
                "parent_id": "0d491fde062e2795a4b38758488a47bf8481d03c",
                "sourceData": "cf43335308de8b7b3ae03735a7f738a6e9246f52",
                "svres": "2af625b5faad4083be881135ad1daf6b304e4c99.svres.gzip",
                "type": "snapshot",
                "user": "jenkins"
            },
            "project": "darpa",
            "projectIDHint": "354c9c94-7b57-4427-8ce0-c41f1e59e6c2",
            "snapshot": " ver. 2.7.1 200419 05:55",
            "sourceIncluded": true
        }
    },
    {
        "id": "4f706c3a-fdac-475e-bb0a-8bdd753879ca",
        "name": " ver. 2.7.1 130419 07:39",
        "time": "2020-04-03T04:58:53.618464Z",
        "link": "http://swarm-mgr.home:10142/mode/review/project/e6abca1b-23a0-4107-9120-4d8d3596030b/branch/ab179fad-c96a-498d-8407-2a3c82719385/snapshot/4f706c3a-fdac-475e-bb0a-8bdd753879ca",
        "created_by": "importer",
        "created_by_id": "00000000-0000-0000-0000-000000000002",
        "import_time": "2019-04-13T04:33:46Z",
        "details":
        {
            "branch": "master",
            "branchIDHint": "c472aab7-d53f-496c-9d3d-069c0901d9bb",
            "buildObject": "cf43335308de8b7b3ae03735a7f738a6e9246f52",
            "dxrIncluded": false,
            "host":
            {
                "hostname": "seroshki",
                "user": "sergey"
            },
            "id": "4f706c3a-fdac-475e-bb0a-8bdd753879ca",
            "importTime": "2019-04-13T04:33:46Z",
            "migration-data":
            {
                "DXR": "ce03cd6d1bd54eac92e227d4184356a90b64b7c1",
                "SOURCE_TREE": "0949114563f48d5a2df6e94816b7b1095b6c6540",
                "date": "2019-04-13T04:33:46Z",
                "description": " ver. 2.7.1 130419 07:39",
                "id": "0d491fde062e2795a4b38758488a47bf8481d03c",
                "owner": "7a831745d52244db2a4f1692f0c473e61f0f0157",
                "parent_id": "edca1583aa41489d37ae9fb2c348ad066ecd3ab3",
                "sourceData": "cf43335308de8b7b3ae03735a7f738a6e9246f52",
                "svres": "0d491fde062e2795a4b38758488a47bf8481d03c.svres.gzip",
                "type": "snapshot",
                "user": "jenkins"
            },
            "project": "darpa",
            "projectIDHint": "cf032d99-2efe-4ee0-8db8-ee85ff17b296",
            "snapshot": " ver. 2.7.1 130419 07:39",
            "sourceIncluded": false
        }
    }
 ]
Метод /api/public/projects/{project_id}/branch/{branch_id}/snapshots/{snapshot_id}/fullmarkers

Данный метод служит для получения информации о маркерах указанного снимка. Информация о маркере может включать в себя:

  • трассу ошибки (параметр traces, возможные значения true,false)
  • информацию о чекере (параметр checker_info, возможные значения true,false)
  • историю разметки (параметр review_history, возможные значения true,false)
  • история комментария (параметр comment_history, возможные значения true,false)
  • идентификатор пользовательского фильтра (параметр custom_filter)
  • фильтр на параметры маркера

Данный метод может требовать длительного времени выполнения для проектов с большим количеством предупреждений.

Пример кода для получения всего множества информации о маркерах, размеченных со статусом Conf*:

def get_markers (pr_id, br_id, snap_id):
    cl = SvacerClient(host="http://swarm-mgr:10142",user="admin",password="admin")
    f: Filter = Filter(None, None, None, MarkerFilter(severity="*",
                       file="*", checker="*", review="Conf*"))
    param = CreateFilterParam(f)
    resp=cl.get(f"/api/public/projects/{pr_id}/branch/{br_id}/snapshots/{snap_id}/fullmarkers?traces=true&checker_info=true&review_history=true&comment_history=true&filters={param}")
    fmt = json.dumps(resp.json())
    print(f"Response: {fmt}")

get_markers("e6abca1b-23a0-4107-9120-4d8d3596030b","ab179fad-c96a-498d-8407-2a3c82719385","250a4ff1-bfc8-439a-834d-18b62a545d8d")  

Возможный вывод:

[
  {
    "id": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
    "file": "/mnt/scratch/sources/darpa-source/challenges/Divelogger2/lib/string.cc",
    "function": "_ZN6StringaSERKS_",
    "line": 179,
    "locID": 1006,
    "lang": "CXX",
    "tool": "SvEng",
    "warnClass": "ASSIGN_NO_CHECK_FOR_THIS",
    "mtid": "SvEng.LA.4",
    "msg": "Method 'operator=' doesn't check its argument with 'this' pointer.",
    "details": "d96c832835f414a119ea8c7575a8926378fd7cf6",
    "flags": 0,
    "traces": [
      {
        "role": "declaration",
        "locations": [
          {
            "file": "/mnt/scratch/sources/darpa-source/challenges/Divelogger2/lib/string.cc",
            "line": 179,
            "col": 0,
            "spec": false,
            "info": "declaration"
          }
        ]
      }
    ],
    "checker_severity": "Normal",
    "checker_reliability": "High",
    "checker_origin": "builtin",
    "review": {
      "id": "65796",
      "status": "Confirmed",
      "severity": "Major",
      "action": "Fix required",
      "createdBy": "admin",
      "created_by_id": "00000000-0000-0000-0000-000000000000",
      "time": "2025-11-25T15:04:25.216816Z",
      "createdFrom": "ab877a02-03ac-41ac-8b87-46f7e76791ba"
    },
    "review_history": [
      {
        "id": "65796",
        "status": "Confirmed",
        "severity": "Major",
        "action": "Fix required",
        "createdBy": "admin",
        "created_by_id": "00000000-0000-0000-0000-000000000000",
        "time": "2025-11-25T15:04:25.216816Z",
        "createdFrom": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
        "createdFromContext": {
          "projectID": "e6abca1b-23a0-4107-9120-4d8d3596030b",
          "branchID": "ab179fad-c96a-498d-8407-2a3c82719385",
          "snapshotID": "250a4ff1-bfc8-439a-834d-18b62a545d8d",
          "markerID": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
          "projectName": "darpa",
          "branchName": "master",
          "snapshotName": " ver. 2.7.1 300319 07:08",
          "created_by": "importer",
          "created_by_id": "00000000-0000-0000-0000-000000000002",
          "create_ts": "2020-04-03T04:56:40.524836Z"
        }
      },
      {
        "id": "65795",
        "status": "Confirmed",
        "severity": "Major",
        "action": "Undecided",
        "createdBy": "admin",
        "created_by_id": "00000000-0000-0000-0000-000000000000",
        "time": "2025-11-25T15:04:21.683963Z",
        "createdFrom": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
        "createdFromContext": {
          "projectID": "e6abca1b-23a0-4107-9120-4d8d3596030b",
          "branchID": "ab179fad-c96a-498d-8407-2a3c82719385",
          "snapshotID": "250a4ff1-bfc8-439a-834d-18b62a545d8d",
          "markerID": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
          "projectName": "darpa",
          "branchName": "master",
          "snapshotName": " ver. 2.7.1 300319 07:08",
          "created_by": "importer",
          "created_by_id": "00000000-0000-0000-0000-000000000002",
          "create_ts": "2020-04-03T04:56:40.524836Z"
        }
      },
      {
        "id": "63495",
        "status": "Confirmed",
        "severity": "Unspecified",
        "action": "Undecided",
        "createdBy": "reviewer",
        "created_by_id": "077fdd75-b382-4008-97c4-ec6ef8732ccd",
        "time": "2022-04-05T11:47:47.026161Z",
        "createdFrom": "a94f92f4-4e3e-4071-9f10-aa8e4389dd0a",
        "createdFromContext": {
          "projectID": "e6abca1b-23a0-4107-9120-4d8d3596030b",
          "branchID": "ab179fad-c96a-498d-8407-2a3c82719385",
          "snapshotID": "4242b43a-da44-44ca-9b28-770295b98493",
          "markerID": "a94f92f4-4e3e-4071-9f10-aa8e4389dd0a",
          "projectName": "darpa",
          "branchName": "master",
          "snapshotName": " ver. 2.7.1 200419 05:55",
          "created_by": "importer",
          "created_by_id": "00000000-0000-0000-0000-000000000002",
          "create_ts": "2020-04-03T04:56:49.006218Z"
        },
        "reviewer_attrs": ["review_master"]
      }
    ],
    "comments": [
      {
        "id": "e3287eb2-9e64-4780-a1c0-22ed9393a226",
        "text": "aaaaaaaaa",
        "type": "txt",
        "ref": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
        "createdBy": "admin",
        "created_by_id": "",
        "invariant": "",
        "createTs": "2025-11-25T15:04:11.358138Z",
        "updateTs": "2025-11-25T15:04:17.220568Z",
        "history": [
          {
            "id": "019abb8b-4244-7931-81f7-a7d1a4512de0",
            "comment_id": "e3287eb2-9e64-4780-a1c0-22ed9393a226",
            "comment_ref": "ab877a02-03ac-41ac-8b87-46f7e76791ba",
            "text": "aaaa",
            "created_by": "admin",
            "created_by_id": "00000000-0000-0000-0000-000000000000",
            "create_ts": "2025-11-25T15:04:11.358138Z"
          }
        ]
      }
    ],
    "invariant": "1pQ3T5bq6AMYEYJqzFsW+Q"
  }
]