119 lines
4.9 KiB
Python
119 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Audit a generated report JSON before delivery."""
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
EXPECTED_ROLES = (
|
||
|
|
'productStrategy', 'conversion', 'consumerInsight', 'productSupply',
|
||
|
|
'visualDesign', 'executiveDecision', 'editorReview',
|
||
|
|
)
|
||
|
|
VISIBLE_SOURCES = {
|
||
|
|
'experienceSolution.reportSummary': ('reportSummary',),
|
||
|
|
'experienceSolution.ownerOverview': ('ownerOverview',),
|
||
|
|
'experienceSolution.newProductPlans': ('newProductPlans',),
|
||
|
|
'experienceSolution.titleAnalysis': ('titleAnalysis',),
|
||
|
|
'experienceSolution.visualCommerce': ('visualCommerce',),
|
||
|
|
'experienceSolution.detailCommerce': ('detailCommerce',),
|
||
|
|
'experienceSolution.skuAnalysis': ('skuAnalysis',),
|
||
|
|
'experienceSolution.customerExperience': ('customerExperience',),
|
||
|
|
'reportPlan': ('supportAnalysis',),
|
||
|
|
'reviewBatchAnalysis': ('supportAnalysis',),
|
||
|
|
'experienceSolution.productExperience': ('supportAnalysis',),
|
||
|
|
'experienceSolution.validationLoop': ('supportAnalysis',),
|
||
|
|
'experienceSolution.expressionContinuity': ('supportAnalysis',),
|
||
|
|
'dataQuality': ('supportAnalysis',),
|
||
|
|
'consumerResearch': ('reviews', 'supportAnalysis'),
|
||
|
|
'questionResearch': ('supportAnalysis',),
|
||
|
|
'commercialDecision': ('supportAnalysis',),
|
||
|
|
'competitionDecision': ('supportAnalysis',),
|
||
|
|
'merchandisingDecision': ('supportAnalysis',),
|
||
|
|
'productDecision': ('supportAnalysis',),
|
||
|
|
'visualDecision': ('supportAnalysis',),
|
||
|
|
'roleOutputs': ('roleOutputs',),
|
||
|
|
}
|
||
|
|
|
||
|
|
PLACEHOLDERS = {'暂无', '无', '未采集', '未评级', '待补充', '尚未结构化'}
|
||
|
|
|
||
|
|
def has_content(value):
|
||
|
|
if value is None:
|
||
|
|
return False
|
||
|
|
if isinstance(value, str):
|
||
|
|
return bool(value.strip()) and value.strip() not in PLACEHOLDERS
|
||
|
|
if isinstance(value, (int, float, bool)):
|
||
|
|
return True
|
||
|
|
if isinstance(value, list):
|
||
|
|
return any(has_content(item) for item in value)
|
||
|
|
if isinstance(value, dict):
|
||
|
|
return any(has_content(item) for item in value.values())
|
||
|
|
return False
|
||
|
|
|
||
|
|
def get_path(data, path):
|
||
|
|
value = data
|
||
|
|
for part in path.split('.'):
|
||
|
|
if not isinstance(value, dict):
|
||
|
|
return None
|
||
|
|
value = value.get(part)
|
||
|
|
return value
|
||
|
|
|
||
|
|
def evidence_refs(value):
|
||
|
|
refs=[]
|
||
|
|
if isinstance(value, dict):
|
||
|
|
ids=value.get('evidenceIds')
|
||
|
|
if isinstance(ids, list):
|
||
|
|
refs.extend(str(item) for item in ids if item)
|
||
|
|
for child in value.values():
|
||
|
|
refs.extend(evidence_refs(child))
|
||
|
|
elif isinstance(value, list):
|
||
|
|
for child in value:
|
||
|
|
refs.extend(evidence_refs(child))
|
||
|
|
return refs
|
||
|
|
|
||
|
|
def audit(data):
|
||
|
|
ledger=data.get('evidenceLedger') or []
|
||
|
|
known={str(item.get('id')) for item in ledger if isinstance(item,dict) and item.get('id')}
|
||
|
|
refs=evidence_refs(data.get('experienceSolution') or {})+evidence_refs(data.get('roleOutputs') or {})
|
||
|
|
unresolved=sorted(set(refs)-known)
|
||
|
|
roles=data.get('roleOutputs') or {}
|
||
|
|
missing_roles=[role for role in EXPECTED_ROLES if not has_content(roles.get(role))]
|
||
|
|
present_sources=[]
|
||
|
|
mapped_sources=[]
|
||
|
|
for path,targets in VISIBLE_SOURCES.items():
|
||
|
|
if has_content(get_path(data,path)):
|
||
|
|
present_sources.append(path)
|
||
|
|
mapped_sources.append({'source':path,'renderers':list(targets)})
|
||
|
|
images=[item for item in ledger if isinstance(item,dict) and item.get('type')=='image']
|
||
|
|
invalid_images=[item.get('id') for item in images if not has_content((item.get('meta') or {}).get('localUrl') or item.get('value'))]
|
||
|
|
exp=data.get('experienceSolution') or {}
|
||
|
|
required={
|
||
|
|
'reportSummary':has_content(exp.get('reportSummary')),
|
||
|
|
'ownerOverview':has_content(exp.get('ownerOverview')),
|
||
|
|
'newProductPlans':has_content(exp.get('newProductPlans')),
|
||
|
|
}
|
||
|
|
failures=[]
|
||
|
|
failures += [f'缺少核心报告模块:{key}' for key,ok in required.items() if not ok]
|
||
|
|
failures += [f'缺少角色输出:{role}' for role in missing_roles]
|
||
|
|
failures += [f'证据引用无法解析:{eid}' for eid in unresolved]
|
||
|
|
failures += [f'图片证据无可用地址:{eid}' for eid in invalid_images]
|
||
|
|
return {
|
||
|
|
'ok':not failures,
|
||
|
|
'engineVersion':(data.get('meta') or {}).get('engineVersion'),
|
||
|
|
'presentMappedSources':mapped_sources,
|
||
|
|
'sourceCoverage':len(mapped_sources),
|
||
|
|
'roles':{'present':len(EXPECTED_ROLES)-len(missing_roles),'expected':len(EXPECTED_ROLES),'missing':missing_roles},
|
||
|
|
'evidence':{'ledger':len(ledger),'references':len(refs),'unresolved':unresolved},
|
||
|
|
'images':{'total':len(images),'invalid':invalid_images},
|
||
|
|
'failures':failures,
|
||
|
|
}
|
||
|
|
|
||
|
|
def main():
|
||
|
|
if len(sys.argv)!=2:
|
||
|
|
raise SystemExit('用法:python3 audit_report.py <report.json>')
|
||
|
|
source=Path(sys.argv[1])
|
||
|
|
result=audit(json.loads(source.read_text('utf-8')))
|
||
|
|
print(json.dumps(result,ensure_ascii=False,indent=2))
|
||
|
|
raise SystemExit(0 if result['ok'] else 1)
|
||
|
|
|
||
|
|
if __name__=='__main__':
|
||
|
|
main()
|