{
  "build_status": "passed",
  "tests": [
    {
      "id": "surveys-into-the-night",
      "name": "Surveys into the night",
      "status": "warn",
      "detail": "Minutes before civil dawn or after civil dusk (sun below −6°). Sunrise/sunset are shown for context; all positive overlaps are retained.",
      "rows": 81,
      "action": "Inspect the original source record. Correct confirmed Trektellen errors and re-export; locate historical records by their source ID. Leave legitimate periods unchanged.",
      "file": "validation_findings.csv",
      "group": "Surveys",
      "filter_column": "night_minutes",
      "filter_threshold": 30
    },
    {
      "id": "entry-times-outside-their-source-period",
      "name": "Entry times outside their source period",
      "status": "warn",
      "detail": "Largest entry-time gap before the start or after the end of each source period. Differences up to 10 min are hidden by default. Affected entries remain at day resolution.",
      "rows": 69,
      "action": "Check entry times and period bounds in Trektellen. Entries remain at day resolution until corrected.",
      "file": "entry_issues.csv",
      "group": "Counts and data integrity",
      "filter_column": "outside_minutes",
      "filter_threshold": 10
    },
    {
      "id": "entries-missing-a-time",
      "name": "Entries missing a time",
      "status": "warn",
      "detail": "Find entries without a usable time; retain them at day resolution.",
      "rows": 120,
      "action": "Add a time only when the original evidence supports it. Otherwise keep the valid day-level record.",
      "file": "entry_issues.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "source-overlaps",
      "name": "Overlapping surveys",
      "status": "warn",
      "detail": "Compare period bounds. The earlier period is kept; a later overlapping period is excluded in full. Any retained overlap blocks the build.",
      "rows": 1,
      "action": "Inspect both source periods using their native IDs. Correct duplicate or mis-entered headers in Trektellen and re-export when supported.",
      "file": "overlap_review.csv",
      "group": "Surveys",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "daily-count-outliers",
      "name": "Unusual daily counts",
      "status": "warn",
      "detail": "Score = daily count / largest other seasonal value. Compare other positive days for the same taxon and movement category within ±15 calendar days across years, excluding the candidate (at least 20 reference days). Scores above 3 are shown, most unusual first. This measures excess over every other comparable value, including normal flocking peaks; zero and missing values are not scored. Review warnings, not confirmed errors.",
      "rows": 15,
      "action": "Review the original counts and survey effort. Keep genuine migration peaks; correct only confirmed source errors.",
      "file": "count_outliers.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "count-rate-outliers",
      "name": "Unusual daily count rates",
      "status": "warn",
      "detail": "Score = birds per survey hour / largest other seasonal value. Compare other positive rates for the same taxon and movement category within ±15 calendar days across years, excluding the candidate (at least 20 reference days). Scores above 3 are shown, most unusual first. Large rates are only unusual relative to that species’ largest other seasonal rate. Rates require known positive effort and linked numerical counts. Zero and missing values are not scored; genuine exceptional migration can remain valid.",
      "rows": 19,
      "action": "Review the original counts and survey effort. Keep genuine migration peaks; correct only confirmed source errors.",
      "file": "count_outliers.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "csv-requirements",
      "name": "Released CSV requirements",
      "status": "pass",
      "detail": "Check field types, constraints, keys and project rules.",
      "rows": 0,
      "action": "",
      "file": "datapackage_validation.json",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "eligible-count-categories-retained-once",
      "name": "Eligible count categories retained once",
      "status": "pass",
      "detail": "Check every expected source or subgroup ID is released exactly once.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "survey-associations-preserved",
      "name": "Survey associations preserved",
      "status": "pass",
      "detail": "Each released count stays linked to its source survey when that survey is released; splits around weather stops and absences follow the count's own time.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "normal-values-preserved",
      "name": "Main counts preserved",
      "status": "pass",
      "detail": "Sum released subgroups back to each source count.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "reverse-values-preserved",
      "name": "Reverse counts preserved",
      "status": "pass",
      "detail": "Compare each reverse count with its source value.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "local-values-preserved",
      "name": "Local counts preserved",
      "status": "pass",
      "detail": "Compare each local count with its source value.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "count-category-provenance-preserved",
      "name": "Count category provenance preserved",
      "status": "pass",
      "detail": "Check category and original observation identity for every released row.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "reviewed-taxonomy-assignments-preserved",
      "name": "Reviewed taxonomy assignments preserved",
      "status": "pass",
      "detail": "Released taxa are compared with the reviewed projection of historical species text.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "local-collection-dates-preserved",
      "name": "Local dates preserved",
      "status": "pass",
      "detail": "Compare source dates with released dates in Europe/Paris.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "taxon-parents-are-released-and-acyclic",
      "name": "Taxon parents are released and acyclic",
      "status": "pass",
      "detail": "Every parent_taxon_id is a released taxon, and following parents never loops.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "unique-ids",
      "name": "Unique IDs",
      "status": "pass",
      "detail": "Find duplicated survey or observation IDs.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "every-observation-has-its-survey",
      "name": "Observation–survey links",
      "status": "pass",
      "detail": "Check survey references, allowing explicitly flagged missing periods.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "positive-durations",
      "name": "Non-positive survey durations",
      "status": "pass",
      "detail": "Find periods whose end is at or before their start.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Surveys",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "long-surveys",
      "name": "Long surveys",
      "status": "pass",
      "detail": "End minus start. All periods over 12 h are available; 16 h is the initial review threshold. Long summer days can be legitimate. Night overlap is a separate check.",
      "rows": 307,
      "action": "Inspect the original source record. Correct confirmed Trektellen errors and re-export; locate historical records by their source ID. Leave legitimate periods unchanged.",
      "file": "validation_findings.csv",
      "group": "Surveys",
      "filter_column": "duration_hours",
      "filter_threshold": 16
    },
    {
      "id": "timestamps-inside-their-survey",
      "name": "Unflagged times outside surveys",
      "status": "pass",
      "detail": "Find outside-period timestamps without the expected flag; excluded duplicates are skipped.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "taxa-in-source-taxa-csv",
      "name": "Source taxa missing from the crosswalk",
      "status": "pass",
      "detail": "Check source names and IDs against taxonomy/source_taxa.csv.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "every-bird-taxon-has-an-avibase-id",
      "name": "Bird taxa missing an Avibase ID",
      "status": "pass",
      "detail": "Check that every mapped bird taxon has an Avibase ID.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "avibase-ids-in-the-checklists",
      "name": "Avibase IDs absent from checklists",
      "status": "pass",
      "detail": "Look up mapped Avibase IDs in the maintained checklists.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "taxonomy-source",
      "name": "Taxonomy source",
      "status": "pass",
      "detail": "Taxa named from each checklist: {'AviList v2025 (11 Jun 2025)': 217, 'eBird/Clements v2025': 56}.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "stable-observation-ids",
      "name": "Stable observation IDs",
      "status": "pass",
      "detail": "Find Trektellen entries without native data IDs; fallback IDs depend on export row order.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "historical-effort-gaps-inside-declared-windows",
      "name": "Historical effort gaps inside declared windows",
      "status": "pass",
      "detail": "Each added empty interval lies inside its declared day window.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "historical-effort-gaps-do-not-overlap-surveys",
      "name": "Historical effort gaps do not overlap surveys",
      "status": "pass",
      "detail": "Added empty intervals never overlap another released survey of the same day.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "historical-effort-gaps-have-no-counts",
      "name": "Historical effort gaps have no counts",
      "status": "pass",
      "detail": "No count is linked to, or timed inside, an added empty interval (counted, nothing seen).",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "weather-stops-hold-no-counts",
      "name": "Weather stops hold no counts",
      "status": "pass",
      "detail": "A weather stop is complete with no bird: no released count may be linked to it.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "released-surveys-do-not-overlap",
      "name": "Released surveys do not overlap",
      "status": "pass",
      "detail": "Survey rows, including weather stops and added intervals, are compared across the whole table.",
      "rows": 0,
      "action": "",
      "file": "validation_findings.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "attributes-quantity_mismatch",
      "name": "Detail quantities exceed the source count",
      "status": "pass",
      "detail": "Detailed subgroups exceed the source total. Keep the original count without splitting and review the source. Smaller detailed subsets are split with an undescribed remainder automatically.",
      "rows": 0,
      "action": "Check the workbook sheet/row and each detail component. Do not replace the source total with a conflicting detail sum.",
      "file": "attribute_quantity_review.csv",
      "group": "Historical records",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "attributes-unparsed",
      "name": "Historical details with an unrecognised format",
      "status": "pass",
      "detail": "Detail text could not be split into quantity/description components.",
      "rows": 0,
      "action": "Review the original detail and correct the source format only when justified.",
      "file": "attribute_review.csv",
      "group": "Historical records",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "attributes-unassigned",
      "name": "Historical records with no attributes assigned",
      "status": "pass",
      "detail": "No age, sex or plumage could be assigned from the detail. Unknown or mixed descriptions remain empty.",
      "rows": 0,
      "action": "Review config/attributes/historical_attributes.csv. Unknown or ambiguous descriptions can remain unassigned.",
      "file": "attribute_review.csv",
      "group": "Historical records",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "attributes-partial",
      "name": "Historical records with some attributes assigned",
      "status": "pass",
      "detail": "At least one attribute was assigned, but some detail remains unresolved. Supported fields are assigned separately to each subgroup.",
      "rows": 0,
      "action": "Review the supported shared attributes and original group description; do not infer unsupported age or sex.",
      "file": "attribute_review.csv",
      "group": "Historical records",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "count-accounting",
      "name": "Counts accounted for after selection",
      "status": "pass",
      "detail": "Source total = retained birds + overlap, non-bird and no-species exclusions. Exclusions are mutually exclusive.",
      "rows": 61,
      "action": "",
      "file": "reconciliation.csv",
      "group": "Counts and data integrity",
      "filter_column": "",
      "filter_threshold": 0
    },
    {
      "id": "season-coverage",
      "name": "Coverage context within reported seasons",
      "status": "info",
      "detail": "Missing dates, short days and documented incomplete surveys or weather stops are coverage context, not warnings. Completeness applies to each recorded interval, not the whole day. Missing records do not establish zero birds or a closure.",
      "rows": 244,
      "action": "",
      "file": "interruption_review.csv",
      "group": "Coverage and published totals",
      "filter_column": "",
      "filter_threshold": 0
    }
  ],
  "summaries": [
    {
      "name": "Coverage by day and year",
      "file": "daily_coverage.csv",
      "rows": 5107
    },
    {
      "name": "Reviewed entry time corrections",
      "file": "entry_time_corrections.csv",
      "rows": 1018
    },
    {
      "name": "Published annual totals",
      "file": "report_reconciliation.csv",
      "rows": 12027
    }
  ]
}