Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aa79243
feat(harvester): add git diff retrieval pipeline
ParthAggarwal16 Jul 10, 2026
15a0ba3
feat(harvester): parse unified git diffs
ParthAggarwal16 Jul 10, 2026
20f0515
feat(harvester): normalize extracted diff content
ParthAggarwal16 Jul 10, 2026
f3bcb06
Enhance diff pipeline with metadata and normalization
ParthAggarwal16 Jul 10, 2026
a6299e2
feat(harvester): add RFC document data models and artifact.py
ParthAggarwal16 Jul 14, 2026
6986ec1
feat(harvester): read files from repository commits
ParthAggarwal16 Jul 14, 2026
0f24d71
feat(harvester): extract markdown heading hierarchy
ParthAggarwal16 Jul 14, 2026
291d2d4
feat(harvester): build structured document objects
ParthAggarwal16 Jul 14, 2026
ea7189f
feat(harvester): validate structured documents
ParthAggarwal16 Jul 14, 2026
aa3f436
feat(harvester): add content hashing for deduplication
ParthAggarwal16 Jul 16, 2026
6abf604
feat(harvester): add artifact registry
ParthAggarwal16 Jul 16, 2026
5ae56e0
feat(harvester): implement document deduplication
ParthAggarwal16 Jul 16, 2026
57b34d7
feat(harvester): add checkpoint management
ParthAggarwal16 Jul 16, 2026
f0f9368
feat(harvester): orchestrate incremental document processing
ParthAggarwal16 Jul 16, 2026
c46a848
feat(harvester): track deduplication metrics
ParthAggarwal16 Jul 16, 2026
5750abc
resolve the textacy stuff
ParthAggarwal16 Aug 24, 2026
955fff6
feat(harvester): add git diff retrieval pipeline
ParthAggarwal16 Jul 10, 2026
e6465f2
feat(harvester): parse unified git diffs
ParthAggarwal16 Jul 10, 2026
a1100fa
Enhance diff pipeline with metadata and normalization
ParthAggarwal16 Jul 10, 2026
8fba344
feat(harvester): add LlamaIndex document chunking foundation
ParthAggarwal16 Aug 14, 2026
aae28c3
feat(harvester): preserve document structure in chunks
ParthAggarwal16 Aug 14, 2026
f29dd0c
feat(harvester): validate chunk records and benchmark chunking
ParthAggarwal16 Aug 14, 2026
4f4ffa8
fix(harvester): address CodeRabbit chunking review
ParthAggarwal16 Aug 20, 2026
84d33b3
fix(harvester): adding dependencies to the harvester
ParthAggarwal16 Aug 20, 2026
6affe36
test: make harvester benchmarks opt-in
ParthAggarwal16 Aug 21, 2026
9936b4f
feat(module-a): finish harvester handoff and OIE orchestrator
northdpole Aug 29, 2026
f86e5b8
fix(oie): hermetic A→B→C smoke and orchestrator session handoff
northdpole Aug 29, 2026
8e62ba1
fix(harvester): import ChunkingConfig for chunk_document helper
northdpole Aug 29, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ standards_cache.sqlite
!docs/Final_eval_blog_gsoc2026/module_B_final_blog.md
!application/utils/librarian/README.md
!docs/gsoc_2026_module_c/*.md
!docs/gsoc_2026_module_a/*.md
!docs/gsoc_2026_module_b/module_a_contract.md
!docs/gsoc_2026_module_b/module_c_contract.md
!docs/gsoc_2026_module_b/module_b_runbook.md
Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ alembic-guardrail:
[ -d "./venv" ] && . ./venv/bin/activate &&\
python scripts/check_alembic_revision_guardrail.py

oie-pipeline:
[ -d "./venv" ] && . ./venv/bin/activate &&\
PYTHONPATH=. python scripts/run_oie_pipeline.py --cache_file "$(or $(CACHE_FILE),sqlite:///$(CURDIR)/standards_cache.sqlite)" $(OIE_ARGS)

oie-e2e-smoke:
[ -d "./venv" ] && . ./venv/bin/activate &&\
PYTHONPATH=. python scripts/run_oie_e2e_smoke.py

openapi-generate:
[ -d "./venv" ] && . ./venv/bin/activate &&\
python scripts/generate_openapi.py
Expand Down
19 changes: 19 additions & 0 deletions application/cmd/cre_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,25 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover
logger.info("Exported %s rows to %s", rows, csv_out)
return

if getattr(args, "run_harvester", False):
import sys

from application import sqla
from application.utils.harvester.pipeline import run_harvester

db_connect(args.cache_file)
repos_yaml = getattr(args, "harvester_repos_yaml", "") or None
summary = run_harvester(
sqla.session,
args.run_id.strip(),
repos_yaml=repos_yaml,
dry_run=getattr(args, "harvester_dry_run", False),
)
print(summary.to_json())
if summary.status == "degraded":
sys.exit(1)
return

if getattr(args, "run_noise_filter", False):
import sys

Expand Down
99 changes: 99 additions & 0 deletions application/tests/harvester_test/artifact_registry_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import unittest
from datetime import datetime

from application.utils.harvester.artifact_registry import ArtifactRegistry
from application.utils.harvester.models import ArtifactRegistryRecord


class ArtifactRegistryTests(unittest.TestCase):
def test_insert_record(self):
registry = ArtifactRegistry()

record = ArtifactRegistryRecord(
artifact_id="art:test:file.md",
repository="OWASP/ASVS",
locator_path="file.md",
content_hash="abc",
last_commit_sha="123",
last_pipeline_run="run1",
last_processed_at=datetime.now(),
status="new",
)

registry.upsert(record)

self.assertTrue(registry.exists(record.artifact_id))

def test_get_record(self):
registry = ArtifactRegistry()

record = ArtifactRegistryRecord(
artifact_id="art:test:file.md",
repository="OWASP/ASVS",
locator_path="file.md",
content_hash="abc",
last_commit_sha="123",
last_pipeline_run="run1",
last_processed_at=datetime.now(),
status="new",
)

registry.upsert(record)

stored = registry.get(record.artifact_id)
assert stored is not None

self.assertEqual(stored.content_hash, "abc")

def test_update_record(self):
registry = ArtifactRegistry()

record = ArtifactRegistryRecord(
artifact_id="art:test:file.md",
repository="OWASP/ASVS",
locator_path="file.md",
content_hash="abc",
last_commit_sha="123",
last_pipeline_run="run1",
last_processed_at=datetime.now(),
status="new",
)

registry.upsert(record)

record.content_hash = "xyz"
record.status = "updated"

registry.upsert(record)

stored = registry.get(record.artifact_id)
assert stored is not None

self.assertEqual(stored.content_hash, "xyz")
self.assertEqual(stored.status, "updated")

def test_all_records(self):
registry = ArtifactRegistry()

for i in range(3):
registry.upsert(
ArtifactRegistryRecord(
artifact_id=f"art:{i}",
repository="repo",
locator_path=f"{i}.md",
content_hash=str(i),
last_commit_sha="sha",
last_pipeline_run="run",
last_processed_at=datetime.now(),
status="new",
)
)

self.assertEqual(
len(registry.all()),
3,
)


if __name__ == "__main__":
unittest.main()
68 changes: 68 additions & 0 deletions application/tests/harvester_test/checkpoint_manager_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import unittest
from datetime import datetime

from application.utils.harvester.checkpoint_manager import CheckpointManager
from application.utils.harvester.models import CheckpointRecord


class CheckpointManagerTests(unittest.TestCase):
def test_save_checkpoint(self):
manager = CheckpointManager()

checkpoint = CheckpointRecord(
repository="OWASP/ASVS",
pipeline_run_id="run1",
last_processed_commit="abc123",
status="running",
updated_at=datetime.now(),
)

manager.save(checkpoint)

self.assertIsNotNone(manager.get("OWASP/ASVS"))

def test_update_commit(self):
manager = CheckpointManager()

checkpoint = CheckpointRecord(
repository="OWASP/ASVS",
pipeline_run_id="run1",
last_processed_commit="abc123",
status="running",
updated_at=datetime.now(),
)

manager.save(checkpoint)

manager.update_commit(
"OWASP/ASVS",
"deadbeef",
)

stored = manager.get("OWASP/ASVS")
assert stored is not None

self.assertEqual(stored.last_processed_commit, "deadbeef")

def test_mark_completed(self):
manager = CheckpointManager()

checkpoint = CheckpointRecord(
repository="OWASP/ASVS",
pipeline_run_id="run1",
last_processed_commit="abc123",
status="running",
updated_at=datetime.now(),
)

manager.save(checkpoint)
manager.mark_completed("OWASP/ASVS")

stored = manager.get("OWASP/ASVS")
assert stored is not None

self.assertEqual(stored.status, "completed")


if __name__ == "__main__":
unittest.main()
36 changes: 36 additions & 0 deletions application/tests/harvester_test/chunk_pipeline_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import unittest
from unittest.mock import Mock

from application.utils.harvester.chunk_pipeline import DocumentChunkPipeline
from application.utils.harvester.models import Document, IngestChunkRecord


class DocumentChunkPipelineTests(unittest.TestCase):
def test_invalid_record_is_rejected_before_return(self):
document = Mock(spec=Document)
document.text = "Some document text."

chunker = Mock()
chunker.chunk.return_value = ["chunk"]

record_builder = Mock()
invalid_record = Mock(spec=IngestChunkRecord)
record_builder.build.return_value = [invalid_record]

validator = Mock()
validator.validate.side_effect = ValueError("invalid chunk record")

pipeline = DocumentChunkPipeline(
chunker=chunker,
record_builder=record_builder,
validator=validator,
)

with self.assertRaisesRegex(ValueError, "invalid chunk record"):
pipeline.chunk(document)

validator.validate.assert_called_once_with(invalid_record)


if __name__ == "__main__":
unittest.main()
75 changes: 75 additions & 0 deletions application/tests/harvester_test/chunk_record_builder_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import unittest
from datetime import datetime, timezone

from application.utils.harvester.chunk_record_builder import ChunkRecordBuilder
from application.utils.harvester.chunk_record_validator import (
ChunkRecordValidator,
ingest_record_to_payload,
)
from application.utils.harvester.chunker import ChunkInfo
from application.utils.harvester.models import (
Document,
HeadingNode,
Locator,
SourceInfo,
)
from application.utils.noise_filter.schemas import ChangeRecord


class ChunkRecordBuilderTests(unittest.TestCase):
def _document(self, text: str, headings: list[HeadingNode]) -> Document:
return Document(
schema_version="0.2.0",
artifact_id="art:OWASP/ASVS:README.md",
pipeline_run_id="run-1",
text=text,
source=SourceInfo(
type="github",
repository="OWASP/ASVS",
commit_sha="abc1234deadbeef",
committed_at=datetime(2026, 2, 1, 1, 0, 0, tzinfo=timezone.utc),
),
locator=Locator(
kind="repo_path",
id="README.md",
path="README.md",
),
heading_structure=headings,
)

def test_builds_change_record_shaped_payload(self) -> None:
text = "# Root\n\nFirst paragraph."
document = self._document(
text,
[HeadingNode(level=1, text="Root", start_line=1, end_line=3)],
)
chunk = ChunkInfo(text=text, start_char_idx=0, end_char_idx=len(text))
records = ChunkRecordBuilder().build(document, [chunk])
self.assertEqual(len(records), 1)
record = records[0]
self.assertEqual(record.chunk_id, "chk:art:OWASP/ASVS:README.md:0")
self.assertEqual(record.pipeline_run_id, "run-1")
self.assertEqual(record.source_repo, "OWASP/ASVS")
self.assertEqual(record.locator_path, "README.md")
self.assertEqual(record.span.heading_path, ["Root"])

ChunkRecordValidator().validate(record)
payload = ingest_record_to_payload(record)
ChangeRecord.model_validate(payload)

def test_indexes_multiple_chunks(self) -> None:
text = "AAAA\n\nBBBB"
document = self._document(text, [])
chunks = [
ChunkInfo(text="AAAA\n\n", start_char_idx=0, end_char_idx=6),
ChunkInfo(text="BBBB", start_char_idx=6, end_char_idx=10),
]
records = ChunkRecordBuilder().build(document, chunks)
self.assertEqual(records[0].span.index, 0)
self.assertEqual(records[0].span.total, 2)
self.assertEqual(records[1].span.index, 1)
self.assertEqual(records[1].chunk_id, "chk:art:OWASP/ASVS:README.md:1")


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