Skip to content

release: v0.10.0 - #151

Merged
Liewzheng merged 45 commits into
mainfrom
feat/backend-frontend-integration
Sep 4, 2026
Merged

release: v0.10.0#151
Liewzheng merged 45 commits into
mainfrom
feat/backend-frontend-integration

Conversation

@Liewzheng

Copy link
Copy Markdown
Owner

Summary

Review Engine 0.10.0 — a major upgrade centered on persistent storage. One-way migration: the first 0.10.x boot creates the database, applies embedded migrations, and imports ui-state.toml in one transaction; downgrading to 0.9.x is not supported. Back up the config directory before upgrading (see docs/migration-0.10.md).

Highlights

  • Persistent storage layer — PostgreSQL primary, embedded SQLite fallback: new src/store/ module on sqlx 0.8's Any pool; DATABASE_URL set → PostgreSQL, unset → embedded SQLite. 7 tables, migrations embedded via sqlx::migrate!() and applied at startup.
  • Review history survives restarts: TaskStore writes through to the DB; History APIs read from the DB, no longer bounded by the 30-minute in-memory reaper. Startup sweep fails interrupted tasks.
  • Configuration in the database: git platforms and LLM providers move from ui-state.toml into the DB via a one-shot transactional import; all credentials stored enc:-encrypted (ChaCha20-Poly1305). REVIEW_DISABLE_DB=1 restores full 0.9 behaviour.
  • MR discussion context: Note webhooks ingested into mr_discussions (idempotent upsert, self-echo guard) and injected into expert prompts.
  • Webhook signing token and related security hardening.
  • Storage backend visibility: GET /api/v1/system/health gains storage_backend (postgresql / sqlite / disabled).

Fixed (incl. E2E-found release blockers)

  • PostgreSQL ?$n placeholder rewriting (sqlx Any does not translate; every bound statement failed on PG with 42601).
  • llm_providers.temperature declared float8 on PG (f64 bind/decode mismatch broke config read-back).
  • durationMs wrap-around guard; History list projection self-consistency.
  • SPA history-mode deep links no longer 404 (ServeDir::fallback serves index.html); diagnosable static-dir miss.
  • Webhook-triggered first reviews get project context without a local checkout; adjudication pass skips loudly (fail-open) when no checkout exists (RENG-25).
  • Tolerant GlobalReviewContext YAML parsing (RENG-26); History author column shows the commit author (RENG-27); /api/v1/reviews pinned as sole history list endpoint (RENG-29); Dashboard recent-reviews status stays on one line (RENG-30).

Full details in CHANGELOG.md [0.10.0].

Verification

  • cargo fmt --check / cargo clippy --all-targets --all-features / cargo test / cargo build — see CI
  • cargo audit — see CI (audit.yml)
  • E2E rounds against local GitLab EE testbed, incl. teardown contract (0.9.50)

Known issues

  • GitLab 19.x system hooks do not deliver MR note events — use a project-level webhook; the pre-review API pull covers the gap.
  • Deferred to 0.10.x: config-directory isolation incomplete, legacy webhookSecret masking alignment, no deep DB probe in /system/health yet.

…ke tests

- sqlx 0.8 with default-features=false; adds `any` and `macros` on top of
  the design-doc feature list: the Any driver and the migrate!() macro are
  otherwise not compiled in.
- migrations/0001_init.sql: 7 tables per design/persistence.md §3.2, with one
  deviation proven by smoke test — timestamp columns are TEXT carrying
  fixed-width RFC 3339 UTC strings, because the Any driver has no chrono
  Type impls and SQLite refuses String decode from TIMESTAMP-typed columns.
