Repository: Seddryck/Packata
Pull request #177: feat: add property "kind" to resources and infer it from "paths"
Author: login: Seddryck
Labels: name: new-feature
Created: 2025-05-10T06:23:10Z | Merged: 2025-05-10T07:19:04Z

Close #171 

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new "Kind" property for resources, allowing classification as "local", "remote", "service", or "virtual".
  - Added support for automatic inference of the "Kind" property based on resource paths and schemes.
- **Bug Fixes**
  - Improved nullability handling in inference methods for better static analysis and reliability.
- **Refactor**
  - Unified and streamlined the resource inference system for extensibility and maintainability.
- **Tests**
  - Added comprehensive tests for "Kind" inference and refactored existing inference tests for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #171: Extend resource descriptor with kind to determine what kind of resource is expected
local: default except if path is starting with HTTP or https. Paths are relative
remote: should be downloaded locally
service: is a remote service to query (database or rest, graphQL)
virtual: is something to be computed such as a view on top of other resources

A SQLite or duckdb database will be written as local or remote. Format should determine if the resource must be queried with DuckDB or SQLite protocol.

Diff:
diff --git a/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs b/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
index 49bb50c..8f5261a 100644
--- a/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
+++ b/src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs
@@ -53,6 +53,21 @@ public void Deserialize_Package_Success((Stream Stream, IDataPackageSerializer S
         });
     }
 
+
+    [TestCaseSource(nameof(GetData))]
+    public void Deserialize_ResourceKind_Success((Stream Stream, IDataPackageSerializer Serializer) value)
+    {
+        using var streamReader = new StreamReader(value.Stream);
+        var dataPackage = value.Serializer.Deserialize(streamReader, new LocalDirectoryDataPackageContainer(), new StorageProvider());
+        Assert.That(dataPackage.Resources, Is.Not.Null);
+        Assert.That(dataPackage.Resources, Has.Count.EqualTo(1));
+        Assert.Multiple(() =>
+        {
+            Assert.That(dataPackage.Resources[0].Kind, Is.EqualTo("local"));
+        });
+    }
+
+
     [TestCaseSource(nameof(GetData))]
     public void Deserialize_Metrics_Success((Stream Stream, IDataPackageSerializer Serializer) value)
     {
diff --git a/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json b/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json
index 60486be..b3869ca 100644
--- a/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json
+++ b/src/Packata.Core.Testing/Serialization/Json/Resources/extension.json
@@ -13,6 +13,7 @@
             "name": "measures",
             "path": "sensor_measures.csv",
             "type": "table",
+            "kind": "local",
             "$schema": "https://packata.dev/profiles/1.0/dataresource.json",
             "title": "Sensors' temperatures & hygrometry",
             "format": "csv",
diff --git a/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml b/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
index b98f979..9314ca9 100644
--- a/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
+++ b/src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml
@@ -10,6 +10,7 @@ resources:
 - name: measures
   path: sensor_measures.csv
   type: table
+  kind: local
   "$schema": https://packata.dev/profiles/1.0/dataresource.json
   title: "Sensors' temperatures & hygrometry"
   format: csv
diff --git a/src/Packata.Core/Resource.Props.cs b/src/Packata.Core/Resource.Props.cs
index 879ce00..417a7c4 100644
--- a/src/Packata.Core/Resource.Props.cs
+++ b/src/Packata.Core/Resource.Props.cs
@@ -47,6 +47,13 @@ public partial class Resource
     /// <example>"table"</example>
     public string? Type { get; set; }
 
+    /// <summary>
+    /// The type of the resource.
+    /// </summary>
+    /// <remarks>Specifies the nature of the resource, such as "table".</remarks>
+    /// <example>"virtual", "local", "service", "remote"</example>
+    public string? Kind { get; set; }
+
     /// <summary>
     /// The file format of the resource.
     /// </summary>
diff --git a/src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs b/src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs
index 6e94298..8c5507f 100644
--- a/src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs
+++ b/src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs
@@ -15,8 +15,11 @@ public void TryInfer_ShouldReturnFalse_WhenFormatIsNullOrEmpty()
 
             var result = inference.TryInfer(resource, out var dialect);
 
-            Assert.That(result, Is.False);
-            Assert.That(dialect, Is.Null);
+            using (Assert.EnterMultipleScope())
+            {
+                Assert.That(result, Is.False);
+                Assert.That(dialect, Is.Null);
+            }
         }
 
         [Test]
@@ -33,11 +36,16 @@ public void TryInfer_ShouldReturnTrueAndSetDialect_WhenFormatIsKnown(
 
             Assert.That(result, Is.True);
             Assert.That(dialect, Is.Not.Null);
-            Assert.That(dialect!.Delimiter, Is.EqualTo(delimiter));
-            Assert.That(dialect.QuoteChar, Is.EqualTo(quoteChar));
-            Assert.That(dialect.EscapeChar, Is.EqualTo(escapeChar));
-            Assert.That(dialect.DoubleQuote, Is.EqualTo(doubleQuote));
-            Assert.That(dialect.LineTerminator, Is.EqualTo(lineTerminator));
+            Assert.That(dialect, Is.TypeOf<TableDelimitedDialect>());
+            var tdDialect = (TableDelimitedDialect)dialect;
+            using (Assert.EnterMultipleScope())
+            {
+                Assert.That(tdDialect.Delimiter, Is.EqualTo(delimiter));
+                Assert.That(tdDialect.QuoteChar, Is.EqualTo(quoteChar));
+                Assert.That(tdDialect.EscapeChar, Is.EqualTo(escapeChar));
+                Assert.That(tdDialect.DoubleQuote, Is.EqualTo(doubleQuote));
+                Assert.That(tdDialect.LineTerminator, Is.EqualTo(lineTerminator));
+            }
         }
 
         [Test]
@@ -48,8 +56,11 @@ public void TryInfer_ShouldReturnFalse_WhenFormatIsUnknown()
 
             var result = inference.TryInfer(resource, out var dialect);
 
-            Assert.That(result, Is.False);
-            Assert.That(dialect, Is.Null);
+            using (Assert.EnterMultipleScope())
+            {
+                Assert.That(result, Is.False);
+                Assert.That(dialect, Is.Null);
+            }
         }
     }
 }
diff --git a/src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs b/src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs
index 2311c80..196433d 100644
--- a/src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs
+++ b/src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs
@@ -45,7 +45,9 @@ public void TryInfer_ShouldReturnTrueAndSetDialect_WhenMediaTypeIsKnown(string m
 
             Assert.That(result, Is.True);
             Assert.That(dialect, Is.Not.Null);
-            Assert.That(dialect!.Delimiter, Is.EqualTo(delimiter));
+            Assert.That(dialect, Is.TypeOf<TableDelimitedDialect>());
+            var tdDialect = (TableDelimitedDialect)dialect;
+            Assert.That(tdDialect.Delimiter, Is.EqualTo(delimiter));
         }
 
         [Test]
diff --git a/src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs b/src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs
index 55892ea..5ac020c 100644
--- a/src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs
+++ b/src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs
@@ -20,11 +20,9 @@ public void Enrich_ShouldSetCompression_WhenCompressionIsNull()
                     return true;
                 });
 
-            var service = new ResourceInferenceService(
-                Array.Empty<IDialectInference>(),
-                Array.Empty<IFormatInference>(),
-                new[] { mockCompression.Object }
-            );
+            var builder = new ResourceInferenceServiceBuilder();
+            builder.AddStrategy(mockCompression.Object);
+            var service = builder.Build();
 
             var resource = new Resource { Compression = null };
             service.Enrich(resource);
@@ -35,11 +33,7 @@ public void Enrich_ShouldSetCompression_WhenCompressionIsNull()
         [Test]
         public void Enrich_ShouldNotSetCompression_WhenCompressionIsAlreadySet()
         {
-            var service = new ResourceInferenceService(
-                Array.Empty<IDialectInference>(),
-                Array.Empty<IFormatInference>(),
-                Array.Empty<ICompressionInference>()
-            );
+            var service = ResourceInferenceServiceBuilder.None;
 
             var resource = new Resource { Compression = "gzip" };
             service.Enrich(resource);
@@ -52,18 +46,16 @@ public void Enrich_ShouldSetDialect_WhenDialectIsNull()
         {
             var mockDialect = new Mock<IDialectInference>();
             mockDialect
-                .Setup(m => m.TryInfer(It.IsAny<Resource>(), out It.Ref<TableDelimitedDialect?>.IsAny))
-                .Returns((Resource resource, out TableDelimitedDialect? dialect) =>
+                .Setup(m => m.TryInfer(It.IsAny<Resource>(), out It.Ref<TableDialect?>.IsAny))
+                .Returns((Resource resource, out TableDialect? dialect) =>
                 {
                     dialect = new TableDelimitedDialect { Delimiter = ',', QuoteChar = '"', DoubleQuote = true };
                     return true;
                 });
 
-            var service = new ResourceInferenceService(
-                new[] { mockDialect.Object },
-                Array.Empty<IFormatInference>(),
-                Array.Empty<ICompressionInference>()
-            );
+            var builder = new ResourceInferenceServiceBuilder();
+            builder.AddStrategy(mockDialect.Object);
+            var service = builder.Build();
 
             var resource = new Resource { Dialect = null };
             service.Enrich(resource);
@@ -88,11 +80,9 @@ public void Enrich_ShouldSetFormat_WhenFormatIsNull()
                     return true;
                 });
 
-            var service = new ResourceInferenceService(
-                Array.Empty<IDialectInference>(),
-                new[] { mockFormat.Object },
-                Array.Empty<ICompressionInference>()
-            );
+            var builder = new ResourceInferenceServiceBuilder();
+            builder.AddStrategy(mockFormat.Object);
+            var service = builder.Build();
 
             var resource = new Resource { Format = null };
             service.Enrich(resource);
@@ -113,11 +103,9 @@ public void Enrich_ShouldNotSetFormat_WhenFormatIsNotNull()
                     return true;
                 });
 
