Files
obsidian-vault/personal/projects/personal-os/personal-os-evidence-graph-design-2026-10-04.md
T

24 KiB
Executable File
Raw Blame History

status, updated, canonical_source
status updated canonical_source
draft-review 2026-10-04 /Users/admin/Developer/personal-os/planning/personal-os-evidence-graph-design-2026-10-04.md

Personal OS — граф доказательств и связывание источников

Уточнение по результатам полного аудита: системное ревью R1, разделы 05-10. DDL ниже - иллюстрация, не готовая production-миграция: нужны версионированные type/attribute tables, derivation lineage, run metadata, проверка типов и атомарная публикация. Детерминированная проверка JSON/evidence refs не доказывает смысловую истинность LLM-утверждения.

Статус: проект для согласования, не реализован · 2026-10-04. Ниже нет разрешения на миграцию БД, запуск агента или расписания. Это детализация архитектурного проекта.

Что есть сейчас

schema.sql хранит задачи Asana в tasks, комментарии/события в stories и только связи задача↔задача в task_edges. Поле tasks.project_gid содержит один проект для отображения, хотя задача может входить в несколько проектов. gh-snapshot.js извлекает Asana URL из PR body, а context-links.js связывает URL, worktree, SHA и ветку. work-timeline.js временно связывает окна ActivityWatch и сессии с PR/задачами. Эти разнородные связи не записываются в общую БД с доказательствами. Визуального просмотрщика графа нет.

Решение по хранилищу

Предлагается тот же PostgreSQL, отдельная схема evidence_graph; действующие tasks/stories/task_edges остаются до проверенного переноса. На первом срезе доминируют выборки на 1–3 шага внутри проекта, поэтому индексированные таблицы узлов/рёбер и WITH RECURSIVE достаточны. Отдельная Neo4j/Graphiti увеличила бы число сервисов и необходимость синхронизировать два источника истины. Пересмотреть после измерения реальных запросов и объёма графа. Векторы используются для поиска кандидатов, не как доказательство тождества.

Логическая схема (DDL до миграции)

CREATE SCHEMA evidence_graph;

CREATE TABLE evidence_graph.entity_kinds (
  key text PRIMARY KEY,
  schema_version integer NOT NULL,
  json_schema jsonb NOT NULL,
  display_group text NOT NULL,
  privacy_class text NOT NULL
);

CREATE TABLE evidence_graph.predicate_types (
  key text PRIMARY KEY,
  allowed_from text[] NOT NULL,
  allowed_to text[] NOT NULL,
  transitive boolean NOT NULL DEFAULT false,
  evidence_required boolean NOT NULL DEFAULT true
);

