This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch agent/fix-resource-growth-availability
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git

commit 828d096d69ad836ed6b2bea1d0005d382340871b
Author: FreeOnePlus <[email protected]>
AuthorDate: Sat Aug 1 14:44:32 2026 +0800

    fix: preserve partial resource history access
---
 CHANGELOG.md                                   |  3 +
 docs/tool-registry.md                          |  2 +-
 doris_mcp_server/tools/capability_detector.py  | 73 ++++++++++++++++++++++++
 doris_mcp_server/tools/doris_feature_matrix.py | 17 +++++-
 test/tools/test_capability_detector.py         | 37 +++++++++++++
 test/tools/test_capability_registry.py         | 77 ++++++++++++++++++++++++++
 test/tools/test_doris_feature_matrix.py        | 17 ++++++
 7 files changed, 223 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20641c7..70200b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -68,6 +68,9 @@ under **Unreleased** until a new version is selected and 
published.
 - Declared the exact `storage`, `query_volume`, and `user_activity` selectors
   for resource-growth analysis, including their recorded evidence sources, so
   Hosts reject ambiguous values before dispatching a Doris query.
+- Kept resource-growth analysis callable in degraded mode when only audit-log
+  history or partition-creation history is readable, instead of hiding every
+  selector behind the audit-log capability probe.
 - Updated JWT decoding type contracts for PyJWT 2.13 while preserving the
   existing signature, claim, audience, issuer, and unsafe-debug validation
   behavior.
diff --git a/docs/tool-registry.md b/docs/tool-registry.md
index a11acb3..0b7d47c 100644
--- a/docs/tool-registry.md
+++ b/docs/tool-registry.md
@@ -57,7 +57,7 @@
 | `doris_cluster.get_compaction_status` | 
`child:doris_cluster:get_compaction_status` | 
`child:call:doris_cluster:get_compaction_status` | `compaction_task_tracker`, 
`legacy_compaction_summary` |
 | `doris_cluster.get_workload_group_status` | 
`child:doris_cluster:get_workload_group_status` | 
`child:call:doris_cluster:get_workload_group_status` | `workload_group_metrics` 
|
 | `doris_cluster.get_compute_group_status` | 
`child:doris_cluster:get_compute_group_status` | 
`child:call:doris_cluster:get_compute_group_status` | `compute_group` |
-| `doris_cluster.analyze_resource_growth` | 
`child:doris_cluster:analyze_resource_growth` | 
`child:call:doris_cluster:analyze_resource_growth` | 
`historical_resource_metrics` |
+| `doris_cluster.analyze_resource_growth` | 
`child:doris_cluster:analyze_resource_growth` | 
`child:call:doris_cluster:analyze_resource_growth` | 
`all_recorded_resource_history`, `audit_resource_history`, 
`partition_creation_history` |
 | `doris_cluster.get_runtime_capabilities` | 
`child:doris_cluster:get_runtime_capabilities` | 
`child:call:doris_cluster:get_runtime_capabilities` | `capability_snapshot` |
 | `doris_pipeline.get_ingestion_status` | 
`child:doris_pipeline:get_ingestion_status` | 
`child:call:doris_pipeline:get_ingestion_status` | `load_jobs`, 
`continuous_load` |
 | `doris_pipeline.diagnose_ingestion` | 