-            var service = new ResourceInferenceService(
-                Array.Empty<IDialectInference>(),
-                new[] { mockFormat.Object },
-                Array.Empty<ICompressionInference>()
-            );
+            var builder = new ResourceInferenceServiceBuilder();
+            builder.AddStrategy(mockFormat.Object);
+            var service = builder.Build();
 
             var resource = new Resource { Format = "bar" };
             service.Enrich(resource);
@@ -128,11 +116,7 @@ public void Enrich_ShouldNotSetFormat_WhenFormatIsNotNull()
         [Test]
         public void Enrich_ShouldNotSetDialect_WhenDialectIsAlreadySet()
         {
-            var service = new ResourceInferenceService(
-                Array.Empty<IDialectInference>(),
-                Array.Empty<IFormatInference>(),
-                Array.Empty<ICompressionInference>()
-            );
+            var service = ResourceInferenceServiceBuilder.None;
 
             var resource = new Resource
             {
@@ -147,6 +131,39 @@ public void Enrich_ShouldNotSetDialect_WhenDialectIsAlreadySet()
             Assert.That(dialect.QuoteChar, Is.EqualTo('"'));
             Assert.That(dialect.DoubleQuote, Is.True);
         }
+
+        [Test]
+        public void Enrich_ShouldSetKind_WhenKindIsNull()
+        {
+            var mockKind = new Mock<IKindInference>();
+            mockKind
+                .Setup(m => m.TryInfer(It.IsAny<Resource>(), out It.Ref<string?>.IsAny))
+                .Returns((Resource resource, out string value) =>
+                {
+                    value = "mssql";
+                    return true;
+                });
+
+            var builder = new ResourceInferenceServiceBuilder();
+            builder.AddStrategy(mockKind.Object);
+            var service = builder.Build();
+
+            var resource = new Resource { Kind = null };
+            service.Enrich(resource);
+
+            Assert.That(resource.Kind, Is.EqualTo("mssql"));
+        }
+
+        [Test]
+        public void Enrich_ShouldNotSetKind_WhenKindIsAlreadySet()
+        {
+            var service = ResourceInferenceServiceBuilder.None;
+
+            var resource = new Resource { Kind = "service" };
+            service.Enrich(resource);
+
+            Assert.That(resource.Kind, Is.EqualTo("service"));
+        }
     }
 }
 
diff --git a/src/Packata.ResourceReaders.Testing/Inference/SchemeBasedKindInferenceTests.cs b/src/Packata.ResourceReaders.Testing/Inference/SchemeBasedKindInferenceTests.cs
new file mode 100644
index 0000000..3a87cee
--- /dev/null
+++ b/src/Packata.ResourceReaders.Testing/Inference/SchemeBasedKindInferenceTests.cs
@@ -0,0 +1,38 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Packata.Core;
+using Packata.ResourceReaders.Inference;
+using Packata.Core.Storage;
+using Moq;
+
+namespace Packata.ResourceReaders.Testing.Inference;
+public class SchemeBasedKindInferenceTests
+{
+    private static readonly string[] _remoteArray = ["http", "https"];
+    private static readonly string[] _serviceArray = ["mssql", "pgsql"];
+
+    [Test]
+    [TestCase("data/file.csv", "local")]
+    [TestCase("file.csv", "local")]
+    [TestCase("http://www.data.org/file.csv", "remote")]
+    [TestCase("mssql://192.168.16.45/db", "service")]
+    [TestCase(null, "virtual")]
+    public void TryInfer_Path_Success(string? path, string expected)
+    {
+        bool remote(string value) => (_remoteArray).Any(x => x.Equals(value, StringComparison.OrdinalIgnoreCase));
+        bool service(string value) => (_serviceArray).Any(x => x.Equals(value, StringComparison.OrdinalIgnoreCase));
+        var inference = new SchemeBasedKindInference(remote, service);
+
+        var factory = new PathFactory(Mock.Of<IDataPackageContainer>(), Mock.Of<IStorageProvider>());
+        var resource = path  is null
+            ? new Resource()
+            : new Resource() { Paths = [factory.Create(path)] };
+
+        Assert.That(inference.TryInfer(resource, out var result), Is.True);
+        Assert.That(result, Is.EqualTo(expected));
+    }
+}
diff --git a/src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs b/src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs
index a0ae6fd..42d38e5 100644
--- a/src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs
+++ b/src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs
@@ -65,7 +65,7 @@ public void Create_ShouldEnrichDialect_WhenDialectIsNull()
                 };
             });
 
-        var factory = new ResourceReaderFactory(ResourceInferenceService.None);
+        var factory = new ResourceReaderFactory(ResourceInferenceServiceBuilder.None);
         var reader = factory.Create(resource);
         Assert.That(resource.Dialect, Is.Null);
 
diff --git a/src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs b/src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs
index 3f2acaf..d291820 100644
--- a/src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs
+++ b/src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -13,7 +14,7 @@ public abstract class BaseCompressionInference : ICompressionInference
     protected BaseCompressionInference(IDictionary<string, string> compressionMappings)
         => CompressionMappings = compressionMappings;
 
