fffiloni commited on
Commit
e9fb2a1
·
verified ·
1 Parent(s): 97ceb69

Upload 6 files

Browse files
Files changed (3) hide show
  1. CHANGELOG.md +42 -3
  2. README.md +28 -3
  3. app.py +184 -8
CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
 
1
 
2
- ## v190.14 Build-first explorer and linked validation stats polish
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  - Treat linked validation runs as children of Build Runs in Runs Explorer by default.
5
  - Rework Run Stats around Build Runs, linked tests, automatic success, and effective success.
@@ -15,9 +54,9 @@
15
  - Active Run snapshots and run summaries can now surface manual validation status, effective verdict, latency, and ZeroGPU duration measurements sourced from linked Space Test.
16
  - Runs Explorer labels linked validation runs, shows manual validation passed on Build cards, and opens the parent Build Run when a linked validation is selected.
17
 
18
- # Agentic Space Factory v190.14
19
 
20
- ## v190.14 smoke retry and final summary hardening
21
 
22
  - Generation smoke payloads now preserve Gradio schema choice value types before calling predict.
23
  - If Gradio rejects a smoke payload with a choice type mismatch, the worker records the first error, coerces args to schema choices, and retries once.
 
1
+ # v190.19 — Eval archive effective outcomes + owner flush
2
 
3
+ - Eval records now preserve automatic outcome and add effective outcome metadata for linked Space Test validations.
4
+ - Backend archive publishing enriches records with linked validation summaries, endpoint discovery metrics, smoke retry metrics and stricter privacy flags without storing prompts, args, target Space IDs or generated code.
5
+ - Eval publish statuses now use schema v1.3 and republish when a parent Build Run gains new effective validation state.
6
+ - Instance owners / configured eval admins can flush archived eval records from the mounted eval bucket while preserving archive configuration.
7
+
8
+ # v190.19 — Build completion matrix + endpoint discovery-first Space Test
9
+
10
+ - Added canonical `space_test_policy` for replay/complete/recover/blocked/unavailable Build Run states.
11
+ - Linked Space Test can now discover Gradio endpoints before validation when a Build Run has no known endpoint schema.
12
+ - Space Test UI consumes policy labels/messages and shows discovery-first validation state.
13
+ - Runs Explorer/Stats can surface recoverable and needs-validation states without treating Space Test as standalone.
14
+
15
+ ## v190.19 — Linked validation hardening and effective status UX
16
+
17
+ - Fixed linked Space Test parent update after smoke success by sourcing `api_name` and `expected_output_type` from the smoke payload/environment instead of out-of-scope variables.
18
+ - Prevented failed linked validation attempts from downgrading a parent Build Run that already has a successful linked validation.
19
+ - Clarified effective status filters/stats, Eval Archive effective hints, Space Test context, and latency/ZeroGPU recommendation provenance.
20
+
21
+ ## v190.16 — Cascade delete linked Space Tests
22
+
23
+ - Deleting a Build Run now cascades to its linked Space Test validation runs so no hidden validation artifacts are orphaned.
24
+ - Deleting a linked validation run updates the parent Build Run linked validation metadata and manual validation status.
25
+ - The delete modal now reports linked validation counts and the frontend removes parent/child cache entries together.
26
+
27
+ ## v190.16 — Build-first explorer compatibility cleanup
28
+
29
+ - Keeps Runs Explorer simple after the linked-only Space Test change: Build Runs remain the primary list items, linked validations are summarized on their parent, and no standalone/legacy validation section is introduced.
30
+ - Hardens the run cache, stats, pagination, and selection paths against null, partial, or legacy Build Run records that do not contain manual validation metadata.
31
+ - Fixes linked validation parent selection without duplicating variables in the snapshot path.
32
+ - Keeps the v190.12 generic Gradio schema-choice smoke retry intact.
33
+
34
+
35
+ ## v190.16 hotfix — Runs Explorer null-safe linked validation cache
36
+
37
+ - Hardened Runs Explorer against null or malformed run summaries returned by stale bucket/list responses.
38
+ - Preserved build-first linked validation grouping while filtering invalid cache entries safely.
39
+ - Added regression coverage for null-safe linked validation parent lookup.
40
+
41
+ ## v190.16 — Build-first explorer and linked validation stats polish
42
 
