From 21d02094a4db55dfc85a1980a9d03a6c36573522 Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Mon, 27 Jul 2026 00:40:29 +0200 Subject: [PATCH 1/4] Report geography no dataset declared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nearly every process of some SimaPro databases leaves the Geography field at "Unspecified" — 20537 of 21510 entries in Agribalyse 3.2 — and the only geography left is the code inside the dataset name ("… {FR}", "…//[RER]", "…/CN U"). The parser reads it, which is what makes those databases usable, but the result is a reading of a name and not a declaration; an EcoSpold dataset with no geography at all is filled in with "GLO" just as quietly. Downstream both look like any other geography, so nothing could tell a maintainer how much of a database's geography is actually source data. Activity now carries where its location came from, recorded at parse time because that is the only place the difference still exists, and the quality report counts the entries whose geography was not declared. Info severity: the geography stays usable, and plain absence is still the missing-metadata warning it was. --- CHANGELOG.md | 10 +++++++++ src/API/DatabaseHandlers.hs | 1 + src/API/Resources.hs | 5 ++++- src/API/Types.hs | 1 + src/BrightwayExcel/Parser.hs | 1 + src/Database/Quality.hs | 34 +++++++++++++++++++++++++---- src/EcoSpold/Parser1.hs | 5 ++++- src/EcoSpold/Parser2.hs | 5 ++++- src/ILCD/Parser.hs | 1 + src/SimaPro/Parser.hs | 18 ++++++++++----- src/Types.hs | 26 ++++++++++++++++++++++ test/BM25Spec.hs | 1 + test/BrightwayExcelWriterSpec.hs | 1 + test/CopySpec.hs | 2 ++ test/CrossDBActivityLinkSpec.hs | 1 + test/CrossDBRegionalLCIAFixture.hs | 1 + test/CrossLinkingSpec.hs | 1 + test/DeleteSelectionSpec.hs | 2 ++ test/EcoSpold1Spec.hs | 1 + test/EcoSpold1WriterSpec.hs | 5 +++-- test/EcoSpold2WriterSpec.hs | 6 +++++ test/ExportSpec.hs | 1 + test/FuzzySpec.hs | 1 + test/GapReportSpec.hs | 2 ++ test/ILCDParserSpec.hs | 2 ++ test/ILCDWriterSpec.hs | 2 ++ test/LoaderSpec.hs | 1 + test/MatrixConstructionSpec.hs | 4 ++++ test/MinimalCoverIntegrationSpec.hs | 3 +++ test/QualityReportSpec.hs | 20 +++++++++++++++++ test/RegionalLCIASpec.hs | 2 ++ test/RelinkMappingSpec.hs | 2 ++ test/SetupInfoSpec.hs | 1 + test/SimaProParserSpec.hs | 16 +++++++++++++- test/SimaProWriterSpec.hs | 7 +++++- test/StrictPinSpec.hs | 3 +++ test/WasteTreatmentSignSpec.hs | 1 + 37 files changed, 180 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e72c4e1..3aadd786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ ## [Unreleased] ### Added +- The quality report now says when a geography was never declared. Nearly every + process of some SimaPro databases leaves the `Geography` field at + `Unspecified` — 97% of Agribalyse 3.2 — and the only geography left is the + code inside the dataset name (`… {FR}`, `…//[RER]`, `…/CN U`). VoLCA reads + it, which is what makes those databases usable, but the result is a reading of + a name and not a declaration, and downstream the two are the same text. An + EcoSpold dataset with no geography at all is filled in with `GLO`, likewise + silently. The new `undeclaredGeography` check counts both, one finding per + entry, so a maintainer can see how much of a database's geography is source + data before treating it as such. - A warning now reports factor lines that match a flow but cannot be converted into its unit — a per-kilogram factor against a flow measured in cubic metres, for instance. Refusing such a factor is correct (the diff --git a/src/API/DatabaseHandlers.hs b/src/API/DatabaseHandlers.hs index d99d0638..4bbb86f8 100644 --- a/src/API/DatabaseHandlers.hs +++ b/src/API/DatabaseHandlers.hs @@ -342,6 +342,7 @@ qualityReportToAPI mLimit r = , qraDuplicateActivities = checkToAPI mLimit (Quality.qrDuplicateActivities r) , qraSuspiciousAmounts = checkToAPI mLimit (Quality.qrSuspiciousAmounts r) , qraMissingMetadata = checkToAPI mLimit (Quality.qrMissingMetadata r) + , qraUndeclaredGeography = checkToAPI mLimit (Quality.qrUndeclaredGeography r) , qraFormulaConsistency = checkToAPI mLimit (Quality.qrFormulaConsistency r) , qraTruncatedNameCollisions = checkToAPI mLimit (Quality.qrTruncatedNameCollisions r) , qraMissingPedigree = checkToAPI mLimit (Quality.qrMissingPedigree r) diff --git a/src/API/Resources.hs b/src/API/Resources.hs index 271a227c..a4973bcd 100644 --- a/src/API/Resources.hs +++ b/src/API/Resources.hs @@ -478,7 +478,10 @@ description r = case r of \coproducts carry one), entries duplicated outright \ \(same name, location and reference product), non-finite amounts or a \ \zero reference amount, missing metadata (description, \ - \classification, location, units absent from the registry), stored \ + \classification, location, units absent from the registry), \ + \geography the source never declared — read off the dataset name \ + \(SimaPro writes 'Unspecified' in whole databases) or filled in by the \ + \loader — stored \ \amounts that disagree with the formulas documenting them \ \(mathematicalRelation, checked at parse time), distinct names \ \that merge under SimaPro's 80-character truncation, exchanges \ diff --git a/src/API/Types.hs b/src/API/Types.hs index 1cb48d41..21354f44 100644 --- a/src/API/Types.hs +++ b/src/API/Types.hs @@ -847,6 +847,7 @@ data QualityReportAPI = QualityReportAPI , qraDuplicateActivities :: QualityCheckAPI , qraSuspiciousAmounts :: QualityCheckAPI , qraMissingMetadata :: QualityCheckAPI + , qraUndeclaredGeography :: QualityCheckAPI , qraFormulaConsistency :: QualityCheckAPI , qraTruncatedNameCollisions :: QualityCheckAPI , qraMissingPedigree :: QualityCheckAPI diff --git a/src/BrightwayExcel/Parser.hs b/src/BrightwayExcel/Parser.hs index 464e445f..efb9c1b4 100644 --- a/src/BrightwayExcel/Parser.hs +++ b/src/BrightwayExcel/Parser.hs @@ -328,6 +328,7 @@ rawToActivity cfg ra = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = fromMaybe "" (metaText meta "location") + , activityLocationSource = declaredLocationSource (fromMaybe "" (metaText meta "location")) , activityUnit = refUnitName , exchanges = exchanges' , activityParams = M.empty diff --git a/src/Database/Quality.hs b/src/Database/Quality.hs index f534e972..d5876930 100644 --- a/src/Database/Quality.hs +++ b/src/Database/Quality.hs @@ -10,10 +10,11 @@ amounts that aren't finite, missing metadata, stored amounts that disagree with the formulas documenting them, distinct names that merge under SimaPro's 80-character truncation, exchanges without the pedigree scores their database otherwise carries, reference products nothing in the -database consumes, land transformation whose "to" and "from" areas -don't balance within an activity, oxygen-demand or organic-carbon -measures reported in a physically impossible order, CAS numbers -whose check digit doesn't confirm them, allocation percentages +database consumes, geography no dataset declared (read off the name +or filled in by the loader instead), land transformation whose "to" and +"from" areas don't balance within an activity, oxygen-demand or +organic-carbon measures reported in a physically impossible order, CAS +numbers whose check digit doesn't confirm them, allocation percentages outside the 0-100% range, and amounts too small to have been measured. Every check is a pure scan over a 'SimpleDatabase', so the report is @@ -46,6 +47,7 @@ import Types ( BiosphereFlow (..), Exchange (..), FormulaCheck (..), + LocationSource (..), Severity (..), SimpleDatabase (..), TechRole (..), @@ -102,6 +104,7 @@ data QualityReport = QualityReport , qrDuplicateActivities :: !QualityCheck , qrSuspiciousAmounts :: !QualityCheck , qrMissingMetadata :: !QualityCheck + , qrUndeclaredGeography :: !QualityCheck , qrFormulaConsistency :: !QualityCheck , qrTruncatedNameCollisions :: !QualityCheck , qrMissingPedigree :: !QualityCheck @@ -125,6 +128,7 @@ qualityChecks r = , qrDuplicateActivities r , qrSuspiciousAmounts r , qrMissingMetadata r + , qrUndeclaredGeography r , qrFormulaConsistency r , qrTruncatedNameCollisions r , qrMissingPedigree r @@ -225,6 +229,7 @@ qualityReport dbName db = , qrDuplicateActivities = QualityCheck True (worstFirst duplicateOffenders) , qrSuspiciousAmounts = QualityCheck True (worstFirst amountOffenders) , qrMissingMetadata = QualityCheck True (worstFirst metadataOffenders) + , qrUndeclaredGeography = QualityCheck True (worstFirst geographyOffenders) , qrFormulaConsistency = QualityCheck formulaApplicable (worstFirst formulaOffenders) , qrTruncatedNameCollisions = QualityCheck True (worstFirst truncationOffenders) , qrMissingPedigree = QualityCheck pedigreeApplicable (worstFirst pedigreeOffenders) @@ -435,6 +440,27 @@ qualityReport dbName db = | (key, act) <- entries ] + -- A geography the source declares is a fact about the dataset; one read off + -- the dataset name is this loader's reading of a string, and one supplied by + -- the loader is neither. SimaPro writes "Unspecified" in the Geography field + -- of entire databases, so their geography survives only in names like + -- "… {FR}"; an EcoSpold dataset with no geography is filled in with "GLO". + -- Both are usable and neither was declared, and downstream the two are the + -- same text — hence Info, and hence a count: a maker reads it before + -- treating the geography as source data. + geographyOffenders = + [ offender InfoSev key act Nothing detail + | (key, act) <- entries + , Just detail <- [undeclaredGeography (activityLocationSource act) (activityLocation act)] + ] + undeclaredGeography src loc = case src of + LocationDeclared -> Nothing + LocationInferredFromName -> + Just ("geography \"" <> loc <> "\" was read from the dataset name, not declared by the source") + LocationUnspecified + | T.null (T.strip loc) -> Just "the source declares no geography" + | otherwise -> Just ("the source declares no geography; \"" <> loc <> "\" stands in for it") + -- Names that only differ beyond SimaPro's cap become one name on export — -- the over-grouping the allocation check above tolerates at parse time -- turns into data loss on the way out. One finding per distinct name, each diff --git a/src/EcoSpold/Parser1.hs b/src/EcoSpold/Parser1.hs index 0737ebae..b32bbaf1 100644 --- a/src/EcoSpold/Parser1.hs +++ b/src/EcoSpold/Parser1.hs @@ -453,6 +453,9 @@ buildResult :: ParseState -> Either String (Activity, [TechnosphereFlow], [Biosp buildResult st = let name = fromMaybe "Unknown Activity" (psActivityName st) location = fromMaybe "GLO" (psLocation st) + -- "GLO" above is this loader's stand-in, not something the dataset said: + -- a dataset without a geography is recorded as declaring none. + locationSource = maybe LocationUnspecified declaredLocationSource (psLocation st) refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st) description = reverse (psDescription st) classifications = @@ -460,7 +463,7 @@ buildResult st = filter (not . T.null . snd) [("Category", psActivityCategory st), ("SubCategory", psActivitySubCategory st)] - activity = Activity name description M.empty classifications location refUnit (reverse $ psExchanges st) M.empty M.empty Nothing Nothing Nothing Nothing Nothing + activity = Activity name description M.empty classifications location locationSource refUnit (reverse $ psExchanges st) M.empty M.empty Nothing Nothing Nothing Nothing Nothing pack act = ( act , reverse (psTechFlows st) diff --git a/src/EcoSpold/Parser2.hs b/src/EcoSpold/Parser2.hs index 3b016ee8..712b486d 100644 --- a/src/EcoSpold/Parser2.hs +++ b/src/EcoSpold/Parser2.hs @@ -776,13 +776,16 @@ parseWithXeno xmlContent processId = do buildResult st _pid = let name = fromMaybe "Unknown Activity" (psActivityName st) location = fromMaybe "GLO" (psLocation st) + -- "GLO" above is this loader's stand-in, not something the dataset + -- said: a dataset without a geography declares none. + locationSource = maybe LocationUnspecified declaredLocationSource (psLocation st) description = reverse (psDescription st) -- Reverse to get correct order refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st) nativeType = ecoSpoldNativeType (psActivityType st) (psSpecialActivityType st) pairs = reverse (psExchanges st) formulaCheck = checkFormulas (psParams st) pairs -- Apply cutoff strategy to exchanges - activity = Activity name description M.empty (psClassifications st) location refUnit (map fst pairs) (psParams st) (psParamExprs st) Nothing Nothing nativeType Nothing formulaCheck + activity = Activity name description M.empty (psClassifications st) location locationSource refUnit (map fst pairs) (psParams st) (psParamExprs st) Nothing Nothing nativeType Nothing formulaCheck techs = reverse (psTechFlows st) bios = reverse (psBioFlows st) wastes = reverse (psWasteFlows st) diff --git a/src/ILCD/Parser.hs b/src/ILCD/Parser.hs index 3ec7d4a9..7a762719 100644 --- a/src/ILCD/Parser.hs +++ b/src/ILCD/Parser.hs @@ -560,6 +560,7 @@ buildActivity flowInfoMap techFlowDB bioFlowDB wasteFlowDB unitDB p = , activitySynonyms = M.empty , activityClassification = iprClassifications p , activityLocation = iprLocation p + , activityLocationSource = declaredLocationSource (iprLocation p) , activityUnit = refUnit , exchanges = map (mkExchange (iprRefFlowIdx p)) (iprExchanges p) , activityParams = M.empty diff --git a/src/SimaPro/Parser.hs b/src/SimaPro/Parser.hs index 581c22f1..6b587020 100644 --- a/src/SimaPro/Parser.hs +++ b/src/SimaPro/Parser.hs @@ -19,7 +19,7 @@ module SimaPro.Parser ( normalizeSimaProCompartment, extractLocation, Located (..), - LocationSource (..), + NameReading (..), -- * Shared utilities (used by Method.ParserSimaPro) defaultConfig, @@ -791,14 +791,16 @@ generateUnitUUID unitName = -- Conversion to volca Types -- ============================================================================ -{- | How a location was read out of a SimaPro name. +{- | How a location was read out of a SimaPro name. (Not to be confused with +'Types.LocationSource', which says whether an activity's location was declared +at all — every reading here is an inferred one by that measure.) A tag is a location the producer wrote down. A slash suffix is a guess made by cutting the name, and names end in a slash for reasons that have nothing to do with geography — in "Already packed - PP/PE", PE is a plastic, not Peru. So the ordering here says a tag beats a suffix, wherever each of the two sits. -} -data LocationSource +data NameReading = Tagged | SlashSuffix deriving (Eq, Ord, Show) @@ -810,7 +812,7 @@ data Located = Located informational, shortened for a suffix, which is part of the name. -} , locatedLocation :: Text - , locatedSource :: LocationSource + , locatedSource :: NameReading } deriving (Eq, Show) @@ -931,7 +933,8 @@ processBlockToActivity unitCfg GlobalParams{..} ProcessBlock{..} = processReading = extractLocation pbName -- The Geography field, when the producer filled it in. It outranks anything - -- read out of a name, being the one place meant to hold a location. + -- read out of a name, being the one place meant to hold a location — a name + -- reading is recorded as such below, for the quality report. statedLocation = mfilter ((/= "unspecified") . T.toLower) (nonEmptyText pbLocation) {- Trimmed Process name (without curly-brace location tag). Empty when the @@ -1013,6 +1016,10 @@ processBlockToActivity unitCfg GlobalParams{..} ProcessBlock{..} = . sortOn locatedSource $ catMaybes readings effectiveLoc = fromMaybe readLocation statedLocation + effectiveLocSource + | isJust statedLocation = LocationDeclared + | T.null effectiveLoc = LocationUnspecified + | otherwise = LocationInferredFromName allocFormula = mfilter (not . isNumericFormula) (nonEmptyText (prAllocRaw prod)) activity = Activity @@ -1027,6 +1034,7 @@ processBlockToActivity unitCfg GlobalParams{..} ProcessBlock{..} = , ("Category", prCategory prod) ] , activityLocation = effectiveLoc + , activityLocationSource = effectiveLocSource , activityUnit = effUnitName , exchanges = productExchange : map (scaleExchange allocFraction) sharedExchanges , activityParams = env diff --git a/src/Types.hs b/src/Types.hs index ab919af3..95373963 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -483,6 +483,31 @@ data FormulaCheck = FormulaCheck } deriving (Generic, NFData, Store) +{- | Where an activity's 'activityLocation' came from. A format with a geography +field publishes one ('LocationDeclared'); when it is blank or says +@Unspecified@, the parser falls back to the code embedded in the dataset name +(@"… {FR}"@, @"…//[RER]"@, @"…/CN U"@), which is a reading of the name and not +a declaration ('LocationInferredFromName'). 'LocationUnspecified' when neither +yielded anything, and 'activityLocation' is empty. + +Recorded at parse time because only the parser can still tell the two apart: +downstream, a declared @FR@ and a guessed @FR@ are the same text. The quality +report is what surfaces the difference. +-} +data LocationSource + = LocationDeclared + | LocationInferredFromName + | LocationUnspecified + deriving (Show, Eq, Generic, NFData, Store) + +{- | The source of a geography a format declares directly: the field itself when +it carries something, nothing to infer from when it doesn't. +-} +declaredLocationSource :: Text -> LocationSource +declaredLocationSource loc + | T.null (T.strip loc) = LocationUnspecified + | otherwise = LocationDeclared + {- | Base LCA activity Note: ProcessId is the index in dbActivities vector, UUIDs stored in dbProcessIdTable -} @@ -492,6 +517,7 @@ data Activity = Activity , activitySynonyms :: !(M.Map Text (S.Set Text)) -- Synonyms by language, same structure as flows , activityClassification :: !(M.Map Text Text) -- Classifications (ISIC, CPC, etc.) , activityLocation :: !Text -- Location code (e.g. FR, RER) + , activityLocationSource :: !LocationSource -- Whether the source declared that location or the parser read it off the dataset name , activityUnit :: !Text -- Reference unit , exchanges :: ![Exchange] -- List of exchanges , activityParams :: !(M.Map Text Double) -- Resolved dataset parameter values (SimaPro parameters, EcoSpold2 variables) diff --git a/test/BM25Spec.hs b/test/BM25Spec.hs index 8248d446..98170258 100644 --- a/test/BM25Spec.hs +++ b/test/BM25Spec.hs @@ -29,6 +29,7 @@ mkActivity name loc xs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = xs , activityParams = M.empty diff --git a/test/BrightwayExcelWriterSpec.hs b/test/BrightwayExcelWriterSpec.hs index 3f468ca9..3586a2d5 100644 --- a/test/BrightwayExcelWriterSpec.hs +++ b/test/BrightwayExcelWriterSpec.hs @@ -261,6 +261,7 @@ elec = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kilowatt hour" , exchanges = [prodExch, gasExch, co2Exch] , activityParams = M.empty diff --git a/test/CopySpec.hs b/test/CopySpec.hs index 8e9b40bd..7a9c7399 100644 --- a/test/CopySpec.hs +++ b/test/CopySpec.hs @@ -44,6 +44,7 @@ import Types ( Database (..), Exchange (..), GeographyPolicy (..), + LocationSource (..), SparseTriple (..), TechRole (..), TechnosphereFlow (..), @@ -274,6 +275,7 @@ supplierDB offset products = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [refOut] , activityParams = M.empty diff --git a/test/CrossDBActivityLinkSpec.hs b/test/CrossDBActivityLinkSpec.hs index 8b517ee3..7390a5c2 100644 --- a/test/CrossDBActivityLinkSpec.hs +++ b/test/CrossDBActivityLinkSpec.hs @@ -69,6 +69,7 @@ mkActivity name loc exs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty diff --git a/test/CrossDBRegionalLCIAFixture.hs b/test/CrossDBRegionalLCIAFixture.hs index 123afe85..6f66848c 100644 --- a/test/CrossDBRegionalLCIAFixture.hs +++ b/test/CrossDBRegionalLCIAFixture.hs @@ -103,6 +103,7 @@ mkActivity _ loc = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [] , activityParams = M.empty diff --git a/test/CrossLinkingSpec.hs b/test/CrossLinkingSpec.hs index b082746e..229d4518 100644 --- a/test/CrossLinkingSpec.hs +++ b/test/CrossLinkingSpec.hs @@ -445,6 +445,7 @@ mkActivityAt loc = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [] , activityParams = M.empty diff --git a/test/DeleteSelectionSpec.hs b/test/DeleteSelectionSpec.hs index fd75b2b6..c0692030 100644 --- a/test/DeleteSelectionSpec.hs +++ b/test/DeleteSelectionSpec.hs @@ -51,6 +51,7 @@ import Types ( Database (..), Exchange (..), GeographyPolicy (..), + LocationSource (..), ProcessId, SparseTriple (..), TechRole (..), @@ -501,6 +502,7 @@ mkActivity name loc classif exs = , activitySynonyms = M.empty , activityClassification = classif , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty diff --git a/test/EcoSpold1Spec.hs b/test/EcoSpold1Spec.hs index 5af951c6..2177b5fa 100644 --- a/test/EcoSpold1Spec.hs +++ b/test/EcoSpold1Spec.hs @@ -25,6 +25,7 @@ emptyActivity = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [] , activityParams = M.empty diff --git a/test/EcoSpold1WriterSpec.hs b/test/EcoSpold1WriterSpec.hs index f7ced57d..cd690009 100644 --- a/test/EcoSpold1WriterSpec.hs +++ b/test/EcoSpold1WriterSpec.hs @@ -138,7 +138,7 @@ soloDb name prodU extra techs bios wastes = } where ref = TechnosphereExchange prodU 1.0 kgUnit ReferenceProduct UUID.nil Nothing "" Nothing Nothing - act = Activity name [] M.empty M.empty "GLO" "kg" (ref : extra) M.empty M.empty Nothing Nothing Nothing Nothing Nothing + act = Activity name [] M.empty M.empty "GLO" LocationDeclared "kg" (ref : extra) M.empty M.empty Nothing Nothing Nothing Nothing Nothing -- | Empty database: no activities, no flows. emptyDb :: SimpleDatabase @@ -166,7 +166,7 @@ linkedDb link = where supU = supplierLink conU = read "33333333-0000-4000-8000-000000000001" - mkAct nm prodU exs = Activity nm [] M.empty M.empty "GLO" "kg" (refOf prodU : exs) M.empty M.empty Nothing Nothing Nothing Nothing Nothing + mkAct nm prodU exs = Activity nm [] M.empty M.empty "GLO" LocationDeclared "kg" (refOf prodU : exs) M.empty M.empty Nothing Nothing Nothing Nothing Nothing refOf prodU = TechnosphereExchange prodU 1.0 kgUnit ReferenceProduct UUID.nil Nothing "" Nothing Nothing supplier = mkAct "aaa supplier" supU [] -- The consumer's input consumes the supplier's product and links to it. @@ -555,6 +555,7 @@ spec = do M.empty M.empty "GLO" + LocationDeclared "kg" [ref] M.empty diff --git a/test/EcoSpold2WriterSpec.hs b/test/EcoSpold2WriterSpec.hs index 0be33beb..9b252147 100644 --- a/test/EcoSpold2WriterSpec.hs +++ b/test/EcoSpold2WriterSpec.hs @@ -109,6 +109,7 @@ fixtureSimple = M.empty (M.fromList [("ISIC rev.4 ecoinvent", "2011:Manufacture of food products"), ("CPC", "2399: Other food products n.e.c.")]) "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodA 1.0 unitKg ReferenceProduct UUID.nil Nothing "" Nothing Nothing , TechnosphereExchange prodB 2.0 unitMJ Input actB Nothing "" (Just "energy input") Nothing @@ -128,6 +129,7 @@ fixtureSimple = M.empty M.empty "GLO" + LocationDeclared "MJ" [ TechnosphereExchange prodB 1.0 unitMJ ReferenceProduct UUID.nil Nothing "" Nothing Nothing , BiosphereExchange land 0.1 unitM2a Resource "" Nothing Nothing @@ -166,6 +168,7 @@ fixtureDupBio = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodA 1.0 unitKg ReferenceProduct UUID.nil Nothing "" Nothing Nothing , BiosphereExchange co2 0.5 unitKg Emission "" Nothing Nothing @@ -200,6 +203,7 @@ fixtureWithExchange ex = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodA 1.0 unitKg ReferenceProduct UUID.nil Nothing "" Nothing Nothing , ex @@ -237,6 +241,7 @@ fixtureWithBioSynonyms = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodA 1.0 unitKg ReferenceProduct UUID.nil Nothing "" Nothing Nothing , BiosphereExchange co2 0.5 unitKg Emission "" Nothing Nothing @@ -283,6 +288,7 @@ fixtureWasteCoproduct = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodA 1.0 unitKg ReferenceProduct UUID.nil Nothing "" Nothing Nothing , TechnosphereExchange coprodU 0.4 unitKg Coproduct UUID.nil Nothing "" Nothing Nothing diff --git a/test/ExportSpec.hs b/test/ExportSpec.hs index 7284c554..ffc3fdd0 100644 --- a/test/ExportSpec.hs +++ b/test/ExportSpec.hs @@ -71,6 +71,7 @@ buildFixture comp = do M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodU 1.0 unitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing , BiosphereExchange co2U 0.5 unitU Emission "" Nothing Nothing diff --git a/test/FuzzySpec.hs b/test/FuzzySpec.hs index d040298b..4b75e32a 100644 --- a/test/FuzzySpec.hs +++ b/test/FuzzySpec.hs @@ -20,6 +20,7 @@ mkActivity name xs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "FR" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = xs , activityParams = M.empty diff --git a/test/GapReportSpec.hs b/test/GapReportSpec.hs index cf61d42f..2b06ad71 100644 --- a/test/GapReportSpec.hs +++ b/test/GapReportSpec.hs @@ -42,6 +42,7 @@ import Types ( Exchange (..), GeographyPolicy (..), LinkBlocker (..), + LocationSource (..), SimpleDatabase (..), TechRole (..), TechnosphereFlow (..), @@ -84,6 +85,7 @@ mkActivity name exs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "FR" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty diff --git a/test/ILCDParserSpec.hs b/test/ILCDParserSpec.hs index e3fd2648..cd6cb08a 100644 --- a/test/ILCDParserSpec.hs +++ b/test/ILCDParserSpec.hs @@ -32,6 +32,7 @@ activityWithRefExchange fid = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [ TechnosphereExchange @@ -64,6 +65,7 @@ activityWithInputExchange fid = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , activityAllocationPercent = Nothing , activityAllocationFormula = Nothing diff --git a/test/ILCDWriterSpec.hs b/test/ILCDWriterSpec.hs index 5f52676f..f9ea1f60 100644 --- a/test/ILCDWriterSpec.hs +++ b/test/ILCDWriterSpec.hs @@ -524,6 +524,7 @@ multiOutputDb = M.empty M.empty "GLO" + LocationDeclared "kg" [TechnosphereExchange prod 1.0 moUnitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing] M.empty @@ -595,6 +596,7 @@ oneActivityDb bios exs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty diff --git a/test/LoaderSpec.hs b/test/LoaderSpec.hs index d9ffc8ed..b9133508 100644 --- a/test/LoaderSpec.hs +++ b/test/LoaderSpec.hs @@ -45,6 +45,7 @@ minimalActivity name loc exs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty diff --git a/test/MatrixConstructionSpec.hs b/test/MatrixConstructionSpec.hs index c1abd84d..859136a4 100644 --- a/test/MatrixConstructionSpec.hs +++ b/test/MatrixConstructionSpec.hs @@ -163,6 +163,7 @@ spec = do , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "j" , exchanges = [refExchange, bioExchange] , activityParams = M.empty @@ -236,6 +237,7 @@ spec = do , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "j" , exchanges = [refExchange, zeroBio] , activityParams = M.empty @@ -304,6 +306,7 @@ spec = do , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [tRef, tCO2] , activityParams = M.empty @@ -345,6 +348,7 @@ spec = do , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [pRef, pWaste] , activityParams = M.empty diff --git a/test/MinimalCoverIntegrationSpec.hs b/test/MinimalCoverIntegrationSpec.hs index a7d57603..656cbc07 100644 --- a/test/MinimalCoverIntegrationSpec.hs +++ b/test/MinimalCoverIntegrationSpec.hs @@ -29,6 +29,7 @@ import Types ( CrossDBLinkingStats (..), Exchange (..), GeographyPolicy (..), + LocationSource (..), SimpleDatabase (..), TechRole (..), TechnosphereFlow (..), @@ -139,6 +140,7 @@ supplierDB offset = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [refExchange] , activityParams = M.empty @@ -217,6 +219,7 @@ consumerDB offset n = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [refOut, unlinkedInput] , activityParams = M.empty diff --git a/test/QualityReportSpec.hs b/test/QualityReportSpec.hs index 1c075555..a181e00f 100644 --- a/test/QualityReportSpec.hs +++ b/test/QualityReportSpec.hs @@ -31,6 +31,7 @@ import Types ( BiosphereFlow (..), Exchange (..), FormulaCheck (..), + LocationSource (..), NativeProcessId (..), Severity (..), SimpleDatabase (..), @@ -85,6 +86,7 @@ mkActivity name exs = , activitySynonyms = M.empty , activityClassification = M.singleton "ISIC" "1071" , activityLocation = "FR" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty @@ -368,6 +370,24 @@ spec = do it "passes a fully documented activity" $ qcOffenders (qrMissingMetadata (reportOf (mkActivity "bread" [reference breadFlow]))) `shouldBe` [] + describe "undeclared geography check" $ do + it "flags a geography read off the dataset name as info" $ do + let act = (mkActivity "bread {FR}" [reference breadFlow]){activityLocationSource = LocationInferredFromName} + check = qrUndeclaredGeography (reportOf act) + details check `shouldBe` ["geography \"FR\" was read from the dataset name, not declared by the source"] + severities check `shouldBe` [InfoSev] + + it "flags a stand-in geography the source never declared" $ + details (qrUndeclaredGeography (reportOf (mkActivity "bread" [reference breadFlow]){activityLocation = "GLO", activityLocationSource = LocationUnspecified})) + `shouldBe` ["the source declares no geography; \"GLO\" stands in for it"] + + it "flags an entry left with no geography at all" $ + details (qrUndeclaredGeography (reportOf (mkActivity "bread" [reference breadFlow]){activityLocation = "", activityLocationSource = LocationUnspecified})) + `shouldBe` ["the source declares no geography"] + + it "passes a geography the source declared" $ + qcOffenders (qrUndeclaredGeography (reportOf (mkActivity "bread" [reference breadFlow]))) `shouldBe` [] + describe "formula consistency check" $ do it "flags an activity whose formulas diverge, with counts and the example" $ do let fc = FormulaCheck{fcEvaluated = 30, fcDivergent = 12, fcUnevaluable = 3, fcExample = Just "\"a*2\" evaluates to 5.0 but the dataset stores 4.0"} diff --git a/test/RegionalLCIASpec.hs b/test/RegionalLCIASpec.hs index 2f016187..1a6d78bc 100644 --- a/test/RegionalLCIASpec.hs +++ b/test/RegionalLCIASpec.hs @@ -29,6 +29,7 @@ import Types ( BiosphereFlow (..), Database (..), Indexes (..), + LocationSource (..), SparseTriple (..), Unit (..), emptyProductIndex, @@ -85,6 +86,7 @@ mkActivity loc = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [] , activityParams = M.empty diff --git a/test/RelinkMappingSpec.hs b/test/RelinkMappingSpec.hs index 2aa03c11..8fd922fc 100644 --- a/test/RelinkMappingSpec.hs +++ b/test/RelinkMappingSpec.hs @@ -47,6 +47,7 @@ import Types ( Exchange (..), GeographyPolicy (..), LinkBlocker (..), + LocationSource (..), SimpleDatabase (..), TechRole (..), TechnosphereFlow (..), @@ -70,6 +71,7 @@ targetDB = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = loc + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [ TechnosphereExchange diff --git a/test/SetupInfoSpec.hs b/test/SetupInfoSpec.hs index 30eb3ad6..6f9a7150 100644 --- a/test/SetupInfoSpec.hs +++ b/test/SetupInfoSpec.hs @@ -58,6 +58,7 @@ minimalActivity name exs = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = exs , activityParams = M.empty diff --git a/test/SimaProParserSpec.hs b/test/SimaProParserSpec.hs index bbdb44c9..edaa96e8 100644 --- a/test/SimaProParserSpec.hs +++ b/test/SimaProParserSpec.hs @@ -13,7 +13,7 @@ import Expr (evaluate, normalizeExpr) import SimaPro.Parser ( BioExchangeRow (..), Located (..), - LocationSource (..), + NameReading (..), ProductRow (..), TechExchangeRow (..), defaultConfig, @@ -37,6 +37,7 @@ import Types ( Activity (..), BiosphereFlow, Exchange (..), + LocationSource (..), NativeActivityType (..), NativeProcessId (..), Pedigree (..), @@ -1034,6 +1035,18 @@ spec = do (activities, _, _, _, _) <- parseNamedCSV "Widget {FR} U" [] activityLocation (head activities) `shouldBe` "FR" + -- The geography is usable either way, but only the parser still knows + -- which of the two it read, so it records the difference for the + -- quality report: a name is a reading, a Geography field a declaration. + it "records a location read off the name as inferred, not declared" $ do + (activities, _, _, _, _) <- parseNamedCSV "Widget {FR} U" [] + activityLocationSource (head activities) `shouldBe` LocationInferredFromName + + it "records no location source when neither the field nor the name carries one" $ do + (activities, _, _, _, _) <- parseNamedCSV "Widget U" [] + activityLocation (head activities) `shouldBe` "" + activityLocationSource (head activities) `shouldBe` LocationUnspecified + it "parses location from process name //[XX] pattern (ecoinvent 3.9.1 SimaPro export)" $ do (activities, _, _, _, _) <- parseNamedCSV "mango//[BR] mango production" [] activityLocation (head activities) `shouldBe` "BR" @@ -1042,6 +1055,7 @@ spec = do (activities, _, _, _, _) <- parseTestCSV let a = head activities activityLocation a `shouldBe` "GLO" + activityLocationSource a `shouldBe` LocationDeclared -- SimaPro cuts the "Process name" field at 80 characters, which takes -- the "{FR}" tag off the end of a long name and leaves only a slash the diff --git a/test/SimaProWriterSpec.hs b/test/SimaProWriterSpec.hs index 8d92e359..b850a354 100644 --- a/test/SimaProWriterSpec.hs +++ b/test/SimaProWriterSpec.hs @@ -592,6 +592,7 @@ emissionDb comp = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodU 1.0 unitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing , BiosphereExchange bioU 0.5 unitU Emission "" Nothing Nothing @@ -639,6 +640,7 @@ allocationDb = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodU 1.0 unitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing , TechnosphereExchange matU 10.0 unitU Input UUID.nil Nothing "" Nothing Nothing @@ -681,6 +683,7 @@ zeroAllocationDb = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodU 1.0 unitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing , TechnosphereExchange matU 0.0 unitU Input UUID.nil Nothing "" Nothing Nothing @@ -726,6 +729,7 @@ commentDb ped cmt = M.empty M.empty "GLO" + LocationDeclared "kg" [ TechnosphereExchange prodU 1.0 unitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing , TechnosphereExchange matU 2.0 unitU Input UUID.nil Nothing "" cmt ped @@ -762,6 +766,7 @@ namedDb name = M.empty M.empty "GLO" + LocationDeclared "kg" [TechnosphereExchange prodU 1.0 unitU ReferenceProduct UUID.nil Nothing "" Nothing Nothing] M.empty @@ -824,4 +829,4 @@ guardDb alloc ntype exs = } where act = - Activity "guard maker" [] M.empty M.empty "GLO" "kg" exs M.empty M.empty alloc Nothing ntype Nothing Nothing + Activity "guard maker" [] M.empty M.empty "GLO" LocationDeclared "kg" exs M.empty M.empty alloc Nothing ntype Nothing Nothing diff --git a/test/StrictPinSpec.hs b/test/StrictPinSpec.hs index fa68ad3c..c8231d3b 100644 --- a/test/StrictPinSpec.hs +++ b/test/StrictPinSpec.hs @@ -47,6 +47,7 @@ import Types ( Database (..), Exchange (..), GeographyPolicy (..), + LocationSource (..), SparseTriple (..), TechRole (..), TechnosphereFlow (..), @@ -244,6 +245,7 @@ supplierDB offset products = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [refOut] , activityParams = M.empty @@ -306,6 +308,7 @@ consumerDB offset products = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [refOut, unlinkedInput] , activityParams = M.empty diff --git a/test/WasteTreatmentSignSpec.hs b/test/WasteTreatmentSignSpec.hs index 6ec0c9ac..18ce3dc7 100644 --- a/test/WasteTreatmentSignSpec.hs +++ b/test/WasteTreatmentSignSpec.hs @@ -60,6 +60,7 @@ emptyActivity = , activitySynonyms = M.empty , activityClassification = M.empty , activityLocation = "GLO" + , activityLocationSource = LocationDeclared , activityUnit = "kg" , exchanges = [] , activityParams = M.empty From e08eabc8c2e1aed0d7f7043383dd3074d34138ec Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Mon, 27 Jul 2026 10:38:22 +0200 Subject: [PATCH 2/4] Bump the matrix cache schema salt for activityLocationSource The Activity record gained a field in the middle of its Store layout, which is positional: an old cache decoded by this build would misread every field after it, at best failing mid-decode with a misleading "corrupted" message instead of a clean schema-mismatch rebuild. Same situation as salts 9 and 10, same remedy. --- src/Database/Loader.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Database/Loader.hs b/src/Database/Loader.hs index 7409188a..e767c274 100644 --- a/src/Database/Loader.hs +++ b/src/Database/Loader.hs @@ -216,6 +216,9 @@ History of manual bumps: when that name carries one, instead of always being minted from name and location. Old caches key the same dataset under the minted UUID, so a mixed pair would compare as two disjoint databases. +- 13: Activity record gained activityLocationSource (declared, read off the + dataset name, or neither). Old caches miss the field; the Store layout + is positional, so decoding them would misread every field after it. The signature is stored inside the cache file and checked on load. If it doesn't match, the cache is automatically invalidated and rebuilt. @@ -223,7 +226,7 @@ If it doesn't match, the cache is automatically invalidated and rebuilt. schemaSignature :: Word64 schemaSignature = let Fingerprint hi lo = typeRepFingerprint (typeRep (Proxy :: Proxy Database)) - in hi `xor` lo `xor` 12 + in hi `xor` lo `xor` 13 {- | Helper function to parse UUID from Text with deterministic UUID generation fallback. From 461ecb5a255623753803529803d708a6150082b9 Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Mon, 27 Jul 2026 10:38:22 +0200 Subject: [PATCH 3/4] Read the Brightway location metadata once The same lookup was written twice, once for the location and once for its source. --- src/BrightwayExcel/Parser.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/BrightwayExcel/Parser.hs b/src/BrightwayExcel/Parser.hs index efb9c1b4..b2be536a 100644 --- a/src/BrightwayExcel/Parser.hs +++ b/src/BrightwayExcel/Parser.hs @@ -321,14 +321,15 @@ rawToActivity cfg ra = | not (any exchangeIsReference exchanges') ] + location = fromMaybe "" (metaText meta "location") activity = Activity { activityName = raName ra , activityDescription = maybeToList (metaText meta "comment") , activitySynonyms = M.empty , activityClassification = M.empty - , activityLocation = fromMaybe "" (metaText meta "location") - , activityLocationSource = declaredLocationSource (fromMaybe "" (metaText meta "location")) + , activityLocation = location + , activityLocationSource = declaredLocationSource location , activityUnit = refUnitName , exchanges = exchanges' , activityParams = M.empty From db632299051ed05f59e39e22f5f37f6cc071fed5 Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Mon, 27 Jul 2026 10:38:22 +0200 Subject: [PATCH 4/4] Cover the EcoSpold2 GLO stand-in at the parser level The "stands in for it" quality finding was only exercised by building the fixture by hand in QualityReportSpec; nothing proved a real parse of a geography-less dataset produces the GLO stand-in with LocationUnspecified. Two tests pin both sides: a declared geography and the stand-in. --- test/EcoSpold2Spec.hs | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/EcoSpold2Spec.hs b/test/EcoSpold2Spec.hs index a6829b2b..ff9763da 100644 --- a/test/EcoSpold2Spec.hs +++ b/test/EcoSpold2Spec.hs @@ -237,6 +237,31 @@ spec = describe "per-exchange comments" $ do any ("mathematicalRelation" `isInfixOf`) newTexts `shouldBe` False + -- A dataset without a element gets "GLO" as a stand-in; the + -- source declared nothing, and the record keeps that distinction for the + -- quality report. + describe "geography stand-in" $ do + let runOnBytes bytes = withSystemTempDirectory "es2-geo" $ \dir -> do + let path = dir "12345678-1234-5678-9abc-123456789001_12345678-1234-5678-9abc-123456789002.spold" + BS.writeFile path bytes + streamParseActivityAndFlowsFromFile path + + it "records a geography the dataset declares as declared" $ do + result <- runOnBytes (activityTypeFixtureXml "1" Nothing) + case result of + Left err -> expectationFailure $ "Parse failed: " ++ err + Right (act, _, _, _, _) -> do + activityLocation act `shouldBe` "TEST" + activityLocationSource act `shouldBe` LocationDeclared + + it "fills in GLO for a dataset with no geography, recorded as undeclared" $ do + result <- runOnBytes noGeographyXml + case result of + Left err -> expectationFailure $ "Parse failed: " ++ err + Right (act, _, _, _, _) -> do + activityLocation act `shouldBe` "GLO" + activityLocationSource act `shouldBe` LocationUnspecified + {- | Synthetic ecospold2 dataset parameterised on the activityType code and optional specialActivityType code. One reference output, no other exchanges. -} @@ -270,6 +295,28 @@ activityTypeFixtureXml actType mSpec = \ \n\ \\n" +-- | Same dataset as 'activityTypeFixtureXml' but with no @\@ element. +noGeographyXml :: BS.ByteString +noGeographyXml = + "\n\ + \\n\ + \ \n\ + \ \n\ + \ \n\ + \ geography test activity\n\ + \ \n\ + \ \n\ + \ \n\ + \ \n\ + \ geography test product\n\ + \ kg\n\ + \ 0\n\ + \ \n\ + \ \n\ + \ \n\ + \\n" + withWastePatternsFixture :: ((Activity, [TechnosphereFlow], [BiosphereFlow], [WasteFlow], [Unit]) -> IO ()) -> IO () withWastePatternsFixture k = withSystemTempDirectory "es2-waste-spec" $ \dir -> do let path = dir "12345678-1234-5678-9abc-123456789001_12345678-1234-5678-9abc-123456789002.spold"