- src/store/mod.rs: SqlxStore::connect (PG/SQLite URL discrimination),
  connect_default (sqlite://{config_dir}/review.db?mode=rwc),
  new_in_memory (max_connections(1)), migrate (embedded via migrate!()).
  SQLite pools get WAL / foreign_keys / busy_timeout pragmas.
- Verification point A/D smoke tests: schema creation + idempotent re-run,
  `?` placeholder INSERT/SELECT round trip, timestamp round trip with
  ORDER BY correctness. PG side is #[ignore]d behind DATABASE_URL.
- traits.rs: ConfigStore — whole-set load/replace for git_platforms and
  llm_providers (aligned with UiStateFile semantics), legacy gitlab via the
  app_settings 'gitlab' JSON row, arbitrary app_settings JSON get/upsert,
  and config_tables_empty (the §6.1 one-shot import trigger).
- rows.rs: GitPlatformConfig / LLMConfig / PersistedGitlabConfig codecs.
  This is the enc: boundary: token / webhook_secret / webhook_signing_secret
  / api_key are ChaCha20-Poly1305 encrypted at rest (LLM api_key newly inside
  the boundary — 0.9 persisted it plaintext); empty stays empty; values
  without the enc: prefix read back as legacy plaintext. LLM list order is
  round-tripped via a position marker in raw JSON (first entry is the
  fallback primary provider).
- sqlx.rs: SqlxStore impl; ? placeholders, no RETURNING, RFC 3339 TEXT
  timestamps, replace-in-transaction semantics.
- SqlxStore now carries the secrets key: connect() resolves it via
  resolve_ui_state_path + key_path_for, connect_default(config_dir) uses
  {config_dir}/secrets.key, new_in_memory() uses an ephemeral key.
- Migration deviation (verified by smoke test): git_platforms.enabled is
  INTEGER 0/1 instead of BOOLEAN — the Any driver cannot decode SQLite
  columns declared BOOLEAN (only Null/Int4/Integer/Float/Blob/Text pass the
  SQLite→Any type mapping).
…e-test findings

- §3.1 布尔行: BOOLEAN 证伪, DDL 改 INTEGER 0/1 (Any 无法解码 SQLite Bool 声明列)
- §3.2 enabled 列同步改 INTEGER
(时间戳 TEXT 化与 §11 验证点 A 勾选为杜衡并行修订)
Startup sequence (design/persistence.md §6.1, strict order, cli/app.rs):
1. bootstrap_database(): REVIEW_DISABLE_DB=1 bypasses to 0.9 behaviour
   (warn); DATABASE_URL set-but-unreachable is a hard startup error, never a
   silent SQLite fallback (§9); no DATABASE_URL → embedded SQLite at
   {config_dir}/review.db. migrate() failure propagates (exit non-zero).
2. (placeholder comment) §5.3 interrupted sweep — 梁序 step 4.
3. import_ui_state_into_db(): triggers only when all three config tables
   are empty AND ui-state.toml exists; the whole import is ONE transaction
   (ConfigStore::save_ui_state), renamed to ui-state.toml.migrated only
   after every table is written; any error keeps the file and falls back to
   the file replay path.
4. load_and_apply_ui_state_from_db(): DB rows reassemble into a UiStateFile
   and go through the SAME replay_payload + apply_ui_config path, so
   env precedence and masked projections are literally shared with the file
   path. Empty DB → Ok(false) → file replay fallback.

PUT /config (§6.2): state.db set → ConfigStore::save_ui_state (single tx);
else the 0.9 file path. UiStateFile::from_applied env filtering is reused
unchanged — env/CLI values are never persisted anywhere. Persist failure
returns 500, same as a file-write failure today.

AppState gains pub db: Option<Arc<SqlxStore>> (None default; the sync
AppState::new() test path is untouched).

Tests: import happy path (plaintext-LLM-key legacy file → tables populated,
file renamed, all four secret columns enc: at rest, DB replay ≡ file
replay); env precedence matrix against the DB source (env LLM wins
wholesale; gitlab env is fallback-only / DB wins when set); failed import
(dup UNIQUE name) rolls back completely and keeps the file;
put_config_persists_to_db_instead_of_file; REVIEW_DISABLE_DB flag parsing.
…covery

0.10.0 persistence step 4 (design/persistence.md §5):

- store/traits.rs: ReviewStore — create / mark_started / fill_source_meta /
  complete (terminal, single tx: reviews UPDATE + expert_reports replace) /
  mark_cancelled / mark_retry / mark_interrupted (startup sweep).
- store/sqlx.rs: SqlxStore impl; store/rows.rs: TaskEntry⇄reviews and
  ReviewOutput.reports⇄expert_reports codecs (state strings reuse the API
  projection mapping task_status_str so DB/SSE vocabulary cannot drift).
- server/task_queue.rs: TaskStore gains db: Option<Arc<dyn ReviewStore>> +
  set_db; write-through on create_with_request/start/fill_source_meta/
  update(terminal)/delete/retry per §5.2; set_progress stays memory-only;
  the cancelled early-return writes nothing. DB writes are synchronously
  awaited AFTER the in-memory lock is released; failures log and continue,
  terminal writes retry once. db=None is exactly 0.9 behaviour.
- cli/app.rs: after migrate, mark_interrupted() sweeps stale
  pending/running rows to failed('interrupted: server restarted') and the
  DB handle is injected into the task store.
- reaper untouched (memory-only).

Tests: full lifecycle row assertions, expert_reports split, failing-store
injection (path unblocked, terminal retried exactly once), interrupted
sweep state coverage, codec round-trip, cancel/retry write-through,
db=None 0.9 parity. fmt/clippy/test green (1592 passed, 0 failed).
0.10.0 persistence step 5 (design/persistence.md §8.1):

- store/traits.rs: ReviewStore gains the read side — ReviewListQuery
  (handler-normalized params), list_reviews() -> (entries, total),
  get_review() -> Option<TaskEntry>.
- store/sqlx.rs: list pagination via ORDER BY created_at DESC, task_id DESC
  (deterministic tiebreak) + LIMIT/OFFSET, COUNT(*) under the same WHERE;
  status/q/project/repository/date_from/date_to filters; q keeps the 0.9
  case-insensitive literal-substring semantics over source_meta TEXT
  (LOWER(...) LIKE LOWER(?) ESCAPE '\', LIKE wildcards escaped).
- store/rows.rs: REVIEW_COLUMNS + ReviewRowTuple + From tuple -> ReviewRow;
  review_from_row decodes DB rows back to TaskEntry, so the three API
  projection functions (task_to_status / build_review_detail /
  build_review_list_item) keep their signatures and the response shape is
  byte-compatible with 0.9 (timestamps, duration_ms included).
- handlers.rs: list_reviews / get_review switch on AppState::db — Some: DB
  query (get_review overlays live progress/expert_name from memory for
  in-flight tasks; a memory-only task whose create write-through failed is
  still served instead of 404); None (REVIEW_DISABLE_DB=1 / tests): the 0.9
  in-memory path, unchanged.

Tests (src/server/api/review/tests.rs): (a) pagination/status/q/project/
repository/date semantics vs 0.9 + item/top-level key-set parity against
the memory path; (b) live progress/expert_name overlay for in-flight tasks,
pure-history detail from DB, 404 for unknown; (c) db=None fallback for
list+get; (d) empty DB and out-of-range page boundaries. fmt/clippy/test
green (1596 passed, 0 failed).
…ions

0.10.0 persistence step 6a (design/persistence.md §7.1):

- store/traits.rs: DiscussionStore — upsert_note (idempotent on
  (platform, project, mr_iid, note_id), edits update body/author) and
  list_notes (created_at, note_id ascending — the append-only order the
  step-6b context renderer relies on). store/rows.rs: row codecs;
  store/sqlx.rs: SqlxStore impl (? placeholders, ON CONFLICT upsert).
- server/gitlab/hooks.rs: handle_note_hook gains db: Option<Arc<SqlxStore>>;
  ingestion runs after parse, BEFORE the command check (command notes are
  discussion history too). Skips: non-note payloads, non-MR noteables,
  system notes. MR iid falls back to the object_attributes.url tail;
  platform defaults to 'default'; author prefers user.username.
- Self-echo guard (§7.1): (a) notes starting with the published report
  prefix — extracted as publisher::REVIEW_REPORT_PREFIX ("# CodeReview
  Board\n\n", previously inline in lib.rs publish_review); (b) notes
  authored by the service's own GitLab user id, resolved lazily per
  platform via GET /user (new Client::for_instance) and cached for the
  process lifetime (only successes cached; failures retry next note).
  /review and /describe command notes always ingest (user intent).
- handler.rs passes AppState::db through both note-hook call sites.
  db=None keeps exact 0.9 behaviour; ingestion failures are logged, never
  fail the hook.

Tests: ingestion field fidelity (incl. GitLab legacy '… UTC' timestamp
format), platform name + iid URL fallback, redelivery dedup, edit-in-place,
non-MR / system note skips, self-echo guards (a)+(b) with command
exception, db=None 0.9 parity, store-level list ordering. fmt/clippy/test
green (1605 passed, 0 failed).
….10.0 §7.2)

DB-first (mr_discussions from §7.1 webhook ingestion) with a GitLab
discussions-API fallback that back-fills the DB; the rendered section is
prefix-stable (ordered by created_at/note_id, fixed 2000-char body cap),
sha256-recorded into review_contexts, capped at 128 KiB, and attached to
MRInfo.discussion_context between the fixed MR context and the diff. Every
failure path (no DB, API down, empty, oversized) degrades to the 0.9 prompt;
self-echo guards (report prefix, own user id, system notes) match webhook
ingestion while /review /describe command notes are kept.
…-comment tab (§8.3)

raw_comment now resolves aggregated.markdown → consolidated.assessment.tl_dr,
filtering empty strings to None, so team reviews without an aggregator report
no longer render an empty full-comment tab.
…-up)

SqlxStore records its BackendKind (postgresql/sqlite) at connect time
from the URL discrimination in design/persistence.md §4.3, exposed via
backend_kind(). GET /api/v1/system/health gains a read-only
storage_backend field: "postgresql" / "sqlite" / "disabled" (no DB
attached — REVIEW_DISABLE_DB=1, tests, embedded use), for the frontend
config page.
…0.10.0 wrap-up)