43
  - Treat linked validation runs as children of Build Runs in Runs Explorer by default.
44
  - Rework Run Stats around Build Runs, linked tests, automatic success, and effective success.
 
54
  - Active Run snapshots and run summaries can now surface manual validation status, effective verdict, latency, and ZeroGPU duration measurements sourced from linked Space Test.
55
  - Runs Explorer labels linked validation runs, shows manual validation passed on Build cards, and opens the parent Build Run when a linked validation is selected.
56
 
57
+ # Agentic Space Factory v190.16
58
 
59
+ ## v190.16 smoke retry and final summary hardening
60
 
61
  - Generation smoke payloads now preserve Gradio schema choice value types before calling predict.
62
  - If Gradio rejects a smoke payload with a choice type mismatch, the worker records the first error, coerces args to schema choices, and retries once.
README.md CHANGED
@@ -12,10 +12,29 @@ hf_oauth_scopes:
12
  - jobs
13
  - read-billing
14
  ---
15
- ## v190.14 smoke retry and live validation telemetry
16
 
17
- - v190.14 keeps Space Test prefill disabled until a build has reached a final Space state that can be tested manually.
18
- - v190.14 retries generation smoke once when Gradio reports schema-choice type mismatches, and writes terminal summary.json plus live_status.json for cleaner audits.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  - The canonical run snapshot now includes a `live_validation` telemetry model so the Live test phase can show Space build/runtime, health, Gradio endpoint discovery and generation smoke state instead of a generic running message.
20
  - Active polling runs at a faster visible cadence for smoother event updates.
21
 
@@ -395,3 +414,9 @@ The Active Run progress header now separates pipeline completion from product ve
395
  ## v190.9 — Space Test canonical validation persistence
396
 
397
  - v190.9 makes terminal Space Test validation status survive page reloads. Failed, success, partial, and manual validation outcomes override stale `running` launch metadata in the Runs Explorer, delete modal, and progress polling.
 
 
 
 
 
 
 
12
  - jobs
13
  - read-billing
14
  ---
 
15
 
16
+ ## v190.19 Linked validation hardening
17
+
18
+ - v190.19 hardens linked Space Test success/failure paths: parent Build Run updates read API/output metadata from the smoke result, failed manual attempts cannot downgrade an already validated parent, and latency/ZeroGPU recommendations expose their linked-test hardware provenance.
19
+ - Runs Explorer and Run Stats now treat automatic and effective statuses explicitly so manually validated partial builds remain discoverable in both Partial and Validated views.
20
+ - Space Test is contextualized from the selected Build Run even before pressing Prefill, while remaining linked-only for launches.
21
+
22
+
23
+ ## v190.16 Cascade delete linked Space Tests
24
+
25
+ - Build Run deletion now removes linked Space Test validation runs from the run bucket as part of the same cleanup.
26
+ - Linked validation deletion keeps the parent Build Run but refreshes its manual validation metadata.
27
+ - Runs Explorer remains Build-first and clears parent/child rows from cache immediately after deletion.
28
+
29
+ ## v190.16 Build-first explorer compatibility
30
+
31
+ - v190.16 keeps Runs Explorer simple: Build Runs are the primary objects, linked Space Tests are summarized on their parent, and legacy/incomplete Build Runs remain visible without requiring validation metadata.
32
+ - It hardens the run cache against null or partial records and keeps validation runs out of the primary explorer by default.
33
+
34
+ ## v190.16 smoke retry and live validation telemetry
35
+
36
+ - v190.16 keeps Space Test prefill disabled until a build has reached a final Space state that can be tested manually.
37
+ - v190.16 retries generation smoke once when Gradio reports schema-choice type mismatches, and writes terminal summary.json plus live_status.json for cleaner audits.
38
  - The canonical run snapshot now includes a `live_validation` telemetry model so the Live test phase can show Space build/runtime, health, Gradio endpoint discovery and generation smoke state instead of a generic running message.
