Ship a damaged-package photo triage service with quality checks, slice evaluation, serving bundles, and review monitoring.
Shipment rows, ranked items, and warehouse time series all fit into tables. A customer return adds a new input type: a photo of a package that may be crushed, torn, blurred, dark, or unrelated to the order.
Earlier, you traced a convolutional neural network (CNN) over a damaged-package image patch. This capstone turns that spatial reasoning into a product: an image triage endpoint that flags likely visible damage, rejects unusable photos, preserves evidence for human review, and never turns an uncertain image score directly into a refund.
ShopFlow receives return photos from customers and warehouse intake stations. The useful product question isn't "does the model recognize every defect?" It's: which photo should a specialist inspect first, and when is the photo too weak to support any decision?
Use three operational outcomes:
| Action | Evidence | Product behavior |
|---|---|---|
request_new_photo | image is too blurred, dark, or incomplete | ask for a clearer upload before assessing damage |
normal_review | usable image, low damage score | keep ordinary return workflow |
priority_damage_review | usable image, high damage score | surface to specialist with photo and score trace |
The classifier isn't a refund policy. Product eligibility still depends on order ownership, item type, return window, and specialist judgment. This separation prevents a shadow or reflection in a photo from issuing a costly action.
A model card should state the intended use, input constraints, decision threshold, evaluated slices, and known failure cases. Model cards were proposed as structured reports for exactly this type of deployed-model context: users need more than a metric without its operating conditions.[1]
For tabular models, leakage may be a future delivery timestamp. For photos, leakage often hides in nearly identical pixels. A customer may upload three bursts of the same crushed box. A warehouse may photograph one parcel from four angles. If related images land in both train and test sets, the model can memorize one package rather than generalize to new damage.
Your manifest should contain:
| Field | Why it matters |
|---|---|
case_id and capture_day | group all photos for one physical package and preserve time ordering |
source | separate customer phone uploads from warehouse inspection cameras |
quality_label | distinguish unusable evidence from visible damage |
damage_label | record specialist-confirmed visible damage only on usable photos |
split | hold out later shipments, never random photos from the same case |
reviewer_id and guideline_version | audit disagreement or changed label definitions |
Evaluate at least daylight versus dark uploads, customer versus warehouse source, packaging type, and visible-defect size. A global score can hide the exact failure that matters: small tears disappearing in dark phone images.
Use the CNN learned earlier as a baseline, then fine-tune a pretrained image encoder only if you record its preprocessing and measure it under the same split. A later deep-dive explains Vision Transformer image encoders; this capstone doesn't require that architecture.[2]
Start with the dataset boundary. Multiple photos from one physical package belong in one split even when filenames differ. Keep time direction intact too: later return cases should test a model trained on earlier cases.
The local manifest below is small enough to inspect line by line. This historical evaluation fixture contains frozen specialist labels, reviewer IDs, and guideline versions for dataset audit. Damage labels appear only when a specialist judged the image usable. Unusable photos keep confirmed_damage=None because poor evidence shouldn't become supervision. Candidate scores arrive in a separate object, so route code can't quietly read labels as model output.
1from collections import Counter, defaultdict
2from dataclasses import dataclass
3import json
4
5@dataclass(frozen=True)
6class PhotoManifestRow:
7 case_id: str
8 photo_id: str
9 capture_day: int
10 split: str
11 source: str
12 packaging: str
13 quality_label: str
14 confirmed_damage: bool | None
15 reviewer_id: str
16 guideline_version: str
17
18PHOTOS = [
19 PhotoManifestRow("R-401", "R-401-a", 1, "train", "customer_phone", "corrugated", "usable", True, "S-12", "visible-damage-v1"),
20 PhotoManifestRow("R-401", "R-401-b", 2, "train", "customer_phone", "corrugated", "usable", True, "S-12", "visible-damage-v1"),
21 PhotoManifestRow("R-402", "R-402-a", 3, "validation", "customer_phone", "corrugated", "unusable", None, "S-08", "visible-damage-v1"),
22 PhotoManifestRow("R-403", "R-403-a", 4, "validation", "warehouse_camera", "mailer", "usable", False, "S-08", "visible-damage-v1"),
23 PhotoManifestRow("R-404", "R-404-a", 5, "test", "customer_phone", "mailer", "usable", True, "S-12", "visible-damage-v1"),
24 PhotoManifestRow("R-404", "R-404-b", 6, "test", "customer_phone", "mailer", "usable", True, "S-12", "visible-damage-v1"),
25 PhotoManifestRow("R-405", "R-405-a", 7, "test", "warehouse_camera", "corrugated", "usable", False, "S-08", "visible-damage-v1"),
26 PhotoManifestRow("R-406", "R-406-a", 8, "test", "customer_phone", "corrugated", "unusable", None, "S-12", "visible-damage-v1"),
27 PhotoManifestRow("R-407", "R-407-a", 9, "test", "customer_phone", "corrugated", "usable", False, "S-12", "visible-damage-v1"),
28 PhotoManifestRow("R-408", "R-408-a", 10, "test", "warehouse_camera", "corrugated", "usable", True, "S-08", "visible-damage-v1"),
29 PhotoManifestRow("R-409", "R-409-a", 11, "test", "warehouse_camera", "corrugated", "unusable", None, "S-08", "visible-damage-v1"),
30]
31
32print("photos:", len(PHOTOS))
33print("cases:", len({photo.case_id for photo in PHOTOS}))1photos: 11
2cases: 91splits_by_case = defaultdict(set)
2for photo in PHOTOS:
3 splits_by_case[photo.case_id].add(photo.split)
4
5split_days = {
6 split: [photo.capture_day for photo in PHOTOS if photo.split == split]
7 for split in ("train", "validation", "test")
8}
9manifest_checks = {
10 "case_groups_do_not_cross_splits": all(len(splits) == 1 for splits in splits_by_case.values()),
11 "time_ordered_splits": (
12 all(split_days.values())
13 and max(split_days["train"]) < min(split_days["validation"])
14 and max(split_days["validation"]) < min(split_days["test"])
15 ),
16 "usable_labels_complete": all(photo.confirmed_damage is not None for photo in PHOTOS if photo.quality_label == "usable"),
17 "unusable_labels_abstain": all(photo.confirmed_damage is None for photo in PHOTOS if photo.quality_label == "unusable"),
18 "label_lineage_recorded": all(photo.reviewer_id and photo.guideline_version for photo in PHOTOS),
19}
20
21print("split photo counts:", dict(Counter(photo.split for photo in PHOTOS)))
22print("R-401 splits:", sorted(splits_by_case["R-401"]))
23print(json.dumps(manifest_checks, indent=2))1split photo counts: {'train': 2, 'validation': 2, 'test': 7}
2R-401 splits: ['train']
3{
4 "case_groups_do_not_cross_splits": true,
5 "time_ordered_splits": true,
6 "usable_labels_complete": true,
7 "unusable_labels_abstain": true,
8 "label_lineage_recorded": true
9}The fixture uses explicit splits so the invariant stays visible. A larger pipeline can use a group-aware splitter, then freeze and audit the resulting manifest. The important claim isn't that one splitter solves every dataset: no physical case may cross evaluation boundaries, and later cases remain later.
The model endpoint should receive a preprocessing result and a damage score, then choose a review route. Quality checks run before the damage threshold. Otherwise a confidently scored blur, dark frame, or unrelated object can create an unsupported escalation.
The score fixture below is separate from the manifest. Labels stay available for offline evaluation, but the router never reads them. A production system may run its cheap quality checks before an expensive damage model; this compact fixture keeps both outputs so you can test that an unsupported high damage score still abstains.
1@dataclass(frozen=True)
2class ModelOutput:
3 damage_probability: float
4 blur_score: float
5 brightness: float
6 box_visible: bool
7
8MODEL_OUTPUTS = {
9 "R-401-a": ModelOutput(0.91, 0.12, 0.66, True),
10 "R-401-b": ModelOutput(0.88, 0.10, 0.70, True),
11 "R-402-a": ModelOutput(0.93, 0.71, 0.51, True),
12 "R-403-a": ModelOutput(0.18, 0.08, 0.75, True),
13 "R-404-a": ModelOutput(0.83, 0.10, 0.64, True),
14 "R-404-b": ModelOutput(0.79, 0.14, 0.61, True),
15 "R-405-a": ModelOutput(0.32, 0.11, 0.68, True),
16 "R-406-a": ModelOutput(0.94, 0.12, 0.12, True),
17 "R-407-a": ModelOutput(0.73, 0.09, 0.65, True),
18 "R-408-a": ModelOutput(0.63, 0.07, 0.72, True),
19 "R-409-a": ModelOutput(0.81, 0.08, 0.74, False),
20}
21
22BUNDLE = {
23 "bundle_id": "damage-cnn-v1",
24 "previous_bundle": "damage-cnn-v0",
25 "preprocessing": "parcel-rgb-224-center-crop-v1",
26 "label_guideline": "visible-damage-v1",
27 "route_policy": "quality-first-review-v1",
28 "damage_threshold": 0.70,
29 "max_blur": 0.45,
30 "min_brightness": 0.20,
31}
32
33print("bundle:", BUNDLE["bundle_id"], "threshold=", BUNDLE["damage_threshold"])1def route(photo: PhotoManifestRow, model_output: ModelOutput) -> dict[str, object]:
2 if not model_output.box_visible:
3 action, reason = "request_new_photo", "package_not_visible"
4 elif model_output.blur_score > BUNDLE["max_blur"] or model_output.brightness < BUNDLE["min_brightness"]:
5 action, reason = "request_new_photo", "image_quality_gate"
6 elif model_output.damage_probability >= BUNDLE["damage_threshold"]:
7 action, reason = "priority_damage_review", "damage_threshold"
8 else:
9 action, reason = "normal_review", "below_threshold"
10
11 return {
12 "route_id": f"{BUNDLE['bundle_id']}:{photo.photo_id}",
13 "photo_id": photo.photo_id,
14 "case_id": photo.case_id,
15 "routed_day": photo.capture_day,
16 "source": photo.source,
17 "packaging": photo.packaging,
18 "bundle_id": BUNDLE["bundle_id"],
19 "previous_bundle": BUNDLE["previous_bundle"],
20 "preprocessing": BUNDLE["preprocessing"],
21 "label_guideline": BUNDLE["label_guideline"],
22 "route_policy": BUNDLE["route_policy"],
23 "damage_threshold": BUNDLE["damage_threshold"],
24 "max_blur": BUNDLE["max_blur"],
25 "min_brightness": BUNDLE["min_brightness"],
26 "damage_probability": model_output.damage_probability,
27 "blur_score": model_output.blur_score,
28 "brightness": model_output.brightness,
29 "box_visible": model_output.box_visible,
30 "action": action,
31 "reason": reason,
32 }
33
34print("route keys:", len(route(PHOTOS[0], MODEL_OUTPUTS["R-401-a"])))1test_photos = [photo for photo in PHOTOS if photo.split == "test"]
2photo_by_id = {photo.photo_id: photo for photo in PHOTOS}
3traces = [route(photo, MODEL_OUTPUTS[photo.photo_id]) for photo in test_photos]
4trace_by_photo = {trace["photo_id"]: trace for trace in traces}
5unsafe_priority_routes = [
6 trace["photo_id"]
7 for photo, trace in zip(test_photos, traces)
8 if trace["action"] == "priority_damage_review" and photo.quality_label != "usable"
9]
10
11for photo_id in ("R-404-a", "R-406-a", "R-409-a"):
12 trace = trace_by_photo[photo_id]
13 print(photo_id, trace["action"], trace["reason"], f"score={trace['damage_probability']}")
14print("route counts:", dict(Counter(trace["action"] for trace in traces)))
15print("unsafe priority routes:", unsafe_priority_routes)1R-404-a priority_damage_review damage_threshold score=0.83
2R-406-a request_new_photo image_quality_gate score=0.94
3R-409-a request_new_photo package_not_visible score=0.81
4route counts: {'priority_damage_review': 3, 'normal_review': 2, 'request_new_photo': 2}
5unsafe priority routes: []Cases R-406-a and R-409-a are the important failure tests. Both have high damage scores. Neither score counts as usable evidence because quality checks fail first. The endpoint asks for another photo instead of escalating an unsupported claim.
Submit an inspectable repository, not a notebook screenshot:
1damage-vision-service/
2 data/
3 label_guidelines.md
4 photo_manifest.parquet
5 split_manifest.json
6 model/
7 train_cnn_baseline.py
8 evaluate_slices.py
9 model_card.md
10 service/
11 preprocess.py
12 route_review.py
13 trace_schema.json
14 monitoring/
15 input_quality_report.py
16 delayed_review_outcomes.py
17 specialist_shadow_receipt.py
18 tests/
19 test_case_groups_do_not_cross_splits.py
20 test_blurry_photo_never_escalates.py
21 test_later_outcomes_join_routes.py
22 test_empty_review_window_holds.py
23 test_route_trace_is_versioned.py
24 test_previous_bundle_required.pyThe serving bundle must pin image resize and crop behavior, color normalization, model weights, label version, route policy, damage threshold, quality-gate thresholds, and previous-bundle pointer. A change from center crop to full-frame resize may change whether a torn corner remains visible; it's a model behavior change even when weights remain constant.
The routing cells emit that trace shape directly. They let a reviewer reconstruct the route:
| Response field | Example |
|---|---|
| bundle and preprocessing | damage-cnn-v1, parcel-rgb-224-center-crop-v1 |
| rollback and label contract | damage-cnn-v0, visible-damage-v1 |
| quality values | blur 0.12, brightness 0.66, box visible true |
| score and action policy | damage 0.91, threshold 0.70, blur maximum 0.45, brightness minimum 0.20 |
| route | priority_damage_review |
| human outcome later | confirmed_damage or not_supported |
Photo models drift when the image source changes. A new warehouse camera, winter lighting, a mobile upload compressor, or new packaging graphics can alter pixels before a confirmed-damage label exists.
Separate immediate checks from delayed quality:
| Window | Monitor | Trigger |
|---|---|---|
| immediate | unreadable image rate, brightness, blur, missing package, latency | investigate capture path or fail to manual intake |
| delayed | specialist-confirmed precision, missed visible damage, route rate by source and packaging | hold promotion or create retraining candidate |
| safety review | unsupported escalations, policy actions attempted without specialist approval | rollback and audit workflow |
Google Cloud's MLOps guidance treats serving, monitoring, validation, metadata, and continuous training as connected stages rather than a one-time deploy step.[3] Apply that same discipline here: a change in image quality creates an investigation or candidate run, never an automatic production replacement.
The final local receipt appends specialist outcomes only after routing, then joins each outcome to a physical case. Each delayed outcome also carries its reviewer and label guideline, so precision and recall can't silently mix definitions. It measures review precision and recall once per usable test case, records immediate request rates by source, proves unusable photos abstain, and keeps the previous bundle beside the candidate. When one package has multiple usable photos, the case escalates if any usable photo crosses the priority threshold. These tiny counts teach the contract; production promotion still needs larger slices and shadow traffic.
1@dataclass(frozen=True)
2class SpecialistOutcome:
3 case_id: str
4 reviewed_day: int
5 confirmed_damage: bool
6 reviewer_id: str
7 guideline_version: str
8
9SPECIALIST_OUTCOMES = [
10 SpecialistOutcome("R-404", 12, True, "S-12", "visible-damage-v1"),
11 SpecialistOutcome("R-405", 12, False, "S-08", "visible-damage-v1"),
12 SpecialistOutcome("R-407", 12, False, "S-12", "visible-damage-v1"),
13 SpecialistOutcome("R-408", 12, True, "S-08", "visible-damage-v1"),
14]
15
16usable_test_case_ids = {
17 photo.case_id
18 for photo in test_photos
19 if photo.quality_label == "usable"
20}
21traces_by_case = defaultdict(list)
22for trace in traces:
23 traces_by_case[trace["case_id"]].append(trace)
24
25print("usable test cases:", sorted(usable_test_case_ids))
26print("specialist outcomes:", len(SPECIALIST_OUTCOMES))1def aggregate_usable_case_action(case_id: str) -> str:
2 usable_traces = [
3 trace
4 for trace in traces_by_case[case_id]
5 if photo_by_id[trace["photo_id"]].quality_label == "usable"
6 ]
7 if not usable_traces:
8 raise ValueError(f"no usable traces for {case_id}")
9 if any(trace["action"] == "priority_damage_review" for trace in usable_traces):
10 return "priority_damage_review"
11 return "normal_review"
12
13case_actions = {
14 case_id: aggregate_usable_case_action(case_id)
15 for case_id in sorted(usable_test_case_ids)
16}
17outcome_by_case = {outcome.case_id: outcome for outcome in SPECIALIST_OUTCOMES}
18joined_outcomes = [
19 (case_id, case_actions[case_id], outcome_by_case[case_id].confirmed_damage)
20 for case_id in sorted(case_actions.keys() & outcome_by_case.keys())
21]
22
23print("case_actions:", case_actions)
24print("joined usable cases:", len(joined_outcomes))1def rate_or_none(numerator: int, denominator: int) -> float | None:
2 return round(numerator / denominator, 3) if denominator else None
3
4true_positives = sum(
5 action == "priority_damage_review" and confirmed_damage
6 for _, action, confirmed_damage in joined_outcomes
7)
8false_positives = sum(
9 action == "priority_damage_review" and not confirmed_damage
10 for _, action, confirmed_damage in joined_outcomes
11)
12false_negatives = sum(
13 action != "priority_damage_review" and confirmed_damage
14 for _, action, confirmed_damage in joined_outcomes
15)
16delayed_quality = {
17 "priority_precision": rate_or_none(true_positives, true_positives + false_positives),
18 "priority_recall": rate_or_none(true_positives, true_positives + false_negatives),
19 "reviewed_usable_cases": len(joined_outcomes),
20}
21
22source_slices = {
23 source: {
24 "photos": len(rows),
25 "request_new_photo_rate": round(
26 sum(trace["action"] == "request_new_photo" for trace in rows) / len(rows),
27 3,
28 ),
29 }
30 for source in sorted({trace["source"] for trace in traces})
31 for rows in [[trace for trace in traces if trace["source"] == source]]
32}
33
34print("delayed_quality:", delayed_quality)
35print("source_slices:", source_slices)1required_trace_fields = {
2 "route_id", "photo_id", "case_id", "routed_day", "source", "packaging",
3 "bundle_id", "previous_bundle", "preprocessing", "label_guideline",
4 "route_policy", "damage_threshold", "max_blur", "min_brightness",
5 "damage_probability", "blur_score", "brightness", "box_visible",
6 "action", "reason",
7}
8outcome_case_ids = [outcome.case_id for outcome in SPECIALIST_OUTCOMES]
9release_gates = {
10 **manifest_checks,
11 "route_ids_unique": len({trace["route_id"] for trace in traces}) == len(traces),
12 "all_test_photos_routed": len(traces) == len(test_photos),
13 "unusable_photos_abstain": all(
14 trace_by_photo[photo.photo_id]["action"] == "request_new_photo"
15 for photo in test_photos
16 if photo.quality_label == "unusable"
17 ),
18 "unsafe_priority_routes_absent": not unsafe_priority_routes,
19 "route_traces_replayable": all(required_trace_fields <= trace.keys() for trace in traces),
20 "specialist_outcome_case_ids_unique": len(set(outcome_case_ids)) == len(outcome_case_ids),
21 "specialist_outcomes_join_usable_test_cases": set(outcome_case_ids) <= usable_test_case_ids,
22 "usable_test_cases_have_specialist_outcomes": usable_test_case_ids <= set(outcome_case_ids),
23 "specialist_outcomes_arrive_after_capture": all(
24 outcome.reviewed_day > max(
25 photo.capture_day for photo in test_photos if photo.case_id == outcome.case_id
26 )
27 for outcome in SPECIALIST_OUTCOMES
28 if outcome.case_id in usable_test_case_ids
29 ),
30 "specialist_outcome_label_contract_matches_bundle": all(
31 outcome.reviewer_id
32 and outcome.guideline_version == BUNDLE["label_guideline"]
33 for outcome in SPECIALIST_OUTCOMES
34 ),
35 "priority_precision_evidence_at_least_0_50": (
36 delayed_quality["priority_precision"] is not None
37 and delayed_quality["priority_precision"] >= 0.50
38 ),
39 "priority_recall_evidence_at_least_0_50": (
40 delayed_quality["priority_recall"] is not None
41 and delayed_quality["priority_recall"] >= 0.50
42 ),
43 "source_slices_recorded": set(source_slices) == {"customer_phone", "warehouse_camera"},
44 "rollback_pointer_recorded": bool(BUNDLE["previous_bundle"]),
45}
46
47print("release_gates_pass:", all(release_gates.values()))1receipt = {
2 "candidate_bundle": BUNDLE["bundle_id"],
3 "previous_bundle": BUNDLE["previous_bundle"],
4 "preprocessing": BUNDLE["preprocessing"],
5 "label_guideline": BUNDLE["label_guideline"],
6 "route_policy": BUNDLE["route_policy"],
7 "route_traces": len(traces),
8 "later_specialist_outcomes": len(SPECIALIST_OUTCOMES),
9 "joined_usable_cases": len(joined_outcomes),
10 "case_actions": case_actions,
11 "delayed_quality": delayed_quality,
12 "source_slices": source_slices,
13 "release_gates": release_gates,
14 "candidate_decision": "candidate_for_specialist_shadow_review" if all(release_gates.values()) else "hold",
15}
16
17print(json.dumps(receipt, indent=2))1{
2 "candidate_bundle": "damage-cnn-v1",
3 "previous_bundle": "damage-cnn-v0",
4 "preprocessing": "parcel-rgb-224-center-crop-v1",
5 "label_guideline": "visible-damage-v1",
6 "route_policy": "quality-first-review-v1",
7 "route_traces": 7,
8 "later_specialist_outcomes": 4,
9 "joined_usable_cases": 4,
10 "case_actions": {
11 "R-404": "priority_damage_review",
12 "R-405": "normal_review",
13 "R-407": "priority_damage_review",
14 "R-408": "normal_review"
15 },
16 "delayed_quality": {
17 "priority_precision": 0.5,
18 "priority_recall": 0.5,
19 "reviewed_usable_cases": 4
20 },
21 "source_slices": {
22 "customer_phone": {
23 "photos": 4,
24 "request_new_photo_rate": 0.25
25 },
26 "warehouse_camera": {
27 "photos": 3,
28 "request_new_photo_rate": 0.333
29 }
30 },
31 "release_gates": {
32 "case_groups_do_not_cross_splits": true,
33 "time_ordered_splits": true,
34 "usable_labels_complete": true,
35 "unusable_labels_abstain": true,
36 "label_lineage_recorded": true,
37 "route_ids_unique": true,
38 "all_test_photos_routed": true,
39 "unusable_photos_abstain": true,
40 "unsafe_priority_routes_absent": true,
41 "route_traces_replayable": true,
42 "specialist_outcome_case_ids_unique": true,
43 "specialist_outcomes_join_usable_test_cases": true,
44 "usable_test_cases_have_specialist_outcomes": true,
45 "specialist_outcomes_arrive_after_capture": true,
46 "specialist_outcome_label_contract_matches_bundle": true,
47 "priority_precision_evidence_at_least_0_50": true,
48 "priority_recall_evidence_at_least_0_50": true,
49 "source_slices_recorded": true,
50 "rollback_pointer_recorded": true
51 },
52 "candidate_decision": "candidate_for_specialist_shadow_review"
53}candidate_for_specialist_shadow_review is narrower than launch approval. The receipt says this frozen bundle deserves comparison beside current production routing. It doesn't claim that four usable local cases prove every camera, lighting condition, package type, or damage shape.
Use the runnable examples as a release harness. Change one condition at a time, predict the failure, then rerun the examples.
R-401-b split from train to validation. Which dataset gate fails?R-406-a brightness in MODEL_OUTPUTS from 0.12 to 0.30. Why does this expose a quality-detector failure rather than prove the escalation is safe?damage_threshold from the response trace. Which receipt gate fails?R-408-a damage probability in MODEL_OUTPUTS from 0.63 to 0.75. Which delayed metrics improve?BUNDLE["previous_bundle"] = "". Why is shadow evidence no longer promotion-ready?SPECIALIST_OUTCOMES with an empty list. Why do evidence gates fail instead of crashing or passing?SpecialistOutcome("R-999", 12, True, "S-12", "visible-damage-v1"). Which join gate fails?R-408 specialist outcome to guideline visible-damage-v2. Which provenance gate fails?| Artifact | Strong submission demonstrates |
|---|---|
| dataset contract | case-grouped time split, quality labels, damage labels, and reviewed slices |
| service | versioned preprocessing and safe quality-first routing with abstention |
| operations | immutable route traces, joined delayed outcomes, model card, source slices, shadow review, and rollback |
| Symptom | Cause | Fix |
|---|---|---|
| Holdout score is unrealistically high | photos from one case crossed splits | group by physical case and time |
| Blurry image triggers damage escalation | score evaluated before quality | gate evidence quality first |
| Specialist metric changes after a later edit | route trace was overwritten with outcome data | append specialist outcomes and join by case ID |
| Precision shifts after guideline update | delayed outcomes omit reviewer or label version | bind each outcome to reviewer and bundle label contract |
| Multi-photo case result depends on row order | case aggregation policy is implicit | define deterministic case-level priority routing |
| New camera changes decisions silently | preprocessing and source drift untracked | log source/quality slices and version bundle |
| Candidate advances without rollback target | receipt omits previous alias | publish immutable candidate and rollback pointer together |
Answer every question, then check your score. Score above 75% to mark this lesson complete.
10 questions remaining.
Model Cards for Model Reporting
Mitchell, M., Wu, S., Zaldivar, A., et al. · 2019 · FAT* 2019
An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale.
Dosovitskiy, A., et al. · 2020 · ICLR 2021
MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.
Google Cloud. · 2026 · Official documentation
Questions and insights from fellow learners.