refactor: DEBT-016 + DEBT-024 - Remove VS-02 dead code, verify test FK handling
**DEBT-016: VS-02 Dead Code Removal (Medium/Low - 2 pts)** - ✅ Deleted 3 dead-code files: - src/KArtSell.Host/Features/SecurityMaster/VS02_SyncSecurityMasterEndpoint.cs - src/KArtSell.Host/Features/SecurityMaster/VS02_SecurityMasterJobs.cs - src/KArtSell.Modules.ModelOperations/Domain/VS02_SecurityMasterPolicy.cs - ✅ Deleted empty SecurityMaster folder Verification: - Endpoints never registered (DISABLED comment in Program.cs) - Schema never created (no migration in git) - No references in codebase - Complies with AGENTS.md v16.0 "necessity-driven" principle **DEBT-024: Integration Test FK Handling (Low/Low - 1 pt)** - ✅ Verified: All DB tests (TradeExecutionTests) correctly seed parent rows - Every Trade creation calls SeedSellDecisionAsync() - Pure-logic tests don't touch DB - No FK constraint violations - Status: Already resolved in current codebase **TECH_DEBT_REGISTER Updates:** - DEBT-016: Backlog → Completed - DEBT-024: Backlog → Confirmed Already Resolved - Cumulative Q3 paydown: +2 pts (DEBT-007: 2 pts + DEBT-016: 2 pts = 4 pts = 100% of target) Governance: AGENTS.md v16.0 compliance - ✅ SOLID: Single responsibility (dead code removal is pure cleanup) - ✅ Necessity: No references, endpoints disabled, schema never created - ✅ Simplicity: Mechanical deletion, no behavior change - ✅ Traceability: DEBT-016 reference in commit message Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,177 +0,0 @@
|
||||
namespace KArtSell.Modules.ModelOperations.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// VS-02 DOMAIN: Security Master Synchronization Policy
|
||||
///
|
||||
/// Handles:
|
||||
/// - Conflict resolution (last-write-wins)
|
||||
/// - Permission rule validation
|
||||
/// - Version management
|
||||
/// - Idempotency keys
|
||||
///
|
||||
/// Pure logic, no I/O, testable, deterministic.
|
||||
/// </summary>
|
||||
|
||||
public record SecurityRule(
|
||||
Guid RuleId,
|
||||
string ResourceName,
|
||||
string Action,
|
||||
int Version,
|
||||
DateTime EffectiveAt,
|
||||
DateTime? ExpiresAt,
|
||||
DateTime PublishedAt,
|
||||
string CorrelationId);
|
||||
|
||||
public record RolePermissionAssignment(
|
||||
Guid RoleId,
|
||||
Guid RuleId,
|
||||
int Version,
|
||||
DateTime AssignedAt,
|
||||
DateTime? RemovedAt);
|
||||
|
||||
public record SyncState(
|
||||
int LocalVersion,
|
||||
int RemoteVersion,
|
||||
List<SecurityRule> LocalRules,
|
||||
List<SecurityRule> RemoteRules,
|
||||
string IdempotencyKey,
|
||||
string CorrelationId);
|
||||
|
||||
public record SyncResult(
|
||||
bool IsSuccess,
|
||||
int NewVersion,
|
||||
List<SecurityRule> AppliedRules,
|
||||
List<string> Conflicts,
|
||||
string? ErrorMessage,
|
||||
string CorrelationId);
|
||||
|
||||
public static class SecurityMasterPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Determine sync action: accept, reject, or rollback
|
||||
///
|
||||
/// Rules:
|
||||
/// 1. If localVersion >= remoteVersion: Already synced (idempotent)
|
||||
/// 2. If localVersion < remoteVersion: Accept all remote rules
|
||||
/// 3. Version conflict: Reject with 409
|
||||
/// 4. Last-write-wins per rule (by PublishedAt timestamp)
|
||||
/// </summary>
|
||||
public static SyncResult ResolveSyncConflict(SyncState state)
|
||||
{
|
||||
if (state.LocalVersion > state.RemoteVersion)
|
||||
{
|
||||
return new SyncResult(
|
||||
IsSuccess: true,
|
||||
NewVersion: state.LocalVersion,
|
||||
AppliedRules: new(),
|
||||
Conflicts: new(),
|
||||
ErrorMessage: "Local version already ahead, no sync needed",
|
||||
CorrelationId: state.CorrelationId);
|
||||
}
|
||||
|
||||
if (state.LocalVersion == state.RemoteVersion)
|
||||
{
|
||||
return new SyncResult(
|
||||
IsSuccess: true,
|
||||
NewVersion: state.LocalVersion,
|
||||
AppliedRules: new(),
|
||||
Conflicts: new(),
|
||||
ErrorMessage: "Versions match, idempotent",
|
||||
CorrelationId: state.CorrelationId);
|
||||
}
|
||||
|
||||
var conflicts = new List<string>();
|
||||
var rulesToApply = new List<SecurityRule>();
|
||||
|
||||
foreach (var remoteRule in state.RemoteRules)
|
||||
{
|
||||
var localRule = state.LocalRules.FirstOrDefault(r => r.RuleId == remoteRule.RuleId);
|
||||
|
||||
if (localRule == null)
|
||||
{
|
||||
rulesToApply.Add(remoteRule);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localRule.PublishedAt < remoteRule.PublishedAt)
|
||||
{
|
||||
rulesToApply.Add(remoteRule);
|
||||
}
|
||||
else if (localRule.PublishedAt == remoteRule.PublishedAt && localRule.Version < remoteRule.Version)
|
||||
{
|
||||
rulesToApply.Add(remoteRule);
|
||||
conflicts.Add($"Version conflict on rule {remoteRule.RuleId}: local {localRule.Version}, remote {remoteRule.Version}");
|
||||
}
|
||||
}
|
||||
|
||||
return new SyncResult(
|
||||
IsSuccess: true,
|
||||
NewVersion: state.RemoteVersion,
|
||||
AppliedRules: rulesToApply,
|
||||
Conflicts: conflicts,
|
||||
ErrorMessage: null,
|
||||
CorrelationId: state.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate rule before applying
|
||||
///
|
||||
/// Checks:
|
||||
/// - Resource name not empty
|
||||
/// - Action in {read, write, execute}
|
||||
/// - EffectiveAt <= ExpiresAt (if set)
|
||||
/// - Timestamps in UTC
|
||||
/// </summary>
|
||||
public static (bool IsValid, List<string> Errors) ValidateRule(SecurityRule rule)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rule.ResourceName))
|
||||
errors.Add("ResourceName cannot be empty");
|
||||
|
||||
var validActions = new[] { "read", "write", "execute" };
|
||||
if (!validActions.Contains(rule.Action.ToLowerInvariant()))
|
||||
errors.Add($"Action must be one of: {string.Join(", ", validActions)}");
|
||||
|
||||
if (rule.ExpiresAt.HasValue && rule.EffectiveAt > rule.ExpiresAt)
|
||||
errors.Add("EffectiveAt must be before or equal to ExpiresAt");
|
||||
|
||||
if (rule.PublishedAt.Kind != DateTimeKind.Utc)
|
||||
errors.Add("PublishedAt must be UTC");
|
||||
|
||||
return (errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if rule is active at given time
|
||||
/// </summary>
|
||||
public static bool IsRuleActive(SecurityRule rule, DateTime asOf)
|
||||
{
|
||||
if (asOf < rule.EffectiveAt)
|
||||
return false;
|
||||
|
||||
if (rule.ExpiresAt.HasValue && asOf > rule.ExpiresAt)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create idempotency key for sync operation
|
||||
/// Format: {fromVersion}:{correlationId}
|
||||
/// </summary>
|
||||
public static string CreateIdempotencyKey(int fromVersion, string correlationId)
|
||||
{
|
||||
return $"sync-{fromVersion}-{correlationId}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect rollback scenario: partial sync that failed mid-transaction
|
||||
///
|
||||
/// If applied rules don't match version increment, rollback needed.
|
||||
/// </summary>
|
||||
public static bool RequiresRollback(int appliedRuleCount, int versionIncrement)
|
||||
{
|
||||
return appliedRuleCount == 0 && versionIncrement > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user