Commit Graph

1786 Commits

Author SHA1 Message Date
agent_coder 3d44fd1943 fix(page-history): устранить замечания ревью #578 (empty/error, i18n ru, a11y, тесты)
F1 (регрессия) — вместо пустой панели показываем явные состояния: ошибка загрузки
истории и «нет истории». Список и правая панель несут свои error/empty-состояния;
heatmap по-прежнему fail-open (пустая сетка) независимо.

F2 (i18n) — добавлены 8 недостающих ключей в ru-RU (Selected version, via, of,
Previous/Next change, Previous/Next month, No revisions found for that day) плюс
новый aria-ключ. ru-RU правится текстовым append'ом — существующие (в т.ч.
дублирующиеся) ключи не тронуты.

F3 (a11y) — новые интерактивы доступны с клавиатуры: строки ревизий и ячейки
календаря — role=button + tabIndex + активация Enter/Space. Heatmap больше не
только-цвет: каждая ячейка несёт aria-label/title «дата: N versions»
(не-цветовой сигнал числа ревизий).

F4 — тесты history-nav-panel.tsx: рендер плотных строк, переключатель «Only
versions» реально фильтрует автосейвы (mutation-check), pick-day скроллит к дню,
error/empty-состояния.

F5 — тесты mini-calendar: границы тиров heat, клавиатурная активация, aria-cue.

