Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5cab1cf
fix(harvester): address repository client review feedback
ParthAggarwal16 Jul 18, 2026
2f981e2
feat(harvester): implement incremental change detection
ParthAggarwal16 Jul 8, 2026
eb72939
fix(harvester): write checkpoints atomically
ParthAggarwal16 Jul 18, 2026
877bf7a
Use immutable commit ranges for change detection
ParthAggarwal16 Jul 23, 2026
967f146
test: replace mocked /tmp/repo path
ParthAggarwal16 Jul 23, 2026
bb90b14
feat(db): add artifact ingestion persistence models
ParthAggarwal16 Jul 23, 2026
ab615d5
Address CodeRabbit review feedback
ParthAggarwal16 Jul 23, 2026
a6929df
feat(harvester): replace filesystem checkpoint store with postgres
ParthAggarwal16 Jul 25, 2026
92f7f0d
test(harvester): match checkout invocation
ParthAggarwal16 Jul 25, 2026
189d84b
Address review feedback for Week 3
ParthAggarwal16 Jul 26, 2026
9819313
docs : fix Revision ID in the migration file
ParthAggarwal16 Jul 26, 2026
8763f21
fix(harvester): address repository client review feedback
ParthAggarwal16 Jul 18, 2026
1d5a9f0
feat(harvester): implement repository file filtering
ParthAggarwal16 Jul 8, 2026
a13641a
fix(harvester): improve filtering benchmark and sync behavior
ParthAggarwal16 Jul 18, 2026
81b757e
fix(harvester): isolate filter defaults and preserve empty overrides
ParthAggarwal16 Jul 29, 2026
105ac27
Use glob-based path filtering for harvester
ParthAggarwal16 Jul 29, 2026
e193949
feat(harvester): improve file filtering and exclusions
ParthAggarwal16 Jul 29, 2026
d5dde16
test: strengthen FileFilter validation
ParthAggarwal16 Jul 29, 2026
d8cafa9
fix(harvester): address repository client review feedback
ParthAggarwal16 Jul 18, 2026
f3c8eb1
fix(harvester): improve filtering benchmark and sync behavior
ParthAggarwal16 Jul 18, 2026
3221820
feat(harvester): add git diff retrieval pipeline
ParthAggarwal16 Jul 10, 2026
2d04894
feat(harvester): parse unified git diffs
ParthAggarwal16 Jul 10, 2026
67658da
feat(harvester): normalize extracted diff content
ParthAggarwal16 Jul 10, 2026
eaa6ae9
Enhance diff pipeline with metadata and normalization
ParthAggarwal16 Jul 10, 2026
79080d7
fix(harvester): address review feedback
ParthAggarwal16 Jul 18, 2026
4e6b8a5
Harden diff retrieval and isolate network benchmark tests
ParthAggarwal16 Jul 30, 2026
a1b2139
Addressing code rabbit comment
ParthAggarwal16 Jul 30, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ tmp/
cres/*
### Local project management tooling
project management scripts/

.harvester_cache/
165 changes: 165 additions & 0 deletions application/database/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import time
import yaml

from datetime import datetime, timezone
from pprint import pprint

from collections import Counter, defaultdict
Expand Down Expand Up @@ -257,6 +258,108 @@ class StagedChangeSet(BaseModel): # type: ignore
created_at = sqla.Column(sqla.DateTime, nullable=False)


class ArtifactIngestEvent(BaseModel): # type: ignore
"""Tracks one harvested artifact persisted per import run."""

__tablename__ = "artifact_ingest_event"
id = sqla.Column(sqla.String, primary_key=True, default=generate_uuid)
run_id = sqla.Column(
sqla.String,
sqla.ForeignKey("import_run.id", onupdate="CASCADE", ondelete="CASCADE"),
nullable=False,
)
artifact_id = sqla.Column(sqla.String, nullable=False)
harvest_mode = sqla.Column(sqla.String, nullable=False)
event_type = sqla.Column(sqla.String, nullable=False)
source_json = sqla.Column(sqla.Text, nullable=False)
locator_json = sqla.Column(sqla.Text, nullable=False)
artifact_json = sqla.Column(sqla.Text, nullable=False)
harvest_json = sqla.Column(sqla.Text, nullable=False)
observed_at = sqla.Column(sqla.DateTime, nullable=False)
created_at = sqla.Column(sqla.DateTime, nullable=False)

__table_args__ = (
sqla.UniqueConstraint(
run_id,
artifact_id,
name="uq_artifact_ingest_event_run_artifact",
),
)


class IngestChunk(BaseModel): # type: ignore
"""Tracks every chunk belonging to an artifact ingest event."""

__tablename__ = "ingest_chunk"
id = sqla.Column(sqla.String, primary_key=True, default=generate_uuid)
artifact_event_id = sqla.Column(
sqla.String,
sqla.ForeignKey(
"artifact_ingest_event.id",
onupdate="CASCADE",
ondelete="CASCADE",
),
nullable=False,
)
chunk_id = sqla.Column(sqla.String, nullable=False)
text = sqla.Column(sqla.Text, nullable=False)
char_count = sqla.Column(sqla.Integer, nullable=False)
span_json = sqla.Column(sqla.Text, nullable=False)
delta_json = sqla.Column(sqla.Text, nullable=True)
created_at = sqla.Column(sqla.DateTime, nullable=False)

__table_args__ = (
sqla.UniqueConstraint(
artifact_event_id,
chunk_id,
name="uq_ingest_chunk_artifact_chunk",
),
)


class HarvesterCheckpoint(BaseModel): # type: ignore
__tablename__ = "harvester_checkpoint"
repository_id = sqla.Column(sqla.String, primary_key=True)
provider = sqla.Column(sqla.String, nullable=False)
owner = sqla.Column(sqla.String, nullable=False)
repository = sqla.Column(sqla.String, nullable=False)
branch = sqla.Column(sqla.String, nullable=False)
last_processed_commit = sqla.Column(sqla.String, nullable=True)
created_at = sqla.Column(
sqla.DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)
updated_at = sqla.Column(
sqla.DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)
__table_args__ = (
sqla.UniqueConstraint(
"provider",
"owner",
"repository",
"branch",
name="uq_harvester_checkpoint_canonical_source",
),
)


def _serialize_json_value(value: Any) -> str:
return flask_json.dumps(value)


def _normalize_utc_datetime(value: Any) -> Any:
from datetime import datetime, timezone

if isinstance(value, datetime):
if value.tzinfo is None:
return value
return value.astimezone(timezone.utc)
return value


def create_import_run(source: str, version: Optional[str] = None) -> ImportRun:
"""Create and persist an import run record. Returns the new ImportRun."""
from datetime import datetime, timezone
Expand Down Expand Up @@ -304,6 +407,68 @@ def get_previous_import_run(source: str, current_run_id: str) -> Optional[Import
)


def create_artifact_ingest_event(
*,
run_id: str,
artifact_id: str,
harvest_mode: str,
event_type: str,
source_json: Any,
locator_json: Any,
artifact_json: Any,
harvest_json: Any,
observed_at: Any,
) -> ArtifactIngestEvent:
from datetime import datetime, timezone

observed_at = _normalize_utc_datetime(observed_at)

event = ArtifactIngestEvent(
id=generate_uuid(),
run_id=run_id,
artifact_id=artifact_id,
harvest_mode=harvest_mode,
event_type=event_type,
source_json=_serialize_json_value(source_json),
locator_json=_serialize_json_value(locator_json),
artifact_json=_serialize_json_value(artifact_json),
harvest_json=_serialize_json_value(harvest_json),
observed_at=observed_at,
created_at=_normalize_utc_datetime(datetime.now(timezone.utc)),
)
sqla.session.add(event)
sqla.session.commit()
return event


def create_ingest_chunk(
*,
artifact_event_id: str,
chunk_id: str,
text: str,
char_count: int,
span_json: Any,
delta_json: Optional[Any] = None,
) -> IngestChunk:
from datetime import datetime, timezone

chunk = IngestChunk(
id=generate_uuid(),
artifact_event_id=artifact_event_id,
chunk_id=chunk_id,
text=text,
char_count=char_count,
span_json=_serialize_json_value(span_json),
delta_json=(
_serialize_json_value(delta_json) if delta_json is not None else None
),
created_at=_normalize_utc_datetime(datetime.now(timezone.utc)),
)
sqla.session.add(chunk)
sqla.session.commit()
return chunk
Comment on lines +410 to +469

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Unhandled IntegrityError on the new unique constraints.

Neither create_artifact_ingest_event nor create_ingest_chunk catches IntegrityError from uq_artifact_ingest_event_run_artifact / uq_ingest_chunk_artifact_chunk. A retried/duplicate ingest (same run_id+artifact_id, or same artifact_event_id+chunk_id) will raise uncaught, leaving the session in a state requiring rollback before further use. CheckpointStore.save() and add_node/add_cre in this same file already establish the pattern of catching IntegrityError, rolling back, and re-raising a clear error.

🐛 Proposed fix
     sqla.session.add(event)
-    sqla.session.commit()
+    try:
+        sqla.session.commit()
+    except IntegrityError:
+        sqla.session.rollback()
+        raise
     return event

(same pattern for create_ingest_chunk)

📝 Committable suggestion

‼️ 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.

Suggested change
def create_artifact_ingest_event(
*,
run_id: str,
artifact_id: str,
harvest_mode: str,
event_type: str,
source_json: Any,
locator_json: Any,
artifact_json: Any,
harvest_json: Any,
observed_at: Any,
) -> ArtifactIngestEvent:
from datetime import datetime, timezone
observed_at = _normalize_utc_datetime(observed_at)
event = ArtifactIngestEvent(
id=generate_uuid(),
run_id=run_id,
artifact_id=artifact_id,
harvest_mode=harvest_mode,
event_type=event_type,
source_json=_serialize_json_value(source_json),
locator_json=_serialize_json_value(locator_json),
artifact_json=_serialize_json_value(artifact_json),
harvest_json=_serialize_json_value(harvest_json),
observed_at=observed_at,
created_at=_normalize_utc_datetime(datetime.now(timezone.utc)),
)
sqla.session.add(event)
sqla.session.commit()
return event
def create_ingest_chunk(
*,
artifact_event_id: str,
chunk_id: str,
text: str,
char_count: int,
span_json: Any,
delta_json: Optional[Any] = None,
) -> IngestChunk:
from datetime import datetime, timezone
chunk = IngestChunk(
id=generate_uuid(),
artifact_event_id=artifact_event_id,
chunk_id=chunk_id,
text=text,
char_count=char_count,
span_json=_serialize_json_value(span_json),
delta_json=(
_serialize_json_value(delta_json) if delta_json is not None else None
),
created_at=_normalize_utc_datetime(datetime.now(timezone.utc)),
)
sqla.session.add(chunk)
sqla.session.commit()
return chunk
def create_artifact_ingest_event(
*,
run_id: str,
artifact_id: str,
harvest_mode: str,
event_type: str,
source_json: Any,
locator_json: Any,
artifact_json: Any,
harvest_json: Any,
observed_at: Any,
) -> ArtifactIngestEvent:
from datetime import datetime, timezone
observed_at = _normalize_utc_datetime(observed_at)
event = ArtifactIngestEvent(
id=generate_uuid(),
run_id=run_id,
artifact_id=artifact_id,
harvest_mode=harvest_mode,
event_type=event_type,
source_json=_serialize_json_value(source_json),
locator_json=_serialize_json_value(locator_json),
artifact_json=_serialize_json_value(artifact_json),
harvest_json=_serialize_json_value(harvest_json),
observed_at=observed_at,
created_at=_normalize_utc_datetime(datetime.now(timezone.utc)),
)
sqla.session.add(event)
try:
sqla.session.commit()
except IntegrityError:
sqla.session.rollback()
raise
return event
def create_ingest_chunk(
*,
artifact_event_id: str,
chunk_id: str,
text: str,
char_count: int,
span_json: Any,
delta_json: Optional[Any] = None,
) -> IngestChunk:
from datetime import datetime, timezone
chunk = IngestChunk(
id=generate_uuid(),
artifact_event_id=artifact_event_id,
chunk_id=chunk_id,
text=text,
char_count=char_count,
span_json=_serialize_json_value(span_json),
delta_json=(
_serialize_json_value(delta_json) if delta_json is not None else None
),
created_at=_normalize_utc_datetime(datetime.now(timezone.utc)),
)
sqla.session.add(chunk)
try:
sqla.session.commit()
except IntegrityError:
sqla.session.rollback()
raise
return chunk
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/database/db.py` around lines 410 - 469, Update
create_artifact_ingest_event and create_ingest_chunk to catch SQLAlchemy
IntegrityError around session.add/commit, roll back sqla.session, and re-raise a
clear domain-appropriate error identifying the duplicate run/artifact or
artifact-event/chunk key. Follow the existing rollback-and-rethrow pattern used
by CheckpointStore.save and add_node/add_cre, while preserving successful
creation and return behavior.



def persist_standard_snapshot(
*,
run_id: str,
Expand Down
168 changes: 168 additions & 0 deletions application/tests/harvester_test/change_detector_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import unittest
from unittest.mock import MagicMock
from unittest.mock import call
from unittest.mock import patch

from application.utils.harvester.change_detector import (
ChangeDetector,
)


class ChangeDetectorTests(unittest.TestCase):
@patch("application.utils.harvester.change_detector.subprocess.run")
def test_get_modified_files_since(self, mock_run):
client = MagicMock()
client.get_local_path.return_value = "repo-under-test"

mock_run.side_effect = [
MagicMock(stdout="resolved_base\n"),
MagicMock(stdout="resolved_target\n"),
MagicMock(stdout="a.md\nb.md\na.md\n"),
]

detector = ChangeDetector(client)

files = detector.get_modified_files_since(
"base",
"target",
)

self.assertEqual(
files,
[
"a.md",
"b.md",
],
)

mock_run.assert_has_calls(
[
call(
[
"git",
"-C",
"repo-under-test",
"rev-parse",
"--verify",
"--end-of-options",
"base^{commit}",
],
capture_output=True,
text=True,
check=True,
timeout=60,
),
call(
[
"git",
"-C",
"repo-under-test",
"rev-parse",
"--verify",
"--end-of-options",
"target^{commit}",
],
capture_output=True,
text=True,
check=True,
timeout=60,
),
call(
[
"git",
"-C",
"repo-under-test",
"diff",
"--name-only",
"resolved_base",
"resolved_target",
],
capture_output=True,
text=True,
check=True,
timeout=60,
),
]
)

@patch("application.utils.harvester.change_detector.subprocess.run")
def test_get_commits_since(self, mock_run):
client = MagicMock()
client.get_local_path.return_value = "repo-under-test"

mock_run.side_effect = [
MagicMock(stdout="resolved_base\n"),
MagicMock(stdout="resolved_target\n"),
MagicMock(stdout="111\n222\n333\n"),
]

detector = ChangeDetector(client)

commits = detector.get_commits_since(
"base",
"target",
)

self.assertEqual(
commits,
[
"111",
"222",
"333",
],
)

self.assertEqual(
mock_run.call_args_list,
[
call(
[
"git",
"-C",
"repo-under-test",
"rev-parse",
"--verify",
"--end-of-options",
"base^{commit}",
],
capture_output=True,
text=True,
check=True,
timeout=60,
),
call(
[
"git",
"-C",
"repo-under-test",
"rev-parse",
"--verify",
"--end-of-options",
"target^{commit}",
],
capture_output=True,
text=True,
check=True,
timeout=60,
),
call(
[
"git",
"-C",
"repo-under-test",
"log",
"--reverse",
"--format=%H",
"resolved_base..resolved_target",
],
capture_output=True,
text=True,
check=True,
timeout=60,
),
],
)


if __name__ == "__main__":
unittest.main()
Loading
Loading