Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENT_DIARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,11 @@
**Tests:** целевые 103 passed; полный прогон 1651 passed / 5 skipped / 91 deselected (169.2s); ruff clean ×4 файла. Новые: TestMapConsistency (3), TestFallbackImports (4), гейт-тесты флага.
**Guard:** TestMapConsistency структурно ловит любое расхождение карт; ungated-путь закреплён негативными тестами (флаг off → пусто). Lazy-цикл parser⇄language_imports (статика) — осознанный техдолг в _ALLOWED_CORE_CYCLES (KNOWN_ISSUES 2026-09-08), деривация карты переведена на module __getattr__ (PEP 562), на import-time ничего не исполняется.
**verified_from_clean_state:** ⚠️ не проверено — чистый clone требует сети (нет в сессии); локально полный pytest зелёный.

## [2026-09-08 19:40] — collect() в Cypher: json_group_array + типизированный декод (fixed)

**Status:** ✅ Fixed. / **Root Cause:** KNOWN_ISSUES 2026-09-07 ⏳ — `_translate_return_expr` заявлял `collect` как Supported, но SQLite не имеет функции COLLECT («no such function»); ни одного теста на `RETURN collect(...)` не было.
**Fix:** `cypher_sql.py` — COLLECT(expr) → `json_group_array(<sql>) FILTER (WHERE <sql> IS NOT NULL)` (семантика Neo4j: null-игнор, пустой матч → []); `collect(*)` и вложенный obtain → явные ValueError; DISTINCT — SyntaxError из парсера (не наш уровень). Маркер `collect_cols` на трансляторе, в `cypher_executor.py` step5 декодится json.loads ТОЛЬКО помеченных колонок со str-значением (try/except → warning, не роняет весь результат).
**Tests:** 13 новых (SQL/E2E/errors incl. decode-collision `'["not_a_list"]'`); файл 93 passed; полный 1663 passed / 6 skipped / 91 deselected (168.6s). ruff clean, verify_diary 15/0. Эксперименты Г1/Г2 (sqlite 3.50.4, Python 3.14.3) — FILTER и empty→[] подтверждены сырым прогоном, см. .agent_task_state.md.
**Guard:** тест «collect() без алиаса → имя колонки = выражение», decode-collision guard (не-decode не-marked колонок), Red Team 5/5 (empty, null, unicode/quotes, DISTINCT, nested/*).
**verified_from_clean_state:** ⚠️ не проверено — чистый clone требует сети (нет в сессии); локально полный pytest 1663 passed green.
32 changes: 5 additions & 27 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
---


**11 entries** — compressed per §4.8 R3 (conclusion-first)
**14 entries** — compressed per §4.8 R3 (conclusion-first; dedup 2026-09-08)

## 2026-09-02 20:51 — drift_gate заблокировал коммит: контроль остановил самого автора

Expand Down Expand Up @@ -52,30 +52,6 @@
- **Fix:** Removed stray `\"\"\"` in same commit 986c9be7.
- **Статус:** ✅ Fixed

## 2026-09-03 — Fake reindex ETA "~8s" + frozen progress in Finalizing (both fixed)

- **Источник:** AGENT_DIARY.md
- **Описание:** **Status:** ✅ Fixed (commit 32f11662; 5 pre-commit hooks OK; full pytest 1587 passed, 2 pre-existing unrelated env_extractor failures)
**Root Cause 1 (ETA "~8s"):** `_enrich_job_response` had a dead h...
- **Статус:** автоматически синхронизировано


## 2026-09-03 19:30 — CI RED: circular import layer ↔ tools_reg (architecture_linter)

- **Источник:** AGENT_DIARY.md
- **Описание:** **Status:** ✅ Fixed (commit f210ed7c; CI all-jobs green on ubuntu+windows)
**Root Cause:** My ETA refactor added `tools_reg → layer` import for `_embed_progress_from_log`, closing an existing `layer →...
- **Статус:** автоматически синхронизировано


## 2026-09-04 11:15 — CI RED: ruff lint errors caught only after push (3 commits)

- **Источник:** AGENT_DIARY.md
- **Описание:** **Status:** ✅ Fixed (commit 986c9be7)
**Root Cause:** Pre-commit hook did not run ruff. CI (`ruff check src/ tests/` in ci.yml) caught F401/W292 only after push, forcing fix-commits. Repeated 3 times ...
- **Статус:** автоматически синхронизировано


## 2026-09-05 — Process leak: hung git cat-file leaks git+git.exe+conhost chains (RAM 81%, ~200 procs)

- **Источник:** AGENT_DIARY.md
Expand Down Expand Up @@ -121,7 +97,7 @@
- **Описание (Cypher, fixed):** работают только запросы с типизированными узлами: `MATCH (n:Variable) RETURN count(n)` → 6435 (1.1ms). НО `MATCH ()-[e:ASSIGNED_FROM]->()` падал `sqlite3.OperationalError: no such column: e`, а `MATCH ()-[:ASSIGNED_FROM]->()` — `no such column: n0.id`. **Fix внесён:** cypher_sql.py — (1) `from_node_alias` резолвится в `n{path_idx*2}` для анонимного левого узла; (2) переменные ребра `[e:]` регистрируются в `edge_vars` и резолвятся в колонки (`e.type/source_id/target_id`), включён `count(e)`. 10 регресс-тестов (SQL + E2E) + 5 Red Team атак (направления `<-`, WHERE e.target_id, OPTIONAL MATCH, оба анонимных конца, collect) — все защищены, корректность результатов подтверждена (count=2 для 2 рёбер). **⚠️ collect() остаётся нерабочим**: `_translate_return_expr` заявляет `collect` как Supported (стр. 434-438 «Supported: count, sum, avg, min, max, collect»), но SQLite не имеет функции COLLECT (Red Team: `no such function: COLLECT`). Ни одного теста на `RETURN collect(...)` нет — заявка и реализация расходятся.
- **Описание (Receipts, fixed):** ActionReceipt компонент реализован (action_receipt.py, TD §11), но в проекте bfe9644b файла `action_receipts.jsonl` НЕТ — писались только в проектах 48baae8f/98d66cfa (19.08); `change_intents.jsonl` (96 записей) остаётся последней живой записью от 13.08. Receipt-путь для текущего проекта не срабатывал при повседневных MCP-вызовах (заполнялся только через lifecycle-tools reindex-путь).
- **Fix (Cypher):** внесён (см. выше, коммит 80a7acf8). **Fix (collect):** либо реализовать JSON-агрегацию `collect()` (json_group_array в SQLite), либо убрать из списка Supported и добавить негативный тест. **Fix (Receipts):** внесён — `_contract_record` в write_tools.py теперь вызывает новый `_contract_receipt()` (ActionReceipt рядом с ChangeIntent), а сам `_contract_record` добавлен во ВСЕ write-пути: replace, insert_before/after, rename (LSP workspace edit + fallback), safe_delete, move (source/target/refs). Receipt-запись warning-only, не ломает write. Тест `tests/test_write_tools.py::test_apply_records_action_receipt` (создание action_receipts.jsonl из реального write-вызова). Коммит см. git log.
- **Статус:** 🟢 Cypher-часть fixed; 🟢 receipts fixed; collect() open
- **Статус:** 🟢 Cypher-часть fixed; 🟢 receipts fixed; 🟢 collect() fixed (2026-09-08: json_group_array + FILTER null-игнор, decode только marked-колонок; 13 новых тестов, полный pytest 1663 passed)


## 2026-09-07 — Lazy-only верификация: память не проверяется без вызова агента; нет TTL/фона (open, эксперимент нужен)
Expand All @@ -134,7 +110,8 @@
- IdleScheduler (`enable_idle_scheduler`, task_queue.py:345) включается только из `record_tool_call()` — после вызова инструмента; VOR туда не подключён; из 3 idle-задач 2 — заглушки (`_improve_summaries_batch`, `_check_index_health` — пустые тела, только debug-лог).
- Со стороны агента: вызвал `intel_get_project_memory` → 110/110 узлов проверено (47 VERIFIED, 63 не-refuted) — работает, но только «по руке».
- **Дизайн-решение для эксперимента (следующий шаг):** непрерывная проверка «без вызова» — (a) idle-тикер VOR в фоне по расписанию с cooldown; (b) react на git/файловые события (HEAD сменился → перепроверка затронутых узлов); (c) TTL/`verified_at` для INCONCLUSIVE → по возрастанию падать в REFUTED label «не подтверждён за N дней». Контр-риск: цена (CPU/disk) непрерывной проверки vs польза свежести — мерить, не угадывать (см. docs/research/universal-engine-study/10-continuous-verification.md).
- **Статус:** ⏳ Open — нужен эксперимент (гипотеза → замер → выбор)
- **Статус:** ⏳ Open 🟡 — нужен эксперимент (гипотеза → замер → выбор)
- **Дедлайн:** 2026-09-15 · **Owner:** ManSio


## 2026-09-08 — B4: статический цикл parser ⇄ language_imports (осознанный техдолг, lazy, allowed)
Expand All @@ -143,4 +120,5 @@
- **Описание:** `src.core.language_imports` импортирует `src.core.indexing.parser` (для деривации карты), а `parser._extract_fallback_imports` импортирует `language_imports` (fallback-режим 2). Статически — цикл; в рантайме ни один импорт при загрузке модулей не выполняется: parser импортирует language_imports только локально в функции; language_imports импортирует parser только лениво (module `__getattr__` → `_derive_language_import_nodes`, PEP 562) при первом обращении к `LANGUAGE_IMPORT_NODES`.
- **Fix:** пара добавлена в `_ALLOWED_CORE_CYCLES` (scripts/architecture_linter.py) с комментарием; `LANGUAGE_IMPORT_NODES` переведён на ленивую деривацию (кэш `_LANGUAGE_IMPORT_NODES_CACHE`, `__getattr__`), прямое обращение к карте внутри модуля заменено на `_get_language_import_nodes()`. Удалить из allowlist после выноса `IMPORT_NODE_MAP` в нейтральный модуль (не историю карт в parser) — тогда language_imports сможет импортировать parser односторонне.
- **Статус:** ✅ Fixed (allowed tech debt, deferred refactor; целевые 68 passed, architecture_linter 4/4 OK)
- **Дедлайн рефактора:** 2026-10-01 · **Owner:** ManSio

3 changes: 3 additions & 0 deletions scripts/verify_diary.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
"from_pretrained",
"getdefaultlocale", "get_inputs", "get_objects", "getrusage",
"is_relative_to",
# SQLite aggregate — упоминается в дневнике как результат трансляции
# Cypher collect() → json_group_array, не символ проекта (2026-09-08).
"json_group_array",
"kill",
"optimize",
"reindexing", "rmtree", "run_in_executor",
Expand Down
19 changes: 17 additions & 2 deletions src/core/search/cypher_executor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Cypher query engine — компонент для подмножества openCypher."""
from __future__ import annotations

import json
import logging
from typing import Any, Dict, Optional

Expand Down Expand Up @@ -84,13 +85,27 @@ def execute(
cursor = conn.execute(sql, sql_params)
rows = cursor.fetchall()

# 5. Format results
# 5. Format results. collect()-колонки (json_group_array возвращает
# строку) декодируются из JSON только по маркеру транслятора —
# обычные строковые колонки, выглядящие как JSON, НЕ трогаются.
collect_cols = set(translator.collect_cols)
columns = [desc[0] for desc in cursor.description] if cursor.description else []
results = []
for row in rows:
result_row = {}
for i, col in enumerate(columns):
result_row[col] = row[i]
value = row[i]
if col in collect_cols and isinstance(value, str):
try:
value = json.loads(value)
except ValueError:
# json_group_array всегда валиден, но guard:
# невалидная JSON-строка — не причина ронять запрос.
logger.warning(
f"Cypher collect column {col!r} is not valid JSON: "
f"{value[:80]!r}"
)
result_row[col] = value
results.append(result_row)

elapsed = (time.monotonic() - start) * 1000
Expand Down
35 changes: 35 additions & 0 deletions src/core/search/cypher_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class CypherToSQL:
def __init__(self, graph):
self._graph = graph
self._cte_counter = 0
# Имена колонок, которые собраны через collect() → будут JSON-докодированы
# в executor (json_group_array возвращает строку).
self.collect_cols: List[str] = []

def translate(self, query: Query) -> Tuple[str, List[Any]]:
"""Генерирует SQL из AST Cypher.
Expand Down Expand Up @@ -73,13 +76,23 @@ def translate(self, query: Query) -> Tuple[str, List[Any]]:
# Фаза 3: RETURN
agg_columns = []
group_by = []
self.collect_cols = []

for item in query.return_items:
sql_col = self._translate_return_expr(item.expression, node_vars, edge_vars)
if self._is_aggregate(item.expression):
agg_columns.append(sql_col)
else:
group_by.append(sql_col)
if re.match(r"collect\(", item.expression, re.IGNORECASE):
# Маркер collect-колонки: executor JSON-декодирует только эти
# колонки (json_group_array возвращает строку). Имя колонки —
# алиас, иначе выражение (всегда есть "." внутри свойства),
# иначе SQL-имя.
self.collect_cols.append(
item.alias
or (item.expression if "." in item.expression else sql_col)
)
# Всегда используем AS для консистентности имён колонок
if item.alias:
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", item.alias):
Expand Down Expand Up @@ -443,6 +456,19 @@ def _translate_return_expr(
if agg_match:
func = agg_match.group(1).upper()
inner = agg_match.group(2)
if func == "COLLECT":
# collect(*) / вложенный вызов — явная ошибка вместо
# тихого невалидного SQL (json_group_array(*) / двойного call).
if inner == "*":
raise ValueError(
"collect(*) is not supported; use a property, "
"e.g. collect(n.name) or collect(n.label)"
)
if "(" in inner:
raise ValueError(
f"collect({inner}) is not supported; use a property, "
f"e.g. collect(n.name)"
)
if inner in node_vars:
# C2: агрегат над узлом-переменной. count(n) → COUNT(n.id)
# (считает не-NULL узлы — точная семантика Cypher; раньше
Expand All @@ -466,6 +492,15 @@ def _translate_return_expr(
f"use a property, e.g. {inner}.type"
)
sql_inner = self._property_ref_to_sql(inner, node_vars, edge_vars)
if func == "COLLECT":
# collect(expr) → json_group_array(expr). NULL-семантика Cypher:
# пустые значения не попадают в список (neo4j collect) →
# FILTER (WHERE IS NOT NULL). Возврат всегда JSON-строка,
# декодируется в executor по маркеру self.collect_cols.
return (
f"json_group_array({sql_inner}) "
f"FILTER (WHERE {sql_inner} IS NOT NULL)"
)
return f"{func}({sql_inner})"

# C4: неизвестная функция в RETURN — явная ошибка вместо невалидного SQL
Expand Down
Loading
Loading