F6 (hover-prefetch) — отложено (опционально, не тривиально в новой раскладке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:15:11 +03:00
agent_coder 642f50e9df feat(page-history): редизайн десктопного окна истории — плотный список + мини-календарь heatmap (#568)
Натягиваю прототип NewDesign/PageHistoryModal на существующую подсистему истории,
переиспользуя всю механику (запросы, дифф, restore, права, пагинация) и добавляя
единственный дешёвый агрегат бэка — «число version-ревизий по дням» под heatmap.

Клиент:
- Новый десктоп-body history-modal-desktop.tsx: одно-строчная шапка (заголовок,
  лейбл выбранной версии, Highlight changes + N/M дифф-навигация, Restore, закрытие)
  над телом «левая навигация 280px | правый рендер версии».
- history-nav-panel.tsx: фильтр «Only versions» + мини-календарь + ПЛОТНЫЙ список
  (одна ревизия = одна строка), группировка по дням (data-day якоря), jump-to-day с
  корректной догрузкой страниц по границе dayISO старейшей строки (не «N страниц»),
  недостижимый день → тост, без зацикливания.
- revision-row.tsx + чистый адаптер utils/revision-row.ts: глиф по идентичности
  (agent → квадратный глиф роли + «via <launcher>», иначе круглый аватар), бейдж
  SAVED только для kind==='manual'; автосейвы приглушены. Два ортогональных сигнала
  сохранены (identity vs intentionality).
- mini-calendar.tsx: heatmap-интенсивность = version-ревизии за день, навигация
  месяцев клиентская, pick-day по dayISO; fail-open на пустую сетку.
- usePageHistoryDayCounts + day-counts-service; historyKindMeta вынесен в чистый
  utils/history-kind-meta.ts (тот же предикат для фильтра и бейджа).
- resolvePrevSnapshotId по-прежнему считается на ПОЛНОМ плоском списке; restore и
  дифф-навигация переиспользуют useHistoryRestore/useDiffNavigation без изменений.
- Мобильная модалка (history-modal-mobile.tsx) не тронута; орфанный
  history-modal-body.tsx удалён.

Сервер:
- POST /pages/history/day-counts {pageId, tz} → [{dayISO, count}] за всю историю,
  view-gated (validateCanView), неверный tz → 400 (как /history/time).
- computeDayCounts + чистый countRevisionsByDay (считает только version-строки),
  переиспользует tz-ядро zonedDayStart/isoDay из work-time/bucket-by-day.ts.

Тесты: клиент vitest (адаптер kind→бейдж/глиф, dense-row рендер, heatmap/календарь);
сервер jest (баркетер + эндпоинт: view-gate, tz→400, группировка по дням).

Удалён прототип NewDesign/PageHistoryModal.tsx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:15:11 +03:00
agent_vscode 85ffdea06b Merge remote-tracking branch 'gitea/develop' into develop 2026-07-12 21:35:41 +03:00
agent_vscode 9aa7620461 fix(client): номер сноски встаёт инлайн, а не на отдельной строке
Номер «N.» рисовался через .definitionContent > :first-child::before, но
tiptap-react оборачивает содержимое NodeViewContent в дополнительный блочный
div [data-node-view-content-react], поэтому :first-child — это обёртка, а не
<p>. Инлайновый ::before на блочной обёртке с блочным <p> внутри падал на
свою строку (регрессия «+1 строка» из #420). Селекторы переведены на
.definitionContent p:first-child / p:last-child — номер и сброс полей теперь
бьют по самому абзацу (контент определения — paragraph+, вложенных абзацев
нет, так что попадание однозначно), работает и с обёрткой, и без неё.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:35:37 +03:00
vvzvlad 710a690c78 Merge pull request 'feat(search) Фаза B PR-1: семантический слой — TEI e5-small + гибрид вектор+RRF (#530)' (#567) from feat/530-search-semantic into develop
Reviewed-on: #567
2026-07-12 20:39:43 +03:00
vvzvlad 50f8ed717a Merge pull request 'feat(work-time): редизайн модалки Time worked — таймлайны с временем суток (#566)' (#575) from feat/566-worktime-modal-redesign into develop
Reviewed-on: #575
2026-07-12 20:21:53 +03:00
agent_coder 1fc9c25681 feat(search): semantic fusion layer (Phase B PR-1) over lexical RRF (#530)
Fuse a vector-similarity branch into #529's lexical RRF. Query embed via a
global TEI sidecar (or per-workspace provider), degrading transparently to the
byte-identical Phase-A lexical path on any failure; a kill-switch and env knobs
gate it.

- ai.service: resolveEmbeddingProvider (workspace→global TEI fallback) with a
  deterministic config fingerprint; embedQuery (query prefix + short 800ms
  timeout); extract embedWithModel core shared with embedTexts.
- page-embedding.repo: vectorCandidateArm fragment (page-level NN, dim +
  active-fingerprint filtered) + fingerprint on insertChunks.
- search.service: 3-branch RRF over lexical ∪ vector candidates; try/catch wraps
  only the embed (permission filter stays outside, fail-closed); semantic
  degrade emits search.semantic.degraded; response gains semantic{state,available,reason}.
- indexer: resolve provider, prepend doc prefix, stamp fingerprint per row.
- migration 20260712T120000: add nullable page_embeddings.fingerprint + composite index.
- infra: TEI embeddings sidecar in docker-compose + EMBEDDING_*/SEARCH_* in .env.example.
- tests: 7 semantic int cases (vector-only hit, sidecar-down, no-provider,
  permission-over-union fail-closed, hung sidecar, lexical∪vector de-dup,
  fingerprint isolation) + fingerprint/prefix/timeout unit tests.

total now = permission-filtered size of (lexical ∪ vector top-N) — a documented
change from Phase A's exact lexical count (falls back to it on degrade).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:21:19 +03:00
vvzvlad e074b101c7 Merge pull request 'feat(#370 Stage A): MCP save_page_version — агентские версии страниц' (#565) from feat/370-agent-save-version into develop
Reviewed-on: #565
2026-07-12 19:56:05 +03:00
vvzvlad 4496afb481 Merge pull request 'recover(page-tree/dnd): вернуть в develop осиротевший фикс drop-раскрытия (#523/#532)' (#573) from fix/523-tree-dnd into develop
Reviewed-on: #573
2026-07-12 19:43:34 +03:00
agent_coder 3cda008012 feat(work-time): redesign «Time worked» modal with time-of-day timelines (#566)
Port the NewDesign/TimeWorkedModal prototype onto the existing work-time
feature with zero backend changes. Each daily track now shows WHEN work
happened: a sticky 00/06/12/18/24 hour axis, shaded night hours (0–6, 21–24),
a per-block hover tooltip "start – end · duration", and a "now" boundary on
today's row. Work vs agent windows keep their existing colour semantics.

The prototype's invented props (DaySummary[], pre-made labels) are replaced by
a pure, unit-tested adapter (work-time-adapter.ts) over the real IPageWorkTime:
windows → time-of-day blocks (epoch kept for a DST-safe tooltip), ms → labels
via the shared formatters, in-place empty-run collapsing, and the today-only
now-line. The agent-only fail-safe (#395/#551) is preserved — the agent
estimate fills the main slot so it never renders empty.

Reuses usePageWorkTime, the endpoint, tz bucketing and format-work-time; deletes
the scratch prototype. New tooltip i18n key added to en-US/ru-RU.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:37:25 +03:00
agent_coder 1644de87f7 fix(#370): F2 terminal skip reply for empty/missing page + F1 lifecycle tests
Reviewer changes-requested on #565.

F2 (real bug): a save on an empty page (or a missing page row) hung the client
for the full 20s ack window and then falsely reported the collab server as
unreachable + advised retry. Root cause: handleSaveVersion gated its
broadcastStateless on `if (result)`, and `result` stays undefined on the two
reachable early returns (isEmptyParagraphDoc and !page), so the server sent
NOTHING and the client waited out its timeout.
- SERVER (persistence.extension.ts): both early-return branches now record a
  skip reason and the tail broadcasts a terminal `version.skipped` reply
  (reason 'empty' | 'page-not-found'). Exactly one terminal reply per handled
  save. Added VERSION_SAVED/VERSION_SKIPPED message consts.
- CLIENT (collaboration.ts): the predicate now matches both version.saved and
  version.skipped; SaveVersionResult gains {saved, skipped, reason}. An empty
  skip resolves to a clean {saved:false, skipped:true, reason:'empty'} (no
  stall); page-not-found throws an immediate, truthful error; the genuine
  no-reply timeout path is unchanged (that is the real "unreachable" case). A
  terminal reply leaves the session cached (healthy connection); only a
  transport failure destroys it. Kept the message-literal in-sync comments.
- tool-specs.ts / pages.ts: description + docstring reflect the new result shape.

F1 (coverage): added tests for the 4 previously-uncovered sendStatelessAndAwait
branches — unrelated-then-real (predicate filters noise), teardown-mid-wait
(rejects + removes the stateless listener, no leak), concurrent-in-flight
guard — plus the F2 empty-skip / page-not-found client outcomes. Server spec
gains empty-page and page-not-found terminal-reply tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:23:07 +03:00
agent_coder 498a87f3c4 fix(#370): add savePageVersion to DocmostClientMethod union (server in-app tool parity)
The Stage-A save_page_version tool's client method must be listed in the
DocmostClientMethod union so Pick<DocmostClient, DocmostClientMethod> exposes it
to the in-app tool adapter; the shared-tool-specs contract spec asserts parity.
Mirrors the existing restorePageVersion entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:23:07 +03:00
agent_coder 67c94de6ef feat(mcp): agent save_page_version tool — collab stateless transport + role prompts (#370)
Stage A of #370: wire the MCP client transport for the already-shipped server
save-version handler (PR-1 #374). An agent can now pin an intentional named
version (kind='agent', derived server-side from the signed actor) of a page's
CURRENT live collaboration content.

- collab-session.ts: add CollabSession.sendStatelessAndAwait(payload, predicate,
  timeoutMs) — sends a stateless message over the live provider and resolves on
  the first matching reply, with a bounded timeout; lifecycle (inflightReject,
  ready-guard, concurrent fail-fast, idle re-arm) mirrors mutate(). Extend
  CollabProviderLike with sendStateless + the stateless on/off overloads.
- collaboration.ts: add savePageVersionRealtime() — under withPageLock, reuse the
  cached agent-authenticated CollabSession (#400), send {type:'save-version'},
  await {type:'version.saved', …}; no REST read (would race the stale page row).
- client/pages.ts: add savePageVersion(pageId) — resolvePageId +
  getCollabTokenWithReauth + writeWithCollabAuthRetry (#486 self-heal).
- tool-specs.ts: add the deferred savePageVersion spec (+ DocmostClientLike Pick);
  auto-registered on both hosts by the shared loops.
- server-instructions.ts: HISTORY family + routing-prose mention.
- ai-chat-tools.service.ts: contract type-assert for the new client method.
- agent-roles-catalog: tell the content-authoring roles (researcher,
  call-summarizer, ru+en) to save a version when the document is done; bump
  their catalog versions.
- test: unit coverage for savePageVersionRealtime (ack + bounded-timeout paths).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:23:07 +03:00
agent_coder be25b31a0e fix(api-key): add missing i18n keys for api-key UI + correct service comment
en-US (source of truth) and ru-RU were missing ~25 strings introduced by the
api-key management UI, so Russian users saw the controls in English and en-US
was incomplete. Add every new t() key to both locales (24 new in en-US, 26 in
ru-RU) with natural Russian translations, placeholders ({{name}}/{{date}})
preserved; existing keys left untouched (insertions only, no reordering).

Also fix the createApiKey() lifecycle comment: the gcTime:0 + invalidation
lives in queries/api-key-query.ts (there is no use-api-key-query file) and the
reset()-after-read of the token lives in components/api-keys-manager.tsx
handleCreate (create-api-key-modal.tsx only resets the form).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:21:27 +03:00
agent_coder d6411424c1 feat(api-key): distinguish expired from expiring-soon + cover service unwrap
Fix 1: an already-expired key rendered the forward-looking "Expiring soon"
badge/tooltip ("expires within 30 days"), which is false for a past expiry.
Add a pure isExpired() helper and, in ApiKeysManager, render a red "Expired"
badge ("This key has expired") for past expiries; keep the orange "Expiring
soon" badge only for keys expiring in the future within 30 days (the two
states are made mutually exclusive at the call site). Extend utils.test.ts
with isExpired coverage (past/future/null/exact-now boundary) and add a
component assertion that an expired key shows "Expired" and NOT "Expiring
soon".

Fix 2: the response-contract unwrap (res.data turning the server envelope
{data:{token,apiKey},success,status} into {token,apiKey}) was exercised by no
test — component tests fully mock the service. Add a service-layer unit test
that mocks the api-client (axios instance) and asserts createApiKey unwraps to
the inner {token,apiKey} payload and getApiKeys resolves to the row array.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:20:17 +03:00
agent_coder 199a9a1750 feat(client): страница управления API-ключами в настройках аккаунта (#506)
Фаза 2 поверх серверных эндпоинтов #501: страница «Настройки → Аккаунт →
API-ключи» (lazy-роут, отдельный чанк) — список, создание, показ токена
один раз и отзыв.

- Список: явная дата истечения (не «через N дней»), подсветка ключей с
  истечением < 30 дней, «использован» с семантикой «в течение последнего
  часа» (last_used_at троттлится на 1 ч серверно).
- Создание: имя + срок (30д/90д/1 год[дефолт]/бессрочный → null). После
  submit — модалка показа токена один раз с копированием и явной датой.
- Токен-материал живёт ТОЛЬКО в state открытой модалки: mutateAsync +
  reset() чистит копию из query-cache, ничего не пишется в localStorage.
  Закрытие модалки — токен исчезает навсегда.
- Отзыв: подтверждение → revoke → строка уходит из списка.
- Admin (CASL Manage на API = owner/admin) видит ключи всего воркспейса с
  колонкой «автор»; обычный член — только свои.

Тесты (vitest): показ-один-раз + отсутствие токена в localStorage/кэше,
явная дата + подсветка < 30 дней, отзыв убирает строку, admin/member вид,
дефолт срока = 1 год и «бессрочный» → null. Стаб ResizeObserver добавлен в
общий vitest.setup для рендера Mantine ScrollArea/Table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:20:17 +03:00
agent_vscode 6b825ad440 fix(client): anchor toasts to the top edge of the viewport
Move the top-anchored Mantine notification containers from top:96px to
top:8px so success toasts (e.g. "Comment resolved successfully") appear
at the very top of the page, above the header/search, instead of
covering page content. Update the accompanying comment to reflect the
new intent (toast renders over the header chrome while visible).
2026-07-12 19:09:30 +03:00
agent_coder 688cb54f26 fix(ai-chat): рекавери режет реплей НИЖЕ настроенного бюджета (#520 Опция B)
Решение владельца по #520 (эпик #497 итерация 5): при повторном overflow реактивная
рекавери ДОЛЖНА иметь право резать бюджет реплея ниже явно настроенного окна —
«чат не должен кирпичиться на overflow» это ИНВАРИАНТ реактивной ветки, а
настроенное окно — заявление о ёмкости модели, не обещание о размере реплея; когда
провайдер 400-ит, реальность бьёт конфиг. (Не конфликтует с #510 Опция A: та про
верхнюю границу НОРМАЛЬНОГО пути — уважать конфиг, пока он влезает.)

- Пол эскалации: max(scaled, min(REPLAY_MIN_FLOOR_TOKENS, floor(0.5×threshold))).
  Было min(REPLAY_MIN_FLOOR_TOKENS, threshold) — жёсткий пол на настроенном бюджете.
  Теперь БОЛЬШОЕ окно эскалирует НИЖЕ настроенного бюджета до фикс. пола 8k; МАЛОЕ
  окно (0.5×threshold < 8k) падает до floor(0.5×threshold) — минимум старый 0.5× cut
  (никогда не хуже, чем раньше), но и не поднимается выше самого бюджета.
  REPLAY_MIN_FLOOR_TOKENS=8k без изменений (константа уже была). Сброс k на чистом
  ходу сохранён (счётчик replayOverflowCount не пишется на чистом финализе).
- Наблюдаемость: warn-лог (настроенный бюджет не влезает, реплей ниже него, уровень
  k) + метка турна metadata.replayBelowConfiguredBudget=true — только когда реплей
  реально ниже настроенного бюджета (не на нормальном in-budget реплее).
- Тесты: перепиновка «не раздувает малый бюджет выше себя» (теперь МОЖЕТ резать ниже,
  Опция B) в обоих spec; новый тест эскалации ниже бюджета (окно 8000→бюджет 5600,
  k=1→2800, сходимость к полу, сброс на чистом ходу) + большое окно (140k→8k
  ступенями); тест метки метадаты. Мутации: пол=бюджет краснит below-budget тесты,
  снятие метки краснит observability-тест.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 18:01:07 +03:00
agent_coder ec5416068b fix(ai-chat): итеративная эскалация реактивной рекавери при overflow (#520)
Остаточный кирпич (#490/#510): при незаданном chatContextWindow база = плоский
дефолт 100k, а реальное окно модели маленькое (<50k). Фиксированный одинарный
cut 0.5×100k=50k всё равно превышал реальное окно → провайдер снова 400,
строка переставлялась replayOverflow, но булев priorOverflowed уже был true →
второго ужатия не происходило. Чат навсегда застревал на 50k и не восстанавливался.

Фикс (без парсинга тел 400):
- Сигнал из булева переведён в счётчик `metadata.replayOverflowCount` = число
  ПОДРЯД идущих overflow-ходов: инкремент (prior+1) на каждом overflow, сброс в 0
  на любом чистом финализе (чистая строка не пишет поле → читается как 0).
  BACK-COMPAT: старая строка с булевым `replayOverflow:true` читается как k=1.
- resolveEffectiveReplayThreshold(threshold, k) = max(floor(threshold·0.5**k),
  min(REPLAY_MIN_FLOOR_TOKENS, threshold)). k=0 → база; k=1 → 0.5×; k=2 → 0.25×;
  большой k → упирается в пол 8k (сходимость). null-база (trimming OFF) не трогается;
  пол никогда не поднимает легитимно малый настроенный бюджет выше него самого.
- Пол REPLAY_MIN_FLOOR_TOKENS=8k: ниже него чат не несёт осмысленный недавний
  контекст, и даже малое реальное окно его вмещает; keep-recent-turns сверху.

Тесты: таблица эскалации (k=0/1/2/большой/null), регрессия остаточного кирпича
(база 100k, окно ~40k → сходится ниже 40k, чего фиксированный 0.5× никогда не мог),
жизненный цикл счётчика на реальном pg (инкремент/сброс/back-compat через jsonb),
мутация **k→**1 краснит convergence-тест.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 18:01:07 +03:00
vvzvlad a990ebd604 Merge pull request 'feat(search) Фаза A: лексический оверхол — ru_en, OR/RRF, операторы, пагинация (#529)' (#538) from feat/529-search-lexical into develop
Reviewed-on: #538
2026-07-12 17:40:43 +03:00
agent_coder fff772cbe2 fix(search): F1 term-cap stack-depth guard + restore/extend coverage, drop dead code (#529)
F1: cap parsed terms at MAX_PARSED_TERMS (64) in parseSearchQuery so a huge
pasted query no longer nests the combined tsquery deep enough to blow Postgres'
stack depth limit (HTTP 500); +@MaxLength(10000) on SearchDTO.query as
defense-in-depth. F2/F3: restore titleOnly text_content leak-guard and
parentPageId subtree-scoping coverage in the lexical int-spec. F4: positive
ancestor-path ordering + non-empty snippet asserts. F5: remove dead
SearchLookupTier enum, unused buildTsQuery + pg-tsquery require (and its tests),
and the discarded parentPageId select in fetchDetails. F6: rewrite the stale
#443 DTO comment (parentPageId/titleOnly read by the engine; substring ignored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:37:07 +03:00
agent_coder 58f13a7efd chore(search): retimestamp ru_en migration to 20260707T130000 to avoid collision (#529)
Develop introduced two migrations at the exact 20260707T120000 stamp
(ai-chat-metadata, page-history-kind). Three files sharing one timestamp
is fragile for deploy ordering, so move #529's ru_en-config migration to a
strictly-later, unique 20260707T130000 so it orders cleanly last. It touches
only the ru_en text-search config (different tables), so running it after the
develop migrations is order-independent and safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:37:07 +03:00
agent_coder 2bb48f841f fix(search): S1/W1/W3/W4/S3/S5/S2 — required-term details, CAP note, restored guards (#529)
S1: fetchDetails now builds rank/highlight/matchedFields from the same
[...positive, ...required] set matchedTerms uses, so a required-only query
(`+кофейня`) no longer reports matchedFields:[] / rank:null.

W1: documented that CANDIDATE_CAP bounds pagination reachability (JS-side), not
query compute — a SQL LIMIT would corrupt the exact permission-filtered total.

S2: one-line note that literal-`%`/`_` search was intentionally dropped (total:0).

W3: restored the trgm-index EXPLAIN guard (search-lookup-explain.int-spec) that
asserts the coalesce-free substring predicates use idx_pages_*_trgm, not Seq Scan.

W4: added an integration ordering test proving title-exact > title-substring >
text-only tier dominance under RRF.

S3: added a test that an exact-title hit survives a tiny CANDIDATE_CAP window.

S5: server pretest now also builds @docmost/mcp so the ToolWriteClass import
resolves from clean CI (mirrors editor-ext/prosemirror-markdown).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:37:07 +03:00
agent_coder daa96eb132 fix(search): B1/W2 — honest page_embeddings.fts swap + true idempotency (#529)
BLOCKER B1: the fts config swap no longer blindly DROP+ADDs the generated
column. swapEmbeddingsFtsConfig now (1) reads the column's actual generation
expression from pg_catalog and TRUE-no-ops when it already references the target
config (real out-of-band escape hatch), and (2) gates the inline ACCESS
EXCLUSIVE rewrite behind SEARCH_EMBEDDINGS_FTS_INLINE_REWRITE (default 'true';
only a literal 'false' opts out), warning that the operator owns the swap.
up() now creates ru_en only when missing (drop-recreate would fail on the fts
hard dependency on a re-run), so up() is genuinely idempotent. down() guards the
config DROP when fts still references ru_en (gated-off path), staying non-fatal.

W2: corrected the header — Kysely runs EACH migration in its own transaction
(not the whole set in ONE); the single-UPDATE atomicity conclusion is unchanged.
Rewrote the runbook: inline path is a full-table ACCESS EXCLUSIVE rewrite of
page_embeddings; removed the false "IF-EXISTS no-op" claim.

Extends the roundtrip test with idempotent-skip (2nd up() no-op) and env-gate
=false (embeddings rewrite skipped, pages.tsv still swapped) assertions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:37:07 +03:00
agent_coder 4c5230d677 test(search): A1 migration down()/up() reversal roundtrip (#529)
Asserts the ru_en migration is reversible in the correct order: down()
reverts pages.tsv trigger + page_embeddings.fts to english BEFORE dropping
the ru_en config, and up() re-applies it idempotently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:37:07 +03:00
agent_coder 50ca27c5d4 feat(search): A9 web-UI — superset types, match/pagination params, operator hint (#529)
- search.types.ts: IPageSearch is the #529 superset (pageId/snippet/score/
  path/matchedFields/matchedTerms; rank/highlight nullable); add the
  IPageSearchResponse pagination envelope and match/limit/offset params.
- search-spotlight.tsx: operator hint ("exact phrase", +required, -excluded).

The web list already reads response.items, so it picks up the new OR order +
superset with no code change; icon/space/highlight remain (acceptance #12).
Full spotlight pagination UI is deferred within this PR — the API + MCP fully
support total/hasMore/offset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:37:07 +03:00
vvzvlad afc50ead38 Merge pull request 'feat(deploy): version-coherence — WS-анонс версии + guarded reload устаревших вкладок (#481)' (#499) from feat/481-version-coherence into develop
Reviewed-on: #499
2026-07-12 17:37:04 +03:00
vvzvlad ec932e89a3 Merge pull request 'feat(page-history): время работы над статьёй — число + суточный punch-card (#395)' (#551) from feat/395-work-time into develop
Reviewed-on: #551
2026-07-12 17:36:27 +03:00
vvzvlad 490d7965a1 Merge pull request 'feat(comment): Undo в тосте резолва (reopen) + сортировка Resolved по времени резолва (#542)' (#548) from feat/542-resolve-undo into develop
Reviewed-on: #548
2026-07-12 17:36:08 +03:00
vvzvlad 32c9361969 Merge pull request 'feat(mcp): буквальная запись markdown (без math/autolink) + getPage format:text (#502)' (#552) from feat/502-mcp-md-modes into develop
Reviewed-on: #552
2026-07-12 17:35:36 +03:00
vvzvlad c30f910d66 Merge pull request 'fix(prosemirror-markdown): markdown-импорт помечает внутренние ссылки на страницы как internal (#522)' (#524) from fix/522-internal-links into develop
Reviewed-on: #524
2026-07-12 17:35:28 +03:00
agent_coder 7007f6bcf9 feat(search): A2-A9 — unified OR-relevance engine, RRF, pagination (#529)
Rewrite SearchService.searchPage into ONE engine for web-UI, MCP agent and
share:

- A2 server-side query parser (search-query-parser.ts): quote-aware
  tokenizer, leading +/- operators (internal -,.,: literal), "phrase",
  metachar-stripped bare terms; tsquery built as a parameterized AST via
  SQL ||/&&/!! (never string-concat). only-negation/empty short-circuit.
- A3 match=auto routes identifier-like terms (10.31.41, esp32,
  WB-MGE-30D86B) to the substring/trigram branch, words to FTS; word/
  prefix/substring overrides.
- A4 RRF (k=60) fuses the FTS branch (ts_rank_cd) and substring branch
  (title-exact>title-sub>text tier) by RANK; ORDER BY rrf DESC, id.
- A5 exact permission-filtered total (fail-closed via filterAccessiblePageIds
  with the #348 hasRestricted fast-path), CANDIDATE_CAP fusion window,
  offset/limit, hasMore, truncatedAtCap.
- A6 single path (spaceId/share/creatorId/titleOnly/parentPageId/match);
  share uses getPageAndDescendantsExcludingRestricted.
- A7 response superset per hit (id/pageId/slugId/icon/title/space/…/rank/
  highlight/snippet/path/score/matchedFields/matchedTerms).
- A8 buildAncestorPaths now skips deleted + cross-space ancestors.
- A9 DTOs: match, offset, total/hasMore/truncatedAtCap/query/matchedFields.

SEARCH_MODE=or|and toggles the parser; SEARCH_CANDIDATE_CAP tunes the
window. Legacy lookup unit/int specs replaced by parser unit tests + a
13-criteria integration spec on real pg (incl. a permission mutation guard
and a fail-closed propagation test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:35:24 +03:00
agent_coder 4bf06c68cf feat(search): A1 — ru_en text-search configuration + config swap (#529)
Introduce a `ru_en` FTS configuration (english_stem over ascii token
classes, russian_stem over Cyrillic ones) and flip every stored + query
side to it IN LOCKSTEP (acceptance #13):

- new migration creates ru_en, swaps the pages.tsv trigger + reindexes
  existing rows (row-lock UPDATE, no ACCESS EXCLUSIVE), and swaps the
  page_embeddings.fts generated column (documented rewrite/lock trade-off
  for large tenants, mirroring the #443 trgm migration). down() reverts
  tsv/fts to english BEFORE dropping the config (dependency order).
- page-embedding.repo.ts hybridSearch query config english -> ru_en, so
  the RAG lexical leg's query config matches its fts column config.
- search.service.ts current query literals english -> ru_en so the column
  and query configs stay paired (this commit is independently revertable;
  the engine itself is rewritten in the A2-A9 commit).

The reindex is atomic within the single migration transaction (this repo's
Migrator wraps all pending migrations in one tx), so no morphology-desync
window exists and no dual-config read path is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:35:24 +03:00
vvzvlad 11eb87d58a Merge pull request 'docs(server): getPageBreadCrumbs — предки не утекают (нисходящее наследование ограничений), задокументировано (#471)' (#550) from docs/471-breadcrumb-permission into develop
Reviewed-on: #550
2026-07-12 17:33:55 +03:00
vvzvlad 661ea8ba07 Merge pull request 'fix(client): reconnect-вход — таймаут-гонка вокруг getRun, hang не залипает FSM в streaming (#541)' (#543) from fix/541-reconnect-timeout into develop
Reviewed-on: #543
2026-07-12 17:32:07 +03:00
agent_coder 79b2da686b fix(work-time): clip cross-class padding; gate & payload fixes (#395)
F1: pad-clip adjacent different-class sessions at the raw-gap midpoint so
workMs and agentOnlyMs can never double-count the same wall-clock (default
agentTGap 7m < pIn+pOut 10m made a work-session-ending-in-agent overlap a
nearby agent_only run). Reword the config guard/comment: cross-class
disjointness is now structural, tGap≥pIn+pOut is a kept sanity bound. Add a
cross-class no-double-count test and a real seeded property/fuzz test (250
random timelines × 4 tz) asserting per-class union, cross-class disjointness,
and Σ per-day activeMs == workMs.

F2: page.controller.spec — add a ForbiddenException view-gate reject test that
asserts the rejection propagates and computeWorkTime is NOT reached, locking
validateCanView before compute.

F3: WorkTimeStat renders for agent-only pages (workMs==0, agentOnlyMs>0) with
an `agent:` headline so the punch-card stays reachable.

F4: drop the dead un-bucketed `sessions` list from the PageWorkTime response
and the client work-time types (no client reads it; bucketByDay still consumes
the core sessions internally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 08:31:42 +03:00
agent_coder 8503ff1f3d fix(ai-chat): hydrate crashed mid-run steps for model replay + cover write/read seams (#492)
F1: the server model-replay loaded history via findAllByChat().map(rowToUiMessage)
WITHOUT hydrating parts from ai_chat_run_steps. A HARD crash mid-run (SIGKILL/OOM)
fires no terminal callback, so the assistant row stays parts:[] and its partial
tool-calls/results/text (durable in the steps table) dropped out of the model's
next-turn context. Hydrate needy assistant rows (role==='assistant' &&
!rowHasInlineParts) via findByMessageIds + hydrateAssistantParts before the replay
map — mirroring the controller's withReconstructedParts exactly — guarded on the
optional repo. Fix the now-false interrupt-resume comment.

F2: add a service int-spec that drives the REAL onStep append-persist WRITE branch
through AiChatService.stream with a real AiChatRunStepRepo injected, asserting the
per-step rows' stepIndex + parts slice and the step-marker metadata match a
single-row flush (catches an stepsPersisted-1 off-by-one).

F3: add a controller int-spec that drives withReconstructedParts through getMessages
WITH the repo present (a mid-run marker-only row + its step rows), asserting the
reconstructed metadata.parts and workspace-scoping.

F4: remove the dead countByMessage (zero prod callers; reconstructRunParts derives
stepsPersisted inline) + its now-unused sql import and the redundant test assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 07:50:50 +03:00
agent_coder 3163f50c98 fix(mcp): route #502 extension flags per write tool; add server import flag (#502 review)
Internal review found the two layered-extension flags landed in the SHARED
markdownToProseMirrorCanonical wrapper, which mis-covered two tools. Move the
decision to each caller by the tool's semantics.

BLOCKER 1 — createPage was NOT covered. createPage POSTs to the server
/pages/import endpoint, which imported with DEFAULTS (math + fuzzy autolink ON),
so an agent's `$x=1$` still became a formula and `www.host` still autolinked.
Add an optional `disableMarkdownExtensions` multipart field to /pages/import
(default OFF, so human file uploads keep math ON); import.controller reads it,
import.service.importPage/processMarkdown thread it into markdownToProseMirror.
MCP createPage sends it true.

BLOCKER 2 — import_page_markdown was wrongly disabled. It goes through the same
wrapper, so hardcoding parseMath:false degraded an exported `$x^2$` to literal
text on re-import, breaking the #328 lossless export→import pair. The wrapper no
longer hardcodes the flags: it takes them from the caller and DEFAULTS to the
package importer defaults (extensions ON). Callers now set them explicitly:
  - updatePageMarkdown (updatePageContentRealtime) -> OFF
  - patch_node/insert_node (importMarkdownFragment) -> OFF (unchanged)
  - import_page_markdown -> DEFAULTS (math ON) — #328 round-trip restored

Tests: rewrite the mcp write test around the corrected per-caller semantics
(agent-write OFF, import_page_markdown DEFAULTS incl. the REAL #328 round-trip);
add a collab-backed wiring test driving client.updatePage (OFF) and
client.importPageMarkdown (DEFAULTS) end-to-end; add a createPage multipart-flag
test; add a server import.service.processMarkdown spec (flag OFF -> literal / no
autolink, default -> math ON for human uploads); reword the prosemirror-markdown
round-trip test to its package-default scope. Mutation-verified both caller
wirings (flip updatePageMarkdown -> defaults reddens the OFF wiring test; make
the wrapper default OFF reddens the #328 round-trip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 06:58:48 +03:00
agent_coder 3e81f64415 fix(work-time): user idle breaks agent burst; cap tz length (#395)
Only an agent-sourced idle pulse extends an agent burst; a user idle
(human supervision) now falls through to the human branch so the session
is classified `work`, not `agent_only`. Add @MaxLength(64) to tz to match
its comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 06:53:59 +03:00
agent_coder d36021a111 fix(comment): clear resolved mark only after reopen confirmed
Move the inline ProseMirror comment-mark clear out of the Undo toast's
onClick and into the reopen mutation's onSuccess branch. Clearing it
eagerly flipped the collab mark to unresolved before the reopen result
was known: on a non-404 failure the RQ cache rolls back to resolved but
the doc kept an active highlight the panel treats as resolved, and a
comment refetch can't heal a divergence that lives in the collab doc.
Mirrors the 404 branch's editor-liveness guard/try-catch and the safe
await-then-mark pattern in comment-list-item. The button-triggered
reopen already sets the mark, so the onSuccess call is an idempotent
no-op there.

Tests: reopen failure (500) leaves the mark untouched; null editorRef
on the success path degrades gracefully.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 06:51:38 +03:00
agent_coder c674db2b2f feat(page-history): «время работы над статьёй» — число в UI + суточный punch-card (#395)
Оценка времени работы над страницей как сессионизация истории page_history по
паузам бездействия (WakaTime-подобно), а не span между крайними правками, плюс
drill-down в суточный таймлайн 24ч×дни. Зависит от #374 (kind + idle-пульс).

Server:
- Чистая computeWorkTime(rows, config) (§5): нормализация+дедуп → коллапс
  агентских всплесков по aiChatId → ОДИН проход сессионизации (порог зависит от
  пары: оба agent → agentTGap, иначе tGap; последняя сессия обязательно
  закрывается) → класс готовой сессии (все agent → agent_only, иначе work) →
  добивка (много-сэмпл → [first−P_in, last+P_out], одиночный скаляр →
  [t−P_single, t]) → метрики = union wall-clock по классу. Детерминированная,
  без БД.
- Чистая bucketByDay(sessions, tz) (§6.3): union work/agent_only РАЗДЕЛЬНО, разрез
  по полуночи tz через нативный Intl (DST-корректно, без «+24ч»). Инвариант
  Σ activeMs == workMs держится по построению.
- PageHistoryRepo.findTimelineByPageId — лёгкая проекция без content, ASC.
- PageHistoryService.computeWorkTime + POST /pages/history/time, гейт
  validateCanView как у /history; атрибуция человек/агент по lastUpdatedSource.

Client:
- usePageWorkTime(pageId) шлёт tz зрителя (Intl локаль).
- Кликабельное число «≈ 4 ч 30 мин» в шапке страницы (порог в тултипе).
- Модалка-punch-card: 24-часовые дорожки по дням, окна work/agent разным цветом,
  одиночные (P_single) приглушены, сумма за день, пустой день «—», сворачивание
  длинных серий пустых дней, подпись tz + T_gap. i18n ru-RU + en-US.

Tests: 22 юнита на computeWorkTime/bucketByDay (§7-фикстура ≈1ч32м vs ≈60h наив,
закрытие последней сессии, коллапс/разрыв всплеска, idle-пульс, DST 23/25ч,
полуночный разрез, инвариант, union-без-задвоения); 5 юнитов гейта контроллера;
int-spec на реальном pg (проекция + совпадение с ядром); 6 клиентских на формат.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 06:32:12 +03:00
agent_coder 44cdb5a4c3 feat(mcp): disable math+autolink in MCP markdown-write; getPage format:text for machine diff (#502)
WRITE: parameterize the canonical importer markdownToProseMirror with
{parseMath, fuzzyLinkify} (defaults true — editor/file-import/git-sync unchanged);
MCP write paths (createPage/updatePageMarkdown/patchNode/insertNode) call with both
false so $...$ stays literal text and bare domains don't autolink (explicit
https:// still links). READ: getPage format:'text' reuses the server jsonToText path
(deterministic flag) for flat machine-diffable text, [image]/[table RxC] placeholders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 06:18:31 +03:00
agent_coder 5b17503df7 docs(page): explain breadcrumb ancestors need no per-ancestor permission filter (#471)
getPageBreadCrumbs returns the full ancestor chain filtered only by
deletedAt; the /breadcrumbs endpoint validates validateCanView on the
target page only. Document why this is safe rather than an accepted leak:
page restrictions inherit down the tree, so viewing a page implies the
right to view every ancestor (canUserAccessPage checks the full ancestor
chain). A hidden ancestor would already hide the target itself, making
per-ancestor filtering redundant. Owner decision: document, no logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 05:58:48 +03:00
agent_coder 12ed3a0332 feat(comment): undo button in resolve toast + sort resolved tab by resolve time
Part 1 (#542): add an inline Undo (reopen) button to the resolve success
toast in useResolveCommentMutation. Undo re-invokes the same mutation via a
self-ref (declared before useMutation, read at click time — no cycle) and
clears the inline comment mark through an editor ref (survives the originating
CommentListItem unmounting). A closured `done` flag guards a fast double-click
(notifications.hide is async). Reopen keeps the plain toast (no Undo).

onError gains a terminal 404 branch: drop the comment from the cache and
unsetComment its orphaned mark (no rollback → no phantom in Resolved, neutral
"Comment no longer exists"). The generic-failure copy is now directional
(resolve vs. re-open). Autoclose policy const RESOLVE_UNDO_AUTOCLOSE_MS=10000.

Part 2: sort the Resolved tab by resolvedAt DESC via an exported
sortResolvedByResolvedAt helper; coerce with new Date(...) because resolvedAt
is an ISO string at runtime (Date only in the optimistic window).

i18n: add "Failed to re-open comment" and "Comment no longer exists" to
en-US/ru-RU. Tests: 6 resolve-undo cases + 4 sort cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 05:45:42 +03:00
agent_coder b3cc6a7ff8 Merge remote-tracking branch 'gitea/develop' into feat/481-version-coherence 2026-07-12 05:36:59 +03:00
agent_coder c42cb42413 docs(reload-guard): align header/docstring wording to the window model (#481 review F2 consistency)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 05:36:58 +03:00
agent_coder ae3dfd8de6 Merge remote-tracking branch 'gitea/develop' into feat/492-incremental-render 2026-07-12 05:34:07 +03:00
agent_coder 41030b2c78 test+docs(deploy): покрыть reactive fail-closed guard + выровнять формулировки под оконный бюджет (#499 ревью)
F1: интеграционный тест на reactive путь при storage, который ЧИТАЕТ, но не
ПИШЕТ (quota / Safari private): getItem→null (бюджет доступен, проходим
hasAutoReloaded), setItem→throw → markAutoReloaded()===false → guard на
chunk-load-error-boundary.tsx:43 обязан отбить reload, иначе реактивный путь
зациклил бы reload без сохранённого бюджета. Симметричный proactive случай уже
покрыт guarded-reload.test.tsx. Мутация (снять guard :43) → тест краснеет.

F2: формулировки "per session / this session" в version-coherence.ts и
guarded-reload.tsx приведены к оконной модели ("per RELOAD_WINDOW_MS window" /
"window budget available") — reload-guard.ts и тест восстановления после окна
уже описывают именно окно, а не постоянный one-shot на сессию.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 05:32:33 +03:00
agent_vscode 03eafa6c68 Merge remote-tracking branch 'gitea/develop' into develop 2026-07-12 05:16:51 +03:00
agent_vscode a42f1ead48 test(ai-chat): align #332 deferred-tool test with #490 cross-turn persistence
The #332 integration test (ai-chat-stream.int-spec.ts) asserted a deferred
tool activated in one turn does NOT leak into the next ("cold start per
turn"). #490 (f5bbfdb2) deliberately reversed that: it persists the
activation set into chat metadata.activatedTools and seeds the next turn
from it, so the model need not re-run loadTools. The test only surfaced now
because the token-estimate CI fix let the integration step run again.

Adopt #490 persistence as the intended behavior:
- flip the turn-2 assertion to expect createPage IS active on the fresh
  turn's first step (seeded from metadata); keep turn-1 cold-start assertions.
- rewrite the test docstring, describe/it titles and comments accordingly.
- fix two stale "not persisted / per-turn" comments in ai-chat.service.ts
  (prepareAgentStep + the streaming-loop activation block); no logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 05:16:47 +03:00