-    public abstract bool TryInfer(Resource resource, out string? compression);
+    public abstract bool TryInfer(Resource resource, [NotNullWhen(true)] out string? compression);
 
     protected bool TryInferFromExtension(string? extension, out string? compression)
     {
diff --git a/src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs b/src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs
index bf54c3d..5094a0a 100644
--- a/src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs
+++ b/src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -16,7 +17,7 @@ public ExtensionBasedCompressionInference(IExtractExtension extractor, IDictiona
         _extractor = extractor;
     }
 
-    public override bool TryInfer(Resource resource, out string? compression)
+    public override bool TryInfer(Resource resource, [NotNullWhen(true)] out string? compression)
     {
         compression = null;
         if (resource.Paths == null || !resource.Paths.Any())
diff --git a/src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs b/src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs
index 507417a..d47389b 100644
--- a/src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs
+++ b/src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -15,7 +16,7 @@ public ExtensionBasedDialectInference(IExtractExtension extractor)
         _extractor = extractor;
     }
 
-    public override bool TryInfer(Resource resource, out TableDelimitedDialect? dialect)
+    public override bool TryInfer(Resource resource, [NotNullWhen(true)] out TableDialect? dialect)
     {
         dialect = null;
         if (_extractor.TryGetPathExtension(resource.Paths.ToArray(), out var extension))
diff --git a/src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs b/src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs
index 4ef0b2b..f415a4d 100644
--- a/src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs
+++ b/src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -15,7 +16,7 @@ public ExtensionBasedFormatInference(IExtractExtension extractor)
         _extractor = extractor;
     }
 
-    public bool TryInfer(Resource resource, out string? format)
+    public bool TryInfer(Resource resource, [NotNullWhen(true)] out string? format)
     {
         format = null;
         if (_extractor.TryGetPathExtension(resource.Paths.ToArray(), out var extension))
diff --git a/src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs b/src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs
index 5d2ec23..3fddc9e 100644
--- a/src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs
+++ b/src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -8,10 +9,10 @@
 namespace Packata.ResourceReaders.Inference;
 public class FormatBasedDialectInference : IDialectInference
 {
-    public virtual bool TryInfer(Resource resource, out TableDelimitedDialect? dialect)
+    public virtual bool TryInfer(Resource resource, [NotNullWhen(true)] out TableDialect? dialect)
         => TryInferFromFormat(resource.Format, out dialect);
 
-    protected bool TryInferFromFormat(string? format, out TableDelimitedDialect? dialect)
+    protected bool TryInferFromFormat(string? format, [NotNullWhen(true)] out TableDialect? dialect)
     {
         dialect = format switch
         {
diff --git a/src/Packata.ResourceReaders/Inference/ICompressionInference.cs b/src/Packata.ResourceReaders/Inference/ICompressionInference.cs
index dceaf07..b6c6684 100644
--- a/src/Packata.ResourceReaders/Inference/ICompressionInference.cs
+++ b/src/Packata.ResourceReaders/Inference/ICompressionInference.cs
@@ -6,7 +6,5 @@
 using Packata.Core;
 
 namespace Packata.ResourceReaders.Inference;
-public interface ICompressionInference
-{
-    bool TryInfer(Resource resource, out string? compression);
-}
+public interface ICompressionInference : IInferenceStrategy<string>
+{ }
diff --git a/src/Packata.ResourceReaders/Inference/IDialectInference.cs b/src/Packata.ResourceReaders/Inference/IDialectInference.cs
index 40692ce..c39f20b 100644
--- a/src/Packata.ResourceReaders/Inference/IDialectInference.cs
+++ b/src/Packata.ResourceReaders/Inference/IDialectInference.cs
@@ -9,13 +9,5 @@ namespace Packata.ResourceReaders.Inference;
 /// <summary>
 /// Interface for strategies that infer table dialect information from a resource.
 /// </summary>
-public interface IDialectInference
-{
-    /// <summary>
-    /// Attempts to infer a table dialect from the given resource.
-    /// </summary>
-    /// <param name="resource">The resource to infer from.</param>
-    /// <param name="dialect">When this method returns, contains the inferred dialect if successful, or null if unsuccessful.</param>
-    /// <returns>true if a dialect was successfully inferred; otherwise, false.</returns>
-    bool TryInfer(Resource resource, out TableDelimitedDialect? dialect);
-}
+public interface IDialectInference : IInferenceStrategy<TableDialect>
+{ }
diff --git a/src/Packata.ResourceReaders/Inference/IFormatInference.cs b/src/Packata.ResourceReaders/Inference/IFormatInference.cs
index 88152fe..acba244 100644
--- a/src/Packata.ResourceReaders/Inference/IFormatInference.cs
+++ b/src/Packata.ResourceReaders/Inference/IFormatInference.cs
@@ -6,7 +6,5 @@
 using Packata.Core;
 
 namespace Packata.ResourceReaders.Inference;
-public interface IFormatInference
-{
-    bool TryInfer(Resource resource, out string? format);
-}
+public interface IFormatInference : IInferenceStrategy<string>
+{ }
diff --git a/src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs b/src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs
new file mode 100644
index 0000000..c13ccfd
--- /dev/null
+++ b/src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs
@@ -0,0 +1,16 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Packata.Core;
+
+namespace Packata.ResourceReaders.Inference;
+public interface IInferenceStrategy<T> : IInferenceStrategy
+{
+    bool TryInfer(Resource resource, [NotNullWhen(true)] out T? value);
+}
+
+public interface IInferenceStrategy
+{ }
diff --git a/src/Packata.ResourceReaders/Inference/IKindInference.cs b/src/Packata.ResourceReaders/Inference/IKindInference.cs
new file mode 100644
index 0000000..09a214c
--- /dev/null
+++ b/src/Packata.ResourceReaders/Inference/IKindInference.cs
@@ -0,0 +1,13 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Packata.Core;
+
+namespace Packata.ResourceReaders.Inference;
+/// <summary>
+/// Interface for strategies that infer kind information from a resource.
+/// </summary>
+public interface IKindInference : IInferenceStrategy<string>
+{ }
diff --git a/src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs b/src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs
index 84f9ebd..8cd449f 100644
--- a/src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs
+++ b/src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -12,7 +13,7 @@ public MediaTypeBasedCompressionInference(IDictionary<string, string> compressio
         : base(compressionMappings)
     { }
 
-    public override bool TryInfer(Resource resource, out string? compression)
+    public override bool TryInfer(Resource resource, [NotNullWhen(true)] out string? compression)
     {
         var mediaType = resource.MediaType;
         compression = null;
diff --git a/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs b/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
index 802a8c2..4b86f0e 100644
--- a/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
+++ b/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -8,7 +9,7 @@
 namespace Packata.ResourceReaders.Inference;
 public class MediaTypeBasedDialectInference : FormatBasedDialectInference
 {
-    public override bool TryInfer(Resource resource, out TableDelimitedDialect? dialect)
+    public override bool TryInfer(Resource resource, [NotNullWhen(true)] out TableDialect? dialect)
     {
         var mediaType = resource.MediaType;
         dialect = null;
diff --git a/src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs b/src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs
index b69e947..8c14c87 100644
--- a/src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs
+++ b/src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -8,7 +9,7 @@
 namespace Packata.ResourceReaders.Inference;
 public class MediaTypeBasedFormatInference : IFormatInference
 {
-    public bool TryInfer(Resource resource, out string? format)
+    public bool TryInfer(Resource resource, [NotNullWhen(true)] out string? format)
     {
         var mediaType = resource.MediaType;
         format = null;
diff --git a/src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs b/src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs
index 431b5c8..efdfcd7 100644
--- a/src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs
+++ b/src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs
@@ -10,82 +10,63 @@
 namespace Packata.ResourceReaders.Inference;
 public class ResourceInferenceService : IResourceInferenceService
 {
-    private readonly IDialectInference[] _dialectStrategies;
-    private readonly IFormatInference[] _formatStrategies;
-    private readonly ICompressionInference[] _compressionStrategies;
+    private readonly IReadOnlyCollection<IInferenceStrategy> _strategies;
 
-    private static readonly IDictionary<string, string> _compressionMappings = GetCompressionMappings();
-    private static Dictionary<string, string> GetCompressionMappings()
-    {
-        var factory = DecompressorFactory.Streaming();
-        var keys = factory.GetSupportedKeys();
-        var dict = new Dictionary<string, string>(keys.Length);
-        foreach (var key in keys)
-            dict.Add(key, factory.GetCompression(key));
-        return dict;
-    }
-
-    public static ResourceInferenceService None => _none;
-    private static readonly ResourceInferenceService _none = new ([], [], []);
-
-    public static ResourceInferenceService Instance => _instance;
-    private static readonly ResourceInferenceService _instance = CreateInstance();
+    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
+        => _strategies = strategies.ToArray();
 
-    private static ResourceInferenceService CreateInstance()
-        => new (
-            [
-                new FormatBasedDialectInference(),
-                new MediaTypeBasedDialectInference(),
-                new ExtensionBasedDialectInference(new ExtractExtensionFromPathsService()),
-            ],
-            [
-                new MediaTypeBasedFormatInference(),
-                new ExtensionBasedFormatInference(new ExtractExtensionFromPathsService()),
-            ],
-            [
-                new MediaTypeBasedCompressionInference(_compressionMappings),
-                new ExtensionBasedCompressionInference(new ExtractExtensionFromPathsService(), _compressionMappings)
-            ]);
-
-    protected internal ResourceInferenceService(IDialectInference[] dialectStrategies, IFormatInference[] formatStrategies, ICompressionInference[] compressionStrategies)
-        => (_dialectStrategies, _formatStrategies, _compressionStrategies) = (dialectStrategies, formatStrategies, compressionStrategies);
-
-    public void Enrich(Resource resource)
+    private static void InferIfUnset<T>(
+        Resource resource,
+        Func<Resource, T?> getter,
+        Action<Resource, T> setter,
+        IEnumerable<IInferenceStrategy<T>> strategies,
+        Func<T?, bool> isUnset)
     {
-        if (string.IsNullOrEmpty(resource.Compression))
+        if (isUnset(getter(resource)))
         {
-            foreach (var strategy in _compressionStrategies)
+            foreach (var strategy in strategies)
             {
-                if (strategy.TryInfer(resource, out var compression))
+                if (strategy.TryInfer(resource, out var value))
                 {
-                    resource.Compression = compression;
+                    setter(resource, value);
                     break;
                 }
             }
         }
+    }
 
-        if (string.IsNullOrEmpty(resource.Format))
-        {
-            foreach (var strategy in _formatStrategies)
-            {
-                if (strategy.TryInfer(resource, out var format))
-                {
-                    resource.Format = format;
-                    break;
-                }
-            }
-        }
+    public void Enrich(Resource resource)
+    {
+        InferIfUnset(
+            resource,
+            r => r.Compression,
+            (r, v) => r.Compression = v,
+            _strategies.OfType<ICompressionInference>(),
+            string.IsNullOrEmpty
+        );
 
-        if (resource.Dialect is null)
-        {
-            foreach (var strategy in _dialectStrategies)
-            {
-                if (strategy.TryInfer(resource, out var dialect))
-                {
-                    resource.Dialect = dialect;
-                    break;
-                }
-            }
-        }
+        InferIfUnset(
+            resource,
+            r => r.Format,
+            (r, v) => r.Format = v,
+            _strategies.OfType<IFormatInference>(),
+            string.IsNullOrEmpty
+        );
+
+        InferIfUnset<TableDialect>(
+            resource,
+            r => r.Dialect,
+            (r, v) => r.Dialect = v,
+            _strategies.OfType<IDialectInference>(),
+            v => v == null
+        );
+
+        InferIfUnset(
+            resource,
+            r => r.Kind,
+            (r, v) => r.Kind = v,
+            _strategies.OfType<IKindInference>(),
+            v => v == null
+        );
     }
 }
diff --git a/src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs b/src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs
new file mode 100644
index 0000000..d99f9d5
--- /dev/null
+++ b/src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs
@@ -0,0 +1,68 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.NetworkInformation;
+using System.Text;
+using System.Threading.Tasks;
+using DubUrl.Mapping;
+using PocketCsvReader.Compression;
+
+namespace Packata.ResourceReaders.Inference;
+internal class ResourceInferenceServiceBuilder
+{
+    private readonly Dictionary<Type, IInferenceStrategy> _strategies = new();
+    private SchemeMapperBuilder? _schemeMapperBuilder;
+
+    public ResourceInferenceServiceBuilder AddStrategy<T>(IInferenceStrategy<T> strategy)
+    {
+        var concreteType = strategy.GetType(); // Use concrete type to deduplicate
+        _strategies[concreteType] = (IInferenceStrategy)strategy;
+        return this;
+    }
+
+    public ResourceInferenceService Build()
+        => new (_strategies.Values);
+
+    private static readonly IDictionary<string, string> _compressionMappings = GetCompressionMappings();
+    private static Dictionary<string, string> GetCompressionMappings()
+    {
+        var factory = DecompressorFactory.Streaming();
+        var keys = factory.GetSupportedKeys();
+        var dict = new Dictionary<string, string>(keys.Length);
+        foreach (var key in keys)
+            dict.Add(key, factory.GetCompression(key));
+        return dict;
+    }
+
+    private Func<string, bool> GetDatabaseSchemes()
+    {
+        static SchemeMapperBuilder create()
+        {
+            var builder = new DubUrl.Mapping.SchemeMapperBuilder();
+            builder.Build();
+            return builder!;
+        }
+        _schemeMapperBuilder ??= create();
+        return (scheme) => _schemeMapperBuilder.CanHandle(scheme);
+    }
+
+    public static ResourceInferenceService None => _none;
+    private static readonly ResourceInferenceService _none = new([]);
+
+    public static ResourceInferenceService Default => _default;
+    private static readonly ResourceInferenceService _default = CreateDefault();
+
+    private static ResourceInferenceService CreateDefault()
+    {
+        var builder = new ResourceInferenceServiceBuilder();
+        builder.AddStrategy(new FormatBasedDialectInference());
+        builder.AddStrategy(new MediaTypeBasedDialectInference());
+        builder.AddStrategy(new ExtensionBasedDialectInference(new ExtractExtensionFromPathsService()));
+        builder.AddStrategy(new MediaTypeBasedFormatInference());
+        builder.AddStrategy(new ExtensionBasedFormatInference(new ExtractExtensionFromPathsService()));
+        builder.AddStrategy(new MediaTypeBasedCompressionInference(_compressionMappings));
+        builder.AddStrategy(new ExtensionBasedCompressionInference(new ExtractExtensionFromPathsService(), _compressionMappings));
+        builder.AddStrategy(new SchemeBasedKindInference((string value) => new string[] { "http", "https" }.Any(x => StringComparer.InvariantCultureIgnoreCase.Compare(x, value) == 0), builder.GetDatabaseSchemes()));
+        return builder.Build();
+    }
+}
diff --git a/src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs b/src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs
new file mode 100644
index 0000000..f740395
--- /dev/null
+++ b/src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Packata.Core;
+using Packata.Core.Storage;
+
+namespace Packata.ResourceReaders.Inference;
+internal class SchemeBasedKindInference : IKindInference
+{
+    private readonly Func<string, bool> _remoteSchemes;
+    private readonly Func<string, bool> _serviceSchemes;
+
+    public SchemeBasedKindInference(Func<string, bool> remoteSchemes, Func<string, bool> serviceSchemes)
+        => (_remoteSchemes, _serviceSchemes) = (remoteSchemes, serviceSchemes);
+
+    public bool TryInfer(Resource resource, [NotNullWhen(true)] out string? kind)
+    {
+        kind = null;
+        if(!resource.Paths.Any())
+        {
+            if (resource.Data is null)
+            {
+                kind = "virtual";
+                return true;
+            }
+            return false;
+        }
+
+        if (resource.Paths.First() is ContainerPath relativePath)
+        {
+                kind = "local";
+                return true;
+        }
+
+        if (resource.Paths.First() is FullyQualifiedPath absolutePath)
+        {
+            var uri = new Uri(absolutePath.Value);
+            if (_remoteSchemes(uri.Scheme))
+            {
+                kind = "remote";
+                return true;
+            }
+
+            if (_serviceSchemes(uri.Scheme))
+            {
+                kind = "service";
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/Packata.ResourceReaders/ResourceReaderFactory.cs b/src/Packata.ResourceReaders/ResourceReaderFactory.cs
index ecab0d1..f4cf2e3 100644
--- a/src/Packata.ResourceReaders/ResourceReaderFactory.cs
+++ b/src/Packata.ResourceReaders/ResourceReaderFactory.cs
@@ -12,7 +12,7 @@ namespace Packata.ResourceReaders;
 public class ResourceReaderFactory
 {
     private Dictionary<string, IResourceReaderFactory> Factories { get; } = [];
-    private readonly IResourceInferenceService _inferenceService = ResourceInferenceService.Instance;
+    private readonly IResourceInferenceService _inferenceService = ResourceInferenceServiceBuilder.Default;
 
     public ResourceReaderFactory()
     {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs (1)</summary><blockquote>

`33-37`: **Fix inconsistent indentation.**

There's extra indentation in the local path handling case that doesn't match the indentation pattern used in the rest of the method.

```diff
 if (resource.Paths.First() is ContainerPath relativePath)
 {
-        kind = "local";
-        return true;
+    kind = "local";
+    return true;
 }
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs (2)</summary><blockquote>

`4-4`: **Remove unused import.**

The `System.Net.NetworkInformation` namespace doesn't appear to be used in this class.

```diff
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net.NetworkInformation;
using System.Text;
using System.Threading.Tasks;
```

---

`65-65`: **Simplify string comparison for remote schemes.**

The current implementation for comparing schemes is more complex than necessary. Consider using a more straightforward approach.

```diff
-builder.AddStrategy(new SchemeBasedKindInference((string value) => new string[] { "http", "https" }.Any(x => StringComparer.InvariantCultureIgnoreCase.Compare(x, value) == 0), builder.GetDatabaseSchemes()));
+builder.AddStrategy(new SchemeBasedKindInference((string value) => new string[] { "http", "https" }.Any(x => x.Equals(value, StringComparison.OrdinalIgnoreCase)), builder.GetDatabaseSchemes()));
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs (1)</summary><blockquote>

`64-70`: **Use `string.IsNullOrWhiteSpace` to detect unset `Kind`**

If `Kind` is a `string`, an empty string should also be considered “unset”.  Using `v => v is null` overlooks that case.

```diff
-            _strategies.OfType<IKindInference>(),
-            v => v == null
+            _strategies.OfType<IKindInference>(),
+            string.IsNullOrWhiteSpace
```

(If `Kind` was recently changed to an enum or value type, ignore this.)  
This small tweak prevents unnecessary strategy invocations when the property is present but empty.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7d63cf19fdb301f1f7154cc2ebcff07a5a221245 and a44ef21aef10a153b8b11207faa557972198e2c4.

</details>

<details>
<summary>📒 Files selected for processing (24)</summary>

* `src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs` (1 hunks)
* `src/Packata.Core.Testing/Serialization/Json/Resources/extension.json` (1 hunks)
* `src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml` (1 hunks)
* `src/Packata.Core/Resource.Props.cs` (1 hunks)
* `src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs` (7 hunks)
* `src/Packata.ResourceReaders.Testing/Inference/SchemeBasedKindInferenceTests.cs` (1 hunks)
* `src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/ICompressionInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/IDialectInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/IFormatInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/IKindInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/ResourceReaderFactory.cs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (8)</summary>

<details>
<summary>src/Packata.ResourceReaders/ResourceReaderFactory.cs (1)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs (2)</summary>

* `ResourceInferenceServiceBuilder` (11-68)
* `ResourceInferenceServiceBuilder` (16-21)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs (2)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs (3)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs (1)</summary>

* `TryInfer` (17-17)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs (3)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs (1)</summary>

* `TryInfer` (17-17)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs (2)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs (3)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs (1)</summary>

* `TryInfer` (12-13)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs (4)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs (1)</summary>

* `TryInfer` (12-13)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>
<details>
<summary>src/Packata.Core/TableDelimitedDialect.cs (1)</summary>

* `TableDelimitedDialect` (11-110)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs (3)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs (1)</summary>

* `TryInfer` (19-32)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (33)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/IFormatInference.cs (1)</summary>

`9-10`: **Good architectural refactoring with interface inheritance.**

The change to have `IFormatInference` inherit from `IInferenceStrategy<string>` is a positive architectural improvement. This refactoring:

1. Eliminates code duplication by removing the explicit `TryInfer` method that was previously declared in this interface
2. Creates a more consistent API across all inference strategy interfaces
3. Improves type safety with better nullability annotations from the parent interface
4. Enhances extensibility by standardizing the interface pattern

This change aligns well with the broader refactoring effort mentioned in the summary, where similar interfaces like `ICompressionInference`, `IDialectInference`, and the new `IKindInference` now follow the same pattern.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs (2)</summary>

`3-3`: **Good addition of required namespace.**

Adding the `System.Diagnostics.CodeAnalysis` namespace is necessary to support the `[NotNullWhen(true)]` attribute used in the method signature.

---

`20-20`: **Properly annotated nullability contract.**

The addition of the `[NotNullWhen(true)]` attribute to the `out string? compression` parameter correctly indicates that when the method returns `true`, the `compression` parameter will not be null. This annotation enhances static analysis capabilities and makes the method's contract more explicit.

The implementation correctly ensures this contract is fulfilled - when the method returns `true` (via `TryInferFromExtension`), the `compression` parameter is guaranteed to have a non-null value.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs (2)</summary>

`3-3`: **Import for nullability annotations**  
Adding `using System.Diagnostics.CodeAnalysis;` is necessary to enable the `[NotNullWhen(true)]` attribute. This import aligns with the nullability guarantees introduced below.

---

`17-17`: <details>
<summary>✅ Verification successful</summary>

**Standardize nullability contract on TryInfer out parameter**  
The addition of `[NotNullWhen(true)]` on the `compression` out parameter brings this abstract method into alignment with the generic `IInferenceStrategy<T>` interface and enhances static-analysis accuracy. Ensure that all concrete implementations of `TryInfer` are updated to match this signature, and consider adding or updating XML documentation to describe the non-null guarantee when the method returns true.  
   
Run this script to find any overrides missing the new annotation:

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate TryInfer overrides without the NotNullWhen attribute
fd -e cs . src/Packata.ResourceReaders/Inference | xargs rg -n 'TryInfer.*out string\?' | grep -v '\[NotNullWhen'
```

Length of output: 269

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate all classes inheriting BaseCompressionInference
fd -e cs src/Packata.ResourceReaders/Inference | xargs rg -n 'class .*:.*BaseCompressionInference'

# Locate all override implementations of TryInfer and check for NotNullWhen
fd -e cs src/Packata.ResourceReaders/Inference | xargs rg -n 'override .*TryInfer' -n -C1 | grep -B1 -n 'override.*TryInfer'
```

Length of output: 2506

---



</details>

**All `TryInfer` overrides include `[NotNullWhen(true)]`**

I’ve verified that every concrete implementation of `TryInfer` on classes deriving from `BaseCompressionInference` carries the new `[NotNullWhen(true)]` annotation on its out parameter—no missing overrides detected.

• Consider updating the XML doc on `BaseCompressionInference.TryInfer` (and its overrides) to clearly state that the out `compression` is non-null when the method returns `true`.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs (1)</summary>

`3-3`: **Improved nullability annotation with `[NotNullWhen(true)]`**

The addition of `System.Diagnostics.CodeAnalysis` namespace and the `[NotNullWhen(true)]` attribute provides better static analysis for null checking. This attribute explicitly indicates that when the method returns `true`, the `format` parameter will not be null, improving type safety.



Also applies to: 19-19

</details>
<details>
<summary>src/Packata.Core.Testing/Serialization/Json/Resources/extension.json (1)</summary>

`16-16`: **Resource kind property properly implemented**

The addition of the `"kind": "local"` property to the JSON resource definition aligns with the PR objective of adding kind classification to resources. This correctly identifies a file-based resource as local type.

</details>
<details>
<summary>src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml (1)</summary>

`13-13`: **Resource kind property implemented consistently in YAML**

The addition of `kind: local` to the YAML resource definition is consistent with the corresponding JSON changes, ensuring uniform resource classification across different serialization formats.

</details>
<details>
<summary>src/Packata.ResourceReaders/ResourceReaderFactory.cs (1)</summary>

`15-15`: **Builder pattern improves inference service construction**

Changing from direct service instance to `ResourceInferenceServiceBuilder.Default` aligns with the new builder-based pattern for constructing inference services. This change enables the new kind inference functionality while improving extensibility for future inference strategies.

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

`50-56`: **Good addition of the "Kind" property with clear documentation.**

The new `Kind` property is well-documented with appropriate XML comments and examples. The property is properly marked as nullable string, consistent with other properties in the class that represent optional metadata.

This property adds useful information about the resource's nature (virtual, local, service, or remote) which complements the existing `Type` property. The examples in the XML documentation provide clear guidance for potential values.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IKindInference.cs (1)</summary>

`1-14`: **Clean interface definition following established patterns.**

The new `IKindInference` interface properly extends the generic `IInferenceStrategy<string>` interface, following the same pattern as other inference interfaces in the system. The interface is well-documented with a clear summary of its purpose.

This marker interface provides a good abstraction for kind inference implementations and maintains consistency with the architecture for other resource property inference strategies.

</details>
<details>
<summary>src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs (1)</summary>

`68-68`: **Correctly updated test to use the new builder pattern.**

The test has been correctly modified to use `ResourceInferenceServiceBuilder.None` instead of directly using the inference service. This change aligns with the architectural refactoring to use a builder pattern for creating inference services with customizable strategies.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs (2)</summary>

`3-3`: **Appropriate using statement for nullability annotations.**

Added the necessary using statement for the `NotNullWhen` attribute to enhance static analysis capabilities.

---

`12-12`: **Enhanced method signature with nullability annotations.**

Adding the `[NotNullWhen(true)]` attribute to the `out string? format` parameter improves static analysis by indicating that the `format` parameter will not be null when the method returns `true`. This follows best practices for methods using the "Try" pattern and aligns with the interface definition.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs (2)</summary>

`3-3`: **Added CodeAnalysis import to support NotNullWhen attribute**

This import is necessary to support the nullability attribute added to the TryInfer method.

---

`16-16`: **Enhanced nullability information with NotNullWhen attribute**

Adding the `[NotNullWhen(true)]` attribute to the output parameter provides better static analysis by indicating that `compression` is guaranteed to be non-null when the method returns `true`. This improves type safety without changing runtime behavior.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ICompressionInference.cs (1)</summary>

`9-10`: **Good refactoring to use a generic inference interface**

Inheriting from `IInferenceStrategy<string>` instead of declaring the TryInfer method directly creates a more consistent interface hierarchy across inference types. This promotes code reuse and makes the codebase more maintainable.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IDialectInference.cs (1)</summary>

`12-13`: **Standardized inference interface through generic inheritance**

Changing the interface to inherit from `IInferenceStrategy<TableDelimitedDialect>` aligns this interface with other inference interfaces in the codebase. This creates a consistent pattern and better supports the broader refactoring of the inference services.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs (2)</summary>

`3-3`: **Added CodeAnalysis import for NotNullWhen attribute**

This import is necessary to support the nullability attribute added to the TryInfer method.

---

`19-19`: **Improved type safety with NotNullWhen attribute**

Adding the `[NotNullWhen(true)]` attribute to the output parameter enhances static analysis by specifying that `dialect` is guaranteed to be non-null when the method returns `true`. This aligns with the interface changes in `IDialectInference` and improves type safety.

</details>
<details>
<summary>src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs (1)</summary>

`57-68`: **Good test addition for the new Kind property**

The test appropriately verifies that the Resource.Kind property is correctly deserialized from JSON and YAML formats. It follows the established test patterns in the class and reuses the existing test infrastructure.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs (2)</summary>

`3-3`: **LGTM - Adding necessary import for nullability attribute**

This import enables the use of the NotNullWhen attribute to improve static analysis.

---

`12-12`: **Appropriate nullability annotation**

The NotNullWhen attribute correctly indicates that when TryInfer returns true, the dialect parameter is guaranteed to be non-null. This improves static analysis and helps prevent potential null reference exceptions.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

`1-17`: **Well-designed interface hierarchy for inference strategies**

This interface design is well-structured:
- Using a generic interface for type-safe implementations
- Having a non-generic marker interface as base allows for collections of mixed inference strategies
- The NotNullWhen attribute correctly enforces the contract that successful inferences return non-null values

This provides a solid foundation for the various inference strategy implementations including the new Kind property inference.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs (3)</summary>

`3-3`: **LGTM - Adding necessary import for nullability attribute**

This import enables the use of the NotNullWhen attribute to improve static analysis.

---

`12-13`: **Appropriate nullability annotation on public method**

The NotNullWhen attribute correctly indicates that when TryInfer returns true, the dialect parameter is guaranteed to be non-null, improving static analysis.

---

`15-15`: **Good consistency with nullability annotations on protected method**

Applying the same NotNullWhen attribute to the protected method ensures consistent nullability contracts throughout the inheritance chain.

</details>
<details>
<summary>src/Packata.ResourceReaders.Testing/Inference/SchemeBasedKindInferenceTests.cs (1)</summary>

`1-38`: **Well-structured test implementation!**

The test class for `SchemeBasedKindInference` is comprehensive and covers important scenarios including local files, remote URLs, service URLs, and the virtual resource case. The parameterized test approach is an excellent practice that makes the test cases clear and maintainable.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs (1)</summary>

`20-55`: **Well-implemented inference logic.**

The `TryInfer` method correctly handles different types of resources:
1. Virtual resources (no paths, no data)
2. Local resources (ContainerPath)
3. Remote and service resources (FullyQualifiedPath with different schemes)

The method also follows proper null-handling practices with the `[NotNullWhen(true)]` attribute.

</details>
<details>
<summary>src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs (2)</summary>

`23-26`: **Good transition to the builder pattern.**

The tests have been properly refactored to use the new `ResourceInferenceServiceBuilder` instead of direct instantiation of `ResourceInferenceService`. This maintains test behavior while adapting to the architectural changes.



Also applies to: 36-37, 56-59, 83-86, 106-109, 119-120, 147-150, 160-166

---

`135-166`: **Comprehensive Kind inference tests.**

The new tests for Kind inference follow the established pattern and verify both setting Kind when it's null and preserving it when it's already set. Well-structured and thorough.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs (2)</summary>

`13-67`: **Well-designed builder implementation.**

The builder pattern is implemented correctly with methods for adding strategies and building the final service. The static factory methods for creating empty and default instances provide convenience while maintaining flexibility. The design allows for easy extension with new inference strategies.

---

`42-44`: <details>
<summary>❓ Verification inconclusive</summary>

**Clarify the SchemeMapperBuilder.Build() usage.**

The current implementation calls `Build()` but ignores its return value, then returns the builder with a null-forgiving operator. The intention and correctness of this pattern should be verified.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how Build() is used in DubUrl.Mapping.SchemeMapperBuilder

# Find the SchemeMapperBuilder.Build() method definition
rg -A 10 "Build\(\)" | grep -A 10 "SchemeMapperBuilder"
```

Length of output: 55

---

Let’s locate the actual `SchemeMapperBuilder` implementation and inspect its `Build()` method.

---

🏁 Script executed:

```shell
#!/bin/bash
# 1. Find the file defining SchemeMapperBuilder
echo ">> Locating SchemeMapperBuilder implementation..."
builder_file=$(rg -l "class SchemeMapperBuilder" --glob "*.cs")
echo "Found in: $builder_file"

# 2. Show the Build() method around its declaration
echo -e "\n>> Showing Build() method in $builder_file"
rg -n "Build\s*\(" -A 5 "$builder_file"
```

Length of output: 351

---



</details>

**Clarify SchemeMapperBuilder.Build() return usage**

The code currently calls `builder.Build();` but ignores its return value, then returns `builder!`. If `Build()` returns a new, fully-initialized mapper rather than mutating the builder in place, you’ll end up returning an incomplete object. Please confirm whether you should instead be returning the result of `Build()`, for example:

```csharp
// If Build() returns the final mapper:
return builder.Build();
```

or, if `Build()` mutates the builder and returns it, that the null-forgiving operator is still needed.

Location:
- File: `src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs`
- Lines: 42–44

Original snippet:
```csharp
    builder.Build();
    return builder!;
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T06:32:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs (2)</summary><blockquote>

`15-16`: **Add defensive-null check in constructor**

`strategies.ToArray()` will throw a `NullReferenceException` if a caller accidentally passes `null`. It is safer to fail fast with a clear exception message.


```diff
-    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
-        => _strategies = strategies.ToArray();
+    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
+    {
+        if (strategies is null)
+            throw new ArgumentNullException(nameof(strategies));
+
+        _strategies = strategies.ToArray();
+    }
```

---

`18-36`: **Guard against null arguments inside `InferIfUnset`**

`InferIfUnset` can be called with null arguments from outside this class, resulting in runtime exceptions. Consider adding parameter validation for robustness.


```diff
-    private static void InferIfUnset<T>(
+    private static void InferIfUnset<T>(
         Resource resource,
         Func<Resource, T?> getter,
         Action<Resource, T> setter,
         IEnumerable<IInferenceStrategy<T>> strategies,
         Func<T?, bool> isUnset)
     {
+        if (resource is null)           throw new ArgumentNullException(nameof(resource));
+        if (getter is null)             throw new ArgumentNullException(nameof(getter));
+        if (setter is null)             throw new ArgumentNullException(nameof(setter));
+        if (strategies is null)         throw new ArgumentNullException(nameof(strategies));
+        if (isUnset is null)            throw new ArgumentNullException(nameof(isUnset));
+
         if (isUnset(getter(resource)))
         {
             foreach (var strategy in strategies)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs (1)</summary><blockquote>

`48-50`: **Good addition of type verification before accessing properties**

The added type check `Assert.That(dialect, Is.TypeOf<TableDelimitedDialect>())` ensures the inferred dialect is of the expected concrete type before casting. This is a good practice when working with inheritance hierarchies and prevents potential runtime errors if the inference system is modified to return different implementations of the base type.

For consistency with the FormatBasedDialectInferenceTests.cs file, consider also wrapping related assertions in `Assert.EnterMultipleScope()` blocks.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a44ef21aef10a153b8b11207faa557972198e2c4 and 83ea1487aff83168e28973745a96e91e31a0c85a.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs` (3 hunks)
* `src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs` (1 hunks)
* `src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs` (7 hunks)
* `src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/IDialectInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs` (2 hunks)
* `src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* src/Packata.ResourceReaders/Inference/IDialectInference.cs
* src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
* src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs
* src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs
* src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>src/Packata.ResourceReaders.Testing/Inference/MediaTypeBasedDialectInferenceTests.cs (1)</summary><blockquote>

<details>
<summary>src/Packata.Core/TableDelimitedDialect.cs (1)</summary>

* `TableDelimitedDialect` (11-110)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs (1)</summary><blockquote>

<details>
<summary>src/Packata.Core/TableDelimitedDialect.cs (1)</summary>

* `TableDelimitedDialect` (11-110)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs (12)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs (2)</summary>

* `ResourceInferenceService` (23-24)
* `ResourceInferenceService` (55-67)

</details>
<details>
<summary>src/Packata.Core/Resource.Props.cs (1)</summary>

* `Resource` (13-145)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs (1)</summary>

* `TryInfer` (19-32)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs (1)</summary>

* `TryInfer` (12-13)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs (1)</summary>

* `TryInfer` (12-38)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs (1)</summary>

* `TryInfer` (20-55)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs (1)</summary>

* `TryInfer` (20-36)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs (1)</summary>

* `TryInfer` (19-41)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs (1)</summary>

* `TryInfer` (12-12)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs (1)</summary>

* `TryInfer` (12-49)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs (1)</summary>

* `TryInfer` (16-34)

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/IResourceInferenceService.cs (1)</summary>

* `Enrich` (11-11)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders.Testing/Inference/FormatBasedDialectInferenceTests.cs (3)</summary>

`18-22`: **Good improvement in test organization and error reporting**

Using `Assert.EnterMultipleScope()` improves the error reporting by grouping related assertions together. If multiple assertions fail, all failures will be reported at once, which makes debugging easier.

---

`37-48`: **Excellent addition of type verification before accessing properties**

The added type check `Assert.That(dialect, Is.TypeOf<TableDelimitedDialect>())` ensures the inferred dialect is of the expected concrete type before casting. This is a good practice when working with inheritance hierarchies and prevents potential runtime errors if the inference system is modified to return different implementations of the base type.

The use of an assertion scope for property assertions further improves test clarity and error reporting.

---

`59-63`: **Good improvement in test organization and error reporting**

Similar to the first test case, using `Assert.EnterMultipleScope()` improves error reporting by grouping related assertions together.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs (2)</summary>

`56-62`: **The dialect inference implementation looks correct**

The code now correctly uses the `TableDialect` type for dialect inference. Since the `Dialect` property in `Resource` class is of type `TableDialect?`, there's no risk of overwriting non-tabular dialects.

---

`64-70`: **Good implementation of the new Kind property inference**

The kind inference follows the same pattern as other property inferences and correctly uses the `IKindInference` strategies. From the provided context, the `SchemeBasedKindInference` implementation would classify resources as "virtual", "local", "remote", or "service" based on the resource's paths or data presence.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T07:13:22Z

Review comments:
diff_hunk: @@ -10,82 +10,63 @@
 namespace Packata.ResourceReaders.Inference;
 public class ResourceInferenceService : IResourceInferenceService
 {
-    private readonly IDialectInference[] _dialectStrategies;
-    private readonly IFormatInference[] _formatStrategies;
-    private readonly ICompressionInference[] _compressionStrategies;
+    private readonly IReadOnlyCollection<IInferenceStrategy> _strategies;
 
-    private static readonly IDictionary<string, string> _compressionMappings = GetCompressionMappings();
-    private static Dictionary<string, string> GetCompressionMappings()
-    {
-        var factory = DecompressorFactory.Streaming();
-        var keys = factory.GetSupportedKeys();
-        var dict = new Dictionary<string, string>(keys.Length);
-        foreach (var key in keys)
-            dict.Add(key, factory.GetCompression(key));
-        return dict;
-    }
-
-    public static ResourceInferenceService None => _none;
-    private static readonly ResourceInferenceService _none = new ([], [], []);
-
-    public static ResourceInferenceService Instance => _instance;
-    private static readonly ResourceInferenceService _instance = CreateInstance();
+    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
+        => _strategies = strategies.ToArray();
path: src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add defensive-null check in constructor**

`strategies.ToArray()` will throw a `NullReferenceException` if a caller accidentally passes `null`.  It is safer to fail fast with a clear exception message.

```diff
-    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
-        => _strategies = strategies.ToArray();
+    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
+    {
+        if (strategies is null)
+            throw new ArgumentNullException(nameof(strategies));
+
+        _strategies = strategies.ToArray();
+    }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
    {
        if (strategies is null)
            throw new ArgumentNullException(nameof(strategies));

        _strategies = strategies.ToArray();
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T06:32:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,82 +10,63 @@
 namespace Packata.ResourceReaders.Inference;
 public class ResourceInferenceService : IResourceInferenceService
 {
-    private readonly IDialectInference[] _dialectStrategies;
-    private readonly IFormatInference[] _formatStrategies;
-    private readonly ICompressionInference[] _compressionStrategies;
+    private readonly IReadOnlyCollection<IInferenceStrategy> _strategies;
 
-    private static readonly IDictionary<string, string> _compressionMappings = GetCompressionMappings();
-    private static Dictionary<string, string> GetCompressionMappings()
-    {
-        var factory = DecompressorFactory.Streaming();
-        var keys = factory.GetSupportedKeys();
-        var dict = new Dictionary<string, string>(keys.Length);
-        foreach (var key in keys)
-            dict.Add(key, factory.GetCompression(key));
-        return dict;
-    }
-
-    public static ResourceInferenceService None => _none;
-    private static readonly ResourceInferenceService _none = new ([], [], []);
-
-    public static ResourceInferenceService Instance => _instance;
-    private static readonly ResourceInferenceService _instance = CreateInstance();
+    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
+        => _strategies = strategies.ToArray();
 
-    private static ResourceInferenceService CreateInstance()
-        => new (
-            [
-                new FormatBasedDialectInference(),
-                new MediaTypeBasedDialectInference(),
-                new ExtensionBasedDialectInference(new ExtractExtensionFromPathsService()),
-            ],
-            [
-                new MediaTypeBasedFormatInference(),
-                new ExtensionBasedFormatInference(new ExtractExtensionFromPathsService()),
-            ],
-            [
-                new MediaTypeBasedCompressionInference(_compressionMappings),
-                new ExtensionBasedCompressionInference(new ExtractExtensionFromPathsService(), _compressionMappings)
-            ]);
-
-    protected internal ResourceInferenceService(IDialectInference[] dialectStrategies, IFormatInference[] formatStrategies, ICompressionInference[] compressionStrategies)
-        => (_dialectStrategies, _formatStrategies, _compressionStrategies) = (dialectStrategies, formatStrategies, compressionStrategies);
-
-    public void Enrich(Resource resource)
+    private static void InferIfUnset<T>(
+        Resource resource,
+        Func<Resource, T?> getter,
+        Action<Resource, T> setter,
+        IEnumerable<IInferenceStrategy<T>> strategies,
+        Func<T?, bool> isUnset)
     {
-        if (string.IsNullOrEmpty(resource.Compression))
+        if (isUnset(getter(resource)))
         {
-            foreach (var strategy in _compressionStrategies)
+            foreach (var strategy in strategies)
             {
-                if (strategy.TryInfer(resource, out var compression))
+                if (strategy.TryInfer(resource, out var value))
                 {
-                    resource.Compression = compression;
+                    setter(resource, value);
                     break;
                 }
             }
         }
+    }
 
-        if (string.IsNullOrEmpty(resource.Format))
-        {
-            foreach (var strategy in _formatStrategies)
-            {
-                if (strategy.TryInfer(resource, out var format))
-                {
-                    resource.Format = format;
-                    break;
-                }
-            }
-        }
+    public void Enrich(Resource resource)
+    {
+        InferIfUnset(
+            resource,
+            r => r.Compression,
+            (r, v) => r.Compression = v,
+            _strategies.OfType<ICompressionInference>(),
+            string.IsNullOrEmpty
+        );
 
-        if (resource.Dialect is null)
-        {
-            foreach (var strategy in _dialectStrategies)
-            {
-                if (strategy.TryInfer(resource, out var dialect))
-                {
-                    resource.Dialect = dialect;
-                    break;
-                }
-            }
-        }
+        InferIfUnset(
+            resource,
+            r => r.Format,
+            (r, v) => r.Format = v,
+            _strategies.OfType<IFormatInference>(),
+            string.IsNullOrEmpty
+        );
+
+        InferIfUnset<TableDelimitedDialect>(
+            resource,
+            r => r.Dialect as TableDelimitedDialect,
+            (r, v) => r.Dialect = v,
+            _strategies.OfType<IDialectInference>(),
+            v => v == null
+        );
path: src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`Dialect` detection may overwrite an already-set non-tabular dialect**

`r => r.Dialect as TableDelimitedDialect` returns `null` whenever the dialect is set to _any_ other implementation, causing the method to treat the property as “unset” and potentially overwrite a valid value.

Consider checking the property directly for `null` instead:

```diff
-        InferIfUnset<TableDelimitedDialect>(
-            resource,
-            r => r.Dialect as TableDelimitedDialect,
-            (r, v) => r.Dialect = v,
+        InferIfUnset<IDialect>(
+            resource,
+            r => r.Dialect,
+            (r, v) => r.Dialect = v,
             _strategies.OfType<IDialectInference>(),
-            v => v == null
+            v => v is null
         );
```

(Adjust generic constraints/interfaces as needed.)  
Failing to do so could silently change semantics for resources whose dialect is already provided.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        InferIfUnset<IDialect>(
            resource,
            r => r.Dialect,
            (r, v) => r.Dialect = v,
            _strategies.OfType<IDialectInference>(),
            v => v is null
        );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T06:32:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,82 +10,63 @@
 namespace Packata.ResourceReaders.Inference;
 public class ResourceInferenceService : IResourceInferenceService
 {
-    private readonly IDialectInference[] _dialectStrategies;
-    private readonly IFormatInference[] _formatStrategies;
-    private readonly ICompressionInference[] _compressionStrategies;
+    private readonly IReadOnlyCollection<IInferenceStrategy> _strategies;
 
-    private static readonly IDictionary<string, string> _compressionMappings = GetCompressionMappings();
-    private static Dictionary<string, string> GetCompressionMappings()
-    {
-        var factory = DecompressorFactory.Streaming();
-        var keys = factory.GetSupportedKeys();
-        var dict = new Dictionary<string, string>(keys.Length);
-        foreach (var key in keys)
-            dict.Add(key, factory.GetCompression(key));
-        return dict;
-    }
-
-    public static ResourceInferenceService None => _none;
-    private static readonly ResourceInferenceService _none = new ([], [], []);
-
-    public static ResourceInferenceService Instance => _instance;
-    private static readonly ResourceInferenceService _instance = CreateInstance();
+    protected internal ResourceInferenceService(IEnumerable<IInferenceStrategy> strategies)
+        => _strategies = strategies.ToArray();
 
-    private static ResourceInferenceService CreateInstance()
-        => new (
-            [
-                new FormatBasedDialectInference(),
-                new MediaTypeBasedDialectInference(),
-                new ExtensionBasedDialectInference(new ExtractExtensionFromPathsService()),
-            ],
-            [
-                new MediaTypeBasedFormatInference(),
-                new ExtensionBasedFormatInference(new ExtractExtensionFromPathsService()),
-            ],
-            [
-                new MediaTypeBasedCompressionInference(_compressionMappings),
-                new ExtensionBasedCompressionInference(new ExtractExtensionFromPathsService(), _compressionMappings)
-            ]);
-
-    protected internal ResourceInferenceService(IDialectInference[] dialectStrategies, IFormatInference[] formatStrategies, ICompressionInference[] compressionStrategies)
-        => (_dialectStrategies, _formatStrategies, _compressionStrategies) = (dialectStrategies, formatStrategies, compressionStrategies);
-
-    public void Enrich(Resource resource)
+    private static void InferIfUnset<T>(
+        Resource resource,
+        Func<Resource, T?> getter,
+        Action<Resource, T> setter,
+        IEnumerable<IInferenceStrategy<T>> strategies,
+        Func<T?, bool> isUnset)
     {
-        if (string.IsNullOrEmpty(resource.Compression))
+        if (isUnset(getter(resource)))
         {
-            foreach (var strategy in _compressionStrategies)
+            foreach (var strategy in strategies)
             {
-                if (strategy.TryInfer(resource, out var compression))
+                if (strategy.TryInfer(resource, out var value))
                 {
-                    resource.Compression = compression;
+                    setter(resource, value);
                     break;
                 }
             }
         }
+    }
path: src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Guard against null arguments inside `InferIfUnset`**

`InferIfUnset` is part of the public surface (used by `Enrich`) and can be called with a `null` resource or `null` strategy list from outside this class, resulting in runtime exceptions.  
Adding early parameter validation keeps the method self-contained and more robust.

```diff
-    private static void InferIfUnset<T>(
+    private static void InferIfUnset<T>(
         Resource resource,
         Func<Resource, T?> getter,
         Action<Resource, T> setter,
         IEnumerable<IInferenceStrategy<T>> strategies,
         Func<T?, bool> isUnset)
     {
+        if (resource is null)           throw new ArgumentNullException(nameof(resource));
+        if (getter is null)             throw new ArgumentNullException(nameof(getter));
+        if (setter is null)             throw new ArgumentNullException(nameof(setter));
+        if (strategies is null)         throw new ArgumentNullException(nameof(strategies));
+        if (isUnset is null)            throw new ArgumentNullException(nameof(isUnset));
+
         if (isUnset(getter(resource)))
         {
             foreach (var strategy in strategies)
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    private static void InferIfUnset<T>(
        Resource resource,
        Func<Resource, T?> getter,
        Action<Resource, T> setter,
        IEnumerable<IInferenceStrategy<T>> strategies,
        Func<T?, bool> isUnset)
    {
        if (resource is null)     throw new ArgumentNullException(nameof(resource));
        if (getter is null)       throw new ArgumentNullException(nameof(getter));
        if (setter is null)       throw new ArgumentNullException(nameof(setter));
        if (strategies is null)   throw new ArgumentNullException(nameof(strategies));
        if (isUnset is null)      throw new ArgumentNullException(nameof(isUnset));

        if (isUnset(getter(resource)))
        {
            foreach (var strategy in strategies)
            {
                if (strategy.TryInfer(resource, out var value))
                {
                    setter(resource, value);
                    break;
                }
            }
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T06:32:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-10T06:23:16Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a new "kind" property to the `Resource` class, representing the resource's nature (e.g., "local", "remote", "service", "virtual"). It adds inference strategies and infrastructure to automatically deduce and set this property, including a scheme-based kind inference. The resource inference service and builder are refactored for extensibility and unified strategy handling, and corresponding tests and serialization data are updated to support and verify the new functionality.

## Changes

| File(s)                                                                                           | Change Summary |
|-------------------------------------------------------------------------------------------------|----------------|
| `src/Packata.Core/Resource.Props.cs`                                                            | Added nullable `Kind` property to `Resource` class to represent resource type/nature. |
| `src/Packata.Core.Testing/Serialization/ExtensionSerializerTests.cs`                            | Added test for deserialization of the new `Kind` property in resources. |
| `src/Packata.Core.Testing/Serialization/Json/Resources/extension.json`<br>`src/Packata.Core.Testing/Serialization/Yaml/Resources/extension.yaml` | Added `"kind": "local"`/`kind: local` to resource descriptors in test data files. |
| `src/Packata.ResourceReaders/Inference/IInferenceStrategy.cs`                                   | Introduced generic `IInferenceStrategy<T>` and marker interface `IInferenceStrategy`. |
| `src/Packata.ResourceReaders/Inference/IKindInference.cs`                                       | Added `IKindInference` interface for kind inference strategies. |
| `src/Packata.ResourceReaders/Inference/SchemeBasedKindInference.cs`                             | Added `SchemeBasedKindInference` class for inferring resource kind based on path schemes. |
| `src/Packata.ResourceReaders/Inference/ResourceInferenceServiceBuilder.cs`                      | Introduced builder class for assembling `ResourceInferenceService` instances with customizable strategies; added static `Default` and `None` services. |
| `src/Packata.ResourceReaders/Inference/ResourceInferenceService.cs`                             | Refactored to use unified strategy collection, added generic inference helper, removed static singletons, added support for "kind" inference. |
| `src/Packata.ResourceReaders/Inference/ICompressionInference.cs`<br>`src/Packata.ResourceReaders/Inference/IFormatInference.cs`<br>`src/Packata.ResourceReaders/Inference/IDialectInference.cs` | Refactored interfaces to inherit from `IInferenceStrategy<T>`, removing explicit method declarations. |
| `src/Packata.ResourceReaders/Inference/ExtensionBasedCompressionInference.cs`<br>`src/Packata.ResourceReaders/Inference/ExtensionBasedDialectInference.cs`<br>`src/Packata.ResourceReaders/Inference/ExtensionBasedFormatInference.cs`<br>`src/Packata.ResourceReaders/Inference/FormatBasedDialectInference.cs`<br>`src/Packata.ResourceReaders/Inference/MediaTypeBasedCompressionInference.cs`<br>`src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs`<br>`src/Packata.ResourceReaders/Inference/MediaTypeBasedFormatInference.cs`<br>`src/Packata.ResourceReaders/Inference/BaseCompressionInference.cs` | Updated method signatures to use `[NotNullWhen(true)]` attribute for improved nullability contracts. |
| `src/Packata.ResourceReaders/ResourceReaderFactory.cs`                                          | Updated to use `ResourceInferenceServiceBuilder.Default` for default inference service. |
| `src/Packata.ResourceReaders.Testing/Inference/ResourceInferenceServiceTests.cs`                | Refactored tests to use new builder; added tests for kind inference logic. |
| `src/Packata.ResourceReaders.Testing/Inference/SchemeBasedKindInferenceTests.cs`                | Added tests for scheme-based kind inference. |
| `src/Packata.ResourceReaders.Testing/ResourceReaderFactoryTests.cs`                             | Updated to use `ResourceInferenceServiceBuilder.None` for empty inference service. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Resource as Resource
    participant InferenceService as ResourceInferenceService
    participant KindStrategy as IKindInference
    participant OtherStrategies as IInferenceStrategy<T>

    Resource->>InferenceService: Enrich(resource)
    loop For each property (Compression, Format, Dialect, Kind)
        InferenceService->>OtherStrategies: TryInfer(resource, out value)
        alt property is unset and inference succeeds
            InferenceService->>Resource: Set property to inferred value
        end
    end
    InferenceService->>KindStrategy: TryInfer(resource, out kind)
    alt Kind is unset and inference succeeds
        InferenceService->>Resource: Set Kind property
    end
```

## Possibly related PRs

- Seddryck/Packata#143: Introduced the original inference framework for dialect and compression properties with their inference strategies and tests; this PR extends that infrastructure by adding kind inference and refactoring the inference service and builder.

## Suggested labels

`new-feature`

## Poem

> A hop, a skip, a brand new kind—  
> Now resources are neatly defined!  
> With inference clever and builder anew,  
> The codebase grows strong, robust, and true.  
> From local to service, remote or virtual,  
> Each resource’s nature now feels less controversial.  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Seddryck/Packata&utm_content=177):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-05-10T06:32:56Z
body: ## [Codecov](https://app.codecov.io/gh/Seddryck/Packata/pull/177?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) Report
Attention: Patch coverage is `97.72727%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 77.51%. Comparing base [(`7d63cf1`)](https://app.codecov.io/gh/Seddryck/Packata/commit/7d63cf19fdb301f1f7154cc2ebcff07a5a221245?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) to head [(`83ea148`)](https://app.codecov.io/gh/Seddryck/Packata/commit/83ea1487aff83168e28973745a96e91e31a0c85a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).

| [Files with missing lines](https://app.codecov.io/gh/Seddryck/Packata/pull/177?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) | Patch % | Lines |
|---|---|---|
| [...ourceReaders/Inference/SchemeBasedKindInference.cs](https://app.codecov.io/gh/Seddryck/Packata/pull/177?src=pr&el=tree&filepath=src%2FPackata.ResourceReaders%2FInference%2FSchemeBasedKindInference.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier#diff-c3JjL1BhY2thdGEuUmVzb3VyY2VSZWFkZXJzL0luZmVyZW5jZS9TY2hlbWVCYXNlZEtpbmRJbmZlcmVuY2UuY3M=) | 90.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/Seddryck/Packata/pull/177?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #177      +/-   ##
==========================================
+ Coverage   76.93%   77.51%   +0.57%     
==========================================
  Files         106      108       +2     
  Lines        1795     1841      +46     
  Branches      283      284       +1     
==========================================
+ Hits         1381     1427      +46     
  Misses        319      319              
  Partials       95       95              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Seddryck/Packata/pull/177?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-10T07:12:23Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Seddryck_Packata&pullRequest=177) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=Seddryck_Packata&pullRequest=177&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Seddryck_Packata&pullRequest=177&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Seddryck_Packata&pullRequest=177&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Seddryck_Packata&pullRequest=177&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Seddryck_Packata&pullRequest=177&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Seddryck_Packata&pullRequest=177)


author_association: NONE