Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions app/api/Data/ProductModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,11 @@ public sealed class RequirementFollowUpEvent
public DateTime OccurredAt { get; set; }
}

// An evidence link added to or removed from a requirement, with who did it and when (ADR-054).
// Kind, Label, and Title are copied from the link, which a removal deletes. EvidenceId is the
// link's ID, kept without a foreign key for the same reason. Rows are only ever added.
// A change to a requirement's evidence, with who made it and when (ADR-054, ADR-056). Action is
// "added", "removed", "confirmed" or "rejected" (a suggestion), or "moved_in" or "moved_out",
// where OtherRequirementId is the requirement on the other side of the move. Kind, Label, and
// Title are copied from the link, which a removal deletes. EvidenceId is the link's ID, or a
// rejected suggestion's, kept without a foreign key for the same reason. Rows are only ever added.
public sealed class RequirementEvidenceEvent
{
public Guid Id { get; set; }
Expand All @@ -151,6 +153,29 @@ public sealed class RequirementEvidenceEvent
public required string Title { get; set; }
public required string Actor { get; set; }
public DateTime OccurredAt { get; set; }
public Guid? OtherRequirementId { get; set; }
}

// A pull request whose text closes an issue linked to the requirement (ADR-056). It is not
// evidence: nothing reads this table when counting, reviewing, or judging evidence, and it enters
// requirement_evidence only when a person confirms it (ADR-036). Status is "pending",
// "confirmed", or "rejected"; a decided row stays, so the same pull request is not suggested
// again. Reference is the keyword and issue found, such as "Closes #184", never the text around it.
public sealed class RequirementEvidenceSuggestion
{
public Guid Id { get; set; }
public Guid RequirementId { get; set; }
public long RepositoryId { get; set; }
public int Number { get; set; }
public required string Title { get; set; }
public required string State { get; set; }
public string? Author { get; set; }
public required string Url { get; set; }
public required string Reference { get; set; }
public string Status { get; set; } = "pending";
public DateTime FoundAt { get; set; }
public string? DecidedBy { get; set; }
public DateTime? DecidedAt { get; set; }
}

// A person's decision on a requirement (ADR-038). Evidence is a JSON array of the evidence links
Expand Down
35 changes: 34 additions & 1 deletion app/api/Data/SpecThreadDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed class SpecThreadDbContext(DbContextOptions<SpecThreadDbContext> op
public DbSet<RequirementEvidence> RequirementEvidence => Set<RequirementEvidence>();
public DbSet<RequirementReview> RequirementReviews => Set<RequirementReview>();
public DbSet<RequirementEvidenceEvent> RequirementEvidenceEvents => Set<RequirementEvidenceEvent>();
public DbSet<RequirementEvidenceSuggestion> RequirementEvidenceSuggestions => Set<RequirementEvidenceSuggestion>();
public DbSet<Team> Teams => Set<Team>();
public DbSet<TeamMember> TeamMembers => Set<TeamMember>();
public DbSet<UserOnboarding> UserOnboardings => Set<UserOnboarding>();
Expand Down Expand Up @@ -266,7 +267,10 @@ protected override void OnModelCreating(ModelBuilder model)
var evidenceEvent = model.Entity<RequirementEvidenceEvent>();
evidenceEvent.ToTable("requirement_evidence_events", table =>
{
table.HasCheckConstraint("ck_requirement_evidence_events_action", "action IN ('added', 'removed')");
table.HasCheckConstraint("ck_requirement_evidence_events_action",
"action IN ('added', 'removed', 'confirmed', 'rejected', 'moved_in', 'moved_out')");
table.HasCheckConstraint("ck_requirement_evidence_events_move",
"(action IN ('moved_in', 'moved_out')) = (other_requirement_id IS NOT NULL)");
table.HasCheckConstraint("ck_requirement_evidence_events_kind", "kind IN ('issue', 'pull_request', 'commit', 'release')");
table.HasCheckConstraint("ck_requirement_evidence_events_label", "length(btrim(label)) > 0");
});
Expand All @@ -280,10 +284,39 @@ protected override void OnModelCreating(ModelBuilder model)
evidenceEvent.Property(x => x.Title).HasColumnName("title");
evidenceEvent.Property(x => x.Actor).HasColumnName("actor");
evidenceEvent.Property(x => x.OccurredAt).HasColumnName("occurred_at").HasDefaultValueSql("now()");
evidenceEvent.Property(x => x.OtherRequirementId).HasColumnName("other_requirement_id");
evidenceEvent.HasIndex(x => new { x.RequirementId, x.OccurredAt });
evidenceEvent.HasOne<Requirement>().WithMany().HasForeignKey(x => x.RequirementId).OnDelete(DeleteBehavior.Restrict);
evidenceEvent.HasOne<Requirement>().WithMany().HasForeignKey(x => x.OtherRequirementId).OnDelete(DeleteBehavior.Restrict);
evidenceEvent.HasOne<AuthUser>().WithMany().HasForeignKey(x => x.Actor).OnDelete(DeleteBehavior.Restrict);

var suggestion = model.Entity<RequirementEvidenceSuggestion>();
suggestion.ToTable("requirement_evidence_suggestions", table =>
{
table.HasCheckConstraint("ck_requirement_evidence_suggestions_status", "status IN ('pending', 'confirmed', 'rejected')");
table.HasCheckConstraint("ck_requirement_evidence_suggestions_state", "state IN ('open', 'closed', 'merged')");
table.HasCheckConstraint("ck_requirement_evidence_suggestions_number", "number > 0 AND repository_id > 0");
table.HasCheckConstraint("ck_requirement_evidence_suggestions_decision",
"(status = 'pending') = (decided_by IS NULL) AND (decided_by IS NULL) = (decided_at IS NULL)");
});
suggestion.HasKey(x => x.Id);
suggestion.Property(x => x.Id).HasColumnName("id").HasDefaultValueSql("gen_random_uuid()");
suggestion.Property(x => x.RequirementId).HasColumnName("requirement_id");
suggestion.Property(x => x.RepositoryId).HasColumnName("repository_id");
suggestion.Property(x => x.Number).HasColumnName("number");
suggestion.Property(x => x.Title).HasColumnName("title");
suggestion.Property(x => x.State).HasColumnName("state");
suggestion.Property(x => x.Author).HasColumnName("author");
suggestion.Property(x => x.Url).HasColumnName("url");
suggestion.Property(x => x.Reference).HasColumnName("reference");
suggestion.Property(x => x.Status).HasColumnName("status").HasDefaultValue("pending");
suggestion.Property(x => x.FoundAt).HasColumnName("found_at").HasDefaultValueSql("now()");
suggestion.Property(x => x.DecidedBy).HasColumnName("decided_by");
suggestion.Property(x => x.DecidedAt).HasColumnName("decided_at");
suggestion.HasIndex(x => new { x.RequirementId, x.RepositoryId, x.Number }).IsUnique();
suggestion.HasOne<Requirement>().WithMany().HasForeignKey(x => x.RequirementId).OnDelete(DeleteBehavior.Restrict);
suggestion.HasOne<AuthUser>().WithMany().HasForeignKey(x => x.DecidedBy).OnDelete(DeleteBehavior.Restrict);

var review = model.Entity<RequirementReview>();
review.ToTable("requirement_reviews", table =>
{
Expand Down
Loading
Loading