fix(health): complete consultation evidence contract
This commit is contained in:
@@ -754,6 +754,90 @@ test("generatePersonalReport returns a ready document that passes the server par
|
||||
assert.equal(reparsed.ok, true);
|
||||
});
|
||||
|
||||
test("generatePersonalReport keeps health writer output non-medical and parameter-sensitive", async () => {
|
||||
const base = bundleV2Fixture();
|
||||
const d1 = base.charts.find((chart) => chart.id === "D1");
|
||||
const chartTemplate = base.charts.find((chart) => chart.id === "D10");
|
||||
assert.ok(d1);
|
||||
assert.ok(chartTemplate);
|
||||
const techniques = ["D1", "D6", "D8", "D30", "Vimshottari"] as const;
|
||||
const techniqueIds = techniques.map((name) => `ev-tech-${name.toLowerCase()}`);
|
||||
const bundle = finalizeReportEvidenceBundleV2({
|
||||
...withoutBundleHash(base),
|
||||
requestedThemes: ["health_pressure"],
|
||||
charts: [
|
||||
d1,
|
||||
...(["D6", "D8", "D30"] as const).map((id) => ({
|
||||
...chartTemplate,
|
||||
id,
|
||||
title: `${id} health divisional chart`,
|
||||
})),
|
||||
],
|
||||
claimCards: [{
|
||||
id: "ev-claim-health-pressure",
|
||||
theme: "health_pressure",
|
||||
section: "健康压力(非医疗)",
|
||||
conclusion: "D6/D8/D30 与 Vimshottari 只支持非医疗的压力与恢复节奏观察。",
|
||||
supportingFacts: techniqueIds.map((evidenceRef, index) => ({
|
||||
id: `ev-fact-health-${index + 1}`,
|
||||
label: evidenceRef,
|
||||
value: "该层已执行并仅用于非医疗叙事。",
|
||||
evidenceRef,
|
||||
status: "verified" as const,
|
||||
})),
|
||||
counterFacts: [],
|
||||
executedTechniqueRefs: techniqueIds,
|
||||
assertionLevel: "parameter_sensitive",
|
||||
timingBoundary: null,
|
||||
verificationQuestions: [],
|
||||
}],
|
||||
blockedSections: [],
|
||||
executionLedger: techniqueIds.map((id, index) => ({
|
||||
id,
|
||||
technique: techniques[index],
|
||||
status: "verified" as const,
|
||||
executed: true,
|
||||
note: "健康主题最低证据已执行",
|
||||
})),
|
||||
evidenceRefs: techniqueIds.map((id, index) => ({
|
||||
id,
|
||||
technique: techniques[index],
|
||||
status: "verified" as const,
|
||||
})),
|
||||
});
|
||||
const calls = { count: 0 };
|
||||
const result = await generatePersonalReport({
|
||||
reportId: "22222222-2222-4222-8222-222222222222",
|
||||
bundle,
|
||||
depth: "standard",
|
||||
agent: fakeAgent(agentOutput({
|
||||
thematicNarrative: [{
|
||||
id: "theme-health_pressure",
|
||||
theme: "health_pressure",
|
||||
title: "健康压力(非医疗)",
|
||||
narrative: "本段仅描述压力负荷与恢复节奏,不构成医疗诊断或治疗建议。",
|
||||
actions: ["如有持续不适,请咨询合格医疗专业人员。"],
|
||||
caveats: ["非医疗边界:占星内容不能替代专业诊疗。"],
|
||||
claimStatus: "parameter_sensitive",
|
||||
evidenceRefs: techniqueIds,
|
||||
}],
|
||||
}), calls),
|
||||
now: () => new Date("2026-09-03T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
assert.equal(calls.count, 1);
|
||||
assert.equal(bundle.claimCards[0].assertionLevel, "parameter_sensitive");
|
||||
assert.equal(result.status, "ready");
|
||||
if (result.status !== "ready") return;
|
||||
const parsed = safeParseServerReportDocument(result.document);
|
||||
assert.equal(parsed.ok, true);
|
||||
if (!parsed.ok) return;
|
||||
const health = parsed.document.thematicNarrative.find((section) => section.id === "theme-health_pressure");
|
||||
assert.ok(health);
|
||||
assert.match(`${health.narrative} ${health.caveats.join(" ")}`, /非医疗|不构成医疗/);
|
||||
assert.equal(health.claimStatus, "parameter_sensitive");
|
||||
});
|
||||
|
||||
test("generatePersonalReport fails with report_guard_rejected on guard rejection", async () => {
|
||||
const bundle = bundleV2Fixture();
|
||||
const result = await generatePersonalReport({
|
||||
|
||||
@@ -1847,7 +1847,7 @@ _ROUTE_REQUIRED_LAYERS = {
|
||||
'career': ('D1', 'D10', 'A10', 'dasha_boundaries', 'narayana_dasha'),
|
||||
'marriage': ('D1', 'D9', 'UL', 'A7', 'dasha_boundaries'),
|
||||
'wealth': ('D1', 'D2', 'D11', 'ashtakavarga', 'dasha_boundaries'),
|
||||
'health': ('D1', 'D6', 'D8', 'D9', 'dasha_boundaries'),
|
||||
'health': ('D1', 'D6', 'D8', 'D30', 'dasha_boundaries'),
|
||||
'education': ('D1', 'D9', 'D24', 'dasha_boundaries'),
|
||||
'migration': ('D1', 'D4', 'D12', 'dasha_boundaries'),
|
||||
'family': ('D1', 'D7', 'D9', 'D12', 'dasha_boundaries'),
|
||||
|
||||
@@ -951,6 +951,11 @@ class UnifiedConsultationOrchestrator:
|
||||
or official.get("raw")
|
||||
)
|
||||
official_state = self._vedastro_cloud_state(vedastro_official)
|
||||
route_name = str(
|
||||
(route_packet or {}).get("question_type")
|
||||
or (route_packet or {}).get("primary_theme")
|
||||
or "general"
|
||||
)
|
||||
raw_response_section = (
|
||||
self._section(raw_response, "vedastro_official.raw_response")
|
||||
if official_state == "official_verified"
|
||||
@@ -1012,6 +1017,20 @@ class UnifiedConsultationOrchestrator:
|
||||
"sudarshana": self._section(sudarshana_packet, "chart.modules.sudarshana"),
|
||||
"sahams": self._section(sahams_packet, "chart.modules.sahams"),
|
||||
}
|
||||
if route_name == "health":
|
||||
transits = modules.get("transits") if isinstance(modules.get("transits"), dict) else {}
|
||||
sections["transit"] = {
|
||||
"status": (
|
||||
"used"
|
||||
if str(transits.get("status") or "").lower() in {"executed", "used", "ready"}
|
||||
else "blocked"
|
||||
),
|
||||
"source_path": "modules.transits",
|
||||
}
|
||||
sections["non_medical_boundary"] = {
|
||||
"status": "used",
|
||||
"source_path": "route_profile_contract.health.non_medical_boundary",
|
||||
}
|
||||
for division in FORMAL_DIVISIONS:
|
||||
if division == 1:
|
||||
continue
|
||||
@@ -1033,9 +1052,7 @@ class UnifiedConsultationOrchestrator:
|
||||
return {
|
||||
"status": "complete" if not missing else "partial",
|
||||
"route": dict(route_packet),
|
||||
"required_sections": self.evidence_packet_required_sections(
|
||||
str((route_packet or {}).get("question_type") or (route_packet or {}).get("primary_theme") or "general")
|
||||
),
|
||||
"required_sections": self.evidence_packet_required_sections(route_name),
|
||||
"sections": sections,
|
||||
"functional_benefic_malefic": functional_layer,
|
||||
"signals": [item for item in signals if isinstance(item, dict)],
|
||||
|
||||
@@ -602,6 +602,7 @@ def _sections(**overrides: str) -> dict:
|
||||
'D7': 'used',
|
||||
'D8': 'used',
|
||||
'D9': 'used',
|
||||
'D30': 'used',
|
||||
'D10': 'used',
|
||||
'D11': 'used',
|
||||
'D12': 'used',
|
||||
@@ -724,6 +725,85 @@ def test_wealth_route_requires_the_hora_chart() -> None:
|
||||
assert context['core_status'] == 'degraded'
|
||||
|
||||
|
||||
def test_health_route_requires_d30_instead_of_d9() -> None:
|
||||
context = _build_consumer_context(
|
||||
question='请分析健康压力窗口',
|
||||
route_packet={'question_type': 'health', 'primary_theme': 'health'},
|
||||
chart={'success': True},
|
||||
rectification=_confirmed_birth_time(),
|
||||
machine_evidence_packet=_sections(D30='missing'),
|
||||
vedastro_official={'status': 'blocked'},
|
||||
)
|
||||
|
||||
assert context['missing_route_layers'] == ['D30']
|
||||
assert context['core_status'] == 'degraded'
|
||||
|
||||
|
||||
def test_health_workflow_keeps_three_windows_and_blocked_transit_visible(monkeypatch) -> None:
|
||||
handler = _handler()
|
||||
chart = _base_chart()
|
||||
chart['modules']['transits'] = {
|
||||
'status': 'blocked',
|
||||
'reason': 'fixture_no_transit_runtime',
|
||||
'boundary': 'observation windows, not guaranteed events',
|
||||
}
|
||||
|
||||
monkeypatch.setattr(handler, '_compute_chart', lambda body: chart)
|
||||
monkeypatch.setattr(handler, '_compute_rectification_gate', lambda body: {
|
||||
'success': True,
|
||||
'effective_accuracy': 'minute',
|
||||
'lagna_boundary': {'is_sensitive': False},
|
||||
'summary': {'recommended_events': [], 'warned': [], 'disabled': []},
|
||||
})
|
||||
monkeypatch.setattr(handler, '_compute_thematic_report', lambda body: {
|
||||
'success': True,
|
||||
'endpoint': 'thematic_report',
|
||||
'themes': {},
|
||||
})
|
||||
monkeypatch.setattr(handler, '_compute_vedastro_gateway_run', lambda body: {
|
||||
'scope': 'vedastro_gateway_run',
|
||||
'status': 'official_blocked',
|
||||
'official_closure_state': 'official_blocked',
|
||||
'official_closure_reason': 'fixture_no_network',
|
||||
})
|
||||
|
||||
result = handler._compute_consultation_workflow({
|
||||
'entry_mode': 'direct_chart',
|
||||
'question': '请看健康压力、风险和恢复窗口',
|
||||
'theme': ['health'],
|
||||
'year': _BIRTH['year'],
|
||||
'month': _BIRTH['month'],
|
||||
'day': _BIRTH['day'],
|
||||
'hour': _BIRTH['hour'],
|
||||
'minute': _BIRTH['minute'],
|
||||
'lat': 39.9042,
|
||||
'lon': 116.4074,
|
||||
'tz': 8,
|
||||
'current_date': _REFERENCE_DATE,
|
||||
})
|
||||
|
||||
assert result['routing']['question_type'] == 'health'
|
||||
assert result['unified_orchestrator']['route_profile_contract']['sections'] == [
|
||||
'pressure_windows',
|
||||
'event_risk_windows',
|
||||
'recovery_support_windows',
|
||||
'non_medical_boundary',
|
||||
'verification_questions',
|
||||
]
|
||||
sections = result['machine_evidence_packet']['sections']
|
||||
assert all(sections[layer]['status'] == 'used' for layer in ('D6', 'D8', 'D30'))
|
||||
assert sections['transit']['status'] == 'blocked'
|
||||
assert sections['non_medical_boundary']['status'] == 'used'
|
||||
assert all(layer in result['consumer_context']['available_layers'] for layer in ('D6', 'D8', 'D30'))
|
||||
assert 'health_non_medical_boundary' in result['consumer_context']['domain_boundaries']
|
||||
audit = {
|
||||
row['technique']: row['status']
|
||||
for row in result['consumer_context']['technique_audit_table']
|
||||
if isinstance(row, dict)
|
||||
}
|
||||
assert audit['Transits / Sade Sati'] == 'blocked'
|
||||
|
||||
|
||||
def test_domain_boundary_follows_the_route_not_the_wording() -> None:
|
||||
context = _build_consumer_context(
|
||||
question=_relationship_shaped_question(),
|
||||
|
||||
Reference in New Issue
Block a user