Skip to content

Commit 48f8754

Browse files
Your Nameclaude
andcommitted
fix(scip): ruby encoding fallback + surface swallowed nightly failures
- ruby was missing from effective_encoding's spec-convention fallback table added by D4 (7672f52): every scip-ruby occurrence had no byte span, so ingest_occurrences_with_proof_context's identity_version >= 2 gate never matched a row and ruby_overlay_upgrades_ambiguous_case_when_calls_on_the_multi_lang_fixture failed 6 nightly runs straight (2026-08-01 through 08-07). Verified against scip-ruby's real source (SCIPIndexer.cc sets text_document_encoding=UTF8 but never calls set_position_encoding anywhere in the repo) and end-to-end against the real pinned scip-ruby-v0.4.7 binary -- the ignored test now passes for real, not just at the unit level. - cargo test's lib harness never installed a tracing subscriber, so run_overlay_for_with_catalog's tracing::warn! on a real indexer subprocess failure -- already carrying a real stderr tail from runner.rs's run_indexer -- was a complete no-op. Nightly python/js/etc. failures showed only the generic "expected at least one edge upgraded to formal" with zero diagnostic value, which is why the python/js regression (unrelated to the ruby one, still under investigation) went unnoticed for weeks. Added init_test_tracing() (with_test_writer, so output only surfaces on a failing test like println! does) to all 11 real-indexer nightly tests plus the calm-cli CLI-level mirror test. Verified by forcing a real 1ms timeout locally: the real subprocess-failure reason now prints even without --nocapture, matching exactly how the nightly workflow invokes cargo test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0ab9bca commit 48f8754

5 files changed

Lines changed: 123 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/calm-cli/tests/scip_overlay_cli.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,12 @@ fn calm_index_cli_upgrades_a_real_edge_on_the_fixture() {
6868
)
6969
.unwrap();
7070
assert_eq!(
71-
conf, "formal",
71+
conf,
72+
"formal",
7273
"expected the CLI's one-shot `calm index` to run the SCIP overlay \
73-
and upgrade this edge to formal, same as the background indexer does"
74+
and upgrade this edge to formal, same as the background indexer does \
75+
-- child stdout={}\nchild stderr={}",
76+
String::from_utf8_lossy(&output.stdout),
77+
String::from_utf8_lossy(&output.stderr)
7478
);
7579
}

crates/calm-core/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ tokio = { workspace = true, optional = true }
104104
[dev-dependencies]
105105
tempfile = "3"
106106
proptest = "1"
107+
# Only for the nightly real-indexer `#[ignore]`d tests in scip/mod.rs --
108+
# without a subscriber installed, cargo test's lib harness leaves every
109+
# `tracing::warn!`/`info!` in the whole crate a silent no-op, which is
110+
# exactly how run_overlay_for_with_catalog's real subprocess-failure reason
111+
# (already captured with a real stderr tail by runner.rs's `run_indexer`)
112+
# was vanishing from nightly CI logs instead of explaining why e.g.
113+
# scip-python/scip-js failed. See `init_test_tracing` in scip/mod.rs.
114+
tracing-subscriber = { workspace = true }
107115

108116
[features]
109117
# Semantic embeddings: pure-Rust static code embeddings (model2vec-rs).