The config page Advanced card gains a permanently-disabled row displaying
the persistence backend in use (PostgreSQL / SQLite / disabled), sourced
from the storage_backend field added to GET /api/v1/system/health in
d0a8c82. The service layer normalizes the one snake_case key to
storageBackend and validates it against the known kinds; a health-check
failure or an older server simply hides the row (fail-silent).

Also drops an unused catch binding in Configuration.vue flagged by
eslint (no-unused-vars).
…ore layer

sqlx 0.8.6's Any driver passes statement text through verbatim — it does
NOT translate `?` to $1..$n (no placeholder/rewrite logic anywhere in
sqlx-core-0.8.6/src/any/), so every bound DML failed on real PostgreSQL
with 42601 syntax error (0.10.0 E2E). The earlier assumption baked into
src/store/mod.rs and design/persistence.md §3.1 was wrong.

- src/store/placeholders.rs: lexer-based rewriter; `?` inside
  single-quoted literals (incl. '' escapes), double-quoted identifiers,
  -- line comments and nested /* */ block comments stays literal.
  Covered by unit tests for mixed literals, escaped quotes, comments,
  consecutive/double-digit placeholders, and unterminated input.
- src/store/mod.rs: adapt_sql(kind, sql) / SqlxStore::sql() — the single
  adaptation point; PG rewrites (borrowing when there is nothing to
  rewrite), SQLite passes the text through unchanged. Module doc and the
  ignored PG smoke test corrected.
