diff --git a/src/api/SIGCM2.Application/Abstractions/Persistence/IAvisoQueryRepository.cs b/src/api/SIGCM2.Application/Abstractions/Persistence/IAvisoQueryRepository.cs
new file mode 100644
index 0000000..a077780
--- /dev/null
+++ b/src/api/SIGCM2.Application/Abstractions/Persistence/IAvisoQueryRepository.cs
@@ -0,0 +1,24 @@
+namespace SIGCM2.Application.Abstractions.Persistence;
+
+///
+/// Query-only access to Aviso counts by Rubro.
+/// CAT-002 introduces the contract. The real Dapper-based impl lands in PRD-002
+/// (when dbo.Aviso exists). Until then, NullAvisoQueryRepository is the binding.
+///
+public interface IAvisoQueryRepository
+{
+ ///
+ /// Returns the count of avisos (active, non-archived) assigned to the given rubro.
+ ///
+ Task CountAvisosEnRubroAsync(int rubroId, CancellationToken ct = default);
+
+ ///
+ /// Returns a dictionary of { rubroId → count } for the provided ids.
+ /// Used by GetRubroTreeQueryHandler to avoid N+1 when populating TieneAvisos per node.
+ /// The implementation MUST do a single query; the stub returns an empty dictionary
+ /// (every rubro gets 0 via dictionary.GetValueOrDefault).
+ ///
+ Task> CountAvisosBatchAsync(
+ IReadOnlyCollection rubroIds,
+ CancellationToken ct = default);
+}
diff --git a/src/api/SIGCM2.Application/Avisos/NullAvisoQueryRepository.cs b/src/api/SIGCM2.Application/Avisos/NullAvisoQueryRepository.cs
new file mode 100644
index 0000000..223c146
--- /dev/null
+++ b/src/api/SIGCM2.Application/Avisos/NullAvisoQueryRepository.cs
@@ -0,0 +1,22 @@
+using SIGCM2.Application.Abstractions.Persistence;
+
+namespace SIGCM2.Application.Avisos;
+
+///
+/// STUB — PRD-002 reemplaza con AvisoQueryRepository contra dbo.Aviso.
+/// Returns 0 / empty dictionary so every handler guard passes and every tree node shows TieneAvisos=false.
+/// This is intentional for CAT-002: the mechanism is installed; the data feed arrives in PRD-002.
+///
+public sealed class NullAvisoQueryRepository : IAvisoQueryRepository
+{
+ private static readonly IReadOnlyDictionary Empty =
+ new Dictionary(capacity: 0);
+
+ public Task CountAvisosEnRubroAsync(int rubroId, CancellationToken ct = default)
+ => Task.FromResult(0);
+
+ public Task> CountAvisosBatchAsync(
+ IReadOnlyCollection rubroIds,
+ CancellationToken ct = default)
+ => Task.FromResult(Empty);
+}
diff --git a/src/api/SIGCM2.Application/DependencyInjection.cs b/src/api/SIGCM2.Application/DependencyInjection.cs
index 7d7f85f..2b10fbb 100644
--- a/src/api/SIGCM2.Application/DependencyInjection.cs
+++ b/src/api/SIGCM2.Application/DependencyInjection.cs
@@ -67,6 +67,8 @@ using SIGCM2.Application.Rubros.Move;
using SIGCM2.Application.Rubros.GetTree;
using SIGCM2.Application.Rubros.GetById;
using SIGCM2.Application.Rubros.Dtos;
+using SIGCM2.Application.Abstractions.Persistence;
+using SIGCM2.Application.Avisos;
namespace SIGCM2.Application;
@@ -152,7 +154,10 @@ public static class DependencyInjection
services.AddScoped>, ListIngresosBrutosQueryHandler>();
services.AddScoped>, GetHistorialIngresosBrutosQueryHandler>();
- // Rubros (CAT-001)
+ // Rubros (CAT-001 + CAT-002)
+ // CAT-002: Regla de Oro Rama vs Hoja — stub binding until PRD-002 provides real impl
+ services.AddScoped();
+
services.AddScoped, CreateRubroCommandHandler>();
services.AddScoped, UpdateRubroCommandHandler>();
services.AddScoped, DeactivateRubroCommandHandler>();
diff --git a/tests/SIGCM2.Application.Tests/Avisos/NullAvisoQueryRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Avisos/NullAvisoQueryRepositoryTests.cs
new file mode 100644
index 0000000..4afa33d
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Avisos/NullAvisoQueryRepositoryTests.cs
@@ -0,0 +1,35 @@
+using FluentAssertions;
+using SIGCM2.Application.Avisos;
+
+namespace SIGCM2.Application.Tests.Avisos;
+
+public class NullAvisoQueryRepositoryTests
+{
+ private readonly NullAvisoQueryRepository _repo = new();
+
+ [Fact]
+ public async Task CountAvisosEnRubroAsync_Always_ReturnsZero()
+ {
+ var result = await _repo.CountAvisosEnRubroAsync(rubroId: 99);
+
+ result.Should().Be(0);
+ }
+
+ [Fact]
+ public async Task CountAvisosBatchAsync_WithIds_ReturnsDictionaryAllZero()
+ {
+ var result = await _repo.CountAvisosBatchAsync([1, 2, 3]);
+
+ result.GetValueOrDefault(1, 0).Should().Be(0);
+ result.GetValueOrDefault(2, 0).Should().Be(0);
+ result.GetValueOrDefault(3, 0).Should().Be(0);
+ }
+
+ [Fact]
+ public async Task CountAvisosBatchAsync_EmptyIds_ReturnsEmptyDictionary()
+ {
+ var result = await _repo.CountAvisosBatchAsync([]);
+
+ result.Should().HaveCount(0);
+ }
+}