crates/calm-core/src/scip/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,32 @@ pub struct OverlayStatus {
979979
mod tests {
980980
use super::*;
981981

982+
/// Call at the top of every nightly real-indexer `#[ignore]`d test below.
983+
/// `cargo test`'s lib harness installs no `tracing` subscriber by
984+
/// default (only `calm-cli`'s/`calm-server`'s real `main()` does), so
985+
/// every `tracing::warn!`/`info!` in the whole crate is normally a
986+
/// silent no-op during tests -- including `run_overlay_for_with_catalog`'s
987+
/// `tracing::warn!("SCIP overlay ({}) run failed, keeping syntactic
988+
/// graph: {e}", ...)`, whose `{e}` already carries a real stderr tail
989+
/// from the failed subprocess (`runner.rs`'s `run_indexer`). Without this,
990+
/// that real reason vanishes and a nightly CI failure shows only the
991+
/// generic `assert!(stats.upgraded > 0)` message with zero diagnostic
992+
/// value -- root-caused 2026-08-07 chasing scip-python/scip-js nightly
993+
/// failures that reproduced on a live GitHub Actions re-run but not
994+
/// locally, with no way to see why. `with_test_writer()` routes output
995+
/// through cargo test's own per-test capture (shown only on failure,
996+
/// like `println!`); `try_init().ok()` makes repeat calls from multiple
997+
/// tests in the same process safe (first one wins, rest are no-ops).
998+
fn init_test_tracing() {
999+
let _ = tracing_subscriber::fmt()
1000+
.with_test_writer()
1001+
.with_env_filter(
1002+
tracing_subscriber::EnvFilter::try_from_default_env()
1003+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
1004+
)
1005+
.try_init();
1006+
}
1007+
9821008
#[test]
9831009
fn provider_cache_key_tracks_shared_typescript_context_without_cross_language_churn() {
9841010
let root = tempfile::tempdir().unwrap();
@@ -1365,6 +1391,7 @@ mod tests {
13651391
#[test]
13661392
#[ignore]
13671393
fn overlay_upgrades_a_real_edge_on_the_fixture() {
1394+
init_test_tracing();
13681395
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
13691396
.join("tests/fixtures/rust_workspace");
13701397
let mut conn = Connection::open_in_memory().unwrap();
@@ -1409,6 +1436,7 @@ mod tests {
14091436
#[test]
14101437
#[ignore]
14111438
fn go_overlay_upgrades_a_real_edge_on_the_multi_lang_fixture() {
1439+
init_test_tracing();
14121440
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
14131441
.join("tests/fixtures/multi_lang_workspace/go");
14141442
let mut conn = Connection::open_in_memory().unwrap();
@@ -1557,6 +1585,7 @@ mod tests {
15571585
#[test]
15581586
#[ignore]
15591587
fn go_workspace_overlay_upgrades_edges_in_both_member_modules() {
1588+
init_test_tracing();
15601589
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
15611590
.join("tests/fixtures/go_workspace");
15621591
let mut conn = Connection::open_in_memory().unwrap();
@@ -1606,6 +1635,7 @@ mod tests {
16061635
#[test]
16071636
#[ignore]
16081637
fn python_overlay_upgrades_a_real_edge_on_the_multi_lang_fixture() {
1638+
init_test_tracing();
16091639
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
16101640
.join("tests/fixtures/multi_lang_workspace/python");
16111641
let mut conn = Connection::open_in_memory().unwrap();
@@ -1647,6 +1677,7 @@ mod tests {
16471677
#[test]
16481678
#[ignore]
16491679
fn js_overlay_upgrades_a_real_edge_on_the_multi_lang_fixture() {
1680+
init_test_tracing();
16501681
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
16511682
.join("tests/fixtures/multi_lang_workspace/js");
16521683
let mut conn = Connection::open_in_memory().unwrap();
@@ -1692,6 +1723,7 @@ mod tests {
16921723
#[test]
16931724
#[ignore]
16941725
fn java_overlay_upgrades_a_real_edge_on_the_multi_lang_fixture() {
1726+
init_test_tracing();
16951727
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
16961728
.join("tests/fixtures/multi_lang_workspace/java");
16971729
let mut conn = Connection::open_in_memory().unwrap();
@@ -1753,6 +1785,7 @@ mod tests {
17531785
#[test]
17541786
#[ignore]
17551787
fn kotlin_overlay_upgrades_ambiguous_smart_cast_calls_on_the_multi_lang_fixture() {
1788+
init_test_tracing();
17561789
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
17571790
.join("tests/fixtures/multi_lang_workspace/kotlin");
17581791
let mut conn = Connection::open_in_memory().unwrap();
@@ -1876,6 +1909,7 @@ mod tests {
18761909
#[test]
18771910
#[ignore]
18781911
fn ruby_overlay_upgrades_ambiguous_case_when_calls_on_the_multi_lang_fixture() {
1912+
init_test_tracing();
18791913
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
18801914
.join("tests/fixtures/multi_lang_workspace/ruby");
18811915
let mut conn = Connection::open_in_memory().unwrap();
@@ -1979,6 +2013,7 @@ mod tests {
19792013
#[test]
19802014
#[ignore]
19812015
fn csharp_overlay_upgrades_a_real_edge_on_the_multi_lang_fixture() {
2016+
init_test_tracing();
19822017
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
19832018
.join("tests/fixtures/multi_lang_workspace/csharp");
19842019
let mut conn = Connection::open_in_memory().unwrap();
@@ -2047,6 +2082,7 @@ mod tests {
20472082
#[test]
20482083
#[ignore]
20492084
fn php_overlay_upgrades_a_real_edge_on_the_multi_lang_fixture() {
2085+
init_test_tracing();
20502086
let fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
20512087
.join("tests/fixtures/multi_lang_workspace/php");
20522088
// Composer-managed `vendor/` is gitignored and not committed (see
@@ -2132,6 +2168,7 @@ mod tests {
21322168
#[test]
21332169
#[ignore]
21342170
fn clang_overlay_upgrades_a_real_edge_on_the_c_fixture() {
2171+
init_test_tracing();
21352172
let src_fixture = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
21362173
.join("tests/fixtures/multi_lang_workspace/c");
21372174
let tmp = tempfile::tempdir().unwrap();

crates/calm-core/src/scip/parse.rs

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,51 @@ pub enum EncodingProvenance {
1111
/// The SCIP document declared a non-`Unspecified` encoding — the
1212
/// producing indexer followed the protocol.
1313
Declared,
14-
/// The document left `position_encoding` unset; this provider is one of
15-
/// the languages SCIP's own spec (`scip.proto`'s
16-
/// `Document.position_encoding` doc comment) documents a convention for,
17-
/// so that convention was applied as a fallback instead of failing
18-
/// closed. See `effective_encoding`.
14+
/// The document left `position_encoding` unset; this provider is one
15+
/// CALM has real evidence for (either SCIP's own documented spec
16+
/// convention, or a specific indexer's real source verified by hand),
17+
/// so a guessed encoding was applied as a fallback instead of failing
18+
/// closed. See `effective_encoding` for exactly which providers and
19+
/// which kind of evidence backs each.
1920
Fallback,
2021
/// The document left `position_encoding` unset and no fallback applies
2122
/// (unknown/generic provider) — `start_byte`/`end_byte` stay `None`.
2223
Unresolved,
2324
}
2425

25-
/// SCIP's own documented convention for indexers that omit
26-
/// `Document.position_encoding` (`scip-0.9.0/src/generated/scip.rs`'s doc
27-
/// comment on that field, itself a transcription of `scip.proto`): JVM/.NET/
28-
/// JS/TS indexers should emit UTF-16, Python UTF-32, Go/Rust/C++ UTF-8. This
29-
/// is an evidence CONTRACT scoped to exactly those verified languages, never
30-
/// a global fallback — a provider outside this table keeps failing closed,
31-
/// including any future/unknown provider. A `declared` encoding (anything
32-
/// other than `Unspecified`) always wins outright: the indexer's own
33-
/// self-report has protocol precedence over a guess derived from its
34-
/// implementation language.
26+
/// Fallback position-encoding table for indexers that omit
27+
/// `Document.position_encoding`. This is an evidence CONTRACT — a provider
28+
/// outside this table keeps failing closed, including any future/unknown
29+
/// provider — but it's backed by two distinct kinds of evidence, both
30+
/// treated identically at runtime (guessed, not declared; see
31+
/// `EncodingProvenance::Fallback` and its defense-in-depth cross-check in
32+
/// `parse_index`'s `guessed_alt_byte_range`):
33+
///
34+
/// - `python` / `javascript`+`java`+`csharp` / `rust`+`go`+`c`: SCIP's own
35+
/// documented convention (`scip-0.9.0/src/generated/scip.rs`'s doc
36+
/// comment on `Document.position_encoding`, itself a transcription of
37+
/// `scip.proto`): Python -> UTF-32, JVM/.NET/JS/TS -> UTF-16,
38+
/// Go/Rust/C++ -> UTF-8.
39+
/// - `ruby`: NOT part of that spec table (see
40+
/// `effective_encoding_unknown_or_unverified_provider_stays_fail_closed`
41+
/// for what still fails closed), but independently verified against
42+
/// `scip-ruby`'s real source (github.com/sourcegraph/scip-ruby,
43+
/// `scip_indexer/SCIPIndexer.cc`): it sets
44+
/// `Metadata.text_document_encoding` to UTF-8 but never calls
45+
/// `set_position_encoding` anywhere in the codebase (0 hits repo-wide,
46+
/// verified via GitHub code search 2026-08-07) — Sorbet's underlying
47+
/// `core::Loc`/`Range` machinery is byte-oriented like the C/C++-family
48+
/// indexers above, so UTF-8 is a verified guess, not a spec-table one.
49+
/// Root-caused from the nightly regression in
50+
/// `ruby_overlay_upgrades_ambiguous_case_when_calls_on_the_multi_lang_
51+
/// fixture` that started at D4 (7672f52, 2026-08-01): before this entry,
52+
/// every scip-ruby occurrence had no byte span at all (`Unresolved`), so
53+
/// `IngestStats::upgraded` stayed 0.
54+
///
55+
/// A `declared` encoding (anything other than `Unspecified`) always wins
56+
/// outright regardless of the above: the indexer's own self-report has
57+
/// protocol precedence over any guess derived from its implementation
58+
/// language.
3559
fn effective_encoding(
3660
declared: scip::types::PositionEncoding,
3761
provider_lang: &str,
@@ -45,7 +69,11 @@ fn effective_encoding(
4569
"javascript" | "java" | "csharp" => {
4670
Some(PositionEncoding::UTF16CodeUnitOffsetFromLineStart)
4771
}
72+
// scip.proto's own documented convention (see doc comment above).
4873
"rust" | "go" | "c" => Some(PositionEncoding::UTF8CodeUnitOffsetFromLineStart),
74+
// Not spec-documented -- verified against scip-ruby's real source
75+
// instead (see doc comment above).
76+
"ruby" => Some(PositionEncoding::UTF8CodeUnitOffsetFromLineStart),
4977
_ => None,
5078
};
5179
match fallback {
@@ -469,14 +497,37 @@ mod tests {
469497
}
470498
}
471499

500+
#[test]
501+
fn effective_encoding_ruby_fallback_is_utf8_verified_from_scip_ruby_source() {
502+
use scip::types::PositionEncoding::*;
503+
// Ruby isn't in scip.proto's documented convention table (see the
504+
// test above), so this fallback rests on different evidence:
505+
// scip-ruby's real source (sourcegraph/scip-ruby, scip_indexer/
506+
// SCIPIndexer.cc) declares `text_document_encoding = UTF8` but never
507+
// calls `set_position_encoding` anywhere in the repo (0 hits via
508+
// GitHub code search, verified 2026-08-07) -- so its ranges are
509+
// left Unspecified and byte-oriented like the C/C++-family
510+
// indexers, matching the same UTF-8 guess used for rust/go/c above.
511+
// Root-caused from the `ruby_overlay_upgrades_ambiguous_case_when_
512+
// calls_on_the_multi_lang_fixture` nightly regression that started
513+
// at D4 (7672f52, 2026-08-01): before this fallback existed, every
514+
// scip-ruby occurrence had no byte span at all, so
515+
// `IngestStats::upgraded` stayed 0.
516+
let (encoding, provenance) = effective_encoding(UnspecifiedPositionEncoding, "ruby");
517+
assert_eq!(encoding, UTF8CodeUnitOffsetFromLineStart);
518+
assert_eq!(provenance, EncodingProvenance::Fallback);
519+
}
520+
472521
#[test]
473522
fn effective_encoding_unknown_or_unverified_provider_stays_fail_closed() {
474523
use scip::types::PositionEncoding::*;
475-
// PHP/Ruby are real providers but NOT in SCIP's documented
476-
// encoding-convention table — this is an evidence CONTRACT scoped to
477-
// spec-verified languages, not a global fallback, so they (and any
478-
// future/unknown provider) must keep failing closed.
479-
for lang in ["php", "ruby", "some-future-language", ""] {
524+
// PHP is a real provider but has no verified encoding evidence
525+
// (neither in SCIP's documented spec table nor independently
526+
// confirmed against its real indexer source the way ruby was — see
527+
// `effective_encoding`'s doc comment) — this is an evidence
528+
// CONTRACT, not a global fallback, so it (and any future/unknown
529+
// provider) must keep failing closed.
530+
for lang in ["php", "some-future-language", ""] {
480531
let (encoding, provenance) = effective_encoding(UnspecifiedPositionEncoding, lang);
481532
assert_eq!(encoding, UnspecifiedPositionEncoding, "lang={lang}");
482533
assert_eq!(provenance, EncodingProvenance::Unresolved, "lang={lang}");

0 commit comments

Comments
 (0)