- src/store/sqlx.rs: every statement (ConfigStore / ReviewStore /
  DiscussionStore, incl. tx helpers, the interrupted sweep, the dynamic
  list pagination and both upserts) now passes through adapt_sql once.
- design/persistence.md: §3.1 placeholder row and verification point A
  corrected; Migrator is unaffected because the Any migrate path
  delegates to the real underlying driver.

Verified on postgres:16-alpine: migrate_on_postgres_smoke green, server
health reports storage_backend=postgresql, ui-state.toml import fills
git_platforms/llm_providers/app_settings (secrets enc:-prefixed), the
restart sweep flips pending/running to failed/interrupted, and review
list pagination/filters return correct pages.
…e durations

F1 (PG release blocker): llm_providers.temperature was REAL, which PG
parses as float4, while the store binds/decodes f64 — read-back failed
with 'mismatched types: f64 is not compatible with SQL type REAL'.
The failure chain was silent: ui-state import succeeded (file renamed
to .migrated), but the restart DB replay died on decrypting/decoding,
leaving GET /config empty while /health stayed green. SQLite's REAL is
8 bytes, which is why unit tests and the SQLite E2E never saw it.
0.10.0 is unreleased with no databases to migrate, so the column simply
becomes DOUBLE PRECISION (float8); SQLite maps that to the same 8-byte
REAL affinity. Decode stays f64. A new ignored PG test
(llm_providers_temperature_round_trip_on_postgres) gates the exact
read path, and cleans up its enc: rows so shared scratch databases are
not polluted with foreign-key ciphertext. migrations/0001_init.sql and
design/persistence.md document why REAL is banned for float columns.
No other float columns exist in the schema (audited: temperature is the
only one).