CREATE TABLE evidence_graph.entities (
  id uuid PRIMARY KEY,
  kind text NOT NULL REFERENCES evidence_graph.entity_kinds(key),
  label text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE evidence_graph.entity_attributes (
  entity_id uuid PRIMARY KEY REFERENCES evidence_graph.entities(id),
  schema_version integer NOT NULL,
  value jsonb NOT NULL
);

CREATE TABLE evidence_graph.identifiers (
  entity_id uuid NOT NULL REFERENCES evidence_graph.entities(id),
  system text NOT NULL,               -- asana, github, git, zoom, obsidian, codex
  key text NOT NULL,                  -- Asana GID; repo+PR number; repo+SHA;
                                      -- Zoom meeting UUID; path+revision, etc.
  PRIMARY KEY (system, key)
);

CREATE TABLE evidence_graph.source_records (
  id uuid PRIMARY KEY,
  system text NOT NULL,
  source_key text NOT NULL,
  version_hash text NOT NULL,
  occurred_at timestamptz,
  fetched_at timestamptz NOT NULL,
  locator text NOT NULL,              -- URL or private local reference
  excerpt text,                       -- minimum evidence, no full transcript
  payload_ref text,                   -- local protected raw payload if needed
  UNIQUE (system, source_key, version_hash)
);

CREATE TABLE evidence_graph.relations (
  id uuid PRIMARY KEY,
  from_id uuid NOT NULL REFERENCES evidence_graph.entities(id),
  predicate text NOT NULL REFERENCES evidence_graph.predicate_types(key),
                                      -- belongs_to, parent_of, depends_on,
                                      -- addresses, changes, mentions, discusses,
                                      -- observed_in, opened_in, supersedes
  to_id uuid NOT NULL REFERENCES evidence_graph.entities(id),
  state text NOT NULL CHECK (state IN ('candidate','accepted','rejected')),
  method text NOT NULL,               -- api, exact_id, exact_url, sha,
                                      -- branch, text_extraction, user
  valid_from timestamptz,
  valid_to timestamptz,
  observed_at timestamptz NOT NULL,
  decided_at timestamptz,
  decided_by text
);

CREATE TABLE evidence_graph.relation_evidence (
  relation_id uuid NOT NULL REFERENCES evidence_graph.relations(id),
  source_record_id uuid NOT NULL REFERENCES evidence_graph.source_records(id),
  span text,                           -- page/line/transcript timestamp
  PRIMARY KEY (relation_id, source_record_id, span)
);

CREATE TABLE evidence_graph.claims (
  id uuid PRIMARY KEY,
  subject_id uuid NOT NULL REFERENCES evidence_graph.entities(id),
  dimension text NOT NULL,            -- implementation_status, decision,
                                      -- blocker, next_action, owner
  value jsonb NOT NULL,
  state text NOT NULL CHECK (state IN ('candidate','accepted','rejected','superseded')),
  valid_from timestamptz,
  valid_to timestamptz,
  observed_at timestamptz NOT NULL,
  supersedes_id uuid REFERENCES evidence_graph.claims(id)
);

CREATE TABLE evidence_graph.claim_evidence (
  claim_id uuid NOT NULL REFERENCES evidence_graph.claims(id),
  source_record_id uuid NOT NULL REFERENCES evidence_graph.source_records(id),
  span text,
  PRIMARY KEY (claim_id, source_record_id, span)
);

CREATE INDEX relations_from ON evidence_graph.relations(from_id, predicate, state);
CREATE INDEX relations_to ON evidence_graph.relations(to_id, predicate, state);
CREATE INDEX claims_subject ON evidence_graph.claims(subject_id, dimension, state);

identifiers разрешает внешние ID в один внутренний UUID. GitHub PR идентифицируется как github:owner/repo#number, файл — как git:repo@commit:path, а не только path: один путь может меняться. Worktree имеет свой локальный ID и может быть перемещён; его связь с repo/commit версионируется. Zoom UUID идентифицирует встречу, timestamp + transcript version — фрагмент. Если внешний ID неизвестен, объект остаётся отдельным кандидатом; одно совпадение имени его не сливает.

Время события (valid_from/valid_to) отделено от времени получения (observed_at/fetched_at). Исправление не переписывает старое доказательство; новая запись закрывает срок действия прежнего утверждения. source_records остаётся append-only. Вывод проекта — материализованная версионированная проекция из принятых claims и relations, с run_id, версией правил и свежестью каждого источника; схема проекции будет определена в миграции.

Расширяемая предметная модель

Предыдущий список kind был слишком узким: граф не ограничен техническими объектами и одним проектом. entities.kind хранит ссылку на версионированный каталог entity_kinds (key, schema_version, json_schema, display_group, privacy_class). Аналогичный каталог predicate_types задаёт допустимые пары типов, направление, транзитивность, обязательность evidence и правила вывода. Тип добавляется миграцией каталога и адаптером/экстрактором; произвольный unknown_object допускается как кандидат с исходным фрагментом, но не как автоматически принятая сущность. Схема конкретных атрибутов хранится в entity_attributes или типизированной таблице при появлении частых запросов. Произвольность типов не означает произвольность предикатов или молчаливое слияние похожих названий.

Начальный каталог шире: person, team, project, initiative, phase, milestone, decision, proposal, goal, capability, outcome, career_goal, competency, contribution, feedback, opportunity, meeting, task, pr, commit, file, session, source. Концептуальные сущности (goal, competency) могут не иметь внешнего ID: их внутренний UUID создаётся после явного упоминания с evidence; совпадение текста остаётся кандидатом на слияние. Каталог и разрешённые связи — часть версионированной модели, а не набор свободных LLM-тегов.

Последовательности проектов и этапов

phase → part_of → project; milestone → part_of → phase; project A → precedes / enables / replaces → project B; initiative → contains → project. Каждое precedes/enables/replaces — отдельное доказуемое отношение, а не сортировка по дате. У последовательности есть основание, время и состояние; циклы precedes отвергаются валидатором. Если B начался после A, это само по себе не доказывает причинную связь. История проекта показывается как цепь proposal → decision → implementation → verification → release → measured outcome, причём отсутствующий этап остаётся unknown. Разные альтернативы могут ветвиться и отменяться.

Проекты, связанные непрямо

Поиск выполняется по типизированному пути с ограничением глубины и времени, например project A → depends_on → capability X → delivered_by → project B. Ответ хранит весь путь рёбер и evidence по каждому шагу; он не превращается автоматически в прямое A related_to B. Связь shares_person, shares_repo или общая встреча означает лишь пересечение контекста. Агент может предложить новую прямую смысловую связь, но для неё нужно отдельное доказательство. Индексированный WITH RECURSIVE/path query выбирает допустимые предикаты; UI показывает краткое объяснение пути и раскрывает его шаги. Глубина 2–3 — стартовый предел интерфейса, не ограничение модели.

Карьерный путь

Карьерная модель: person → pursues → career_goal; career_goal → requires → competency; person → made → contribution; contribution → concerns → project/decision/people/outcome; feedback → assesses → competency/contribution; milestone → advances → career_goal. Техническое наблюдение о работе, подтверждённый вклад, внешняя оценка и вывод о росте — разные сущности/claims. Проектный аналитик поставляет подтверждённые решения и результаты; карьерный аналитик добавляет кандидатов contribution с указанием роли Алекса и источника, а подтверждённый feedback и собственные цели Алекса задают интерпретацию. Часы кодинга, количество PR и число встреч не становятся оценкой уровня.

Пример: предложение Core Data + последующий обоснованный отказ дают вклад исследовал альтернативу и скорректировал решение с двумя источниками. Они не дают implemented и не доказывают карьерный рост или impact без отдельного feedback/результата. Во вторничную подготовку попадают свежие подтверждённые вклады, пробелы относительно явно заданной цели и один вопрос к advisor; неоднозначные сигналы обозначаются как вопросы, а не как оценка личности. Обзор карьеры показывает хронологию цель → действия/вклад → feedback → следующий шаг и происхождение каждого вывода. Текущий personal/career/profile.md — ручная заметка с одним кейсом, не такая модель.

Для надёжного карьерного контура дополнительно нужны: датированные цели и история их пересмотра, модель роли/атрибуции вклада, допустимые уровни чувствительности данных и правила ретроспекции за месяцы. Источник исторических целей уже есть: agent/career-notes.md (EP2 → EP3, Advisor Readiness, primary AOR), но актуальность апрельского обзора требует пересверки; миссия октября задаёт направление к координации, ревью и менеджменту. Это открытые решения, а не уже реализованные возможности.

Кто и как создаёт рёбра

Источник → связь Правило Начальное состояние
Asana task → project / parent / dependency GID из API, все memberships, parent и dependencies; хранить API record как evidence accepted
GitHub PR → issue / файлы / commit GitHub API linked issue, список changed files и head SHA accepted для отношения в GitHub
PR → Asana task Точный Asana URL/GID в PR body или комментарии accepted как mentions; addresses только при явной формулировке цели
Worktree/session/window → PR/task repo + точный SHA; активный URL окна; совпадение интервала observed_in/opened_in accepted; worked_on остаётся candidate. Одна ветка без SHA — candidate
Zoom/Obsidian → project/task/PR Точный URL/ID в тексте → mentions; LLM выделяет решение/статус с цитатой и timestamp Точная ссылка accepted; смысловая привязка и вывод о статусе candidate

Агент получает ограниченный набор исходных записей, извлекает сущности и утверждения в структурированный JSON: subject, predicate/dimension, object/value, source_record_id, span, event_time. Валидатор проверяет существование source record, точные идентификаторы, типы допустимых рёбер, отсутствие нескольких возможных целей и противоречия с более поздними решениями. Отсутствие подтверждения даёт candidate или unknown. Ни близость текста, ни открытая вкладка, ни дата последней активности не означают, что задача реализована или человек ждёт действий.

Пример для Core Data: беседа «предложение инициализировать модели» даёт candidate claim proposed; последующее явное решение «отклонить, оставить реализацию без изменений как совместимую с Xcode 27» создаёт accepted claim decision=rejected, implementation=unchanged, связанный с исходным фрагментом и закрывающий прежний актуальный claim. PR или коммит без доказанного отношения к этой конкретной задаче не меняет её статус. При конфликте одинаково авторитетных свидетельств результат conflict, с обеими ссылками и запросом на разбор.

Просмотр графа

Локальный read-only API: GET /entities/{id}/graph?depth=2&at=...&types=..., GET /paths?from=...&to=...&at=... и GET /relations/{id}/evidence. SQL выбирает исходную сущность и соседей, фильтрует принятые/кандидатные связи и время; сервер ставит лимит узлов. Веб-экран на Cytoscape.js: начальная точка — выбранная сущность, раскрытие узла по нажатию, фильтры типов/времени, сплошные accepted и пунктирные candidate рёбра. Отдельные представления: карта проекта/инициативы, последовательность этапов и карьерная хронология; все читают один граф. Боковая панель показывает почему связь существует: правило/агент, исходный URL/фрагмент, время, актуальность и решение пользователя. Для iPad тот же ограниченный API. Сначала важен просмотр и исправление связей, не свободная навигация по всем событиям дня.

Порядок реализации и критерий готовности первого среза

  1. Зафиксировать каталог типов/предикатов и эталонные кейсы: отклонённый Core Data, последовательность двух проектов, непрямой путь, карьерный вклад без внедрения и неоднозначные совпадения имени.
  2. Подготовить миграцию схемы, backfill текущих Asana task edges и PR с сохранением source refs; проверить количество и отсутствие дублей, затем отдельно согласовать применение.
  3. Добавить детерминированные Asana/GitHub/URL/SHA линкеры и replay из source_records; проверить на эталонных кейсах.
  4. Добавить агентное извлечение только для Zoom/заметок с очередью candidate, валидатором и пользовательской коррекцией.
  5. Добавить project-scoped API/экран графа и проверить, что каждая уверенная карточка проекта раскрывает цепочку до источника.

Первый срез готов, когда согласованный набор кейсов воспроизводим после replay, нет ошибочных accepted связей/статусов в ручной проверке, и в каждом представлении можно открыть исходное доказательство каждого уверенного вывода. Это предлагаемый gate, подлежит согласованию.

Основание решения

  • PostgreSQL WITH RECURSIVE поддерживает обход иерархий/связей в существующей БД.
  • Asana task API содержит множественные project memberships, parent и dependencies; project_gid одного проекта недостаточен.
  • GitHub GraphQL PR содержит связи PR с issue; документация связывания различает ручные и текстовые ссылки.
  • Graphiti использует entities, temporal facts и raw episodes для provenance; Microsoft GraphRAG показывает извлечение entities/relationships/claims из текстовых фрагментов. Для Personal OS это образцы модели, не решение установить эти фреймворки.
  • Cytoscape.js предоставляет интерактивный граф, фильтры и обход соседей в браузере.
  • W3C PROV-O формализует происхождение сущностей и действий; типизированные рёбра Graphiti показывают ограничение допустимых типов концов связи и generic RelatesTo только без более точного отношения.