@tikomni/skills 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/package.json +1 -1
  2. package/skills/creator-analysis/SKILL.md +34 -10
  3. package/skills/creator-analysis/references/contracts/creator-card-fields.md +2 -0
  4. package/skills/creator-analysis/references/contracts/work-card-fields.md +40 -4
  5. package/skills/creator-analysis/references/platform-guides/douyin.md +41 -36
  6. package/skills/creator-analysis/references/platform-guides/generic.md +11 -7
  7. package/skills/creator-analysis/references/platform-guides/xiaohongshu.md +45 -30
  8. package/skills/creator-analysis/references/schemas/author-analysis-v2.schema.json +224 -95
  9. package/skills/creator-analysis/references/workflow.md +8 -3
  10. package/skills/creator-analysis/scripts/author_home/adapters/platform_adapters.py +205 -21
  11. package/skills/creator-analysis/scripts/author_home/analyzers/author_analysis_v2_support.py +54 -11
  12. package/skills/creator-analysis/scripts/author_home/analyzers/prompt_first_analyzers.py +200 -13
  13. package/skills/creator-analysis/scripts/author_home/analyzers/sampled_work_batch_explainer.py +113 -42
  14. package/skills/creator-analysis/scripts/author_home/asr/home_asr.py +65 -7
  15. package/skills/creator-analysis/scripts/author_home/builders/home_builders.py +82 -18
  16. package/skills/creator-analysis/scripts/author_home/collectors/homepage_collectors.py +198 -32
  17. package/skills/creator-analysis/scripts/author_home/orchestrator/run_author_analysis.py +374 -31
  18. package/skills/creator-analysis/scripts/author_home/orchestrator/work_analysis_artifacts.py +68 -12
  19. package/skills/creator-analysis/scripts/core/storage_router.py +3 -0
  20. package/skills/creator-analysis/scripts/writers/write_author_homepage_samples.py +3 -2
  21. package/skills/creator-analysis/scripts/writers/write_benchmark_card.py +314 -137
@@ -22,6 +22,7 @@ from scripts.core.config_loader import resolve_storage_paths
22
22
  from scripts.core.storage_router import render_output_filename, resolve_json_filename_pattern
23
23
 
24
24
  from scripts.author_home.adapters.platform_adapters import adapt_douyin_author_home, adapt_xhs_author_home
25
+ from scripts.author_home.analyzers.author_analysis_v2_support import prepare_author_analysis_bundle
25
26
  from scripts.author_home.orchestrator.work_analysis_artifacts import orchestrate_work_analysis_artifacts
26
27
  from scripts.core.progress_report import ProgressReporter
27
28
  from scripts.author_home.analyzers.prompt_first_analyzers import run_prompt_first_author_analysis
@@ -32,6 +33,145 @@ from scripts.author_home.collectors.homepage_collectors import collect_douyin_au
32
33
  CollectorFn = Callable[..., Dict[str, Any]]
33
34
 
34
35
 