F2: TaskEntry::duration_ms wrapped to ~2^64 when a hand-seeded row has
created_at later than completed_at (negative i64 cast to u64). All four
millisecond-span projections (duration_ms, elapsed_ms, and the two
inline SSE elapsed calculations) now go through one millis_between
helper: saturating_sub on timestamps, clamped at 0. Unit test covers
inverted spans and clock skew.

Verified on postgres:16-alpine: both ignored store tests green; server
boot imports ui-state.toml, restart replays from the DB, and GET /config
is byte-identical across the restart (gitPlatforms/gitlab/llm intact,
temperature 0.3); an inverted-timestamp review row reports durationMs 0.
…lumns

0.10.0 E2E-A 观察点 4:reviews 表的 project/repository 物化列有值、但
列表 API 响应 project:null。语义设计是「物化列供过滤、投影走
source_meta JSON」(design/persistence.md §5.2,写穿同步维护),两者一致
时无问题;但 fill_source_meta 的 UPDATE 失败只记日志不重试
(task_queue.rs),加上手工种子/遗留行不经过 codec,漂移行客观存在——
于是出现「?project=X 过滤命中、行里 project 却显示空」的自相矛盾。

rows.rs review_from_row 之前把已 SELECT 出来的物化列直接丢弃。现在
decode source_meta 后,对空白(None/纯空白,口径同 fill_source_meta 的
is_blank)的 project/repository 用物化列回炉;JSON 非空值永远是主
源,列只做兜底,不臆造值(列也为空则保持 None)。

附实证(隔离 server + 手插漂移行):修复前过滤 total=2 而漂移行
project=null;修复后漂移行 project='grp/proj',与过滤语义一致;正常行
响应逐字段不变(status/project/repository/result.consolidated.assessment
全保留)。

测试:
- store::rows::tests ×3 — 列回炉 / JSON 主源优先 / 不臆造空值(codec 层
  钉住语义)。
- api::review::tests::list_reviews_db_projection_values_complete — 用真实
  写穿路径(record_task_started → record_task_outcome,即 webhook 所用
  helper)建行,断言 DB 路径列表项的 status/project/repository/branch/
  author/duration/内嵌 assessment 全部带值,并与 0.9 内存路径逐值对齐
  (既有 (a) 只比键集合,project:null 漂移能漏过)。
- api::review::tests::list_reviews_db_drifted_row_projects_materialized_column
  — API 层钉住漂移行:过滤命中什么,投影就显示什么。

注:status 在两层投影里都是非 Option 字符串(来自 reviews.state 列解
码),DB 路径不可能产出缺 status 的列表项;state 解码失败是整列 500
而非字段缺失。
E2E 实测两个静态服务缺陷:

1. 前端 SPA 由裸 ServeDir serve,没有 not-found 回退——直接打开或刷新
   /history、/config 等 history-mode 子路由返回 ServeDir 的裸 404;从根
   路径进站再客户端跳转则正常,所以只有深链接/刷新中招。
2. static_dir() 按 CWD 找 ./frontend/dist,从非仓库根目录启动时静默
   退化成 "Dashboard coming soon" 占位页,无任何日志,排查全靠猜。

修复:

- ServeDir 挂上 axum 回退 handler(get + 闭包):文件不存在时,对「非
  /api/ 前缀、且末段不含扩展名」的 GET 路径重新读盘 serve index.html
  (200 + no-cache, must-revalidate,与 / 的缓存契约一致——它就是
  index.html)。不缓存到内存:原地升级会在 server 运行期间替换 dist,
  内存副本会引用已消失的 hashed chunk(正是 cache-control 修复过的
  白屏缺陷)。
- 用 ServeDir::fallback 而非 not_found_service:后者用 SetStatus 把
  回退响应强制改写为 404,深链接会拿到「200 的 body + 404 的 status」
  (实测复现)。handler 自己按路径裁决:深链接 200,/api/ 与带扩展名
  的文件请求显式 404——未匹配的 API 路由和缺失 hashed asset 的 404
  契约均不变(HTML 200 会把部署事故藏成白屏)。
