Skip to content

Commit 2ba28b5

Browse files
committed
fix: sanitize inverted affiliation dates in git integration
Reject inverted or incomplete project-registry stint date ranges before they are written to member organizations and segment affiliations. Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent 0982ee1 commit 2ba28b5

3 files changed

Lines changed: 120 additions & 1 deletion

File tree

services/apps/git_integration/src/crowdgit/models/affiliation_info.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import orjson
88
from loguru import logger
9-
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
9+
from pydantic import BaseModel, Field, TypeAdapter, ValidationError, model_validator
1010

1111

1212
class AffiliationContributor(BaseModel):
@@ -41,6 +41,20 @@ class AffiliationOrganizationStint(BaseModel):
4141

4242
model_config = {"populate_by_name": True}
4343

44+
@model_validator(mode="after")
45+
def sanitize_date_range(self) -> AffiliationOrganizationStint:
46+
if self.date_end is not None and self.date_start is None:
47+
self.date_start = None
48+
self.date_end = None
49+
elif (
50+
self.date_start is not None
51+
and self.date_end is not None
52+
and self.date_end < self.date_start
53+
):
54+
self.date_start = None
55+
self.date_end = None
56+
return self
57+
4458

4559
class AffiliationContributorEntry(BaseModel):
4660
contributor: AffiliationContributor

services/apps/git_integration/src/crowdgit/services/affiliation/affiliation_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ def get_extraction_prompt(self, content_to_analyze: str) -> str:
329329
330330
Time period (only when the file states it):
331331
- "dateStart" and "dateEnd" as ISO dates (YYYY-MM-DD).
332+
- dateStart must be on or before dateEnd; if unclear, use null for both.
332333
- Use null for any bound the file does not state (open-ended or undated).
333334
- When a contributor has multiple affiliations over time, emit a separate
334335
entry for each period. Do not merge, deduplicate, or keep only the latest.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from datetime import date
2+
3+
import orjson
4+
5+
from crowdgit.models.affiliation_info import (
6+
AffiliationContributor,
7+
AffiliationContributorEntry,
8+
AffiliationOrganizationStint,
9+
RepoAffiliationRegistry,
10+
)
11+
12+
13+
def test_stint_nulls_inverted_date_range():
14+
stint = AffiliationOrganizationStint(
15+
domain="example.com",
16+
date_start=date(2025, 8, 11),
17+
date_end=date(2022, 9, 20),
18+
)
19+
20+
assert stint.date_start is None
21+
assert stint.date_end is None
22+
23+
24+
def test_stint_nulls_end_without_start():
25+
stint = AffiliationOrganizationStint(
26+
domain="example.com",
27+
date_end=date(2022, 9, 20),
28+
)
29+
30+
assert stint.date_start is None
31+
assert stint.date_end is None
32+
33+
34+
def test_stint_keeps_valid_date_range():
35+
stint = AffiliationOrganizationStint(
36+
domain="example.com",
37+
date_start=date(2019, 6, 24),
38+
date_end=date(2022, 9, 20),
39+
)
40+
41+
assert stint.date_start == date(2019, 6, 24)
42+
assert stint.date_end == date(2022, 9, 20)
43+
44+
45+
def test_snapshot_load_sanitizes_inverted_dates():
46+
snapshot = [
47+
{
48+
"contributor": {"email": "user@example.com"},
49+
"organizations": [
50+
{
51+
"domain": "example.com",
52+
"dateStart": "2025-08-11",
53+
"dateEnd": "2022-09-20",
54+
}
55+
],
56+
}
57+
]
58+
59+
registry = RepoAffiliationRegistry.from_db(
60+
{
61+
"repoId": "00000000-0000-0000-0000-000000000001",
62+
"status": "success",
63+
"snapshot": orjson.dumps(snapshot).decode(),
64+
}
65+
)
66+
67+
assert registry.snapshot is not None
68+
stint = registry.snapshot[0].organizations[0]
69+
assert stint.date_start is None
70+
assert stint.date_end is None
71+
72+
73+
def test_snapshot_load_keeps_valid_dates():
74+
snapshot = [
75+
AffiliationContributorEntry(
76+
contributor=AffiliationContributor(email="user@example.com"),
77+
organizations=[
78+
AffiliationOrganizationStint(
79+
domain="example.com",
80+
date_start=date(2019, 6, 24),
81+
date_end=date(2022, 9, 20),
82+
)
83+
],
84+
)
85+
]
86+
87+
registry = RepoAffiliationRegistry(
88+
repo_id="00000000-0000-0000-0000-000000000001",
89+
status="success",
90+
snapshot=snapshot,
91+
)
92+
93+
serialized = registry.snapshot_for_db()
94+
loaded = RepoAffiliationRegistry.from_db(
95+
{
96+
"repoId": "00000000-0000-0000-0000-000000000001",
97+
"status": "success",
98+
"snapshot": serialized,
99+
}
100+
)
101+
102+
stint = loaded.snapshot[0].organizations[0]
103+
assert stint.date_start == date(2019, 6, 24)
104+
assert stint.date_end == date(2022, 9, 20)

0 commit comments

Comments
 (0)