diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index d8375944ff..e50796feed 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -131,7 +131,7 @@ CI watching must be bounded. Do not loop indefinitely. ## Status Contract -- `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `mac_profile`, `db_size_bytes`, `wal_size_bytes`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `page_count`, `freelist_count`, `page_size`), `symbols_by_language`, `process`, `last_index_run`, `hooks`, MCP-only `mcp_session`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields. +- `status --json` and related JSON/MCP payloads currently expose the trust fields documented in `README.md` and `DEVELOPER_GUIDE.md`, including `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `language_readiness`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir`, `data_dir_source`, `data_dir_mode`, `mac_profile`, `db_size_bytes`, `wal_size_bytes`, `db_pragma_settings` (`journal_mode`, `synchronous`, `wal_autocheckpoint`, `page_count`, `freelist_count`, `page_size`), `symbols_by_language`, `process`, `last_index_run`, `hooks`, MCP-only `mcp_session`, and the `status --check`-only `stale_after_seconds` / `index_age_seconds` threshold audit fields. - When any readiness field is degraded, the CLI adds `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, and `readiness_degradations[]`. `degraded_root_cause` is the primary stable machine code; `readiness_degradations[]` lists every degraded field with `root_cause`, human reason, and remediation strings. - `issues_table_available` reports physical `file_issues` table presence only. `file_issues_data_current` reports whether the table is also stamped current for the active index generation. - `index_writer_version` records the `cdidx` version that last wrote to the DB (stamped into `codeindex_meta` as `cdidx_writer_version` on every full scan, update, and MCP index). `index_newer_than_reader` flips to `true` whenever any persisted numeric contract stamp in `codeindex_meta` (or unknown `PRAGMA user_version` readiness bits) exceeds the current binary's compiled maximum, so an older CLI re-opening a DB written by a newer CLI degrades loudly with an audit trail instead of silently dropping back to text-search fallbacks. `index_newer_than_reader_reason` enumerates the specific newer-than-reader stamps. diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 472ddaefe4..7641a72e8c 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -320,6 +320,7 @@ Current stable codes and triggers: | `fold_ready=false` | aggregate fold readiness bit is degraded | `cdidx backfill-fold` or full rebuild | | `sql_graph_contract_ready=false` | SQL graph rows do not match the current call-column / qualified-name contract | `cdidx index ` | | `hotspot_family_ready=false` | one or more hotspot-family languages lack current authoritative family stamps | `cdidx index ` | +| `partial_family_key_population` | hotspot-family metadata is stamped but some indexed symbols still have NULL `family_key` values | `cdidx index ` | | `graph_table_available=false` | `symbol_references` is missing or not graph-ready | `cdidx index ` | | `issues_table_available=false` | `file_issues` is missing or not issue-ready | `cdidx index ` | | `csharp_symbol_name_ready=false` | C# canonical symbol-name stamps are stale | `cdidx index ` | @@ -895,7 +896,7 @@ Adding `--json-envelope` to a query command (`search`, `definition`, `references Every top-level CLI/MCP JSON DTO (`StatusResult`, `RepoMapResult`, `SymbolAnalysisResult`, `ImpactAnalysisResult`, `OutlineResult`, `FileExcerptResult`, `CompactSearchResult`, `SymbolResult`, `DefinitionResult`, `UnusedSymbolResult`, `ReferenceResult`, `CallerResult`, `CalleeResult`, `FileResult`, `FileFindResult`) carries an `api_version` string field stamped from `JsonOutputContract.ApiVersion`. The same value is mirrored on the `--json-envelope` `metadata` block. This describes the JSON output contract, not the cdidx binary version (which is still surfaced via `version.json` and `cdidx --version`). Bump `JsonOutputContract.ApiVersion` only on **breaking** shape changes — renames, removals, or type changes of an existing field. Additive changes (new optional fields, new readiness flags, new enum values) keep the version stable so older consumers continue to parse the payload. Strict downstream consumers should pin against the major value and degrade gracefully when it changes. Issue #1555. -The documented `status --json` trust contract spans `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir_mode`, `mac_profile`, `stale_after_seconds`, `index_age_seconds`, the remediation fields `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, `readiness_degradations`, and MCP-only `mcp_session`. MCP `mcp_session` is session-scoped diagnostics, not persisted DB state, and contains `log_level`, `roots`, optional `client_info`, and optional `client_capabilities`. Keep this list synchronized with `README.md` and `AGENT_GUIDE.md`; `DocumentationStatusContractTests` fails when any required field is missing from one of those docs. +The documented `status --json` trust contract spans `fold_ready`, `fold_ready_reason`, `graph_table_available`, `issues_table_available`, `file_issues_data_current`, `migration_in_progress`, `sql_graph_contract_ready`, `sql_graph_contract_degraded_reason`, `hotspot_family_ready`, `hotspot_family_degraded_reason`, `language_readiness`, `csharp_symbol_name_ready`, `csharp_metadata_target_ready`, `csharp_metadata_target_degraded_reason`, `indexed_head_commit`, `worktree_head_changed`, `indexed_head_sha`, `indexed_head_branch`, `indexed_head_timestamp`, `commits_ahead_of_indexed_head`, `index_writer_version`, `index_newer_than_reader`, `index_newer_than_reader_reason`, `unknown_extension_file_count`, `path_case_sensitive`, `data_dir_mode`, `mac_profile`, `stale_after_seconds`, `index_age_seconds`, the remediation fields `degraded_root_cause`, `degraded_reason`, `recommended_action`, `alternative_action`, `readiness_degradations`, and MCP-only `mcp_session`. MCP `mcp_session` is session-scoped diagnostics, not persisted DB state, and contains `log_level`, `roots`, optional `client_info`, and optional `client_capabilities`. Keep this list synchronized with `README.md` and `AGENT_GUIDE.md`; `DocumentationStatusContractTests` fails when any required field is missing from one of those docs. `references` already prefixes each human-readable row with `reference_kind`, and `callers` does the same for its grouped caller rows. When one grouped container mixes kinds (for example `call` and `subscribe` on the same event member), the human-readable label joins the distinct kinds with `+` (for example `call+subscribe`) instead of collapsing to a single preferred label, and the reference-kind column widens dynamically to fit the longest label in the batch so mixed rows do not overrun the neighbouring column. JSON output for `callers` and `callees` keeps the scalar `reference_kind` for back-compat (it reports the preferred summary kind `instantiate` > `subscribe` > `MIN(call)`) and adds a sorted `reference_kinds` array plus a `has_mixed_reference_kinds` bool so consumers can detect mixed containers without trusting a single collapsed label. This lets terminal users distinguish `call` / `instantiate` / `subscribe` / mixed without re-running the command with `--json` and lets AI clients answer mixed-kind questions without chasing a second `--exact` query. diff --git a/README.md b/README.md index b21b08f248..8a03ebaccc 100644 --- a/README.md +++ b/README.md @@ -168,14 +168,14 @@ The documented `status --json` trust contract covers these fields: fold_readyfold_ready_reasongraph_table_availableissues_table_available file_issues_data_currentmigration_in_progressdegraded_root_causereadiness_degradations sql_graph_contract_readysql_graph_contract_degraded_reasonhotspot_family_readyhotspot_family_degraded_reason -csharp_symbol_name_readycsharp_metadata_target_readycsharp_metadata_target_degraded_reasonindexed_head_commit -worktree_head_changedindexed_head_shaindexed_head_branchindexed_head_timestamp -commits_ahead_of_indexed_headindex_writer_versionindex_newer_than_readerindex_newer_than_reader_reason -unknown_extension_file_countpath_case_sensitivedata_dirdata_dir_source -data_dir_modemac_profiledb_size_byteswal_size_bytes -db_pragma_settingssymbols_by_languageprocesslast_index_run -hooksstale_after_secondsindex_age_secondsdegraded_reason -recommended_actionalternative_actionmcp_session +language_readinesscsharp_symbol_name_readycsharp_metadata_target_readycsharp_metadata_target_degraded_reason +indexed_head_commitworktree_head_changedindexed_head_shaindexed_head_branch +indexed_head_timestampcommits_ahead_of_indexed_headindex_writer_versionindex_newer_than_reader +index_newer_than_reader_reasonunknown_extension_file_countpath_case_sensitivedata_dir +data_dir_sourcedata_dir_modemac_profiledb_size_bytes +wal_size_bytesdb_pragma_settingssymbols_by_languageprocess +last_index_runhooksstale_after_secondsindex_age_seconds +degraded_reasonrecommended_actionalternative_actionmcp_session @@ -391,14 +391,14 @@ upgrade / downgrade 後はインストール済み補完 script を再生成し fold_readyfold_ready_reasongraph_table_availableissues_table_available file_issues_data_currentmigration_in_progressdegraded_root_causereadiness_degradations sql_graph_contract_readysql_graph_contract_degraded_reasonhotspot_family_readyhotspot_family_degraded_reason -csharp_symbol_name_readycsharp_metadata_target_readycsharp_metadata_target_degraded_reasonindexed_head_commit -worktree_head_changedindexed_head_shaindexed_head_branchindexed_head_timestamp -commits_ahead_of_indexed_headindex_writer_versionindex_newer_than_readerindex_newer_than_reader_reason -unknown_extension_file_countpath_case_sensitivedata_dirdata_dir_source -data_dir_modemac_profiledb_size_byteswal_size_bytes -db_pragma_settingssymbols_by_languageprocesslast_index_run -hooksstale_after_secondsindex_age_secondsdegraded_reason -recommended_actionalternative_actionmcp_session +language_readinesscsharp_symbol_name_readycsharp_metadata_target_readycsharp_metadata_target_degraded_reason +indexed_head_commitworktree_head_changedindexed_head_shaindexed_head_branch +indexed_head_timestampcommits_ahead_of_indexed_headindex_writer_versionindex_newer_than_reader +index_newer_than_reader_reasonunknown_extension_file_countpath_case_sensitivedata_dir +data_dir_sourcedata_dir_modemac_profiledb_size_bytes +wal_size_bytesdb_pragma_settingssymbols_by_languageprocess +last_index_runhooksstale_after_secondsindex_age_seconds +degraded_reasonrecommended_actionalternative_actionmcp_session diff --git a/changelog.d/unreleased/1485.fixed.md b/changelog.d/unreleased/1485.fixed.md new file mode 100644 index 0000000000..0210413a38 --- /dev/null +++ b/changelog.d/unreleased/1485.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 1485 +affected: + - src/CodeIndex/Database/DbReader.cs + - src/CodeIndex/Database/DegradationReasonCodes.cs + - tests/CodeIndex.Tests/DbReaderTests.cs +--- + +## English + +- **Hotspot family readiness now verifies row-level family keys (#1485)** - `status` and hotspot readers now degrade when current hotspot-family metadata is stamped but indexed symbols still have NULL `family_key` values. + +## 日本語 + +- **hotspot family readiness が行単位の family key を検証するようになりました (#1485)** - current な hotspot-family metadata が stamp 済みでも、indexed symbol に NULL の `family_key` が残っている場合は `status` と hotspot reader が degraded として扱います。 diff --git a/changelog.d/unreleased/1607.fixed.md b/changelog.d/unreleased/1607.fixed.md new file mode 100644 index 0000000000..c93b3ac3e3 --- /dev/null +++ b/changelog.d/unreleased/1607.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 1607 +affected: + - src/CodeIndex/Database/DbReader.cs +--- + +## English + +- **Read-only fallback now verifies fold-ready rows before trusting stale ready bits (#1607)** - readers opened through the read-only fallback path now demote `fold_ready` when the ready bit is set but folded-name rows are incomplete. + +## 日本語 + +- **read-only fallback が stale ready bit を信頼する前に fold-ready 行を検証するようになりました (#1607)** - read-only fallback 経路で開いた reader は、ready bit が立っていても folded-name 行が未完成なら `fold_ready` を degraded に降格します。 diff --git a/changelog.d/unreleased/1647.fixed.md b/changelog.d/unreleased/1647.fixed.md new file mode 100644 index 0000000000..a6e97fa572 --- /dev/null +++ b/changelog.d/unreleased/1647.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 1647 +affected: + - src/CodeIndex/Database/DbReader.cs + - src/CodeIndex/Database/DbReader.FilesStatus.cs + - src/CodeIndex/Models/QueryResults.cs + - README.md + - DEVELOPER_GUIDE.md + - AGENT_GUIDE.md + - tests/CodeIndex.Tests/DbReaderTests.cs +--- + +## English + +- **Status now exposes per-language readiness details (#1647)** - `status --json` includes `language_readiness` so clients can distinguish language-scoped readiness from aggregate readiness fields. + +## 日本語 + +- **status が言語別 readiness の詳細を返すようになりました (#1647)** - `status --json` に `language_readiness` を追加し、クライアントが aggregate readiness と言語単位の readiness を区別できるようにしました。 diff --git a/src/CodeIndex/Database/DbReader.FilesStatus.cs b/src/CodeIndex/Database/DbReader.FilesStatus.cs index f00a8de90a..e7a4cb2d86 100644 --- a/src/CodeIndex/Database/DbReader.FilesStatus.cs +++ b/src/CodeIndex/Database/DbReader.FilesStatus.cs @@ -475,6 +475,7 @@ public StatusResult GetStatus() : _csharpMetadataTargetDegradedReason; var sqlGraphContractSignal = GetSqlGraphContractSignal(lang: null); var hotspotFamilySignal = GetHotspotFamilySignal(lang: null); + var languageReadiness = GetLanguageReadiness(); var foldReadyReason = ResolveFoldReadyReason(); var foldReady = _foldReady && foldReadyReason == null; @@ -559,6 +560,7 @@ GROUP BY COALESCE(f.lang, 'unknown'), s.kind MigrationInProgress = batchInProgress, HotspotFamilyReady = hotspotFamilySignal.Ready, HotspotFamilyDegradedReason = hotspotFamilySignal.DegradedReason, + LanguageReadiness = languageReadiness.Count > 0 ? languageReadiness : null, CSharpSymbolNameReady = csharpSymbolNameReady, CSharpMetadataTargetReady = csharpMetadataTargetReady, CSharpMetadataTargetDegradedReason = csharpMetadataTargetDegradedReason, diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 16c965eef4..353a712536 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -17,6 +17,12 @@ public readonly record struct HotspotFamilySignal( bool Relevant, string? DegradedReason); +public sealed record LanguageReadinessSignal( + [property: System.Text.Json.Serialization.JsonPropertyName("ready")] bool Ready, + [property: System.Text.Json.Serialization.JsonPropertyName("degraded_reason")] + [property: System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + string? DegradedReason); + public readonly record struct SqlGraphContractSignal( bool Ready, bool Relevant, @@ -728,7 +734,76 @@ private string ResolveHotspotFamilyDegradedReason(string lang) return string.IsNullOrWhiteSpace(fingerprint) ? DegradationReasonCodes.HotspotFamilyDisabledAtIndexTime - : DegradationReasonCodes.HotspotFamilyMetadataStale; + : HasIncompleteHotspotFamilyRows(lang) + ? DegradationReasonCodes.HotspotFamilyRowsIncomplete + : DegradationReasonCodes.HotspotFamilyMetadataStale; + } + + private bool HasIncompleteHotspotFamilyRows(string lang) + { + if (!_symbolColumns.Contains("family_key")) + return true; + + using var cmd = _conn.CreateCommand(); + cmd.CommandText = @" + SELECT EXISTS( + SELECT 1 + FROM symbols s + JOIN files f ON f.id = s.file_id + WHERE f.lang = @lang + AND s.name IS NOT NULL + AND s.family_key IS NULL + AND EXISTS ( + SELECT 1 + FROM symbols s2 + JOIN files f2 ON f2.id = s2.file_id + WHERE f2.lang = f.lang + AND s2.name = s.name + AND s2.kind = s.kind + AND COALESCE(s2.container_qualified_name, '') = COALESCE(s.container_qualified_name, '') + AND s2.family_key IS NOT NULL + ) + LIMIT 1)"; + cmd.Parameters.AddWithValue("@lang", lang); + var raw = cmd.ExecuteScalar(); + return raw is long l ? l != 0 : raw is int i && i != 0; + } + + public Dictionary> GetLanguageReadiness() + { + var result = new Dictionary>(StringComparer.Ordinal); + + foreach (var lang in _indexedHotspotFamilyLanguages.OrderBy(value => value, StringComparer.Ordinal)) + { + var signal = GetHotspotFamilySignal(lang); + AddLanguageReadiness(result, lang, "hotspot_family", signal.Ready, signal.DegradedReason); + } + + if (ScopeMayIncludeCSharpFiles("csharp", pathPatterns: null, excludePathPatterns: null, excludeTests: false, since: null)) + { + AddLanguageReadiness(result, "csharp", "symbol_name", _csharpSymbolNameContractCurrent, + _csharpSymbolNameContractCurrent ? null : DegradationReasonCodes.CSharpSymbolNameNotReady); + AddLanguageReadiness(result, "csharp", "metadata_target", _csharpMetadataTargetReady, + _csharpMetadataTargetReady ? null : _csharpMetadataTargetDegradedReason); + } + + return result; + } + + private static void AddLanguageReadiness( + Dictionary> result, + string lang, + string feature, + bool ready, + string? degradedReason) + { + if (!result.TryGetValue(lang, out var features)) + { + features = new Dictionary(StringComparer.Ordinal); + result[lang] = features; + } + + features[feature] = new LanguageReadinessSignal(ready, degradedReason); } private string GetHotspotFamilyRecoveryAction(IReadOnlyList languages) @@ -767,7 +842,7 @@ private static int ParseFoldVersion(SqliteConnection conn) { if (_foldReady) { - if (ShouldVerifyFoldReadyRows() && HasIncompleteFoldRows()) + if ((_readOnlyFallback || ShouldVerifyFoldReadyRows()) && HasIncompleteFoldRows()) return DegradationReasonCodes.FoldReadyBitSetButRowsIncomplete; return null; @@ -843,7 +918,8 @@ private HashSet LoadHotspotFamilyReadyLanguages(SqliteConnection conn) if (raw is string s && int.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var version) && version == DbContext.HotspotFamilyVersion - && !string.IsNullOrWhiteSpace(fingerprint)) + && !string.IsNullOrWhiteSpace(fingerprint) + && !HasIncompleteHotspotFamilyRows(lang)) { readyLangs.Add(lang); } diff --git a/src/CodeIndex/Database/DegradationReasonCodes.cs b/src/CodeIndex/Database/DegradationReasonCodes.cs index dc84f44f35..c511632d31 100644 --- a/src/CodeIndex/Database/DegradationReasonCodes.cs +++ b/src/CodeIndex/Database/DegradationReasonCodes.cs @@ -19,6 +19,7 @@ public static class DegradationReasonCodes public const string HotspotFamilySupportNotIndexed = "hotspot_family_support_not_indexed"; public const string HotspotFamilyMetadataStale = "hotspot_family_metadata_stale"; public const string HotspotFamilyDisabledAtIndexTime = "hotspot_family_disabled_at_index_time"; + public const string HotspotFamilyRowsIncomplete = "partial_family_key_population"; public const string GraphTableMissing = "graph_table_available=false"; public const string IssuesTableMissing = "issues_table_available=false"; public const string FileIssuesDataStale = "file_issues_data_current=false"; @@ -42,6 +43,7 @@ public static class DegradationReasonCodes HotspotFamilySupportNotIndexed, HotspotFamilyMetadataStale, HotspotFamilyDisabledAtIndexTime, + HotspotFamilyRowsIncomplete, GraphTableMissing, IssuesTableMissing, FileIssuesDataStale, @@ -146,6 +148,11 @@ private static DegradationReasonMetadata CreateMetadata(string code) "Cross-file hotspot grouping metadata was stamped without marker fingerprints, so authoritative family grouping cannot be trusted.", "Run `cdidx index ` to rebuild and stamp authoritative hotspot families.", "Run `cdidx index --rebuild` for a full rebuild."), + HotspotFamilyRowsIncomplete => new( + code, + "Cross-file hotspot grouping metadata is stamped, but some indexed symbols still lack family keys.", + "Run `cdidx index ` to restamp authoritative hotspot families.", + "Run `cdidx index --rebuild` for a full rebuild."), GraphTableMissing => new( code, "Reference / caller / callee / unused counts are degraded to 0 because the symbol_references table is missing.", diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index de7b56144a..170ff33f7a 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -626,6 +626,9 @@ public class StatusResult [JsonPropertyName("hotspot_family_degraded_reason")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? HotspotFamilyDegradedReason { get; set; } + [JsonPropertyName("language_readiness")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary>? LanguageReadiness { get; set; } /// /// True when C# canonical symbol-name upgrades (for operators, conversion operators, /// indexers) have been applied to all indexed C# rows in this DB. False means exact-name diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 31e3a9d96f..c67bdd0dd0 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -4125,6 +4125,65 @@ WHERE file_id IN ( Assert.Contains("hotspot_family_support_not_indexed=csharp", signal.DegradedReason); } + [Fact] + public void GetHotspotFamilySignal_CurrentStampWithPartialFamilyRowsIsDegraded() + { + InsertIndexedFile("src/Api.Part1.cs", "csharp", + """ + public partial class Api + { + public void Run() { } + } + """); + InsertIndexedFile("src/Api.Part2.cs", "csharp", + """ + public partial class Api + { + public void Run(int value) { } + } + """); + + using (var cmd = _db.Connection.CreateCommand()) + { + cmd.CommandText = """ + UPDATE symbols + SET family_key = NULL + WHERE file_id IN ( + SELECT id FROM files WHERE path = 'src/Api.Part2.cs' + ) + """; + cmd.ExecuteNonQuery(); + } + + var reader = new DbReader(_db.Connection); + var signal = reader.GetHotspotFamilySignal("csharp"); + + Assert.True(signal.Relevant); + Assert.False(signal.Ready); + Assert.Contains("partial_family_key_population=csharp", signal.DegradedReason); + } + + [Fact] + public void GetStatus_ExposesPerLanguageReadinessMap() + { + InsertIndexedFile("src/Api.Part1.cs", "csharp", + """ + public partial class Api + { + public void Run() { } + } + """); + + var reader = new DbReader(_db.Connection); + var status = reader.GetStatus(); + + Assert.NotNull(status.LanguageReadiness); + Assert.True(status.LanguageReadiness!.ContainsKey("csharp")); + Assert.True(status.LanguageReadiness["csharp"]["hotspot_family"].Ready); + Assert.True(status.LanguageReadiness["csharp"].ContainsKey("symbol_name")); + Assert.True(status.LanguageReadiness["csharp"].ContainsKey("metadata_target")); + } + [Fact] public void GetHotspotFamilySignal_MissingMarkerFingerprintIsStillDegraded() { diff --git a/tests/CodeIndex.Tests/golden/status.json b/tests/CodeIndex.Tests/golden/status.json index aa31679b19..d498c44408 100644 --- a/tests/CodeIndex.Tests/golden/status.json +++ b/tests/CodeIndex.Tests/golden/status.json @@ -83,6 +83,22 @@ "migration_in_progress": false, "hotspot_family_ready": false, "hotspot_family_degraded_reason": "cross-file hotspot family grouping is degraded (hotspot_family_support_not_indexed=csharp); Run \u0060cdidx index \u003CprojectPath\u003E\u0060 to rebuild and stamp authoritative hotspot families.", + "language_readiness": { + "csharp": { + "hotspot_family": { + "ready": false, + "degraded_reason": "cross-file hotspot family grouping is degraded (hotspot_family_support_not_indexed=csharp); Run \u0060cdidx index \u003CprojectPath\u003E\u0060 to rebuild and stamp authoritative hotspot families." + }, + "symbol_name": { + "ready": false, + "degraded_reason": "csharp_symbol_name_ready=false" + }, + "metadata_target": { + "ready": false, + "degraded_reason": "csharp_metadata_target_stamp_outdated" + } + } + }, "csharp_symbol_name_ready": false, "csharp_metadata_target_ready": false, "csharp_metadata_target_degraded_reason": "csharp_metadata_target_stamp_outdated",