Asteria Fleet Data Quality Hub — reconciliation tasks
What these tasks look like
Each task ships two payloads and points at a shared read-only data hub:
payloads/case_scope.json — the scope: collection_id, a business cutoff /
as-of, the focus/anchor/decision IDs you must report on, and (sometimes)
status thresholds or a certification gate + status→action map.
payloads/answer_template.json — the answer contract: exact keys, enums,
array lengths, ordering rules, and numeric precision. This file is the
spec. Read every field description, enum, pattern, minItems/maxItems,
multipleOf, and ordering note before writing any code.
Connection details (base URL and a read-only bearer token) are provided
separately in environment_access.md. Read them from that file at runtime;
never hardcode them. references/hub_client.py does this for you.
Deliver one JSON object matching the template exactly — no commentary, no
Markdown, no extra keys (additionalProperties is false in most contracts).
Step 0 — orient
- Read
case_scope.json and the whole answer_template.json. List every
output key and its ordering/rounding/enum constraints.
- Discover the schema from the hub catalog before querying (do not assume
column names). The hub exposes stable logical views:
v_contacts,
v_fuel_transactions, v_freight_charges, v_maintenance_events,
v_source_snapshots, v_reference_aliases, v_unit_conversions,
v_fx_rates.
- Pull data through the hub's read-only SQL query interface with a body of the
form
{"query": "SELECT ... FROM v_* WHERE collection_id='...'"}. It is SQL
over the views and supports WHERE/GROUP BY/JOIN/aggregates. It truncates
at 2000 rows (a truncated flag tells you). Every single collection fits
under that cap, so pull the whole collection in one query and reconcile in
code. sqlite_master/DDL are blocked.
Universal reconciliation model (all families)
Snapshots & the authoritative source. A collection has several snapshots in
v_source_snapshots, one per source_system, each with a snapshot_status
(CERTIFIED / PROVISIONAL / STALE), a business_cutoff, and a row_count.
The authoritative snapshot is the CERTIFIED one; its snapshot_id and
row_count are what "authoritative_*" fields want.
Cutoff. Keep rows whose business date (e.g. purchased_at,
service_date, event time) is <= the scope cutoff. Do not filter on
ingested_at — rows that landed in the hub after the cutoff are still in scope.
Dedup → logical records. The same logical entity (transaction/charge/event)
appears in more than one snapshot. Group by the stable business ID
(transaction_id, charge_id, event_id); the retained/logical record is
the one from the authoritative (CERTIFIED) snapshot (fall back to whatever
exists if it is not present there). Then:
raw_row_count = in-scope raw rows across all snapshots.
logical_*_count = distinct business IDs.
duplicate_raw_count = raw − logical.
- A duplicate group reports the business ID, its snapshot IDs (sorted), the raw
occurrence count, and the retained (certified) snapshot ID.
Confirm there are no cross-snapshot duplicates hiding under different business
IDs (same asset/time/type) before trusting the count.
Reference tables.
v_reference_aliases (domain, alias_text, canonical_value, valid_from,
valid_to, reference_status): map free text → a canonical category. Use only
aliases that are ACTIVE and whose validity window covers the record's
business date. Temporal traps exist: an alias can be ACTIVE yet not-yet-
effective (future valid_from), and the in-window mapping can be flagged
INACTIVE/PROVISIONAL.
v_unit_conversions (kind, from_unit, to_unit, factor, precision):
multiply by factor to reach the canonical unit (self-unit factor = 1).
v_fx_rates (rate_date, currency, usd_per_unit, rate_status): prefer
the CERTIFIED rate whose rate_date equals the record's business date;
usd = amount * usd_per_unit; USD→USD = 1.
Description → category matching (transaction families). Tokenise the
description into lowercase alphanumeric tokens. Scan left to right taking the
longest contiguous alias-token subsequence that matches (so
premium unleaded beats bare unleaded; filler like with liftgate accessorial
is ignored). Match whole-token phrases, not raw substrings. Collect the distinct
canonical values:
- 0 → unrecognized; ≥2 distinct → ambiguous (both quarantine).
- exactly 1 → recognized category (the "actual" class).
Classification (transaction families). For each logical record:
invalid_* = a required physical measure is missing or ≤ 0 (quantity, weight,
distance).
- quarantined = unrecognized OR ambiguous OR any invalid measure.
- valid = recognized-unique AND all required measures > 0.
- mismatch = valid AND recognized ≠ expected category. Mismatch is defined
on valid records only (this matters).
- exception = mismatch OR quarantined.
- Invariants (verify them):
valid + quarantined = logical; mismatch and
quarantine are disjoint and their union size = the exception count;
quarantine reason buckets are typically disjoint and sum to the quarantine
count. record_status values like REVIEW/BILLED/POSTED are not an
exclusion criterion.
Normalized totals. Sum over valid records only (quarantined excluded;
valid mismatches ARE included). Convert units and FX as above, group by the
recognized category, and round to the template's precision. Per-group counts
must sum to the overall valid count.
Family specifics
A. Contact-master reconciliation (people/orgs) — v_contacts
Columns include row_id, source_system, email, phone, city, region,
consent_status, record_status, verified_flag, master_hint. Typical
shape: N entities × several source systems, a block of no-contact rows, and a
block of identity edge-cases.
- Normalise email (NFKC, strip, lowercase; treat
'', none, null,
n/a, nan, … as empty; usable ⇔ has @ and a dotted domain) and phone
(digits only; usable ⇔ ≥ 7 digits).
- Cluster by usable email (primary). Phone is a valid secondary merge
key except shared identifiers: a phone flagged
master_hint = SHARED-HELPDESK, or any phone tied to ≥ 2 distinct emails,
must NOT merge people (shared helpdesk line). Name is never a merge key
(same name + different contact = different people). NOISY-* hint rows are
isolated singletons — not merge signals.
- Quarantine row = no usable email and no usable phone.
canonical_entity_count / canonical_person_count INCLUDES quarantined
(no-contact) rows as singleton entities. Region/depot rollups also include
them and must sum to the canonical count (strong self-check; the synthetic
data tends to split evenly across regions).
duplicate/merged cluster count = clusters with > 1 member.
- Readiness / dispatch. Eligible ⇔
ACTIVE and has a usable channel. A
channel/person is "ready"/"dispatchable" only when consent is GRANTED.
Produce whichever partition the template asks for — either
dispatchable / blocked-consent / blocked-no-contact / blocked-inactive, or
both / email_only / phone_only / not_ready — and make the buckets sum to the
stated denominator.
- Field-level precedence. Different fields may come from different sources;
the answer often reports a per-field
*_source_system and a
resolution_outcome (SINGLE_SOURCE, FIELD_LEVEL_PRECEDENCE_APPLIED,
CONTESTED_NO_AUTOMERGE, NO_USABLE_CONTACT). Resolve each field from the
highest-precedence source holding a usable value, and keep the choice
consistent everywhere. The exact per-field source authority (name vs contact
vs depot vs consent) and the canonical-consent rule for a merged person are
the hardest, least-obvious sub-problem — pin them down from the focus/anchor
probe rows the scope highlights rather than assuming a fixed order.
- Contested identifier watchlist case = the anchor row's usable identifier
(e.g. phone) is shared across different people.
B. Transaction normalization — fuel & freight
v_fuel_transactions / v_freight_charges. Apply the universal transaction
model. Fuel: volume→L, spend→USD. Freight: weight→KG, distance→KM, spend→USD,
and quarantine on non-positive weight or distance. Merchant/carrier rankings
order by an exposure/exception measure descending, then ID ascending, limited to
the scope's top-N, reporting per-entity component counts (mismatch / quarantine
/ exception).
C. Maintenance-log integrity — v_maintenance_events
Columns include event_id, asset_id, event_time_raw, odometer_value/unit,
labor_hours.
- Issue flags on the retained record: missing timestamp (null/blank),
invalid timestamp (unparseable), invalid odometer (null/negative), negative
labor (< 0), extreme labor (> a full-day threshold, e.g. 24h).
invalid_event_ids = the deduped union of those. valid_event_count =
logical − invalid.
- Odometer regression = within an asset's valid events ordered by time, a
reading below the previous one. Regressions stay valid and are reported
separately (asset IDs + event IDs), not counted as invalid.
- Corrected distance = per asset (last − first reliable odometer, converted
to km), summed across assets.
- Asset risk ranking: rejected-event count desc, then regression-event count
desc, then asset ID asc.
Status / certification decisions
- If the scope gives
status_thresholds (e.g. a max quarantine rate for PASS vs
PASS_WITH_EXCEPTIONS), compute the rate the template defines (e.g. quarantined
÷ canonical entities, rounded) and map status → action via the scope's
status_action_map.
- If the scope gives a certification gate (e.g. "odometer regression ⇒ HOLD /
BLOCK_AND_REMEDIATE"), apply it.
- When no threshold is supplied, the release/close decision is whatever the
scope's rule implies — derive it; do not assume. Different tasks resolve to
different statuses (some to HOLD when issues exist, some to PASS/RELEASE
because exceptions are only flagged, not blocking).
Opaque control-code panels
Some tasks ask for compact codes for scoped IDs — identity/outreach/field-
provenance (IC-* / OR-* / FP-*), reference-policy/source-basis/ledger-
disposition (RB-* / SB-* / LD-*), or maintenance-source/history-route
(MS-* / HR-*). Their expansions are intentionally withheld. Method:
- Classify each scoped ID into its data-derived disposition — e.g. source
basis (certified-only / provisional-only / both), disposition (valid /
rejected / regression), alias
reference_status, identity outcome
(clean-merge / contested / no-contact / single-source), readiness bucket.
- Assign one code per disposition, consistently, inside the family's allowed
enum (
FIELD_PROVENANCE→FP, IDENTITY→IC, OUTREACH→OR).
Getting the structure right (a distinct code per distinct disposition) is what
you can control; never emit one blanket code for every case.
Output discipline (check before returning)
- Exactly the required top-level keys; nothing extra; no nulls where a value is
required.
- Enums spelled exactly; IDs match their
pattern; array lengths match
minItems/maxItems.
- Every list ordered as specified (usually lexicographic/ascending; ranked
arrays by their sort keys); dedup sets; sort
member/evidence lists.
- Numbers rounded to the stated precision (
multipleOf); integer fields integer.
- Self-consistency:
valid + quarantine = logical; reason buckets sum to
quarantine; region/class rollups sum to their totals; readiness buckets sum to
their denominator.
- Emit only the JSON object — no prose, no Markdown fences.
See references/pipeline.md for a per-family checklist and
references/hub_client.py for a ready-to-use read-only query helper.
1---2name: asteria-fleet-dq-reconciliation-23description: Solve an Asteria "Fleet Data Quality Hub" reconciliation task. Use when a task gives you payloads/case_scope.json plus payloads/answer_template.json and asks you to reconcile overlapping source records from a read-only data hub (catalog, schema, SQL query, source-snapshot, contact/transaction/maintenance, and reference alias/conversion/fx interfaces) and return exactly one JSON object. Covers three task families: contact-master reconciliation, transaction normalization (fuel/freight), and maintenance-log integrity.4---56# Asteria Fleet Data Quality Hub — reconciliation tasks78## What these tasks look like9Each task ships two payloads and points at a shared read-only data hub:1011- `payloads/case_scope.json` — the scope: `collection_id`, a business cutoff /12 as-of, the focus/anchor/decision IDs you must report on, and (sometimes)13 status thresholds or a certification gate + status→action map.14- `payloads/answer_template.json` — the answer contract: exact keys, enums,15 array lengths, ordering rules, and numeric precision. **This file is the16 spec. Read every field description, `enum`, `pattern`, `minItems/maxItems`,17 `multipleOf`, and ordering note before writing any code.**1819Connection details (base URL and a read-only bearer token) are provided20separately in `environment_access.md`. Read them from that file at runtime;21never hardcode them. `references/hub_client.py` does this for you.2223Deliver **one JSON object** matching the template exactly — no commentary, no24Markdown, no extra keys (`additionalProperties` is false in most contracts).2526## Step 0 — orient271. Read `case_scope.json` and the whole `answer_template.json`. List every28 output key and its ordering/rounding/enum constraints.292. Discover the schema from the hub catalog before querying (do not assume30 column names). The hub exposes stable logical views: `v_contacts`,31 `v_fuel_transactions`, `v_freight_charges`, `v_maintenance_events`,32 `v_source_snapshots`, `v_reference_aliases`, `v_unit_conversions`,33 `v_fx_rates`.343. Pull data through the hub's read-only SQL query interface with a body of the35 form `{"query": "SELECT ... FROM v_* WHERE collection_id='...'"}`. It is SQL36 over the views and supports `WHERE/GROUP BY/JOIN/aggregates`. **It truncates37 at 2000 rows** (a `truncated` flag tells you). Every single collection fits38 under that cap, so pull the whole collection in one query and reconcile in39 code. `sqlite_master`/DDL are blocked.4041## Universal reconciliation model (all families)4243**Snapshots & the authoritative source.** A collection has several snapshots in44`v_source_snapshots`, one per `source_system`, each with a `snapshot_status`45(`CERTIFIED` / `PROVISIONAL` / `STALE`), a `business_cutoff`, and a `row_count`.46The **authoritative snapshot is the CERTIFIED one**; its `snapshot_id` and47`row_count` are what "authoritative_*" fields want.4849**Cutoff.** Keep rows whose *business date* (e.g. `purchased_at`,50`service_date`, event time) is `<=` the scope cutoff. Do **not** filter on51`ingested_at` — rows that landed in the hub after the cutoff are still in scope.5253**Dedup → logical records.** The same logical entity (transaction/charge/event)54appears in more than one snapshot. Group by the stable business ID55(`transaction_id`, `charge_id`, `event_id`); the **retained/logical record is56the one from the authoritative (CERTIFIED) snapshot** (fall back to whatever57exists if it is not present there). Then:58- `raw_row_count` = in-scope raw rows across all snapshots.59- `logical_*_count` = distinct business IDs.60- `duplicate_raw_count` = raw − logical.61- A duplicate group reports the business ID, its snapshot IDs (sorted), the raw62 occurrence count, and the retained (certified) snapshot ID.63Confirm there are no cross-snapshot duplicates hiding under *different* business64IDs (same asset/time/type) before trusting the count.6566**Reference tables.**67- `v_reference_aliases` (`domain`, `alias_text`, `canonical_value`, `valid_from`,68 `valid_to`, `reference_status`): map free text → a canonical category. Use only69 aliases that are `ACTIVE` **and** whose validity window covers the record's70 business date. Temporal traps exist: an alias can be `ACTIVE` yet not-yet-71 effective (future `valid_from`), and the in-window mapping can be flagged72 `INACTIVE`/`PROVISIONAL`.73- `v_unit_conversions` (`kind`, `from_unit`, `to_unit`, `factor`, `precision`):74 multiply by `factor` to reach the canonical unit (self-unit factor = 1).75- `v_fx_rates` (`rate_date`, `currency`, `usd_per_unit`, `rate_status`): prefer76 the `CERTIFIED` rate whose `rate_date` equals the record's business date;77 `usd = amount * usd_per_unit`; USD→USD = 1.7879**Description → category matching (transaction families).** Tokenise the80description into lowercase alphanumeric tokens. Scan left to right taking the81**longest** contiguous alias-token subsequence that matches (so82`premium unleaded` beats bare `unleaded`; filler like `with liftgate accessorial`83is ignored). Match whole-token phrases, not raw substrings. Collect the distinct84canonical values:85- 0 → **unrecognized**; ≥2 distinct → **ambiguous** (both quarantine).86- exactly 1 → **recognized** category (the "actual" class).8788**Classification (transaction families).** For each logical record:89- `invalid_*` = a required physical measure is missing or ≤ 0 (quantity, weight,90 distance).91- **quarantined** = unrecognized OR ambiguous OR any invalid measure.92- **valid** = recognized-unique AND all required measures > 0.93- **mismatch** = *valid* AND recognized ≠ expected category. Mismatch is defined94 on valid records only (this matters).95- **exception** = mismatch OR quarantined.96- Invariants (verify them): `valid + quarantined = logical`; mismatch and97 quarantine are **disjoint** and their union size = the exception count;98 quarantine reason buckets are typically disjoint and sum to the quarantine99 count. `record_status` values like REVIEW/BILLED/POSTED are **not** an100 exclusion criterion.101102**Normalized totals.** Sum over **valid** records only (quarantined excluded;103valid mismatches ARE included). Convert units and FX as above, group by the104**recognized** category, and round to the template's precision. Per-group counts105must sum to the overall valid count.106107## Family specifics108109### A. Contact-master reconciliation (people/orgs) — `v_contacts`110Columns include `row_id`, `source_system`, `email`, `phone`, `city`, `region`,111`consent_status`, `record_status`, `verified_flag`, `master_hint`. Typical112shape: N entities × several source systems, a block of no-contact rows, and a113block of identity edge-cases.114115- **Normalise** email (NFKC, strip, lowercase; treat `''`, `none`, `null`,116 `n/a`, `nan`, … as empty; usable ⇔ has `@` and a dotted domain) and phone117 (digits only; usable ⇔ ≥ 7 digits).118- **Cluster** by usable email (primary). Phone is a valid **secondary** merge119 key **except shared identifiers**: a phone flagged120 `master_hint = SHARED-HELPDESK`, or any phone tied to ≥ 2 distinct emails,121 must NOT merge people (shared helpdesk line). **Name is never a merge key**122 (same name + different contact = different people). `NOISY-*` hint rows are123 isolated singletons — not merge signals.124- **Quarantine row** = no usable email and no usable phone.125- **`canonical_entity_count` / `canonical_person_count` INCLUDES quarantined126 (no-contact) rows as singleton entities.** Region/depot rollups also include127 them and must sum to the canonical count (strong self-check; the synthetic128 data tends to split evenly across regions).129- `duplicate/merged cluster count` = clusters with > 1 member.130- **Readiness / dispatch.** Eligible ⇔ `ACTIVE` and has a usable channel. A131 channel/person is "ready"/"dispatchable" only when consent is `GRANTED`.132 Produce whichever partition the template asks for — either133 dispatchable / blocked-consent / blocked-no-contact / blocked-inactive, or134 both / email_only / phone_only / not_ready — and make the buckets sum to the135 stated denominator.136- **Field-level precedence.** Different fields may come from different sources;137 the answer often reports a per-field `*_source_system` and a138 `resolution_outcome` (`SINGLE_SOURCE`, `FIELD_LEVEL_PRECEDENCE_APPLIED`,139 `CONTESTED_NO_AUTOMERGE`, `NO_USABLE_CONTACT`). Resolve each field from the140 highest-precedence source holding a usable value, and keep the choice141 consistent everywhere. **The exact per-field source authority (name vs contact142 vs depot vs consent) and the canonical-consent rule for a merged person are143 the hardest, least-obvious sub-problem** — pin them down from the focus/anchor144 probe rows the scope highlights rather than assuming a fixed order.145- **Contested identifier** watchlist case = the anchor row's usable identifier146 (e.g. phone) is shared across different people.147148### B. Transaction normalization — fuel & freight149`v_fuel_transactions` / `v_freight_charges`. Apply the universal transaction150model. Fuel: volume→L, spend→USD. Freight: weight→KG, distance→KM, spend→USD,151and quarantine on non-positive weight or distance. Merchant/carrier rankings152order by an exposure/exception measure descending, then ID ascending, limited to153the scope's top-N, reporting per-entity component counts (mismatch / quarantine154/ exception).155156### C. Maintenance-log integrity — `v_maintenance_events`157Columns include `event_id`, `asset_id`, `event_time_raw`, `odometer_value/unit`,158`labor_hours`.159- **Issue flags** on the retained record: missing timestamp (null/blank),160 invalid timestamp (unparseable), invalid odometer (null/negative), negative161 labor (< 0), extreme labor (> a full-day threshold, e.g. 24h).162- `invalid_event_ids` = the deduped union of those. `valid_event_count` =163 logical − invalid.164- **Odometer regression** = within an asset's *valid* events ordered by time, a165 reading below the previous one. Regressions stay **valid** and are reported166 separately (asset IDs + event IDs), not counted as invalid.167- **Corrected distance** = per asset (last − first reliable odometer, converted168 to km), summed across assets.169- **Asset risk ranking**: rejected-event count desc, then regression-event count170 desc, then asset ID asc.171172## Status / certification decisions173- If the scope gives `status_thresholds` (e.g. a max quarantine rate for PASS vs174 PASS_WITH_EXCEPTIONS), compute the rate the template defines (e.g. quarantined175 ÷ canonical entities, rounded) and map status → `action` via the scope's176 `status_action_map`.177- If the scope gives a certification gate (e.g. "odometer regression ⇒ HOLD /178 BLOCK_AND_REMEDIATE"), apply it.179- When no threshold is supplied, the release/close decision is whatever the180 scope's rule implies — **derive it; do not assume.** Different tasks resolve to181 different statuses (some to HOLD when issues exist, some to PASS/RELEASE182 because exceptions are only flagged, not blocking).183184## Opaque control-code panels185Some tasks ask for compact codes for scoped IDs — identity/outreach/field-186provenance (`IC-* / OR-* / FP-*`), reference-policy/source-basis/ledger-187disposition (`RB-* / SB-* / LD-*`), or maintenance-source/history-route188(`MS-* / HR-*`). Their expansions are intentionally withheld. Method:1891. Classify each scoped ID into its **data-derived disposition** — e.g. source190 basis (certified-only / provisional-only / both), disposition (valid /191 rejected / regression), alias `reference_status`, identity outcome192 (clean-merge / contested / no-contact / single-source), readiness bucket.1932. Assign one code per disposition, consistently, inside the family's allowed194 enum (`FIELD_PROVENANCE→FP`, `IDENTITY→IC`, `OUTREACH→OR`).195Getting the *structure* right (a distinct code per distinct disposition) is what196you can control; never emit one blanket code for every case.197198## Output discipline (check before returning)199- Exactly the required top-level keys; nothing extra; no nulls where a value is200 required.201- Enums spelled exactly; IDs match their `pattern`; array lengths match202 `minItems/maxItems`.203- Every list ordered as specified (usually lexicographic/ascending; ranked204 arrays by their sort keys); dedup sets; sort `member`/`evidence` lists.205- Numbers rounded to the stated precision (`multipleOf`); integer fields integer.206- Self-consistency: `valid + quarantine = logical`; reason buckets sum to207 quarantine; region/class rollups sum to their totals; readiness buckets sum to208 their denominator.209- Emit only the JSON object — no prose, no Markdown fences.210211See `references/pipeline.md` for a per-family checklist and212`references/hub_client.py` for a ready-to-use read-only query helper.