Skip to main content

Module generation

Module generation 

Source
Expand description

Formulation input domain model and draft generation for the generate feature.

Roadmap (see docs/sdsforge-architecture.md’s “Generation architecture” section):

  • #8 input/validateProductInput and friends, deterministic offline validation.
  • #9 draft — maps a validated ProductInput to a partial SdsRoot (Section 1/3 only).
  • #10 provenance/unresolved/result — wraps #9’s draft in a GenerationResult that explains what’s populated, where it came from, what’s still missing, and whether the draft is releasable.
  • #11+ chematic integration, CLI, GUI — not yet implemented.

Modules§

field_path
Canonical MHLW JSON path fragments, so every call site builds paths the same way instead of hand-formatting strings.

Structs§

ComponentInput
One ingredient of a product formulation.
ConcentrationRange
A single component’s concentration in a mixture.
EvidenceSource
A reference to a piece of supporting evidence — a test report, a supplier specification, a database entry. Stores identifying metadata only (a reference string, issuer, date); never the document itself or arbitrary binary data.
EvidenceSummary
Deterministic tally over a GenerationResult’s provenance/unresolved records — never manually incremented elsewhere, always recomputed by compute_evidence_summary from the records themselves, so the counts can’t drift from what’s actually in the report.
ExplosiveLimitsEvidence
Explosive (flammability) limits need two bounds; otherwise the same shape as MeasuredValueEvidence, including the required conditions key (see that struct’s doc comment).
FieldPolicy
What evidence is even eligible for a given field, and whether missing it blocks release. Data, not scattered conditionals — see PRODUCT_LEVEL_POLICIES below.
FieldProvenance
A single field’s origin: where its value came from, how, and how confident the generator is in it. One FieldProvenance per populated field.
GenerationArtifacts
The three artifacts a generate invocation publishes, as ready-to-write strings. Keeps filesystem concerns out of sdsforge-core while giving Rust callers the same three outputs the CLI writes to disk.
GenerationReport
Everything needed to understand an SDS draft’s completeness, without the draft itself. Never embeds SdsRoot — see tests:: generation_report_json_does_not_contain_full_sds.
GenerationResult
The full generation report: the official SDS draft plus everything needed to understand its incompleteness. sds is the only part that belongs in official_sds.jsonfindings/unresolved/provenance/ evidence_summary/release_status describe the draft, they are never written into it (see tests::official_sds_json_has_no_report_keys).
MeasuredPropertiesInput
All measured-property evidence a caller supplies for the seven properties super::unresolved::PRODUCT_LEVEL_POLICIES covers. Each field is a Vec, not an Option — zero entries means nothing was supplied (stays Unresolved/HumanReviewRequired, unchanged from commit #10); more than one entry makes disagreement between reports representable, which a single Option slot could never express.
MeasuredValueEvidence
A measured value for one of the four properties whose MHLW schema field has a real numeric+unit shape (flash point, boiling point, vapor pressure). evidence_id must match an EvidenceSource::id in super::ProductInput::evidence — a value with no resolvable evidence reference can never become Confirmed, only Supplied at best (see resolution logic in super::result). conditions deliberately has no #[serde(default)]: it must remain a required key so an omitted measurement condition is a parse error, not a silently-accepted empty condition set. (MeasurementConditions’ own three fields are individually Option, so conditions: {} is valid — the acceptance policies in super::resolve are what reject an insufficiently-specified condition set, not the parser.)
MeasurementConditions
Measurement conditions for a physical/chemical property. Kept small — this only needs enough structure to say what evidence the user must provide, not to model a general laboratory information system.
NotApplicableReason
Placeholder for a deliberate “this field does not apply to this product” determination. Not constructed anywhere in this commit’s generation logic — see docs/sdsforge-architecture.md’s generation-architecture section: when applicability genuinely can’t be determined (e.g. this commit’s ProductInput carries no physical-state field), the correct reason is UnresolvedReason::HumanReviewRequired, not a guessed NotApplicable. Defined now so super::FieldStatus compiles and so a future commit that can determine applicability has a type to use.
ProductInput
Raw input to the generate feature: a product’s identity, supplier, formulation, and any measured-property evidence. This is the domain model — deliberately separate from the MHLW-schema SdsRoot in crate::schema, which is the output shape.
ReleaseGateResult
RequiredInput
One piece of evidence a human would need to supply to resolve a field. Kept small — enough to explain what’s needed, not a full lab-data model.
SectionDraftResult
A partial SDS draft covering only Section 1 (Identification) and Section 3 (Composition) — the sections derivable from supplied product/ supplier/CAS/concentration data without product testing. Every other SdsRoot field is left None.
SupplierInput
Supplier contact details for Section 1 (Identification).
TestResultEvidence
For the three properties whose MHLW schema field is free text with no structured numeric value at all (self-reactivity, oxidizing properties, metal corrosivity) — result describes the test outcome directly rather than a value+unit pair, since that’s the actual shape of the target field.
UnresolvedField
A field the generator could not populate, with enough detail for a human (or a downstream system) to know what’s missing, why, how bad it is to ship without it, and what evidence would resolve it.

Enums§

ConfidenceLevel
How confident the generator is in a value, independent of where the value came from. A SupplierSpecification source and a ProductTestReport source can both be High confidence; an UnverifiedUserInput source is Unverified by construction — nothing about passing structural validation (CAS check-digit, concentration bounds) upgrades it, since syntactic validity isn’t evidence that a company name, address, or product identity is factually correct.
EvidenceApplicability
What the evidence actually was measured on — a mixture property can legitimately be confirmed by finished-product or same/equivalent-batch evidence, but never by component-level or reference-substance data (see super::result’s evidence-resolution logic).
EvidenceLevel
Priority-ordered evidence source. Not every level is valid for every field — see super::unresolved::FieldPolicy::allowed_evidence.
FieldStatus
How a field’s value was determined. Calculated/Estimated/Literature must never auto-promote to Confirmed — promotion requires new evidence, not a higher-confidence label on the same evidence.
GenerationArtifactError
RegulatoryImpact
ReleaseStatus
Generation must never mark its own output approved — approval is a separate human act (approver, timestamp, target version), never a side effect of running the generator. Draft/ReviewRequired/Blocked are the only values compute_release_status can produce; Approved exists on the enum for a future explicit human-approval record to set, not for generation code to reach (see tests::generation_never_approves).
SafetyImpact
UnresolvedReason
Why a value couldn’t be filled in.

Constants§

MOLECULAR_FORMULA_POLICY
Molecular formula as returned by crate::enrichment::lookup_cas: an identity candidate, reference-database evidence, no product test required. Included for completeness of the policy table — commit #9 already populates this field when the resolver supplies it; this policy documents the evidence rule that governs it.
PRODUCT_LEVEL_POLICIES
Product-level physical/hazard properties that Commit #9’s Section 1/3 draft never populates — CAS identity and composition alone are not sufficient evidence for any of these (see docs/sdsforge-architecture.md’s “Properties that require product-level evidence” table). Every one of these becomes an UnresolvedField on every generation result, since ProductInput (commit #8) supplies none of the underlying test data.
REPORT_SCHEMA_VERSION
Version of the generation_report.json shape, independent of the crate version — the report format and the crate can each change without the other. No timestamp is embedded by default: repeated serialization of the same GenerationResult must stay byte-equivalent.

Functions§

build_generation_artifacts
build_generation_report
Builds a GenerationReport from a GenerationResult.
build_product_level_unresolved
Builds the always-present product-level UnresolvedFields.
compute_evidence_summary
Recomputes the summary from scratch every time — see EvidenceSummary’s doc comment for why this is deliberate.
compute_release_status
Blocked if anything explicitly blocks release (an unresolved field marked blocks_release, or a CRIT/HIGH validation finding); otherwise ReviewRequired if anything at all remains unresolved or was merely flagged; Draft only when there’s truly nothing to review. Given this feature only ever produces Section 1/3 and always adds the seven product-level unresolved fields, real results are Blocked or ReviewRequiredDraft is reachable but not the common case, exactly as intended: an incomplete draft should not look deceptively finished.
draft_sections_from_resolved_input
Pure, deterministic mapping from ProductInput to a Section 1/3 draft.
evaluate_release_gate
Aggregates a GenerationResult into the CRIT/HIGH findings and required actions that actually block release — deterministic, built from result.unresolved/result.findings only, never a second source of truth. required_actions is deduplicated by exact string match (sufficient for now — multiple unresolved fields commonly share the same recommended action, e.g. two properties both needing “a human must first determine whether this property applies”).
generate_from_detailed_lookups
Pure core of generate_with_detailed_enrichment — no network access, so it’s fully unit-testable with a fixture lookups map (see tests::lookup_error_is_nonblocking_and_retains_supplied_data).
generate_from_normalized_input
Adds chematic-backed chemical-identity normalization on top of generate_from_resolved_input — reuses it rather than duplicating Section 1/3 mapping logic. Strategy: derive a plain HashMap<String, CasInfo> containing only genuinely CasResolution::Resolved (non- ambiguous) candidates and call the existing, unchanged generate_from_resolved_input — an ambiguous CAS is simply absent from that map, so the base pass treats it exactly like today’s “lookup didn’t resolve” case (commit #9’s GEN-CAS-ENRICHMENT-MISSING/ MissingInput, unchanged). This function then does one additive pass over the already-built result: replaces the generic lookup-failure unresolved entry with a specific AmbiguousChemicalIdentity one for ambiguous components, and for resolved components runs the normalizer and layers on canonical-SMILES writing / formula-consistency handling.
generate_from_resolved_input
Pure, deterministic: wraps draft_sections_from_resolved_input (does not duplicate its mapping logic) and adds provenance/unresolved/ evidence-summary/release-status on top. Same resolved shape as commit #9’s function — a HashMap of successful CAS lookups only, so a missing entry cannot be distinguished as “not found” vs. “request failed” here either; see [build_lookup_failure_unresolved]’s doc comment for why that’s a recorded limitation, not a redesign target for this commit.
generate_section_1_and_3
Orchestration: performs CAS enrichment lookups (network I/O against PubChem via crate::enrichment::lookup_cas — no second PubChem client or CAS resolver is implemented here), then delegates all mapping and finding logic to draft_sections_from_resolved_input.
generate_with_detailed_enrichment
Orchestration for the sdsforge generate --enrich CLI path: resolves every distinct CAS number in input through lookup_cas_detailed (deduplicated — one request per distinct CAS, not per component), then delegates everything else to generate_from_detailed_lookups. A network/HTTP/parse failure for one CAS never aborts the whole draft — it’s recorded as a Result::Err and surfaces as a GEN-CAS-LOOKUP-ERROR finding, while every other component’s lookup still proceeds normally.
generate_with_enrichment
Orchestration: performs the CAS lookups (reusing crate::enrichment::lookup_cas, no second PubChem client), then delegates everything else to generate_from_resolved_input. This is the one new orchestration path this commit adds — it does not call commit #9’s generate_section_1_and_3, which would re-fetch the same CAS data over the network a second time for no benefit.
render_review_report
Renders a deterministic, human-readable Markdown summary of a GenerationReport. Never uses an LLM. Always states plainly that the output is an unapproved automated draft — even a (currently unreachable-by-generation-code) ReleaseStatus::Approved still carries that disclaimer, so the wording can never be read as a real approval.
serialize_generation_report
Serializes a GenerationReport as pretty JSON. Contains only what GenerationReport itself carries — no API keys, HTTP headers, evidence binaries, temp-file paths, or ad hoc debug formatting ever enter this type, so there is nothing to redact here.
serialize_official_sds
Serializes only result.sds — the same serialize→prune_empty_fields→ pretty-print path the existing to-json/render commands already use, so official_sds.json matches the formatting of every other JSON this crate writes. No report metadata, evidence references, or normalization diagnostics are ever added.
validate_product_input
Deterministic, offline validation of a ProductInput before any SdsRoot fragment is generated from it. Checks only what’s derivable from the raw input itself: CAS format/check-digit, duplicate CAS across components, and per-component/aggregate concentration sanity.