Instruction file imported from rajibmahata/Legal-Document-RAG-System-LEXVAULT (
.github/instructions/dev-agent-1-domain-application.instructions.md). Copyright stays with the author.
Dev Agent 1 — Domain + Application Layer
Role
You are responsible for the innermost two layers of the Clean Architecture for LexVault:
LegalDocRAG.Domain— pure business entities, enums, value objects (zero external dependencies)LegalDocRAG.Application— use cases, CQRS commands/queries/handlers, and interface contracts (depends only on Domain + MediatR)
Layer Rules (strictly enforced)
Domain Layer (LegalDocRAG.Domain)
- NO NuGet packages allowed (not even MediatR)
- All classes are plain C# — no framework attributes
- Use
recordfor immutable entities:public sealed record LegalDocument(...) - Use
sealedon all concrete types - Value objects: immutable records with validation in constructor
- Enums in
Enums/subfolder - Entities in
Entities/subfolder
Application Layer (LegalDocRAG.Application)
- Allowed NuGet:
MediatR,FluentValidation - All interfaces are in
Interfaces/— they define contracts implemented by Infrastructure - CQRS: Commands in
**/Commands/, Handlers in**/Handlers/, Queries in**/Queries/ - Handlers receive interfaces via constructor injection — NEVER use concrete types
- Handlers must use
CancellationToken cton all async operations - Validation via
AbstractValidator<T>registered asIValidator<T> - No
HttpClient, noDbContext, no file I/O — call interfaces only
Domain Entities to Implement
Entities/LegalDocument.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record LegalDocument
{
public required string Id { get; init; }
public required string FileName { get; init; }
public required DocType DocType { get; init; }
public required Jurisdiction Jurisdiction { get; init; }
public required DateTimeOffset UploadedAt { get; init; }
public int ChunkCount { get; init; }
public string? Description { get; init; }
}
Entities/DocumentChunk.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record DocumentChunk
{
public required string DocId { get; init; }
public required int ChunkIndex { get; init; }
public required string Text { get; init; }
public string? SectionTitle { get; init; }
public int PageNumber { get; init; }
public IReadOnlyList<string> ClauseTypes { get; init; } = [];
public IReadOnlyList<string> LegalEntities { get; init; } = [];
public IReadOnlyList<string> RiskSignals { get; init; } = [];
public string? ObligationType { get; init; }
public string? Jurisdiction { get; init; }
}
Entities/ConfidenceReport.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record ConfidenceReport
{
public required string DocumentName { get; init; }
public required int OverallScore { get; init; } // 0-100
public required int DenseScore { get; init; } // 0-100
public required int SparseScore { get; init; } // 0-100
public required int ClauseHitRate { get; init; } // 0-100
public required string Verdict { get; init; }
public required long ProcessingTimeMs { get; init; }
public IReadOnlyList<ClauseHit> ClauseHits { get; init; } = [];
public IReadOnlyList<string> MissingClauses { get; init; } = [];
public IReadOnlyList<string> RiskFlags { get; init; } = [];
public IReadOnlyList<MatchedDocument> TopMatchingDocuments { get; init; } = [];
public IReadOnlyList<string> MatchedKeywords { get; init; } = [];
public string? Recommendation { get; init; }
}
Entities/ClauseHit.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record ClauseHit
{
public required string ClauseName { get; init; }
public required bool Matched { get; init; }
public int OptionalHitCount { get; init; }
public required float Weight { get; init; }
}
Entities/MatchedDocument.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record MatchedDocument
{
public required string DocId { get; init; }
public required string Title { get; init; }
public required float Similarity { get; init; }
public required string DocType { get; init; }
}
Entities/ClauseMatchResult.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record ClauseMatchResult
{
public required IReadOnlyList<ClauseHit> Hits { get; init; }
public required float HitRate { get; init; } // 0.0–1.0
public required IReadOnlyList<string> MissingClauses { get; init; }
public required IReadOnlyList<string> MatchedKeywords { get; init; }
}
Entities/KnowledgeBaseStats.cs
namespace LegalDocRAG.Domain.Entities;
public sealed record KnowledgeBaseStats
{
public required long TotalChunks { get; init; }
public required int TotalDocuments { get; init; }
public required IReadOnlyList<string> SupportedClauses { get; init; }
public required IReadOnlyDictionary<string, int> DocumentsByType { get; init; }
public required IReadOnlyDictionary<string, int> DocumentsByJurisdiction { get; init; }
}
Enums/DocType.cs
namespace LegalDocRAG.Domain.Enums;
public enum DocType { Contract, Statute, CaseLaw, Policy, SOP, NDA, Other }
Enums/Jurisdiction.cs
namespace LegalDocRAG.Domain.Enums;
public enum Jurisdiction { IN, US, UK, EU, AU, SG, Unknown }
Application Interfaces to Implement
Interfaces/IVectorRepository.cs
namespace LegalDocRAG.Application.Interfaces;
public interface IVectorRepository
{
Task EnsureCollectionExistsAsync(CancellationToken ct = default);
Task UpsertChunksAsync(IEnumerable<DocumentChunk> chunks, IEnumerable<float[]> embeddings, CancellationToken ct = default);
Task<List<ScoredChunk>> HybridSearchAsync(float[] denseVector, string? jurisdictionFilter, int topK = 20, CancellationToken ct = default);
Task DeleteDocumentAsync(string docId, CancellationToken ct = default);
Task<List<LegalDocument>> ListDocumentsAsync(CancellationToken ct = default);
Task<KnowledgeBaseStats> GetStatsAsync(CancellationToken ct = default);
}
public sealed record ScoredChunk
{
public required DocumentChunk Chunk { get; init; }
public required float Score { get; init; }
}
Interfaces/IEmbeddingService.cs
namespace LegalDocRAG.Application.Interfaces;
public interface IEmbeddingService
{
Task<float[]> GenerateEmbeddingAsync(string text, CancellationToken ct = default);
Task<IReadOnlyList<float[]>> GenerateBatchEmbeddingsAsync(IEnumerable<string> texts, CancellationToken ct = default);
}
Interfaces/IDocumentChunker.cs
namespace LegalDocRAG.Application.Interfaces;
public interface IDocumentChunker
{
IEnumerable<DocumentChunk> Chunk(string rawText, string docId, string? jurisdiction = null);
}
Interfaces/IClauseMatchEngine.cs
namespace LegalDocRAG.Application.Interfaces;
public interface IClauseMatchEngine
{
ClauseMatchResult MatchClauses(string documentText);
}
Interfaces/IDocumentParser.cs
namespace LegalDocRAG.Application.Interfaces;
public interface IDocumentParser
{
Task<string> ParseAsync(Stream fileStream, string fileExtension, CancellationToken ct = default);
}
Interfaces/ILlmMetadataExtractor.cs
namespace LegalDocRAG.Application.Interfaces;
public interface ILlmMetadataExtractor
{
/// <summary>Called ONCE per document during ingestion. Never called during scoring.</summary>
Task<DocumentMetadata> ExtractMetadataAsync(string documentText, CancellationToken ct = default);
}
public sealed record DocumentMetadata
{
public IReadOnlyList<string> ClauseTypes { get; init; } = [];
public IReadOnlyList<string> LegalEntities { get; init; } = [];
public string? Jurisdiction { get; init; }
public string? ObligationType { get; init; }
public IReadOnlyList<string> RiskSignals { get; init; } = [];
public IReadOnlyList<string> KeyDates { get; init; } = [];
public IReadOnlyList<string> DefinedTerms { get; init; } = [];
}
Interfaces/IIngestionJobService.cs
namespace LegalDocRAG.Application.Interfaces;
public interface IIngestionJobService
{
Task<string> EnqueueIngestionAsync(string filePath, string fileName, DocType docType, Jurisdiction jurisdiction, CancellationToken ct = default);
Task<string> GetJobStatusAsync(string jobId, CancellationToken ct = default);
}
CQRS to Implement
Ingestion Command
// Ingestion/Commands/IngestDocumentCommand.cs
namespace LegalDocRAG.Application.Ingestion.Commands;
public sealed record IngestDocumentCommand : IRequest<string> // returns jobId
{
public required string FileName { get; init; }
public required string FilePath { get; init; }
public required DocType DocType { get; init; }
public required Jurisdiction Jurisdiction { get; init; }
}
Ingestion Handler
The handler must:
- Call
IDocumentParser.ParseAsyncto get raw text - Call
IDocumentChunker.Chunkto get chunks - Call
ILlmMetadataExtractor.ExtractMetadataAsyncon first 5 chunks (representative sample) - Enrich all chunks with extracted metadata
- Call
IEmbeddingService.GenerateBatchEmbeddingsAsyncon all chunks - Call
IVectorRepository.UpsertChunksAsyncto store in Qdrant
Scoring Query
// Scoring/Queries/ScoreDocumentQuery.cs
namespace LegalDocRAG.Application.Scoring.Queries;
public sealed record ScoreDocumentQuery : IRequest<ConfidenceReport>
{
public required string FileName { get; init; }
public required string RawText { get; init; }
public string? JurisdictionFilter { get; init; }
}
Scoring Handler
The handler must:
- Call
IDocumentChunker.Chunkon the raw text - Call
IEmbeddingService.GenerateBatchEmbeddingsAsyncon all chunks - For each chunk, call
IVectorRepository.HybridSearchAsync— take the best score across chunks - Call
IClauseMatchEngine.MatchClauseson the full raw text - Call
IHybridScoreEngine.CalculateScore(injected interface) with the three signals - Return assembled
ConfidenceReport
Critical: No Infrastructure Leakage
- Handlers must NOT reference
HttpClient,QdrantClient,OpenAIClientetc. - Only interface types defined in
Application/Interfaces/may be constructor-injected - If you need a service, define its interface in Application and implement it in Infrastructure