36
+ def _stage_status(*, status: str, ok_count: int, failed_count: int, degraded_count: int, reason_codes: List[str], failure_kind: str = "") -> Dict[str, Any]:
37
+ return {
38
+ "status": status,
39
+ "ok_count": ok_count,
40
+ "failed_count": failed_count,
41
+ "degraded_count": degraded_count,
42
+ "reason_codes": list(dict.fromkeys([code for code in reason_codes if code])),
43
+ "failure_kind": failure_kind or None,
44
+ }
45
+
46
+
47
+ def _reason_codes_from_failed_items(items: List[Dict[str, Any]]) -> List[str]:
48
+ reason_codes: List[str] = []
49
+ for item in items:
50
+ if not isinstance(item, dict):
51
+ continue
52
+ reason = str(item.get("error_reason") or "").strip()
53
+ if not reason:
54
+ continue
55
+ reason_codes.append(reason.split(":", 1)[0])
56
+ return list(dict.fromkeys(reason_codes))
57
+
58
+
59
+ def _merge_normalized_works(*, works: List[Dict[str, Any]], normalized_works: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
60
+ normalized_map = {
61
+ str(item.get("platform_work_id") or "").strip(): item
62
+ for item in normalized_works
63
+ if isinstance(item, dict) and str(item.get("platform_work_id") or "").strip()
64
+ }
65
+ merged: List[Dict[str, Any]] = []
66
+ for work in works:
67
+ if not isinstance(work, dict):
68
+ continue
69
+ platform_work_id = str(work.get("platform_work_id") or "").strip()
70
+ normalized = normalized_map.get(platform_work_id)
71
+ if not isinstance(normalized, dict):
72
+ merged.append(work)
73
+ continue
74
+ merged_work = dict(work)
75
+ for field in (
76
+ "primary_text",
77
+ "primary_text_source",
78
+ "digg_count",
79
+ "comment_count",
80
+ "collect_count",
81
+ "share_count",
82
+ "play_count",
83
+ "performance_score",
84
+ "performance_score_norm",
85
+ "bucket",
86
+ "hook_type",
87
+ "structure_type",
88
+ "cta_type",
89
+ "content_form",
90
+ "style_markers",
91
+ "analysis_eligibility",
92
+ "analysis_exclusion_reason",
93
+ "published_date",
94
+ "publish_time",
95
+ ):
96
+ if field in normalized:
97
+ merged_work[field] = normalized.get(field)
98
+ merged.append(merged_work)
99
+ return merged
100
+
101
+
102
+ def _build_card_stage_status(*, expected_count: int, results: Dict[str, Any], failure_kind: str = "runtime") -> Dict[str, Any]:
103
+ if not isinstance(results, dict) or not results.get("enabled", True):
104
+ return _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["write_card_disabled"])
105
+ ok_count = int(results.get("count") or len(results.get("results") or []))
106
+ failed_items = results.get("failed_items") if isinstance(results.get("failed_items"), list) else []
107
+ degraded_count = len(failed_items) if ok_count > 0 else 0
108
+ failed_count = len(failed_items) if ok_count == 0 and expected_count > 0 else 0
109
+ if failed_count > 0:
110
+ return _stage_status(
111
+ status="failed",
112
+ ok_count=ok_count,
113
+ failed_count=failed_count,
114
+ degraded_count=0,
115
+ reason_codes=_reason_codes_from_failed_items(failed_items) or ["card_write_failed"],
116
+ failure_kind=failure_kind,
117
+ )
118
+ if degraded_count > 0:
119
+ return _stage_status(
120
+ status="degraded",
121
+ ok_count=ok_count,
122
+ failed_count=0,
123
+ degraded_count=degraded_count,
124
+ reason_codes=_reason_codes_from_failed_items(failed_items) or ["card_write_degraded"],
125
+ failure_kind=failure_kind,
126
+ )
127
+ return _stage_status(status="full", ok_count=ok_count, failed_count=0, degraded_count=0, reason_codes=[])
128
+
129
+
130
+ def _build_persist_stage_status(persist_result: Dict[str, Any]) -> Dict[str, Any]:
131
+ if not isinstance(persist_result, dict) or not persist_result.get("enabled", True):
132
+ return _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["persist_disabled"])
133
+ if persist_result.get("ok"):
134
+ return _stage_status(status="full", ok_count=1, failed_count=0, degraded_count=0, reason_codes=[])
135
+ error_reason = str(persist_result.get("error") or persist_result.get("failure_reason") or "persist_failed")
136
+ return _stage_status(
137
+ status="failed",
138
+ ok_count=0,
139
+ failed_count=1,
140
+ degraded_count=0,
141
+ reason_codes=[error_reason.split(":", 1)[0]],
142
+ failure_kind="configuration" if error_reason.startswith("resolve_storage_paths_failed:") else "runtime",
143
+ )
144
+
145
+
146
+ def _build_overall_status(stage_status: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
147
+ ordered = [
148
+ "fetch",
149
+ "asr",
150
+ "sampled_explanations",
151
+ "author_analysis",
152
+ "author_sample_card",
153
+ "sample_work_card",
154
+ "author_card",
155
+ ]
156
+ stages = [stage_status.get(key) if isinstance(stage_status.get(key), dict) else {} for key in ordered]
157
+ if any(stage.get("failure_kind") == "configuration" or stage.get("status") == "failed" and stage.get("failure_kind") == "configuration" for stage in stages):
158
+ reason_codes: List[str] = []
159
+ for stage in stages:
160
+ if stage.get("failure_kind") == "configuration":
161
+ reason_codes.extend(stage.get("reason_codes") or [])
162
+ return _stage_status(status="failed", ok_count=0, failed_count=1, degraded_count=0, reason_codes=reason_codes or ["configuration_failed"], failure_kind="configuration")
163
+ author_analysis = stage_status.get("author_analysis") if isinstance(stage_status.get("author_analysis"), dict) else {}
164
+ author_card = stage_status.get("author_card") if isinstance(stage_status.get("author_card"), dict) else {}
165
+ if author_analysis.get("status") == "fallback" and author_card.get("status") in {"full", "degraded", "fallback"}:
166
+ return _stage_status(status="fallback", ok_count=0, failed_count=0, degraded_count=0, reason_codes=author_analysis.get("reason_codes") or ["author_analysis_fallback"], failure_kind=author_analysis.get("failure_kind") or "runtime")
167
+ if any((stage.get("failed_count") or 0) > 0 or (stage.get("degraded_count") or 0) > 0 or stage.get("status") in {"degraded", "failed"} for stage in stages):
168
+ reason_codes = []
169
+ for stage in stages:
170
+ reason_codes.extend(stage.get("reason_codes") or [])
171
+ return _stage_status(status="degraded", ok_count=0, failed_count=0, degraded_count=1, reason_codes=reason_codes or ["stage_degraded"])
172
+ return _stage_status(status="full", ok_count=1, failed_count=0, degraded_count=0, reason_codes=[])
173
+
174
+
35
175
  def _unsupported(platform: str) -> Dict[str, Any]:
36
176
  return {
37
177
  "platform": platform,
@@ -44,6 +184,18 @@ def _unsupported(platform: str) -> Dict[str, Any]:
44
184
  "missing_fields": [],
45
185
  "extract_trace": [],
46
186
  "fallback_trace": [],
187
+ "stage_status": {
188
+ "fetch": _stage_status(status="failed", ok_count=0, failed_count=1, degraded_count=0, reason_codes=["unsupported_platform"]),
189
+ "asr": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
190
+ "author_sample_card": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
191
+ "sample_work_card": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
192
+ "sampled_explanations": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
193
+ "author_analysis": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
194
+ "author_card": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
195
+ "persist": _stage_status(status="skipped", ok_count=0, failed_count=0, degraded_count=0, reason_codes=["unsupported_platform"]),
196
+ "overall": _stage_status(status="failed", ok_count=0, failed_count=1, degraded_count=0, reason_codes=["unsupported_platform"]),
197
+ },
198
+ "quality_tier": "failed",
47
199
  "request_id": None,
48
200
  }
49
201
 
@@ -112,14 +264,29 @@ def _build_persist_payload(*, result: Dict[str, Any], status: str, written_at: d
112
264
  }
113
265
 
114
266
 
115
- def _persist_output_artifact(*, result: Dict[str, Any], input_value: str, storage_config: Dict[str, Any], persist_output: bool) -> Dict[str, Any]:
267
+ def _persist_output_artifact(*, result: Dict[str, Any], input_value: str, storage_config: Dict[str, Any], persist_output: bool, card_root: Optional[str]) -> Dict[str, Any]:
116
268
  if not persist_output:
117
269
  return {"enabled": False, "skipped": True, "reason": "disabled_by_flag"}
118
270
 
271
+ diagnostics = {
272
+ "effective_card_root": str(card_root or ""),
273
+ "results_root": "",
274
+ "errors_root": "",
275
+ }
119
276
  try:
120
277
  paths = resolve_storage_paths(storage_config or {})
121
278
  except Exception as error:
122
- return {"enabled": True, "ok": False, "error": f"resolve_storage_paths_failed:{error}"}
279
+ return {
280
+ "enabled": True,
281
+ "ok": False,
282
+ "error": f"resolve_storage_paths_failed:{error}",
283
+ "failure_reason": f"resolve_storage_paths_failed:{error}",
284
+ "paths": diagnostics,
285
+ }
286
+
287
+ diagnostics["effective_card_root"] = str(card_root or paths.get("root_dir") or "")
288
+ diagnostics["results_root"] = str(paths.get("results_root") or "")
289
+ diagnostics["errors_root"] = str(paths.get("errors_root") or "")
123
290
 
124
291
  now = datetime.now()
125
292
  date_key = now.strftime("%Y%m%d")
@@ -134,7 +301,17 @@ def _persist_output_artifact(*, result: Dict[str, Any], input_value: str, storag
134
301
  else:
135
302
  target_dir = Path(paths.get("results_root", "")) / date_key
136
303
 
137
- target_dir.mkdir(parents=True, exist_ok=True)
304
+ try:
305
+ target_dir.mkdir(parents=True, exist_ok=True)
306
+ except Exception as error:
307
+ return {
308
+ "enabled": True,
309
+ "ok": False,
310
+ "error": f"persist_target_dir_failed:{type(error).__name__}:{error}",
311
+ "failure_reason": f"persist_target_dir_failed:{type(error).__name__}:{error}",
312
+ "paths": diagnostics,
313
+ "target_dir": str(target_dir),
314
+ }
138
315
  file_name = render_output_filename(
139
316
  pattern=resolve_json_filename_pattern(storage_config),
140
317
  context={
@@ -158,12 +335,23 @@ def _persist_output_artifact(*, result: Dict[str, Any], input_value: str, storag
158
335
  written_at=now,
159
336
  identifier=identifier,
160
337
  )
161
- file_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
338
+ try:
339
+ file_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
340
+ except Exception as error:
341
+ return {
342
+ "enabled": True,
343
+ "ok": False,
344
+ "error": f"persist_write_failed:{type(error).__name__}:{error}",
345
+ "failure_reason": f"persist_write_failed:{type(error).__name__}:{error}",
346
+ "paths": diagnostics,
347
+ "path": str(file_path),
348
+ }
162
349
  return {
163
350
  "enabled": True,
164
351
  "ok": True,
165
352
  "status": status,
166
353
  "path": str(file_path),
354
+ "paths": diagnostics,
167
355
  }
168
356
 
169
357
 
@@ -268,6 +456,21 @@ def run_author_home_analysis(
268
456
  progress=progress.child(scope="author_home.asr") if progress is not None else None,
269
457
  )
270
458
  works = list(asr_bundle.get("works") or [])[:capped_max_items]
459
+ prepared_analysis_bundle = prepare_author_analysis_bundle(
460
+ profile=profile,
461
+ works=works,
462
+ platform=platform,
463
+ )
464
+ works = _merge_normalized_works(
465
+ works=works,
466
+ normalized_works=prepared_analysis_bundle.get("normalized_works") if isinstance(prepared_analysis_bundle.get("normalized_works"), list) else [],
467
+ )
468
+ prepared_analysis_bundle = prepare_author_analysis_bundle(
469
+ profile=profile,
470
+ works=works,
471
+ platform=platform,
472
+ )
473
+ sampled_work_ids = prepared_analysis_bundle.get("sampled_work_ids") if isinstance(prepared_analysis_bundle.get("sampled_work_ids"), list) else []
271
474
 
272
475
  if write_card:
273
476
  work_analysis_bundle = orchestrate_work_analysis_artifacts(
@@ -296,6 +499,7 @@ def run_author_home_analysis(
296
499
  "artifact_root": None,
297
500
  "analysis_logic_version": None,
298
501
  "prompt_contract_hash": None,
502
+ "normalization_version": None,
299
503
  }
300
504
  work_analysis_stats = work_analysis_bundle.get("stats") if isinstance(work_analysis_bundle.get("stats"), dict) else {}
301
505
  work_analysis_failed = work_analysis_bundle.get("failed_items") if isinstance(work_analysis_bundle.get("failed_items"), list) else []
@@ -317,7 +521,11 @@ def run_author_home_analysis(
317
521
 
318
522
  if progress is not None:
319
523
  progress.progress(stage="author_home.analysis", message="running author analysis")
320
- analysis, analysis_missing, analysis_trace = run_prompt_first_author_analysis(profile, works)
524
+ analysis, analysis_missing, analysis_trace = run_prompt_first_author_analysis(
525
+ profile,
526
+ works,
527
+ analysis_bundle=prepared_analysis_bundle,
528
+ )
321
529
  if progress is not None:
322
530
  progress.done(
323
531
  stage="author_home.analysis",
@@ -336,19 +544,17 @@ def run_author_home_analysis(
336
544
  profile=profile,
337
545
  works=works,
338
546
  render_payloads=render_payloads,
547
+ sampled_work_ids=sampled_work_ids,
548
+ sampled_work_explanations=(
549
+ analysis.get("sampled_work_explanations", {}).get("sampled_work_explanations")
550
+ if isinstance(analysis.get("sampled_work_explanations"), dict)
551
+ else {}
552
+ ),
339
553
  card_root=card_root,
340
554
  storage_config=storage_config,
341
555
  write_card=write_card,
342
556
  failed_items=work_analysis_failed,
343
557
  )
344
- author_card_write = build_author_card(
345
- platform=platform,
346
- profile=profile,
347
- analysis_payload=analysis,
348
- card_root=card_root,
349
- storage_config=storage_config,
350
- write_card=write_card,
351
- )
352
558
 
353
559
  asr_trace = list(asr_bundle.get("trace") or [])
354
560
  all_extract_trace = list(raw.get("extract_trace") or []) + asr_trace + work_analysis_trace + analysis_trace
@@ -378,19 +584,135 @@ def run_author_home_analysis(
378
584
  }
379
585
  )
380
586
 
381
- work_analysis_missing: List[Dict[str, str]] = []
382
- for item in work_analysis_failed:
383
- if not isinstance(item, dict):
384
- continue
385
- work_analysis_missing.append(
386
- {
387
- "field": f"works[{item.get('platform_work_id') or 'unknown'}].analysis_artifact",
388
- "reason": str(item.get("error_reason") or "work_analysis_artifact_failed"),
389
- }
390
- )
391
-
392
587
  asr_stats = asr_bundle.get("stats") if isinstance(asr_bundle.get("stats"), dict) else {}
393
588
  asr_checkpoint = asr_bundle.get("checkpoint") if isinstance(asr_bundle.get("checkpoint"), dict) else {}
589
+ author_sample_cards = work_card_write.get("author_sample_cards") if isinstance(work_card_write.get("author_sample_cards"), dict) else {}
590
+ sample_work_cards = work_card_write.get("sample_work_cards") if isinstance(work_card_write.get("sample_work_cards"), dict) else {}
591
+
592
+ eligible_count = len([item for item in works if isinstance(item, dict) and str(item.get("analysis_eligibility") or "") == "eligible"])
593
+ incomplete_count = len([item for item in works if isinstance(item, dict) and str(item.get("analysis_eligibility") or "") != "eligible"])
594
+ if works and eligible_count == 0:
595
+ asr_stage = _stage_status(
596
+ status="failed",
597
+ ok_count=0,
598
+ failed_count=incomplete_count or len(works),
599
+ degraded_count=0,
600
+ reason_codes=["asr_unavailable_for_all_works"],
601
+ failure_kind="runtime",
602
+ )
603
+ elif incomplete_count > 0:
604
+ asr_stage = _stage_status(
605
+ status="degraded",
606
+ ok_count=eligible_count,
607
+ failed_count=0,
608
+ degraded_count=incomplete_count,
609
+ reason_codes=["partial_asr_unavailable"],
610
+ failure_kind="runtime",
611
+ )
612
+ else:
613
+ asr_stage = _stage_status(
614
+ status="full",
615
+ ok_count=eligible_count,
616
+ failed_count=0,
617
+ degraded_count=0,
618
+ reason_codes=[],
619
+ )
620
+
621
+ stage_status: Dict[str, Dict[str, Any]] = {
622
+ "fetch": _stage_status(
623
+ status="full",
624
+ ok_count=len(works),
625
+ failed_count=0,
626
+ degraded_count=0,
627
+ reason_codes=[],
628
+ ),
629
+ "asr": asr_stage,
630
+ "author_sample_card": _build_card_stage_status(
631
+ expected_count=len(works),
632
+ results=author_sample_cards,
633
+ ),
634
+ "sample_work_card": _build_card_stage_status(
635
+ expected_count=len(sampled_work_ids),
636
+ results=sample_work_cards,
637
+ ),
638
+ "sampled_explanations": analysis.get("sampled_explanations_status") if isinstance(analysis.get("sampled_explanations_status"), dict) else _stage_status(
639
+ status="failed",
640
+ ok_count=0,
641
+ failed_count=1,
642
+ degraded_count=0,
643
+ reason_codes=["missing_sampled_explanations_status"],
644
+ ),
645
+ "author_analysis": analysis.get("author_analysis_status") if isinstance(analysis.get("author_analysis_status"), dict) else _stage_status(
646
+ status="failed",
647
+ ok_count=0,
648
+ failed_count=1,
649
+ degraded_count=0,
650
+ reason_codes=["missing_author_analysis_status"],
651
+ ),
652
+ "author_card": _stage_status(
653
+ status="skipped",
654
+ ok_count=0,
655
+ failed_count=0,
656
+ degraded_count=0,
657
+ reason_codes=["pending"],
658
+ ),
659
+ }
660
+
661
+ author_analysis_payload = dict(analysis)
662
+ author_analysis_payload["stage_status"] = stage_status
663
+ author_analysis_payload["quality_tier"] = analysis.get("quality_tier")
664
+ author_analysis_payload["sampled_work_ids"] = sampled_work_ids
665
+
666
+ try:
667
+ author_card_write = build_author_card(
668
+ platform=platform,
669
+ profile=profile,
670
+ analysis_payload=author_analysis_payload,
671
+ card_root=card_root,
672
+ storage_config=storage_config,
673
+ write_card=write_card,
674
+ )
675
+ except Exception as error:
676
+ author_card_write = {
677
+ "ok": False,
678
+ "card_type": "author",
679
+ "card_role": "author_card",
680
+ "error_reason": f"author_card_write_failed:{type(error).__name__}:{error}",
681
+ "routing": {
682
+ "card_role": "author_card",
683
+ "route_key": "author",
684
+ "primary_route_parts": "",
685
+ "explicit_override": False,
686
+ "storage_routes_configured": bool(isinstance(storage_config, dict) and isinstance(storage_config.get("storage_routes"), dict)),
687
+ },
688
+ }
689
+
690
+ stage_status["author_card"] = (
691
+ _stage_status(status="full", ok_count=1, failed_count=0, degraded_count=0, reason_codes=[])
692
+ if author_card_write.get("ok")
693
+ else _stage_status(
694
+ status="failed",
695
+ ok_count=0,
696
+ failed_count=1,
697
+ degraded_count=0,
698
+ reason_codes=[str(author_card_write.get("error_reason") or "author_card_write_failed").split(":", 1)[0]],
699
+ failure_kind="runtime",
700
+ )
701
+ )
702
+ stage_status["overall"] = _build_overall_status(stage_status)
703
+ if author_card_write.get("ok"):
704
+ author_analysis_payload["stage_status"] = stage_status
705
+ try:
706
+ author_card_write = build_author_card(
707
+ platform=platform,
708
+ profile=profile,
709
+ analysis_payload=author_analysis_payload,
710
+ card_root=card_root,
711
+ storage_config=storage_config,
712
+ write_card=write_card,
713
+ )
714
+ except Exception:
715
+ pass
394
716
 
395
717
  if progress is not None:
396
718
  progress.done(
@@ -398,8 +720,8 @@ def run_author_home_analysis(
398
720
  message="author_home card writing finished",
399
721
  data={
400
722
  "author_card_ok": bool(author_card_write.get("ok")),
401
- "work_cards_count": int(work_card_write.get("count") or 0),
402
- "work_card_results": len(work_card_write.get("results") or []),
723
+ "author_sample_cards_count": int(author_sample_cards.get("count") or 0),
724
+ "sample_work_cards_count": int(sample_work_cards.get("count") or 0),
403
725
  },
404
726
  )
405
727
 
@@ -419,14 +741,23 @@ def run_author_home_analysis(
419
741
  f"work_queued={work_analysis_stats.get('queued_count', 0)}",
420
742
  f"work_failed={work_analysis_stats.get('failed_count', 0)}",
421
743
  ],
422
- "confidence": "medium" if not analysis_missing else "low",
423
- "error_reason": None,
424
- "missing_fields": adapter_missing + analysis_missing + asr_missing + work_analysis_missing,
744
+ "confidence": (
745
+ "low"
746
+ if analysis.get("quality_tier") in {"fallback", "failed"} or analysis_missing
747
+ else "medium"
748
+ ),
749
+ "error_reason": (
750
+ ((stage_status.get("overall") or {}).get("reason_codes") or [None])[0]
751
+ if (stage_status.get("overall") or {}).get("status") == "failed"
752
+ else None
753
+ ),
754
+ "missing_fields": adapter_missing + analysis_missing + asr_missing,
425
755
  "extract_trace": all_extract_trace,
426
756
  "fallback_trace": fallback_trace,
427
757
  "request_id": raw.get("request_id"),
428
758
  "author_profile": profile,
429
759
  "works": works,
760
+ "sampled_work_ids": sampled_work_ids,
430
761
  "pagination": {
431
762
  **(raw.get("pagination") or {}),
432
763
  "total_collected": len(works),
@@ -436,6 +767,8 @@ def run_author_home_analysis(
436
767
  "author_analysis_v2": analysis.get("author_analysis_v2") if isinstance(analysis.get("author_analysis_v2"), dict) else {},
437
768
  "author_analysis_input_v1": analysis.get("author_analysis_input_v1") if isinstance(analysis.get("author_analysis_input_v1"), dict) else {},
438
769
  "analysis_validation": analysis.get("validation") if isinstance(analysis.get("validation"), dict) else {},
770
+ "stage_status": stage_status,
771
+ "quality_tier": analysis.get("quality_tier"),
439
772
  "asr_stats": asr_stats,
440
773
  "work_analysis": {
441
774
  "stats": work_analysis_stats,
@@ -443,10 +776,16 @@ def run_author_home_analysis(
443
776
  "artifact_root": work_analysis_bundle.get("artifact_root"),
444
777
  "analysis_logic_version": work_analysis_bundle.get("analysis_logic_version"),
445
778
  "prompt_contract_hash": work_analysis_bundle.get("prompt_contract_hash"),
779
+ "normalization_version": work_analysis_bundle.get("normalization_version"),
446
780
  },
447
781
  "card_write": {
448
782
  "author": author_card_write,
449
- "works": work_card_write,
783
+ "author_sample_cards": author_sample_cards,
784
+ "sample_work_cards": sample_work_cards,
785
+ "works": {
786
+ **author_sample_cards,
787
+ "legacy_alias_of": "author_sample_cards",
788
+ },
450
789
  },
451
790
  "checkpoint": {
452
791
  "last_cursor": ((raw.get("pagination") or {}).get("pages") or [{}])[-1].get("cursor_out") if isinstance((raw.get("pagination") or {}).get("pages"), list) and (raw.get("pagination") or {}).get("pages") else "",
@@ -471,7 +810,10 @@ def run_author_home_analysis(
471
810
  input_value=input_value,
472
811
  storage_config=storage_config or {},
473
812
  persist_output=bool(persist_output),
813
+ card_root=card_root,
474
814
  )
815
+ stage_status["persist"] = _build_persist_stage_status(result.get("output_persist") if isinstance(result.get("output_persist"), dict) else {})
816
+ result["stage_status"] = stage_status
475
817
  if progress is not None:
476
818
  final_event = progress.failed if result.get("error_reason") else progress.done
477
819
  final_event(
@@ -481,7 +823,8 @@ def run_author_home_analysis(
481
823
  "works_count": len(works),
482
824
  "request_id": result.get("request_id"),
483
825
  "author_card_ok": bool((result.get("card_write") or {}).get("author", {}).get("ok")),
484
- "work_cards_count": int(((result.get("card_write") or {}).get("works") or {}).get("count") or 0),
826
+ "author_sample_cards_count": int(((result.get("card_write") or {}).get("author_sample_cards") or {}).get("count") or 0),
827
+ "sample_work_cards_count": int(((result.get("card_write") or {}).get("sample_work_cards") or {}).get("count") or 0),
485
828
  "cache_hit_count": work_analysis_stats.get("cache_hit_count", 0),
486
829
  "queued_count": work_analysis_stats.get("queued_count", 0),
487
830
  "failed_count": work_analysis_stats.get("failed_count", 0),