`child:doris_pipeline:diagnose_ingestion` | 
`child:call:doris_pipeline:diagnose_ingestion` | 
`deterministic_ingestion_diagnosis` |
diff --git a/doris_mcp_server/tools/capability_detector.py 
b/doris_mcp_server/tools/capability_detector.py
index b59e1ae..191f987 100644
--- a/doris_mcp_server/tools/capability_detector.py
+++ b/doris_mcp_server/tools/capability_detector.py
@@ -237,6 +237,13 @@ _DOMAIN_PROBES: Mapping[str, tuple[tuple[str, tuple[str, 
...]], ...]] = {
             ("SELECT `time` FROM internal.__internal_schema.audit_log LIMIT 
1"),
             ("metrics_history_readable",),
         ),
+        (
+            (
+                "SELECT CREATE_TIME, DATA_LENGTH, INDEX_LENGTH "
+                "FROM information_schema.partitions LIMIT 1"
+            ),
+            ("resource_storage_history_readable",),
+        ),
     ),
     "doris_pipeline": (
         (
@@ -499,6 +506,7 @@ class DorisCapabilityDetector:
                     )
                 elif domain_name == "doris_cluster":
                     probes.update(await 
self._safe_probe_cluster_services(auth_context))
+                    probes.update(_combine_cluster_evidence_probes(probes))
                 elif domain_name == "doris_pipeline":
                     probes.update(_combine_pipeline_evidence_probes(probes))
                 elif domain_name == "doris_search":
@@ -1836,6 +1844,71 @@ def _combine_query_evidence_probe(
     )
 
 
+def _combine_cluster_evidence_probes(
+    probes: Mapping[str, CapabilityProbeEvidence],
+) -> dict[str, CapabilityProbeEvidence]:
+    audit = probes.get("metrics_history_readable")
+    storage = probes.get("resource_storage_history_readable")
+    full = (
+        _combine_all_evidence(
+            "resource_history_all_sources_readable",
+            (audit, storage),
+            supported_reason="ALL_RESOURCE_HISTORY_SOURCES_READABLE",
+        )
+        if audit is not None and storage is not None
+        else CapabilityProbeEvidence(
+            probe_id="resource_history_all_sources_readable",
+            status=CapabilityProbeStatus.UNKNOWN,
+            reason_code="CAPABILITY_PROBE_PENDING",
+            evidence_sources=("runtime_probe",),
+        )
+    )
+
+    def partial_source(
+        probe_id: str,
+        source: CapabilityProbeEvidence | None,
+        *,
+        reason_code: str,
+    ) -> CapabilityProbeEvidence:
+        if source is None:
+            return CapabilityProbeEvidence(
+                probe_id=probe_id,
+                status=CapabilityProbeStatus.UNKNOWN,
+                reason_code="CAPABILITY_PROBE_PENDING",
+                evidence_sources=("runtime_probe",),
+            )
+        return CapabilityProbeEvidence(
+            probe_id=probe_id,
+            status=(
+                CapabilityProbeStatus.DEGRADED
+                if source.status is CapabilityProbeStatus.SUPPORTED
+                else source.status
+            ),
+            reason_code=(
+                reason_code
+                if source.status is CapabilityProbeStatus.SUPPORTED
+                else source.reason_code
+            ),
+            evidence_sources=source.evidence_sources or ("runtime_probe",),
+        )
+
+    audit_only = partial_source(
+        "resource_growth_audit_history_readable",
+        audit,
+        reason_code="AUDIT_RESOURCE_HISTORY_ONLY",
+    )
+    storage_only = partial_source(
+        "resource_growth_storage_history_readable",
+        storage,
+        reason_code="PARTITION_CREATION_HISTORY_ONLY",
+    )
+    return {
+        full.probe_id: full,
+        audit_only.probe_id: audit_only,
+        storage_only.probe_id: storage_only,
+    }
+
+
 def _combine_pipeline_evidence_probes(
     probes: Mapping[str, CapabilityProbeEvidence],
 ) -> dict[str, CapabilityProbeEvidence]:
diff --git a/doris_mcp_server/tools/doris_feature_matrix.py 
b/doris_mcp_server/tools/doris_feature_matrix.py
index 66121ea..585dac2 100644
--- a/doris_mcp_server/tools/doris_feature_matrix.py
+++ b/doris_mcp_server/tools/doris_feature_matrix.py
@@ -1592,11 +1592,24 @@ FEATURE_DEFINITIONS = (
         "analyze_resource_growth",
         A,
         _variant(
-            "historical_resource_metrics",
+            "all_recorded_resource_history",
             providers=("metrics_history_provider",),
-            probes=("metrics_history_readable",),
+            probes=("resource_history_all_sources_readable",),
             evidence_quality="recorded",
         ),
+        _variant(
+            "audit_resource_history",
+            providers=("metrics_history_provider",),
+            probes=("resource_growth_audit_history_readable",),
+            evidence_quality="partial",
+            callable_when_degraded=True,
+        ),
+        _variant(
+            "partition_creation_history",
+            probes=("resource_growth_storage_history_readable",),
+            evidence_quality="partial",
+            callable_when_degraded=True,
+        ),
     ),
     _feature(
         "doris_cluster",
diff --git a/test/tools/test_capability_detector.py 
b/test/tools/test_capability_detector.py
index 8c07e1d..b474b22 100644
--- a/test/tools/test_capability_detector.py
+++ b/test/tools/test_capability_detector.py
@@ -169,6 +169,7 @@ async def 
test_detector_builds_version_vector_and_extends_domains_lazily() -> No
     )
     query = await detector.detect_domain(base, "doris_query", None)
     catalog = await detector.detect_domain(base, "doris_catalog", None)
+    cluster = await detector.detect_domain(base, "doris_cluster", None)
 
     assert base.route.fingerprint == "route-a"
     assert base.capability_generation == 3
@@ -211,9 +212,45 @@ async def 
test_detector_builds_version_vector_and_extends_domains_lazily() -> No
         catalog.probe("table_partition_statistics_readable").status
         is CapabilityProbeStatus.SUPPORTED
     )
+    assert (
+        cluster.probe("resource_history_all_sources_readable").status
+        is CapabilityProbeStatus.SUPPORTED
+    )
     assert connection.statements.count("SELECT @@version_comment;") == 1
 
 
