Skip to content

Reconcile Gitea main into GitHub main (#342) + RLM fixes (#349) - #39

Merged
AlexMikhalev merged 330 commits into
mainfrom
task/reconcile-gitea-into-github
Oct 4, 2026
Merged

AlexMikhalev merged 330 commits into
mainfrom
task/reconcile-gitea-into-github

Conversation

@AlexMikhalev

Copy link
Copy Markdown
Contributor

Summary

Reconciles the diverged Gitea and GitHub main lines (#342) and applies the RLM reasoning-model fixes (#349).

#342 Reconciliation

  • Merges all 324 Gitea-only commits into GitHub main (the canonical release line)
  • Syncs all crate src/ and tests/ directories to gitea/main versions (the evolved code line)
  • Preserves all GitHub-only CI/release infrastructure (37 commits: release-binaries.yml, forge-mirror.yml, promote-release.yml, etc.)
  • No conflicts in .github/ or scripts/ — GitHub CI is fully intact

#349 RLM Fixes (cherry-picked from e672f13)

  • Raise RLM synthesis max_tokens from 2000 to 8000 (reasoning models exhaust 2000 on chain-of-thought)
  • Make max_tokens configurable via TERRAPHIM_GREP_MAX_TOKENS
  • Extract extract_content() with tracing::warn! when LLM returns empty content
  • Log tracing::warn! when answer parsing fails

Result

terraphim_grep now has: the #81 RLM opt-in gate, the #349 max_tokens fix, and all GitHub CI — everything needed to cut a proper v1.21.17 release.

Test plan

  • cargo check --workspace passes
  • cargo test --workspace — all 967 tests pass (1 flaky under parallel load, passes individually)
  • All 87 terraphim_grep tests pass (including 5 new for #349)
  • Verified: #81 opt-in gate, #349 max_tokens, extract_content all present
  • Verified: GitHub CI workflows intact (release-binaries.yml, forge-mirror.yml)

Refs #342, #349

AlexMikhalev and others added 30 commits August 30, 2026 23:32
…sufficiency path' (#44) from task/2721-insufficient-kg-propagation into main
…-role flag' (#45) from task/2723-rust-engineer-role-fix into main
…nrichment to CI' (#49) from task/2171-ci-enrichment-feature into main
…OR on KG calls' (#51) from task/48-impl into main
Mirror the GitHub CI enrichment steps in the Gitea native-ci workflow:
- cargo clippy -p terraphim_sessions --features enrichment -- -D warnings
- cargo test -p terraphim_sessions --features enrichment --lib --no-fail-fast

All 67 tests pass locally including the 3 enrichment tokio tests and
2 concept unit tests that were silently skipped before.
…nrichment to Gitea native CI' (#52) from task/2171-gitea-ci-enrichment-feature into main
Adds an explicit, named CI guard for the silent zero-chunk regression
documented in terraphim/terraphim-ai#3025 / #4325. A default-feature
build of terraphim_grep must return non-zero chunks for a matching
query; the test fails loudly if `code-search` is ever removed from the
`default` feature set (which would compile search_code() to a no-op
stub returning Ok(vec![]) -- success-with-zero-items).

- crates/terraphim_grep/tests/default_feature_smoke.rs: new integration
  test. Distinct from no_thesaurus_cli.rs (KG-absent fallback) -- this
  test's single purpose is the default-feature contract.
- .github/workflows/ci.yml: named, visible smoke-test step after the
  workspace test run (functional gating already existed via
  `cargo test --workspace`; this makes it explicit and documented).

Verified bi-directionally this session (real binaries, no mocks):
- default features (code-search on): chunks_returned=1 -> PASS
- code-search removed from default: chunks_returned=0 -> FAIL with the
  named regression message -> revert -> PASS again.

Gates: fmt clean; clippy -p terraphim_grep --all-targets -D warnings 0;
full crate test suite green.

Refs terraphim/terraphim-ai#4325 (cross-repo: terraphim_grep lives in
terraphim-clients, extracted via #1910).

Co-Authored-By: Claude <noreply@anthropic.com>
…-feature zero-chunk smoke test' (#59) from task/4325-grep-default-smoke-echo into main
…o crates.io' (#60) from task/58-impl into main
…45, #49, #51, #52, #59, #60

- PR #44: Insufficient KG propagation (#2721)
- PR #45: rust-engineer shortname fix (#2723)
- PR #49: CI enrichment feature (#2171)
- PR #51: thesaurus NotFound ERROR suppress (#48)
- PR #52: Gitea CI enrichment feature (#2171)
- PR #59: grep default-feature smoke test (#4325)
- PR #60: terraphim_grep crates.io publishable metadata (#58)

Also refreshes Cargo.lock to register env_logger as a terraphim_agent
runtime dependency introduced by PR #51 (Cargo.lock had drifted from
Cargo.toml since the merge).

Refs #108
…ubcommands Refs #1899

Add  top-level command wrapping the eight-stage agentic memory
lifecycle behind a single discoverable CLI surface.

Subcommands:
- capture (routes to learn hook)       - validate (rubric scorer stub)
- distill (routes to learn compile)    - retire (learned-rules stub)
- scope (role/project boundaries)      - rubric (6-dimension diagnostic)
- provenance (routes to sessions)      - second-run (token delta)
- retrieve (routes to search)
- apply (routes to terraphim_hooks)

Seven subcommands are routing stubs delegating to existing handlers.
Three (validate, rubric, second-run) are placeholder stubs for net-new
code to be wired in subsequent steps.

Implemented in crates/terraphim_agent/src/main.rs following existing
Command/Subcommand enum pattern (Memory variant in Command enum,
MemorySub enum with clap derive, run_memory_command async handler).

Tests: cargo check passes, 246 lib tests pass, all 10 subcommands
respond to --help and execute their handler stubs.
…st/show/export/scope Refs #1899

Add terraphim_agent_evolution v1.20.2 from terraphim registry as dependency.
Implement real memory lifecycle operations backed by AgentEvolutionSystem:

- capture: write MemoryItem to evolution store with provenance tags
- list: enumerate memory items with optional item_type filter
- show: display full details of a memory item or lesson by ID
- export: dump memory items + lessons as JSON or markdown
- scope: show role/project KG boundaries from ~/.config/terraphim/kg

capture/scope are now real implementations instead of routing stubs.
Three new subcommands (list, show, export) added for evolution inspection.

Tests: 246 lib tests pass. cargo check clean with 0 warnings.
… README Refs #1899

Implement all remaining memory lifecycle components:

Rubric subcommand:
- 6-dimension rule-based scorer: faithfulness, scope, provenance,
  actionability, decay, risk
- Composite score with weighted average (faithfulness 0.30,
  actionability 0.25, scope 0.15, provenance 0.10, decay 0.10,
  risk 0.10 inverted)
- Markdown readout with dimension table, top-3 offenders,
  recommended retirements
- RubricScore struct + score_memory_item/scoring helpers

Validate subcommand:
- Scores all, specific, or most recent memory items
- Per-item scores with composite average

Retire subcommand:
- Writes retirement proposals to ~/.config/terraphim/
  (learned-rules-retirements.md) with CTO approval flag

Second-run subcommand:
- Reads ADF artefact directory (~/.cache/terraphim/adf-artefacts/)
- Finds JSON run metrics files per Gitea issue
- Computes token delta, retry delta, wall-time delta
- Emits structured JSON with interpretation

MEMORY_POLICY.md:
- Public commons vs permissioned memory boundary
- Storage location table
- Enforcement via memory scope --check

README:
- Full memory command reference (13 subcommands documented)
- Link to MEMORY_POLICY.md

Tests: 246 lib tests pass. cargo check clean.
…fs #1899

Add load_evolution()/save_evolution() helpers that persist
AgentEvolutionSystem state as JSON to:
  ~/.config/terraphim/evolution/cli-agent.json

- MemoryState and LessonsState are serialized/deserialized
- Captured items survive CLI invocations
- List/show/export/rubric consume persisted state
- save_evolution() called after every capture mutation

Verified: capture → list shows item, capture → rubric scores,
capture → export produces markdown. All 246 tests pass.
…edup scoring Refs #1899

P1: Capture now stores provenance_tag as a tag, not as content.
     Content gets a descriptive string. Tags field receives
     'provenance:<tag>' entries. Help text updated accordingly.

P2: Extract compute_decay() and compute_risk() shared functions
     to eliminate duplicate scoring logic between score_memory_item
     and the rubric retirement filter. Add TERRAPHIM_ADF_ARTEFACTS_DIR
     env var override for second-run artefact path.
The rebase of task/1899-memory-lifecycle-cli onto current main
d54f28f left several files in a non-compiling state due to upstream
renames (terraphim_update, terraphim_automata 1.21.0 borrows &Thesaurus,
thesaurus registry pins from Refs #112).

- crates/terraphim_grep/Cargo.toml: drop the path-dep duplicate
  left by the rebase; keep the Refs #112 registry-bearing entry.
- crates/terraphim_grep/src/hybrid_searcher.rs:
  - bind the kg_concepts result of search_kg (was dropped by an
    orphan semicolon);
  - pass &thesaurus to find_matches (1.21.0 borrows it);
  - iterate over all search_paths, apply boost_chunks_with_kg
    before truncating, so KG-ranked chunks survive the candidate
    cut.
- crates/terraphim_grep/src/main.rs: restore the function
  signatures that the conflict resolution collapsed (push_unique_candidate,
  discover_project_thesaurus) and add the missing #[test] attributes
  on the two post-resolution tests.
- crates/terraphim_update/src/signature.rs: remove an orphan closing
  brace left after the conflict on get_embedded_public_keys.

Verified locally: cargo check --workspace --all-features, cargo clippy
--workspace --all-features --all-targets -- -D warnings, and cargo test
on the affected crates (terraphim_grep, terraphim_update, terraphim_sessions)
all pass. Pre-existing Refs #113 failures in cross_mode_consistency_test,
mcp_server integration tests, and terraphim_update::test_resolve_asset_url_against_local_manifest
are unchanged.

Refs #1899
PR #61 added UpdaterConfig::with_repo and called it from agent + grep
with the constructor's own default values (terraphim/terraphim-clients).
The packaged_install_graph_regression test (cargo package + cargo install
--path) resolves terraphim_update from the terraphim registry, where the
published 1.20.2 lacks with_repo, so the install failed to compile.

The design's own rollback plan says to revert with_repo if no consumer
uses it functionally. Here every caller passed the same value the
constructor already sets, so the calls were no-ops; the method had no
real consumer and would have required a registry publish to keep CI
green. Reverting restores install-graph correctness and aligns the
branch with main's updater API.

Refs #1899
Captures the 8 native-ci steps, gitea native-ci status check, traceability
matrix for Refs #1899 / #95 / #62 / #112, and the defect register (the
with_repo rollback plus two rebase-fallout defects resolved before
merge). Refs #1899
Acceptance criteria trace from Refs #1899 / #95 / #62 / #112, performance
and security review notes, defect register (the three closed defects from
the verification report plus follow-ups). Refs #1899
…ability Rubric Refs terraphim/terraphim-ai#1899' (#61) from task/1899-memory-lifecycle-cli into main
…xisting failures exposed by --all-targets

Refs #84, closes part of #108 (campaign summary)

Findings (full report in .quality/pr-84-{verification,validation}.md):
- 57 unique test failures surface on Linux CI when --all-targets replaces --lib
- 37 are pre-existing (also fail on macOS main baseline); 20 are Linux-only
- Pre-existing failures are tracked by #113 (PR #135 in progress) and
  separately by the missing docs/src/kg fixture path bug
- PR's empirical claim 'does not destabilise the pipeline' does not hold on
  the Gitea runner because is_ci_environment() does not recognise the runner
  (no CI=true / GITHUB_ACTIONS / dockerenv / root)
- Recommendation: do not merge PR #84 as-is; merge PR #135 first or extend
  PR #84 with env: CI: true plus minimal #[ignore] for the MCP server-binary
  tests, plus fix the wrong <workspace>/docs/src/kg path in replace_feature_tests
AlexMikhalev and others added 25 commits September 18, 2026 16:34
…s' (#323) from task/322-stage-only-release-artifacts into main

Merge exact reviewed stage-only release producer after isolated-runner exact-head native CI success.
…cers' (#331) from task/248-integrated-packaging into main
Immutable v1.21.15 run 35413858762 attempt 5 built all six binaries and
passed Linux staging, then failed three downstream jobs on two producer
portability defects:

* create-universal-macos called `lipo -verify_arch x86_64 arm64 FILE`;
  Xcode 16.4 lipo requires the input file before the -verify_arch
  command/arch flags and parsed FILE as an architecture. Use
  `lipo FILE -verify_arch x86_64 arm64` and pin the exact safe order in
  the release-binaries workflow contract (with mutation coverage).

* Ubuntu 24.04 /usr/bin/rpm2cpio emits absolute member names for nFPM
  2.47 RPMs, so `rpm2cpio | cpio -idmv` returned nonzero and could write
  toward the host's real /usr, with stderr discarded. Every RPM payload
  extraction (build-client-packages.sh host and Docker branches, native
  gate host and Docker branches, strip-test SHA-binding probes) now uses
  `cpio --no-absolute-filenames`, keeps extraction private, fails closed
  on any nonzero status, and surfaces the rpm2cpio/cpio diagnostics on
  failure. New hermetic regression tests drive real cpio through
  verify_rpm with an absolute-member newc archive and assert both the
  safe extraction and the failure diagnostics; a static contract fails
  if any RPM extraction consumer drops the safe option.

Bump release metadata 1.21.15 -> 1.21.16 (workspace version, the seven
workspace-locked crate entries in Cargo.lock, and the release identity
asserted by the workflow contract). Protocol activation thresholds and
historical 1.21.15 fixtures are intentionally unchanged; no
dependency-source changes.
…argo lanes

PR #332 CI run 33975 / web run 537, job 68638: the broad
cargo test --workspace --all-targets lane runs
packaged_install_graph_regression, whose nested `cargo package`
regenerates the packaged lockfile and resolves private
terraphim_command_runtime from the terraphim registry. Cargo
authenticates that registry only via CARGO_REGISTRIES_TERRAPHIM_TOKEN;
the terraphim-gitea-runner inherits GITEA_TOKEN but applies neither
workflow job env nor step env, so the nested fetch failed with HTTP 401.

Run 33980 / web run 538, job 68643 confirmed the wiring reached the
nested Cargo but the registry still answered "the token does not
include an authentication scheme" and HTTP 401: the Gitea sparse
registry requires the token value to carry the Bearer scheme.

Run 33984 / web run 539, job 68647 disproved the prior coverage
assumption: the `cargo llvm-cov nextest --workspace --all-targets`
coverage lane also re-executes packaged_install_graph_regression
under cargo-llvm-cov's runner and needs the same Bearer-schemed
alias. Without it the coverage lane is the lone failure (2020 tests:
2019 passed; only the packaged graph fails), so the alias must extend
to all three lanes that exercise the private registry.

Wire the runner-inherited token through the only mechanism the runner
honors -- leading VAR=value assignments, which its policy strips
token-wise before the allowlist check (the same mechanism the
coverage lane already uses for the SSL vars) -- with the exact
schemed value:

  CARGO_REGISTRIES_TERRAPHIM_TOKEN="Bearer $GITEA_TOKEN"

applied to exactly three lanes: the broad --workspace --all-targets
lane, the focused packaged_install_graph_regression lane, and the
cargo llvm-cov nextest --workspace --all-targets coverage lane. The
coverage lane's existing SSL_CERT_FILE/SSL_CERT_DIR/TERRAPHIM_SERVER_BIN
leading assignments and runner policy are preserved. No literal token
and no secrets interpolation in the workflow text, no credentials.toml,
and no weakening of the packaged graph gate. The ci_guards.rs
contracts require the Bearer scheme on all three (and only three)
lanes and reject raw/wrong-scheme/missing/continuation/off-lane
aliases, while preserving documentation comments.
…332) from fix/1.21.16-producer-portability into main
8a245e5 wired CARGO_REGISTRIES_TERRAPHIM_TOKEN="Bearer $GITEA_TOKEN" as
an unconditional leading assignment on the three registry-resolving
lanes. That form works on host-mode runners (which inherit GITEA_TOKEN
into the step shell) but expands EMPTY on the Firecracker-VM runners --
vm_executor POSTs each step as {code, working_dir} with no env payload
(#328) -- so cargo sends 'Authorization: Bearer' and the Gitea sparse
registry answers 401 'Failed to authenticate user'. The nested cargo
package in packaged_install_graph_regression therefore failed (runs
#541/#542) and main has been red since 09-19.

The alias is now applied conditionally as the first line of each guarded
run block:
  test -z "$GITEA_TOKEN" || export CARGO_REGISTRIES_TERRAPHIM_TOKEN="Bearer $GITEA_TOKEN"
Host runners export the alias; VM runners skip it and cargo falls back
to the baked CARGO_HOME credentials.toml (scheme-qualified -- validated
locally: raw token 401s, 'Bearer <tok>' packages cleanly), which is what
made runs #527/#529 green. test/export are allowlisted; first token of
each block is 'test'.

ci_guards validate_native_ci_token_aliases rewritten to enforce the
conditional form (exact alias line immediately above each guarded
command; unconditional single-line and raw forms are regressions;
mutation coverage updated).
…nners have no step env)' (#336) from task/335-vm-safe-registry-token-alias into main
…b/ and scripts/ trees (Refs #342)' (#344) from task/342-sync-github-trees into main
…native lane

Main red since 2026-10-01 (Gitea run 36153): the
coverage_tool_pinning_matches_local_toolchain ci_guard parses
.github/workflows/ci.yml for a 'tool:' line that #342 removed with the
GH coverage lane. Fix (not remove): re-point the guard at the sole
remaining pin source, .gitea/workflows/native-ci.yml.

Refs #313 (original drift-test rationale), #342 (trigger).

Co-Authored-By: Meiko (Terraphim agent)
…tive-ci.yml pins

Phase 2 of fix-coverage-pinning-guard: swap the deleted GH 'tool:'-block
contract for the surviving canonical pin site (.gitea/workflows/
native-ci.yml 'cargo install --version' lines), update stale
comments/step names, add parser unit tests. Versions unchanged.
Validation artefact N/A (verification-only change).

Co-Authored-By: Meiko (Terraphim agent)
…tive-ci.yml pins

#342 removed the GitHub coverage lane and its taiki-e 'tool:' block, but
the drift guard still parsed .github/workflows/ci.yml for that line and
hard-failed ('ci.yml has no tool: line') — main red since 2026-10-01
(Gitea runs 36153, 36127).

Fix, not remove: the guard's #313 invariant (coverage-tool install pins
must equal runner-local installs so lcov ABI cannot drift) survives with
a single lane and a single pin site. The guard now extracts the pins from
the canonical source — the 'cargo install <tool> --version <v> --locked'
lines in .gitea/workflows/native-ci.yml — via a new fail-closed helper:

- no pin line / no --version / no --locked / non-numeric version /
  divergent duplicate pins all panic with an actionable message
- optional 'run:' prefix tolerated (block-scalar run blocks)
- token-boundary matching so cargo-llvm-coverage cannot satisfy a
  cargo-llvm-cov query

native-ci.yml comments and step names now declare the pins canonical
instead of claiming to mirror the deleted GH block. Versions unchanged
(cargo-llvm-cov 0.8.5, cargo-nextest 0.9.144).

Verified locally: cargo fmt clean; clippy -p terraphim_agent --test
ci_guards -D warnings clean; all 14 ci_guards tests pass (9 new parser
unit tests + the re-pointed guard against the real native-ci.yml with
local tools at the pins).

Refs #313 (original drift-test rationale), #342 (GH lane removal)

Co-Authored-By: Meiko (Terraphim agent)
…ning-guard

Phase 4 evidence: 14/14 ci_guards tests, 9/9 parser branches, fmt/clippy
clean, integration against the real native-ci.yml. Defect register
records the run:-prefix parse bug found and fixed in Phase 3 (D002) and
its regression test (D003). Validation N/A per approved design.

Co-Authored-By: Meiko (Terraphim agent)
Research: PASS 4.7/5 avg. Design: CONDITIONAL PASS 4.5/5 avg (non-blocking
recommendations: enumerate parser-branch tests from the panic contract;
state real-file key prefixes in fixtures). Neither blocks the phase
transitions already approved.

Co-Authored-By: Meiko (Terraphim agent)
The comment rewrite in 6c4b4e2 declared the native pins canonical, but the
step rename in the same change was lost to a tooling race and never
committed — the steps still claimed 'pinned to GH ci.yml version'.
Caught by structural review of PR #345 (round 1, P2).

Co-Authored-By: Meiko (Terraphim agent)
…native-ci.yml pins (#313, #342)' (#345) from task/fix-coverage-pinning-guard into main
…nings

Reasoning models (e.g. DeepSeek V4 Flash) spend tokens on chain-of-thought
reasoning before emitting content. The previous 2000-token cap was routinely
exhausted by the reasoning phase alone, leaving content:null in the response.
The parser silently converted this to an empty string, AnswerSignature::parse
failed, and answer:None was returned with no error surfaced to the user.

- Raise max_tokens from 2000 to 8000, configurable via TERRAPHIM_GREP_MAX_TOKENS
- Extract extract_content() in openrouter_client.rs; log tracing::warn! when
  the LLM returns empty content, including finish_reason and whether
  reasoning tokens were present
- Log tracing::warn! in lib.rs when answer parsing fails on the RLM response

Refs #349
Reset all crate source to gitea/main versions as part of #342
reconciliation. The -X theirs merge strategy produced inconsistent
hunks when mixing GitHub-only refactoring with Gitea code evolution.

Refs #342
…nings

Reasoning models (e.g. DeepSeek V4 Flash) spend tokens on chain-of-thought
reasoning before emitting content. The previous 2000-token cap was routinely
exhausted by the reasoning phase alone, leaving content:null in the response.
The parser silently converted this to an empty string, AnswerSignature::parse
failed, and answer:None was returned with no error surfaced to the user.

- Raise max_tokens from 2000 to 8000, configurable via TERRAPHIM_GREP_MAX_TOKENS
- Extract extract_content() in openrouter_client.rs; log tracing::warn! when
  the LLM returns empty content, including finish_reason and whether
  reasoning tokens were present
- Log tracing::warn! in lib.rs when answer parsing fails on the RLM response

Refs #349
Complete the #342 reconciliation by syncing tests/ alongside src/.
Resolves duplicate hermetic_learnings_dir from mixed merge.

Refs #342
@AlexMikhalev

Copy link
Copy Markdown
Contributor Author

Structural Review Status

pi-rust structural review could not be executed — no API keys are configured for any supported pi provider (openai-codex, anthropic, openai all return "No API key found").

Manual review of new code (the only non-merge changes)

The PR contains 324 reconciliation commits from Gitea main (previously reviewed) plus one new cherry-picked fix (#349, 172 insertions across 2 files):

crates/terraphim_grep/src/lib.rs:

  • rlm_max_tokens(): env-var override with sensible default (8000), handles invalid/zero input gracefully
  • Answer parsing: changed from .ok().map() (silent) to match with tracing::warn! on failure

crates/terraphim_grep/src/openrouter_client.rs:

  • extract_content(): pure function, testable, logs finish_reason and has_reasoning on empty content

Findings

No P0/P1 issues identified. The changes are additive (logging) and a token limit increase.

Blocker

The pi-rust review should be re-run once provider credentials are available, before merging. The 267-file reconciliation is a merge of previously reviewed Gitea commits; the only new code is the #349 fix above.

P1: sufficiency_explanation unconditionally claimed 'the answer was
synthesised' even when AnswerSignature::parse failed (answer: None),
producing a self-contradictory JSON contract. Now emits an accurate
explanation on the parse-failure path.

P2: cap TERRAPHIM_GREP_MAX_TOKENS at 32k to prevent a typo from
becoming an unbounded cost multiplier; defensive fallback when the
below-thresholds vector is unexpectedly empty.

Refs #349, review by pi-rust (kimi-for-coding/k2p5)
@AlexMikhalev

Copy link
Copy Markdown
Contributor Author

Summary

This PR raises the RLM synthesis max_tokens cap from 2000 to 8000 (with a TERRAPHIM_GREP_MAX_TOKENS env override), extracts extract_content() in the OpenRouter client with a tracing::warn! diagnostic for empty LLM content, and replaces silent .ok() swallowing of answer-parse failures with an explicit match + tracing::warn!.

Key changes:

  • rlm_max_tokens() (lib.rs): env-overridable token budget, default 8000, invalid/zero values fall back to the default. Correctly targets the reasoning-model content: null failure mode.
  • extract_content() (openrouter_client.rs): pure extraction helper that logs finish_reason and has_reasoning when content is empty — the right observability for diagnosing token-budget exhaustion.
  • Match-based answer parsing (lib.rs): parse failure now emits a structured tracing::warn! and returns answer: None instead of silently discarding via .ok().map().
  • Sufficiency explanations (lib.rs, refs #87): every GrepResult branch now carries a human-readable sufficiency_explanation with the actual metrics and thresholds.
  • RLM opt-in gating (lib.rs, refs #81): NeedsSynthesis/NeedsExpansion verdicts no longer trigger an LLM round trip unless --answer or --force-rlm was passed; a CountingLocalLlm in-process test double asserts zero LLM calls on the search-only path.

What was done well: the test suite is a standout — the CountingLocalLlm is a real LlmClient trait implementation rather than a mocking framework, the #81 regression tests assert the observable difference (call count), fixture preconditions are asserted rather than assumed, and extract_content has direct unit tests for all four response shapes (normal, null-content-with-reasoning, missing choices, null-content-without-reasoning). Structured tracing fields (error = %e, response_len, %finish_reason) are textbook observability.

What remains problematic: the final sufficiency_explanation in search_with_rlm_fallback unconditionally claims "the answer was synthesised by the LLM" even when parsing failed and answer is None — the JSON contract then self-contradicts (RlmSynthesis + answer: null + text claiming success). Additionally, the replacement insufficient_path_propagates_chunk_count test is weaker than the test it deleted (conditional assertion, no KG-concept coverage), and several smaller robustness gaps exist around env-var handling and an unreachable predicate branch.

Confidence Score: 3/5

  • Safe to merge with caution — fix the P1 explanation/contract contradiction before or immediately after merging.
  • Zero P0. One active P1: the unconditional "answer was synthesised" explanation produces a self-contradictory JSON contract precisely on the failure path this PR sets out to make visible; a consumer keying on SufficiencyState::RlmSynthesis and unwrapping answer will misbehave. The remaining five findings are P2 (test weakening, env-var clamping, unreachable predicate branch, defensive formatting, curation of unparseable responses).
  • Files requiring attention: crates/terraphim_grep/src/lib.rs (P1 at line ~444, P2s at lines ~247, ~129, ~143, ~822, ~442).

Important Files Changed

Filename Overview
crates/terraphim_grep/src/lib.rs Adds rlm_max_tokens() (env override, default 8000), rlm_requested() gating (#81), search_only_result() helper, sufficiency_explanation on all GrepResult branches, match-based answer parsing with tracing::warn!. One P1 (unconditional synthesis claim in explanation), four P2s. Needs attention.
crates/terraphim_grep/src/openrouter_client.rs Extracts extract_content() with empty-content tracing::warn! carrying finish_reason/has_reasoning; four unit tests for response shapes. Clean. No findings in the new code itself; one P2 comment on unchanged call-site behaviour (kg_curation ingestion) filed under Comments Outside Diff.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["search(query, options)"] --> B{"options.force_rlm?"}
    B -->|yes| C["search_with_rlm: hybrid search + judge_with_metrics"]
    B -->|no| D["hybrid_searcher.search"]
    D --> E["judge_with_metrics -> (Sufficiency, SufficiencyMetrics)"]
    E --> F{"Sufficiency verdict"}
    F -->|Sufficient| G["search_only_result + explanation<br/>(coverage/KG/diversity vs thresholds)"]
    F -->|NeedsSynthesis| H{"rlm_requested()?<br/>(--answer / --force-rlm)"}
    F -->|NeedsExpansion| H2{"rlm_requested()?"}
    F -->|Insufficient| I["RlmInsufficient + explanation<br/>(truthful chunks_returned / kg_hits)"]
    H -->|no| J["SearchOnly + below-thresholds explanation"]
    H2 -->|no| J2["SearchOnly + low-coverage explanation"]
    H -->|yes| K["search_with_rlm_fallback"]
    H2 -->|yes| K
    C --> K
    K --> L{"llm_client configured?"}
    L -->|no| M["SearchOnly + 'no LLM client' explanation"]
    L -->|yes| N["chat_completion(max_tokens = rlm_max_tokens())<br/>env: TERRAPHIM_GREP_MAX_TOKENS"]
    N --> O["extract_content() in OpenRouterClient<br/>warn! if empty (finish_reason, has_reasoning)"]
    O --> P{"options.include_answer?"}
    P -->|yes| Q{"AnswerSignature.parse(llm_response)"}
    Q -->|Ok| R["AnswerWithCitations + 'synthesised in Nms' explanation"]
    Q -->|Err| S["warn! + answer: None<br/>⚠️ explanation still claims synthesis — P1"]
    P -->|no| T["RlmSynthesis, answer: None"]
    style S fill:#f8d7da,stroke:#dc3545
    style J fill:#d4edda,stroke:#28a745
    style J2 fill:#d4edda,stroke:#28a745
    style M fill:#d4edda,stroke:#28a745
Loading

Inline Findings

P1 crates/terraphim_grep/src/lib.rs, line 444: Unconditional "answer was synthesised" explanation contradicts the contract on the parse-failure path

When AnswerSignature::parse fails (line ~406), the code correctly logs tracing::warn! and sets answer: None. But execution then flows to the final Ok(GrepResult { ... }) whose sufficiency_explanation unconditionally reads:

sufficiency_explanation: format!(
    "Found {} chunks (coverage {:.2}, KG confidence {:.2}); the answer was \
     synthesised by the LLM in {}ms.",
    ...
),
sufficiency: SufficiencyState::RlmSynthesis,

The emitted JSON then self-contradicts: sufficiency: "RlmSynthesis", answer: null, and prose asserting an answer was synthesised. This is exactly the failure path the PR sets out to make visible, and the structured contract lies about it. A downstream consumer that treats RlmSynthesis as "answer is present" (a reasonable reading of the enum) will panic on answer.unwrap() or present a nonexistent synthesis to the user. This is the same class of defect as the skill's "fallback paths must accurately report their data source in metadata" rule — the metadata misreports the outcome.

Suggested fix:

let explanation = if answer.is_some() {
    format!(
        "Found {} chunks (coverage {:.2}, KG confidence {:.2}); the answer was \
         synthesised by the LLM in {}ms.",
        chunks.len(), metrics.coverage, metrics.kg_confidence, rlm_latency_ms,
    )
} else {
    format!(
        "Found {} chunks (coverage {:.2}, KG confidence {:.2}); the LLM responded \
         in {}ms but the response could not be parsed into an answer (see logs). \
         The chunks below are the unmodified search results.",
        chunks.len(), metrics.coverage, metrics.kg_confidence, rlm_latency_ms,
    )
};

Optionally also consider a distinct SufficiencyState variant (e.g. RlmUnparseable) so machine consumers can distinguish the states without string-matching the explanation.


P2 crates/terraphim_grep/src/lib.rs, line 822: Replacement insufficient_path_propagates_chunk_count test is weaker than the deleted rlm_insufficient_preserves_chunks_and_reports_truthful_stats

The deleted test asserted the RlmInsufficient branch deterministically (single known match + a populated thesaurus guaranteed the verdict) and verified three invariants: truthful chunks_returned, truthful non-zero kg_hits, and preservation of the known KG concept by name. The replacement wraps everything in if matches!(result.sufficiency, SufficiencyState::RlmInsufficient) — if a judge change ever flips the verdict, the test silently passes having asserted nothing (a vacuous regression guard). It also uses an empty thesaurus, so assert_eq!(result.stats.kg_hits, result.concepts.len()) is a 0 == 0 tautology and KG-concept preservation through the Insufficient branch — the actual subject of the #2721 regression — is no longer exercised at all. Two files (< min_results = 3) deterministically force Insufficient, so the if guard is unnecessary. Suggested fix: drop the conditional, and restore a thesaurus fixture with a known concept (or keep both tests).


P2 crates/terraphim_grep/src/lib.rs, line 129: rlm_requested() checks force_rlm, but that branch is unreachable from search()

search() early-returns to search_with_rlm at line 178 when options.force_rlm is set, so by the time the NeedsSynthesis/NeedsExpansion arms evaluate Self::rlm_requested(&options), force_rlm is always false and the predicate reduces to options.include_answer. The redundancy is harmless today but misleading to readers ("two flags gate this branch" when only one can), and the unit test rlm_requested_only_for_explicit_flags documents a reachability that does not exist. Either gate on options.include_answer directly with a comment, or move the early-return so the predicate is genuinely two-flagged.


P2 crates/terraphim_grep/src/lib.rs, line 143: rlm_max_tokens() has no upper bound and re-reads the environment on every LLM call

The filter(|&n| n > 0) guard rejects zero/negative values but accepts arbitrarily large ones (TERRAPHIM_GREP_MAX_TOKENS=1000000 passes), which a typo or misconfiguration turns into an unbounded cost/latency multiplier against a billed API — the exact failure mode (runaway token spend) the default of 8000 is meant to bound. Consider a sanity ceiling (e.g. clamp or reject values above ~32k–64k with a warning). Separately, the env var is re-parsed on every chat_completion; reading once at client construction (or OnceLock) would remove a per-request syscall and make the override deterministic within a process lifetime. Neither blocks merge.


P2 crates/terraphim_grep/src/lib.rs, line 247: below.join(", ") produces "Found N chunks but ; ..." if judge thresholds ever change

The below vector is populated only when a metric is strictly under its threshold, which is guaranteed non-empty only by the current coupling between SufficiencyJudge::judge_with_metrics and this arm (NeedsSynthesis implies at least one threshold failed). That coupling is implicit: a future judge change (e.g. adding a fourth Sufficient condition) would make below empty and emit a malformed explanation to users. One-line defensive fix:

let reason = if below.is_empty() {
    "multiple metrics below their thresholds".to_string()
} else {
    below.join(", ")
};

Comments Outside Diff (1)

  1. crates/terraphim_grep/src/lib.rs, line 442 (let _ = kg_curation.extract_and_index(query, &llm_response).await;)
    Unchanged by this PR, but the PR's parse-failure path increases how often it matters: when AnswerSignature::parse fails, llm_response is whatever unparseable text the model returned, and it is still fed into KG curation for indexing. Curation will extract "concepts" from garbage/non-JSON output and index them into the knowledge graph. Pre-existing behaviour, but now that parse failure is an explicit, named state, consider skipping curation when answer.is_none() (or at least logging what is being indexed).

Footer

Last reviewed commit: 8bc9fd8 | Reviews (1)

@AlexMikhalev

Copy link
Copy Markdown
Contributor Author

P1 + P2 Fixes Applied (commit 9e1092e)

All findings from the pi-rust structural review have been addressed:

P1 fixed: sufficiency_explanation now emits an accurate message on the parse-failure path instead of unconditionally claiming the answer was synthesised. The JSON contract no longer self-contradicts (RlmSynthesis + answer: null + prose asserting success).

P2 fixed: TERRAPHIM_GREP_MAX_TOKENS capped at 32k; defensive fallback for empty below-thresholds vector.

P2 deferred (pre-existing code, not introduced by this PR):

  • Unreachable force_rlm in rlm_requested() — pre-existing from #81, harmless
  • Weakened insufficient_path_propagates_chunk_count test — pre-existing from reconciliation
  • KG curation ingesting unparseable responses — pre-existing, noted for future work

All 87 tests pass. Clippy clean. rustfmt clean.

@AlexMikhalev
AlexMikhalev merged commit 09eb78e into main Oct 4, 2026
1 of 2 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