39
  - Active polling runs at a faster visible cadence for smoother event updates.
40
 
 
414
  ## v190.9 — Space Test canonical validation persistence
415
 
416
  - v190.9 makes terminal Space Test validation status survive page reloads. Failed, success, partial, and manual validation outcomes override stale `running` launch metadata in the Runs Explorer, delete modal, and progress polling.
417
+
418
+ ## v190.19 Eval archive effective outcomes
419
+
420
+ - Eval archive records now distinguish automatic verdicts from effective verdicts after linked Space Test validation.
421
+ - Archived records include anonymized linked-validation, endpoint-discovery and smoke-retry metrics while preserving privacy constraints.
422
+ - Instance owners can flush all archived eval records from the mounted eval bucket without deleting the eval archive configuration.
app.py CHANGED
@@ -12,7 +12,7 @@ from huggingface_hub import HfApi, attach_huggingface_oauth
12
 
13
  from src.bucket import RunPaths, check_user_bucket, create_user_bucket, delete_run_folder, read_run_bundle, list_recent_runs, write_json, write_launch_metadata
14
  from src.config import settings, user_bucket_source
15
- from src.eval_config import activate_eval_archive_config, disable_eval_archive_config, public_eval_config
16
  from src.eval_archive import maybe_publish_eval_record
17
  from src.auth import extract_oauth_context, public_oauth_context, oauth_warning_messages, verify_token_identity