[email protected]
+async def test_cluster_history_keeps_storage_fallback_without_audit_access() 
-> None:
+    connection = _ProbeConnection()
+    audit_probe = (
+        "SELECT `time` FROM internal.__internal_schema.audit_log LIMIT 1"
+    )
+    connection.failures[audit_probe] = RuntimeError(
+        "Access denied; user lacks SELECT privilege"
+    )
+    manager = _ProbeConnectionManager(connection)
+    detector = DorisCapabilityDetector(manager)  # type: ignore[arg-type]
+    base = await detector.detect_base(
+        None,
+        capability_generation=1,
+        provider_generation="provider.cluster",
+    )
+
+    cluster = await detector.detect_domain(base, "doris_cluster", None)
+
+    assert (
+        cluster.probe("metrics_history_readable").status
+        is CapabilityProbeStatus.UNKNOWN
+    )
+    assert (
+        cluster.probe("resource_storage_history_readable").status
+        is CapabilityProbeStatus.SUPPORTED
+    )
+    storage = cluster.probe("resource_growth_storage_history_readable")
+    assert storage.status is CapabilityProbeStatus.DEGRADED
+    assert storage.reason_code == "PARTITION_CREATION_HISTORY_ONLY"
+
+
 @pytest.mark.asyncio
 async def test_lakehouse_probes_derive_target_sensitive_advanced_facets() -> 
None:
     connection = _ProbeConnection()
diff --git a/test/tools/test_capability_registry.py 
b/test/tools/test_capability_registry.py
index 35bbb87..11d70c3 100644
--- a/test/tools/test_capability_registry.py
+++ b/test/tools/test_capability_registry.py
@@ -415,6 +415,83 @@ def 
test_lakehouse_prefers_4_1_variants_and_falls_back_on_4_0() -> None:
     assert variant_410.callable is True
 
 
[email protected](
+    ("partial_probe", "active_variant"),
+    [
+        (
+            "resource_growth_audit_history_readable",
+            "audit_resource_history",
+        ),
+        (
+            "resource_growth_storage_history_readable",
+            "partition_creation_history",
+        ),
+    ],
+)
+def test_resource_growth_keeps_each_partial_history_source_callable(
+    partial_probe: str,
+    active_variant: str,
+) -> None:
+    evaluator = CapabilityEvaluator(
+        matrix=DORIS_FEATURE_MATRIX,
+        bound_handlers=_BoundHandlers(  # type: ignore[arg-type]
+            "doris_cluster.analyze_resource_growth"
+        ),
+    )
+    domain = DORIS_DOMAIN_CATALOG.resolve_domain("doris_cluster")
+    child = DORIS_DOMAIN_CATALOG.resolve_child(
+        "doris_cluster",
+        "analyze_resource_growth",
+    )
+    probes = {
+        "resource_history_all_sources_readable": CapabilityProbeEvidence(
+            probe_id="resource_history_all_sources_readable",
+            status=CapabilityProbeStatus.UNKNOWN,
+            reason_code="PROBE_PERMISSION_DENIED",
+        ),
+        **{
+            probe_id: CapabilityProbeEvidence(
+                probe_id=probe_id,
+                status=(
+                    CapabilityProbeStatus.DEGRADED
+                    if probe_id == partial_probe
+                    else CapabilityProbeStatus.UNKNOWN
+                ),
+                reason_code=(
+                    "PARTIAL_RESOURCE_HISTORY_ONLY"
+                    if probe_id == partial_probe
+                    else "PROBE_PERMISSION_DENIED"
+                ),
+            )
+            for probe_id in (
+                "resource_growth_audit_history_readable",
+                "resource_growth_storage_history_readable",
+            )
+        },
+    }
+    providers = CapabilityProviderRegistry(
+        {
+            "metrics_history_provider": CapabilityProviderEvidence(
+                provider_id="metrics_history_provider",
+                status=CapabilityProbeStatus.SUPPORTED,
+                reason_code="PROVIDER_CONFIGURED",
+            )
+        }
+    ).snapshot()
+
+    availability = evaluator.evaluate(
+        snapshot=_snapshot(probes=probes),
+        providers=providers,
+        domain=domain,
+        child=child,
+        auth_context=None,
+    )
+
+    assert availability.callable is True
+    assert availability.status is AvailabilityStatus.DEGRADED
+    assert availability.active_variant == active_variant
+
+
 def test_evaluator_normalizes_system_object_probe_evidence_for_manifest() -> 
None:
     evaluator = CapabilityEvaluator(
         matrix=DORIS_FEATURE_MATRIX,
diff --git a/test/tools/test_doris_feature_matrix.py 
b/test/tools/test_doris_feature_matrix.py
index d101224..4316b2e 100644
--- a/test/tools/test_doris_feature_matrix.py
+++ b/test/tools/test_doris_feature_matrix.py
@@ -596,6 +596,23 @@ def 
test_compaction_child_keeps_a_degraded_legacy_variant() -> None:
     assert result.certified is True
 
 
+def test_resource_growth_declares_full_and_partial_evidence_variants() -> None:
+    feature = DORIS_FEATURE_MATRIX.get_feature(
+        "doris_cluster",
+        "analyze_resource_growth",
+    )
+
+    assert tuple(
+        variant.name for variant in feature.support_contract.variants
+    ) == (
+        "all_recorded_resource_history",
+        "audit_resource_history",
+        "partition_creation_history",
+    )
+    assert feature.support_contract.variants[1].callable_when_degraded is True
+    assert feature.support_contract.variants[2].callable_when_degraded is True
+
+
 def test_lineage_native_and_audit_paths_are_both_explicit() -> None:
     feature = DORIS_FEATURE_MATRIX.get_feature(
         "doris_governance",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to