- static_dir() 两个候选路径都不存在时打 WARN,带当前 CWD 和占位页
  后果说明;回退到占位页的逻辑本身不变。

测试:

- 单测(router.rs mod spa_deep_link):决策函数三组——客户端路由回退、
  /api/ 保持 404、带扩展名文件保持 404。
- 集成测试(tests/server/frontend.rs):tempdir 假 dist + 真实 spawn
  server,GET /history、/config、/reviews/42 均 200 且 body 为
  index.html、带 no-cache;GET /api/v1/definitely-not-a-route 与
  /assets/missing-00000000.js 保持 404。
- cargo test 全绿:lib 1502 passed,各 test target 58/31/47/4
  passed,0 failed;cargo fmt --check 干净。
Formalizes the previously untracked Dockerfile.local as a tracked
contributor-facing build path: in-container Rust builder (macOS arm64
has no cross toolchain), COPY migrations for sqlx::migrate! (0.10.0),
and APT_MIRROR build-arg instead of a hardcoded mirror.
docs/migration-0.10.md covers backup, upgrade paths, the automatic
first-boot migration chain, post-upgrade verification, and
troubleshooting; states plainly that downgrading to 0.9.x is not
supported. CHANGELOG 0.10.0 opens with the same one-way-upgrade
declaration; README and docs index link to the guide.
…-clipping

The project el-tag had no max-width, so slugs wider than the 140px
column were cut off by the cell's overflow:hidden with no ellipsis or
way to read the full name. Cap the tag at the cell width with the same
truncation recipe Element Plus uses internally, add a hover tooltip
with the full slug, widen project/status/score columns slightly, and
give the MR title column back some of its excess min-width.
… parsing (RENG-26)