18
  from src.jobs import (
@@ -420,6 +420,19 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
420
  raise HTTPException(status_code=400, detail=str(exc)) from exc
421
  return JSONResponse(cfg)
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  @fastapi_app.get("/api/oauth/diagnostics")
424
  async def api_oauth_diagnostics(request: Request): # type: ignore[no-untyped-def]
425
  ctx = extract_oauth_context(request)
@@ -544,7 +557,7 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
544
  if not isinstance(test_kwargs, dict):
545
  raise ValueError("test_kwargs_json must be a JSON object; it is passed as keyword args to gradio_client.predict().")
546
  parent_build_run_id = validate_run_id(str(payload.get("parent_build_run_id") or ""))
547
- parent_bundle = read_run_bundle(parent_build_run_id, bucket_source=bucket_status.get("bucket_source") or user_bucket_source(username=ctx["username"], bucket_name=bucket_name), token=ctx["token"], include_heavy=False)
548
  parent_summary = parent_bundle.get("summary") or {}
549
  parent_target = parent_summary.get("target_space") or (parent_bundle.get("state") or {}).get("target_space") or (parent_bundle.get("launch") or {}).get("target_space") or ""
550
  requested_target = str(payload.get("target_space_id") or "").strip()
@@ -552,16 +565,25 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
552
  raise ValueError("Linked Space Test requires a parent Build Run with a generated target Space.")
553
  if requested_target != parent_target:
554
  raise ValueError("Space Test must remain linked to the selected Build Run target Space. Standalone validation is not supported in this app.")
 
 
 
 
 
 
 
555
  result = launch_validate_existing_space_job(
556
  token=ctx["token"],
557
  username=ctx["username"],
558
  target_space_id=requested_target,
559
  parent_build_run_id=parent_build_run_id,
560
- api_name=payload.get("api_name"),
561
  test_args_json=json.dumps(test_args, ensure_ascii=False),
562
  test_kwargs_json=json.dumps(test_kwargs, ensure_ascii=False),
563
  expected_output_type=payload.get("expected_output_type"),
564
  live_timeout_seconds=payload.get("live_timeout_seconds") or 1800,
 
 
565
  run_id=payload.get("run_id"),
566
  bucket_name=bucket_name,
567
  )
@@ -723,26 +745,73 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
723
  body = {}
724
  except Exception:
725
  body = {}
 
726
  delete_space = bool(body.get("delete_space"))
727
  bundle: dict[str, Any] = {}
 
 
 
 
 
 
 
 
 
 
728
  space_report: dict[str, Any] = {"space_delete_requested": delete_space, "space_deleted": False}
729
  if delete_space:
730
  try:
731
- bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
732
- associated_space = _associated_space_from_bundle(bundle)
733
- kind = str((bundle.get("summary") or {}).get("kind") or (bundle.get("state") or {}).get("kind") or (bundle.get("launch") or {}).get("kind") or "").lower()
734
- if "validation" in kind or "space_test" in kind:
735
  raise PermissionError("Associated Space deletion is not available for validation runs.")
 
736
  space_report = _delete_associated_space(associated_space, username=ctx["username"], token=ctx["token"])
737
  except Exception as exc: # noqa: BLE001
738
  space_report = {"space_delete_requested": True, "space_deleted": False, "space_delete_error": redact(str(exc))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  try:
740
  delete_report = delete_run_folder(run_id, bucket_source=bucket_source, token=ctx["token"])
741
  except FileNotFoundError:
742
  delete_report = {"matched_count": 0, "deleted_count": 0, "remaining_count": 0}
743
  except Exception as exc: # noqa: BLE001
744
  raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
745
- return JSONResponse({"ok": True, "run_id": run_id, "deleted": True, **delete_report, **space_report})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
 
747
  @fastapi_app.get("/api/runs/{run_id}/progress")
748
  async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_job_logs: bool = False): # type: ignore[no-untyped-def]
@@ -1298,6 +1367,113 @@ def _delete_associated_space(space_id: str, *, username: str, token: str) -> dic
1298
  return {"space_delete_requested": True, "space_deleted": False, "space_already_missing": True, "space_id": cleaned, "space_delete_error": message}
1299
  return {"space_delete_requested": True, "space_deleted": False, "space_id": cleaned, "space_delete_error": message}
1300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1301
  def create_app() -> FastAPI:
1302
  """Create the product FastAPI app.
1303
 
 
12
 
13
  from src.bucket import RunPaths, check_user_bucket, create_user_bucket, delete_run_folder, read_run_bundle, list_recent_runs, write_json, write_launch_metadata
14
  from src.config import settings, user_bucket_source
15
+ from src.eval_config import activate_eval_archive_config, disable_eval_archive_config, flush_eval_archive_records, public_eval_config
16
  from src.eval_archive import maybe_publish_eval_record
17
  from src.auth import extract_oauth_context, public_oauth_context, oauth_warning_messages, verify_token_identity
18
  from src.jobs import (
 
420
  raise HTTPException(status_code=400, detail=str(exc)) from exc
421
  return JSONResponse(cfg)
422
 
423
+ @fastapi_app.post("/api/eval-archive/flush")
424
+ async def api_eval_archive_flush(request: Request): # type: ignore[no-untyped-def]
425
+ ctx = _oauth_context_from_request(request)
426
+ try:
427
+ cfg = flush_eval_archive_records(username=ctx["username"])
428
+ except PermissionError as exc:
429
+ raise HTTPException(status_code=403, detail=str(exc)) from exc
430
+ except FileNotFoundError as exc:
431
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
432
+ except Exception as exc: # noqa: BLE001
433
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
434
+ return JSONResponse(cfg)
435
+
436
  @fastapi_app.get("/api/oauth/diagnostics")
437
  async def api_oauth_diagnostics(request: Request): # type: ignore[no-untyped-def]
438
  ctx = extract_oauth_context(request)
 
557
  if not isinstance(test_kwargs, dict):
558
  raise ValueError("test_kwargs_json must be a JSON object; it is passed as keyword args to gradio_client.predict().")
559
  parent_build_run_id = validate_run_id(str(payload.get("parent_build_run_id") or ""))
560
+ parent_bundle = read_run_bundle(parent_build_run_id, bucket_source=bucket_status.get("bucket_source") or user_bucket_source(username=ctx["username"], bucket_name=bucket_name), token=ctx["token"], include_heavy=True)
561
  parent_summary = parent_bundle.get("summary") or {}
562
  parent_target = parent_summary.get("target_space") or (parent_bundle.get("state") or {}).get("target_space") or (parent_bundle.get("launch") or {}).get("target_space") or ""
563
  requested_target = str(payload.get("target_space_id") or "").strip()
 
565
  raise ValueError("Linked Space Test requires a parent Build Run with a generated target Space.")
566
  if requested_target != parent_target:
567
  raise ValueError("Space Test must remain linked to the selected Build Run target Space. Standalone validation is not supported in this app.")
568
+ parent_view = build_run_view_model(parent_build_run_id, parent_bundle, bucket_source=bucket_status.get("bucket_source") or user_bucket_source(username=ctx["username"], bucket_name=bucket_name))
569
+ space_test_policy = parent_view.get("space_test_policy") or parent_view.get("space_test", {}).get("policy") or {}
570
+ if not space_test_policy.get("enabled"):
571
+ raise ValueError(str(space_test_policy.get("message") or "This Build Run is not eligible for linked Space Test yet."))
572
+ api_name_for_validation = payload.get("api_name")
573
+ if space_test_policy.get("requires_endpoint_discovery") and str(api_name_for_validation or "").strip() in {"", "/generate"}:
574
+ api_name_for_validation = ""
575
  result = launch_validate_existing_space_job(
576
  token=ctx["token"],
577
  username=ctx["username"],
578
  target_space_id=requested_target,
579
  parent_build_run_id=parent_build_run_id,
580
+ api_name=api_name_for_validation,
581
  test_args_json=json.dumps(test_args, ensure_ascii=False),
582
  test_kwargs_json=json.dumps(test_kwargs, ensure_ascii=False),
583
  expected_output_type=payload.get("expected_output_type"),
584
  live_timeout_seconds=payload.get("live_timeout_seconds") or 1800,
585
+ validation_mode=space_test_policy.get("mode"),
586
+ effective_status_on_success=space_test_policy.get("effective_status_on_success") or "validated_after_manual_space_test",
587
  run_id=payload.get("run_id"),
588
  bucket_name=bucket_name,
589
  )
 
745
  body = {}
746
  except Exception:
747
  body = {}
748
+
749
  delete_space = bool(body.get("delete_space"))
750
  bundle: dict[str, Any] = {}
751
+ try:
752
+ bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
753
+ except Exception:
754
+ bundle = {}
755
+
756
+ is_validation = _run_is_validation_like({**bundle, "run_id": run_id})
757
+ linked_children: list[dict[str, Any]] = []
758
+ if not is_validation:
759
+ linked_children = _linked_validation_summaries(run_id, bucket_source=bucket_source, token=ctx["token"])
760
+
761
  space_report: dict[str, Any] = {"space_delete_requested": delete_space, "space_deleted": False}
762
  if delete_space:
763
  try:
764
+ if is_validation:
 
 
 
765
  raise PermissionError("Associated Space deletion is not available for validation runs.")
766
+ associated_space = _associated_space_from_bundle(bundle)
767
  space_report = _delete_associated_space(associated_space, username=ctx["username"], token=ctx["token"])
768
  except Exception as exc: # noqa: BLE001
769
  space_report = {"space_delete_requested": True, "space_deleted": False, "space_delete_error": redact(str(exc))}
770
+
771
+ deleted_linked_validations: list[str] = []
772
+ linked_delete_errors: list[str] = []
773
+ for child in linked_children:
774
+ child_id = str(child.get("run_id") or "").strip()
775
+ if not child_id or child_id == run_id:
776
+ continue
777
+ try:
778
+ delete_run_folder(validate_run_id(child_id), bucket_source=bucket_source, token=ctx["token"])
779
+ deleted_linked_validations.append(child_id)
780
+ except FileNotFoundError:
781
+ deleted_linked_validations.append(child_id)
782
+ except Exception as exc: # noqa: BLE001
783
+ linked_delete_errors.append(f"{child_id}: {redact(str(exc))}")
784
+
785
+ parent_update: dict[str, Any] = {}
786
+ parent_build_run_id = _linked_validation_parent_id({**bundle, "run_id": run_id}) if is_validation else ""
787
  try:
788
  delete_report = delete_run_folder(run_id, bucket_source=bucket_source, token=ctx["token"])
789
  except FileNotFoundError:
790
  delete_report = {"matched_count": 0, "deleted_count": 0, "remaining_count": 0}
791
  except Exception as exc: # noqa: BLE001
792
  raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
793
+
794
+ if is_validation and parent_build_run_id:
795
+ remaining = [row for row in _linked_validation_summaries(parent_build_run_id, bucket_source=bucket_source, token=ctx["token"]) if str(row.get("run_id") or "") != run_id]
796
+ try:
797
+ parent_update = _write_parent_linked_validation_state(parent_build_run_id, remaining, bucket_source=bucket_source, token=ctx["token"])
798
+ except Exception as exc: # noqa: BLE001
799
+ parent_update = {"parent_update_error": redact(str(exc))}
800
+
801
+ response = {
802
+ "ok": True,
803
+ "run_id": run_id,
804
+ "deleted": True,
805
+ **delete_report,
806
+ **space_report,
807
+ "deleted_linked_validations": deleted_linked_validations,
808
+ "linked_delete_errors": linked_delete_errors,
809
+ "parent_build_run_id": parent_build_run_id,
810
+ "parent_update": parent_update,
811
+ }
812
+ if linked_delete_errors:
813
+ response["partial_delete"] = True
814
+ return JSONResponse(response)
815
 
816
  @fastapi_app.get("/api/runs/{run_id}/progress")
817
  async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_job_logs: bool = False): # type: ignore[no-untyped-def]
 
1367
  return {"space_delete_requested": True, "space_deleted": False, "space_already_missing": True, "space_id": cleaned, "space_delete_error": message}
1368
  return {"space_delete_requested": True, "space_deleted": False, "space_id": cleaned, "space_delete_error": message}
1369
 
1370
+
1371
+ def _run_kind_from_bundle(bundle: dict[str, Any]) -> str:
1372
+ summary = bundle.get("summary") or bundle.get("summary_file") or {}
1373
+ state = bundle.get("state") or {}
1374
+ launch = bundle.get("launch") or {}
1375
+ return str(summary.get("kind") or state.get("kind") or launch.get("kind") or bundle.get("kind") or "").lower()
1376
+
1377
+
1378
+ def _run_is_validation_like(bundle_or_summary: dict[str, Any]) -> bool:
1379
+ kind = str(
1380
+ bundle_or_summary.get("kind")
1381
+ or bundle_or_summary.get("run_type")
1382
+ or (bundle_or_summary.get("summary") or {}).get("kind")
1383
+ or (bundle_or_summary.get("state") or {}).get("kind")
1384
+ or (bundle_or_summary.get("launch") or {}).get("kind")
1385
+ or ""
1386
+ ).lower()
1387
+ run_id = str(bundle_or_summary.get("run_id") or (bundle_or_summary.get("summary") or {}).get("run_id") or "").lower()
1388
+ return "validation" in kind or "space_test" in kind or "validate" in kind or run_id.startswith("validate-")
1389
+
1390
+
1391
+ def _linked_validation_parent_id(run: dict[str, Any]) -> str:
1392
+ summary = run.get("summary") or {}
1393
+ state = run.get("state") or {}
1394
+ launch = run.get("launch") or {}
1395
+ return str(
1396
+ run.get("parent_build_run_id")
1397
+ or run.get("parentBuildRunId")
1398
+ or summary.get("parent_build_run_id")
1399
+ or state.get("parent_build_run_id")
1400
+ or launch.get("parent_build_run_id")
1401
+ or ""
1402
+ ).strip()
1403
+
1404
+
1405
+ def _linked_validation_summaries(parent_run_id: str, *, bucket_source: str, token: str | None = None) -> list[dict[str, Any]]:
1406
+ if not parent_run_id:
1407
+ return []
1408
+ try:
1409
+ rows = list_recent_runs(bucket_source=bucket_source, token=token, limit=200)
1410
+ except Exception:
1411
+ return []
1412
+ linked: list[dict[str, Any]] = []
1413
+ for row in rows:
1414
+ if not isinstance(row, dict):
1415
+ continue
1416
+ if not _run_is_validation_like(row):
1417
+ continue
1418
+ if _linked_validation_parent_id(row) == parent_run_id:
1419
+ linked.append(row)
1420
+ linked.sort(key=lambda r: str(r.get("updated_at") or r.get("created_at") or r.get("run_id") or ""), reverse=True)
1421
+ return linked
1422
+
1423
+
1424
+ def _manual_validation_status_from_linked(parent_run_id: str, linked: list[dict[str, Any]]) -> dict[str, Any]:
1425
+ successes = [row for row in linked if str(row.get("status") or row.get("effective_status") or "").lower() in {"full_inference_success", "success", "succeeded", "passed", "validated_after_manual_space_test"}]
1426
+ failures = [row for row in linked if "failed" in str(row.get("status") or "").lower() or "error" in str(row.get("status") or "").lower()]
1427
+ source = successes[0] if successes else (failures[0] if failures else {})
1428
+ if not source:
1429
+ return {"status": "none", "parent_build_run_id": parent_run_id, "validation_run_id": "", "updated_at": utc_now_iso()}
1430
+ status = "success" if successes else "failed"
1431
+ payload: dict[str, Any] = {
1432
+ "status": status,
1433
+ "parent_build_run_id": parent_run_id,
1434
+ "validation_run_id": source.get("run_id") or "",
1435
+ "target_space": source.get("target_space") or source.get("target_space_id") or "",
1436
+ "api_name": source.get("api_name") or "",
1437
+ "updated_at": utc_now_iso(),
1438
+ }
1439
+ if status == "success":
1440
+ payload["effective_status"] = "validated_after_manual_space_test"
1441
+ for key in ["latency_seconds", "observed_latency_seconds", "recommended_zero_gpu_duration_seconds", "recommended_zerogpu_duration_seconds", "recommendation_source", "recommendation_hardware", "hardware_used_for_validation"]:
1442
+ if source.get(key) is not None:
1443
+ payload[key] = source.get(key)
1444
+ payload.setdefault("recommendation_source", "linked_space_test")
1445
+ return payload
1446
+
1447
+
1448
+ def _write_parent_linked_validation_state(parent_run_id: str, linked: list[dict[str, Any]], *, bucket_source: str, token: str | None = None) -> dict[str, Any]:
1449
+ paths = RunPaths(parent_run_id, bucket_source=bucket_source)
1450
+ compact = []
1451
+ for row in linked:
1452
+ compact.append({
1453
+ "validation_run_id": row.get("run_id") or "",
1454
+ "status": row.get("status") or "unknown",
1455
+ "target_space": row.get("target_space") or row.get("target_space_id") or "",
1456
+ "api_name": row.get("api_name") or "",
1457
+ "latency_seconds": row.get("latency_seconds") or row.get("observed_latency_seconds"),
1458
+ "observed_latency_seconds": row.get("observed_latency_seconds") or row.get("latency_seconds"),
1459
+ "recommended_zero_gpu_duration_seconds": row.get("recommended_zero_gpu_duration_seconds") or row.get("recommended_zerogpu_duration_seconds"),
1460
+ "recommendation_source": row.get("recommendation_source") or "",
1461
+ "recommendation_hardware": row.get("recommendation_hardware") or row.get("hardware_used_for_validation") or "",
1462
+ "hardware_used_for_validation": row.get("hardware_used_for_validation") or row.get("recommendation_hardware") or "",
1463
+ "updated_at": row.get("updated_at") or row.get("created_at") or "",
1464
+ })
1465
+ linked_payload = {
1466
+ "parent_build_run_id": parent_run_id,
1467
+ "validations": compact,
1468
+ "updated_at": utc_now_iso(),
1469
+ }
1470
+ manual_status = _manual_validation_status_from_linked(parent_run_id, linked)
1471
+ if manual_status.get("status") == "success":
1472
+ linked_payload["effective_status"] = "validated_after_manual_space_test"
1473
+ write_json(f"{paths.root}/linked_validations.json", linked_payload, token=token)
1474
+ write_json(f"{paths.root}/manual_validation_status.json", manual_status, token=token)
1475
+ return {"linked_validations": linked_payload, "manual_validation_status": manual_status}
1476
+
1477
  def create_app() -> FastAPI:
1478
  """Create the product FastAPI app.
1479