Skip to content

QA plan: Content provenance and classification gate (#66)

Feature: Content provenance and classification (#66)
Capability: Security (#45)
Related: Story #67 (Pre-deploy corpus classification gate); security per-feature requirements (#66/#67)

TypeCoverage
Unit (#66)Ingestion writer creates a corpus_provenance row containing: dataset name, revision SHA, license string, ingestion timestamp
Unit (#66)corpus_provenance row is created even when --limit restricts the row count
Integration (#66)Full rag-ingest load run: corpus_provenance table exists and contains exactly one row per ingestion run with a non-null revision_sha
IaC / CODEOWNERS (#66)CODEOWNERS maps the corpus reference paths (pyproject.toml, ingestion config) to the data-classification owner; a PR changing the corpus reference without that review is rejected
CI gate (#67)CI step parsing the PR diff: if the corpus reference in pyproject.toml or ingestion config changes without a matching update to the data-classification page, the job fails
CI gate (#67)CI step passes when both the corpus reference and the data-classification page are updated in the same PR
Contractcorpus_provenance table schema is documented (column names and types) in the migration file and matches the ingestion writer
  • Story #8 (load and chunk) merged (ingestion pipeline exists)
  • Story #10 (local DB schema) with migrations in place
  • Data classification document exists
  • CODEOWNERS file exists in the app repo
  • Unit tests: provenance writer creates a row with all required fields; 70% per-file floor met for the provenance module
  • Integration: corpus_provenance row exists after rag-ingest load; revision_sha is non-null
  • CI gate: a test PR changing the corpus reference without updating data-classification fails the classification gate job; a PR updating both passes
  • CODEOWNERS entry verified by cat CODEOWNERS | grep corpus in CI
  • Schema in migration matches the model class

Unit and integration: CI (Docker Postgres). CI gate: GitHub Actions (PR diff parser). CODEOWNERS: CI check.

pytest, pytest-cov, Docker Postgres fixture, GitHub Actions (PR diff step), git diff.

def test_provenance_row_created_on_ingest(db_session, parquet_seed_fixture):
# Given the ingestion pipeline and a seed corpus file
runner = IngestRunner(db_session, corpus_path=parquet_seed_fixture)
# When the load command runs
runner.load(limit=10)
# Then a corpus_provenance row exists with all required fields populated
row = db_session.execute(
"SELECT dataset_name, revision_sha, license_string, ingested_at FROM corpus_provenance"
).fetchone()
assert row is not None
assert row.dataset_name == "neural-bridge/rag-dataset-12000"
assert row.revision_sha is not None
assert row.license_string == "apache-2.0"
assert row.ingested_at is not None

CI gate (shell, not pytest):

Terminal window
# Given a PR diff that changes pyproject.toml corpus reference
CHANGED=$(git diff origin/main --name-only | grep -E 'pyproject.toml|ingestion.ya?ml')
if [ -n "$CHANGED" ]; then
# When the data-classification page is checked for a matching update
CLASSIF_CHANGED=$(git diff origin/main --name-only | grep 'data-classification')
# Then the classification page must also be updated
[ -n "$CLASSIF_CHANGED" ] || (echo "ERROR: corpus reference changed without data-classification update" && exit 1)
fi
  • HuggingFace dataset revision SHA is not always stable; the loader must explicitly call hf_hub.snapshot_download with revision="main" and record the resolved SHA, not the branch name.
  • The CI diff parser for the classification gate is fragile if file paths change; anchor it to glob patterns defined in a config rather than hardcoded paths.

Security per-feature requirements (#66/#67): provenance table with dataset name, revision SHA, license, timestamp; CODEOWNERS review gate; CI classification gate. Exit criteria map directly to each row.