The lead-overview response was parsed with strict serde_yaml_ng on the raw
LLM output. LLMs commonly wrap the document in a ```yaml fence — a backtick
at line start is a reserved YAML indicator, so the scanner aborts with
'found character that cannot start any token' and the global context was
silently dropped for the whole expert pass.

Parse the response through a layered fallback instead: strict parse, then
parse after stripping code fences and normalizing tab indentation (illegal
in YAML), then parse the first fenced YAML block only. Reuses the shared
output-parser helpers (clean_yaml / extract_first_fenced_yaml) already used
by the adjudicator and verifier. Full parse failure still degrades to no
global context, unchanged.
…exists

The lead-overview context gather treated MRInfo.project_path as a local
filesystem path, but for webhook/API-triggered reviews it is the provider
slug (group/project) and the server never clones the repository. Every
first review of a repo with no local cache logged
"failed to gather project context: Repository path does not exist: <slug>"
and degraded to an empty ProjectContext.

gather_lead_project_context now only invokes the git-backed gatherer when
the path is an existing directory; otherwise (and on gather failure) it
falls back to a partial context built from the reviewed diff's file list,
so first-time reviews still get a real file tree without new network I/O.

RENG-25
…RENG-27)

The History author column always showed the MR creator (e.g. the GitLab
root account 'Administrator') instead of the person who actually wrote
the commits. Author resolution now prefers the head commit's author and
falls back to the MR creator, per the 2026-09-04 decision:

- Webhook parse: object_attributes.last_commit.author.name now wins over
  object_attributes.author.name, then the trigger user.
- GitLab fetch_mr_info: best-effort GET /repository/commits/<head_sha>
  resolves author_name into the new MRInfo.commit_author; any failure
  degrades to None and never fails the review. GitHub path unchanged.
- source_meta_from_mr_info: author_name = commit_author, falling back to
  pr_author; blank commit authors are treated as absent.

No schema change: author_name lives in the reviews.source_meta JSON
column, so existing history rows are untouched and only new records pick
up the fix.
# Conflicts:
#	src/team/orchestrator/pipeline.rs
GET /api/v1/reviews/history returned 400 'Cannot parse task_id' because
no such route exists: the request is captured by /{task_id} and fails
UUID path-parameter validation. Routing is correct (axum 0.8 has no
registration-order issue here) and every in-tree caller — frontend
services/reviews.ts and docs/rest-api.md — already uses GET /reviews,
so there is nothing to reorder or rewire.

Pin the contract instead of expanding the API surface:
- docs/rest-api.md: GET /reviews is documented as the sole history
  list endpoint; GET /reviews/:task_id now documents the 400 for
  non-UUID task_id alongside the existing 404.
- tests/server/reviews.rs: reviews_history_subpath_is_not_a_route
  locks the semantics end-to-end (list 200 envelope; /reviews/history
  400 naming task_id).
The adjudication pass assumed MRInfo.project_path is a local filesystem
path, but for webhook/API-triggered reviews it is the provider slug
(group/project) and the server never clones the repository. Every file
load failed with "not readable from the local checkout" (INFO, one per
file), no finding was actually adjudicated, yet the pipeline summary
still logged "examined N findings".

Adjudicating against the diff patch alone is not a safe substitute: a
unified diff carries only the changed regions ±3 context lines, so the
full-file ground-truth check the pass exists for (defensive code far
from the hunk) is unsatisfiable, and judging against patch-only content
would risk fail-closed drops on missing data. The only verdict safe
under patch-only ground truth is Confirmed, which is behaviorally
identical to keeping the finding.

So skip explicitly instead: when no local checkout directory exists and
there are candidates at or above the threshold, emit one WARN naming the
reason and the number of findings that pass through unadjudicated
(kept unchanged, fail-open), and make no LLM calls. The per-file skip
inside a real checkout (e.g. files deleted by the MR) is elevated from
INFO to WARN with the kept-finding count, and the pipeline summary now
says "candidates" instead of the misleading "examined". CLI local
reviews (real checkout) are unchanged.

Follow-up (not implemented): full adjudication for server-side reviews
does not require cloning — the provider raw-file API (GitLab
/projects/:id/repository/files/:path/raw?ref=<head_sha>) can supply
ground truth on demand, but threading the provider client into the
provider-agnostic team layer is an architecture change of its own.

RENG-25
The status column was 100px wide, but cell padding stacks (16px from
cellStyle on the td plus Element Plus' default 12px on .cell), leaving
only ~44px of content width. The default .cell word-break: break-all
then split "已完成" into "已完/成".

Widen the column to 108px to match the history table (8aa1740), wrap
the badge + label in a flex cell, and apply the same truncation recipe
so over-long labels (e.g. ja "キャンセル済み") ellipsize instead of
wrapping or being hard-clipped.
Comment thread src/server/api/config/persist.rs Fixed
Comment thread src/store/sqlx.rs Fixed
…erministic

CI (Linux) failed with left=...524657Z vs right=...524657367Z: Utc::now()
returns nanoseconds on Linux (clock_gettime) but only microseconds on
macOS (gettimeofday), so the strict equality assertion was platform-flaky.

Root cause is the test, not the store: encode_ts deliberately stores
timestamps as fixed-width RFC 3339 at microsecond precision
(SecondsFormat::Micros) — the documented codec contract shared by the
SQLite and Postgres backends via the Any driver's TEXT path.

Use a deterministic nanosecond timestamp as input, assert the input
carries sub-microsecond digits (so the test can't silently degenerate),
and assert the round-trip equals the micro-truncated value.
CodeQL flags cleartext logging of sensitive information where test
assert failure messages interpolate the api_key variable. The values
are fake keys, but the rule matches the pattern regardless. Assert
conditions are unchanged; messages now describe the expectation
without printing the key value.
The rsa 0.9.10 crate enters Cargo.lock only as an optional dependency of
sqlx-mysql, which sqlx 0.8 locks in regardless of feature activation. The
workspace builds sqlx with default-features = false and no "mysql"
feature, so neither sqlx-mysql nor rsa is ever compiled (verified with
`cargo tree --all-features --target all -i rsa`, which prints nothing,
and by regenerating the lockfile from scratch, where rsa reappears and
therefore cannot be pruned while sqlx remains a dependency).

The Marvin attack requires a chosen-ciphertext oracle against RSA
private-key decryption. review-engine performs no RSA operations at all
(SQLite/PostgreSQL only; no MySQL code path exists), and upstream
provides no patched rsa release, so ignoring is the only available
remediation. The ignore must be revisited if a fixed rsa lands or if
sqlx's mysql feature is ever enabled.
@Liewzheng
Liewzheng merged commit ef8be34 into main Sep 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants