Skip to main content

gugen/
report.rs

1use crate::composition::Composition;
2use crate::evidence::PlanningEvidence;
3use crate::literature_evidence::LiteratureRouteEvidence;
4use crate::precursor::PrecursorSelection;
5use crate::prior_experiment_evidence::PriorExperimentEvidence;
6use crate::process::{PlannedStep, RouteFamily};
7use crate::provenance::PlanningProvenance;
8use crate::reaction::BalancedReaction;
9use crate::rejection::RejectedCandidate;
10use crate::route_suitability::{RouteSuitabilityAssessment, SuitabilityFinding};
11use crate::score::{ConfidenceAssessment, PlanScoreBreakdown, PlanningAssumption};
12
13/// Bumped whenever `SynthesisPlan`'s or `SynthesisPlanningReport`'s JSON
14/// shape changes in a way a strict deserializer (e.g. one rejecting
15/// unknown fields) would need to account for. `2` as of v0.4.0: adds
16/// `SynthesisPlan.literature_evidence`, confirmed as a breaking addition
17/// by `cargo semver-checks`'s `constructible_struct_adds_field` lint --
18/// see `docs/literature_evidence_integration.md`. `3` as of Phase 26:
19/// adds `SynthesisPlan.prior_experiment_evidence`, the identical breaking
20/// class -- see `docs/prior_experiment_evidence.md`.
21pub const SCHEMA_VERSION: u32 = 3;
22
23#[derive(Debug, Clone, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct TargetSummary {
26    pub composition: Composition,
27    pub structure_present: bool,
28    pub desired_phase: Option<String>,
29}
30
31/// Whether this planner can meaningfully handle the target at all
32/// (AGENTS.md §16). Distinct from per-plan confidence: applicability is
33/// about domain fit, not about how well-evidenced any particular plan is.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum ApplicabilityLevel {
37    InDomain,
38    PartiallyInDomain,
39    OutOfDomain,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct ApplicabilityAssessment {
45    pub level: ApplicabilityLevel,
46    pub rationale: Vec<String>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub enum WarningSeverity {
52    Info,
53    Caution,
54    Severe,
55}
56
57#[derive(Debug, Clone, PartialEq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct PlanningWarning {
60    pub message: String,
61    pub severity: WarningSeverity,
62}
63
64#[derive(Debug, Clone, PartialEq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct UnresolvedRequirement {
67    pub description: String,
68    pub reason: String,
69}
70
71/// Deterministic plan identifier (AGENTS.md §20: "plan IDを決定的にする").
72/// Phase 5 derives this from plan contents; Phase 1 only needs the type.
73#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct PlanId(pub String);
76
77impl std::fmt::Display for PlanId {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.write_str(&self.0)
80    }
81}
82
83/// A candidate synthesis plan (AGENTS.md §6). `steps` is `Vec<PlannedStep>`
84/// rather than the bare `Vec<ProcessStep>` AGENTS.md §6 shows, so each step
85/// can carry the `StepRequirement` §11 mandates. `manual_review_required`
86/// isn't in §6's snippet, but §15 requires the v0.1 JSON plan to carry it
87/// (or an equivalent) regardless -- see [`crate::score_plan`] for why it's
88/// always `true` in v0.1.
89#[derive(Debug, Clone, PartialEq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub struct SynthesisPlan {
92    pub plan_id: PlanId,
93    pub route_family: RouteFamily,
94    pub precursors: Vec<PrecursorSelection>,
95    pub balanced_reaction: Option<BalancedReaction>,
96    pub steps: Vec<PlannedStep>,
97    pub score: PlanScoreBreakdown,
98    pub confidence: ConfidenceAssessment,
99    pub applicability: ApplicabilityAssessment,
100    pub evidence: Vec<PlanningEvidence>,
101    pub warnings: Vec<PlanningWarning>,
102    pub assumptions: Vec<PlanningAssumption>,
103    pub unresolved: Vec<UnresolvedRequirement>,
104    pub manual_review_required: bool,
105    /// Reference-only cross-DOI literature evidence for this plan's exact
106    /// (target, precursors, route_family) (v0.4.0 Integration), from a
107    /// configured `LiteratureEvidenceProvider`. `None` whenever no
108    /// provider is configured, the provider found no matching route, or
109    /// this plan's route family isn't `ConventionalSolidState` (the only
110    /// route family the underlying corpus has evidence for). Never
111    /// derived from or fed into `score`, `confidence`, `evidence`, or
112    /// `steps` -- see `literature_evidence.rs`'s module doc comment for
113    /// why that's structural, not a convention this field happens to
114    /// follow. A field/route showing `Conflict` or
115    /// `has_multiple_operation_shapes` here is a *disclosure*, not a
116    /// planning failure -- it is never auto-resolved, and its presence
117    /// alone must never be read as "condition accuracy improved" (it
118    /// means more literature coverage was found, not that any specific
119    /// value here is correct).
120    pub literature_evidence: Option<LiteratureRouteEvidence>,
121    /// Reference-only prior-experiment evidence for this plan's exact
122    /// (target, precursors, route_family) (Phase 26), from a configured
123    /// `PriorExperimentEvidenceProvider`. `None` whenever no provider is
124    /// configured or no exact-identity match was found -- unlike
125    /// `literature_evidence`, this is **not** restricted to
126    /// `ConventionalSolidState`; `route_family` is already part of the
127    /// match key, so cross-family leakage can't happen regardless. Never
128    /// derived from or fed into `score`, `confidence`, `evidence`, or
129    /// `steps`. **Never a success rate**: the matched records' actual
130    /// process conditions, grades, and catalogs are not required to
131    /// agree with each other or with this plan's own (usually
132    /// unresolved) conditions, so grouping their outcomes together
133    /// describes what was recorded, not a probability of what a new
134    /// attempt would produce -- see `PriorExperimentEvidence::outcome_tally`'s
135    /// own doc comment.
136    pub prior_experiment_evidence: Option<PriorExperimentEvidence>,
137}
138
139/// A plan excluded from the recommended list by route-suitability findings
140/// (Phase 15B) -- `plan` is unchanged from what would otherwise have
141/// appeared in `SynthesisPlanningReport.plans` (same score/confidence/
142/// evidence, since filtering happens after scoring, not instead of it);
143/// `contradicting_findings` is just the `Contradicts` findings that
144/// triggered exclusion (not the full assessment, which may also carry
145/// `Supports`/`Unknown` findings for other purposes).
146#[derive(Debug, Clone, PartialEq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub struct NotRecommendedPlan {
149    pub plan: SynthesisPlan,
150    pub contradicting_findings: Vec<SuitabilityFinding>,
151}
152
153#[derive(Debug, Clone, PartialEq)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct SynthesisPlanningReport {
156    pub schema_version: u32,
157    pub target: TargetSummary,
158    pub applicability: ApplicabilityAssessment,
159    /// One entry per `RouteFamily` variant a `RouteSuitabilityProvider` was
160    /// asked about (Phase 15A) -- target-level, like `applicability`, not
161    /// per-plan, since suitability doesn't depend on which precursor set a
162    /// given `SynthesisPlan` used. Correlate a specific plan to its
163    /// assessment via `SynthesisPlan.route_family`. Always empty when no
164    /// provider is configured (e.g. `Planner::offline_minimal`) -- carries
165    /// no ranking weight in this phase; nothing in `score.rs` reads it.
166    pub route_suitability: Vec<RouteSuitabilityAssessment>,
167    pub plans: Vec<SynthesisPlan>,
168    /// Plans that were built (valid chemistry, a real balanced reaction and
169    /// process template) but excluded from `plans` because
170    /// `route_suitability::derive_recommendation` returned `NotRecommended`
171    /// for that plan's route family (Phase 15B) -- kept here, with the
172    /// specific findings that triggered exclusion, rather than dropped
173    /// silently. Always empty when no `RouteSuitabilityProvider` is
174    /// configured, or when every assessed route family clears the bar.
175    pub not_recommended: Vec<NotRecommendedPlan>,
176    pub rejected_candidates: Vec<RejectedCandidate>,
177    pub unresolved: Vec<UnresolvedRequirement>,
178    pub warnings: Vec<PlanningWarning>,
179    pub provenance: PlanningProvenance,
180}