From 7ca937d98c41166dfc4c3bafd8cf3e5cb09deee0 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 10 Jun 2026 09:21:30 -0400 Subject: [PATCH] Fix audit HIGHs/MEDs in build code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ArchiveIndex: guard rawIndex/rawState with doesFileExist so a fresh clone (gitignored data/ JSONs absent) degrades to empty instead of crashing — the behavior the module doc already promised (AUDIT §1.2) - Commonplace: decode YAML via encodeUtf8, not Char8.pack, which truncates codepoints above 0x7F (AUDIT §3.2) - Stats: DayOfWeek is ISO-numbered (Mon=1..Sun=7); dowOf and weekStart assumed Mon=0..Sun=6, clipping every Sunday cell outside the heatmap viewBox and starting weeks on Sunday (AUDIT §3.1) - Site: epistemicEntry now honors the proved/proven confidence sentinel like Contexts.overallScoreField (AUDIT §2.6) - Contexts: affiliationField returns noResult instead of an empty list, so essays without affiliation no longer render an empty meta row (AUDIT §2.7) Verified: full site build passes; proved page gets score=100 in epistemic-meta.json; empty .meta-affiliation gone; heatmap rows y=22..94 all inside the 104-high viewBox. Co-Authored-By: Claude Fable 5 --- build/ArchiveIndex.hs | 22 +++++++++++++++------- build/Commonplace.hs | 8 ++++++-- build/Contexts.hs | 8 +++++++- build/Site.hs | 10 +++++++--- build/Stats.hs | 7 +++++-- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/build/ArchiveIndex.hs b/build/ArchiveIndex.hs index a797f05..d651f44 100644 --- a/build/ArchiveIndex.hs +++ b/build/ArchiveIndex.hs @@ -132,18 +132,26 @@ activeUrls = unsafePerformIO $ do {-# NOINLINE rawIndex #-} rawIndex :: Map Text IdxEntry rawIndex = unsafePerformIO $ do - decoded <- A.eitherDecodeFileStrict' indexPath - let parsed = either (const Map.empty) id decoded - return $ Map.filterWithKey - (\canon _ -> normalizeUrl canon `Set.member` activeUrls) - parsed + exists <- doesFileExist indexPath + if not exists + then return Map.empty + else do + decoded <- A.eitherDecodeFileStrict' indexPath + let parsed = either (const Map.empty) id decoded + return $ Map.filterWithKey + (\canon _ -> normalizeUrl canon `Set.member` activeUrls) + parsed -- | @url -> status@. Absent/malformed file -> empty (every entry 'Live'). {-# NOINLINE rawState #-} rawState :: Map Text ArchiveStatus rawState = unsafePerformIO $ do - decoded <- A.eitherDecodeFileStrict' statePath - return $ either (const Map.empty) (Map.map seStatus) decoded + exists <- doesFileExist statePath + if not exists + then return Map.empty + else do + decoded <- A.eitherDecodeFileStrict' statePath + return $ either (const Map.empty) (Map.map seStatus) decoded -- | @normalised-url -> slug@: the canonical key and every alias from -- @archive-index.json@, each fed through 'normalizeUrl'. Both keys and diff --git a/build/Commonplace.hs b/build/Commonplace.hs index 62ac381..232c929 100644 --- a/build/Commonplace.hs +++ b/build/Commonplace.hs @@ -9,7 +9,8 @@ module Commonplace import Data.Aeson (FromJSON (..), withObject, (.:), (.:?), (.!=)) import Data.List (nub, sortBy) import Data.Ord (comparing, Down (..)) -import qualified Data.ByteString.Char8 as BS +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE import qualified Data.Yaml as Y import Hakyll hiding (escapeHtml, renderTags) import Contexts (siteCtx) @@ -140,7 +141,10 @@ loadCommonplace :: Compiler [CPEntry] loadCommonplace = do rawItem <- load (fromFilePath "data/commonplace.yaml") :: Compiler (Item String) let raw = itemBody rawItem - case Y.decodeEither' (BS.pack raw) of + -- encodeUtf8, not Char8.pack: Char8 truncates each Char to 8 bits, + -- silently corrupting any codepoint above 0x7F (same hazard Now.hs + -- documents — em-dash 0x2014 would become control char 0x14). + case Y.decodeEither' (TE.encodeUtf8 (T.pack raw)) of Left err -> fail ("commonplace.yaml: " ++ show err) Right entries -> return entries diff --git a/build/Contexts.hs b/build/Contexts.hs index b05c680..4b0d5ce 100644 --- a/build/Contexts.hs +++ b/build/Contexts.hs @@ -22,6 +22,7 @@ module Contexts , recentFirstByDisplay , Revision (..) , getRevisions + , isProvedConfidence ) where import Data.Aeson (Value (..)) @@ -86,7 +87,12 @@ affiliationField = listFieldWith "affiliation-links" ctx $ \item -> do let entries = case lookupStringList "affiliation" meta of Just xs -> xs Nothing -> maybe [] (:[]) (lookupString "affiliation" meta) - return $ map (Item (fromFilePath "") . parseEntry) entries + -- noResult, not an empty list: Hakyll's $if$ treats an empty + -- ListField as truthy, so returning [] would render the wrapper + -- markup (an empty .meta-affiliation row) on every page. + if null entries + then noResult "no affiliation" + else return $ map (Item (fromFilePath "") . parseEntry) entries where ctx = field "affiliation-name" (return . fst . itemBody) <> field "affiliation-url" (\i -> let u = snd (itemBody i) diff --git a/build/Site.hs b/build/Site.hs index 33e5217..8c0c2f0 100644 --- a/build/Site.hs +++ b/build/Site.hs @@ -31,7 +31,7 @@ import Commonplace (commonplaceCtx) import Now (nowCtx) import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx, contentKindField, recentFirstByDisplay, - tagLinksFieldExcludingTopSegment) + tagLinksFieldExcludingTopSegment, isProvedConfidence) import qualified Patterns as P import Photography (photographyRules) import Tags (buildAllTags, applyTagRules, sidecarIdentifier, @@ -1011,8 +1011,12 @@ epistemicEntry item = do , grab "stability" meta ] obj = Map.fromList fields - -- Compute overall-score the same way Contexts.overallScoreField does. - obj' = case ( readMaybe =<< lookupString "confidence" meta :: Maybe Int + -- Compute overall-score the same way Contexts.overallScoreField + -- does, including the "proved"/"proven" sentinel -> 100. + confRaw = lookupString "confidence" meta + confInt | isProvedConfidence confRaw = Just 100 + | otherwise = readMaybe =<< confRaw :: Maybe Int + obj' = case ( confInt , readMaybe =<< lookupString "evidence" meta :: Maybe Int ) of (Just conf, Just ev) -> diff --git a/build/Stats.hs b/build/Stats.hs index d99a5e2..e53f025 100644 --- a/build/Stats.hs +++ b/build/Stats.hs @@ -181,8 +181,11 @@ parseDay :: String -> Maybe Day parseDay = parseTimeM True defaultTimeLocale "%Y-%m-%d" -- | First Monday on or before 'day' (start of its ISO week). +-- 'fromEnum' on 'DayOfWeek' is ISO-numbered (Monday=1 .. Sunday=7), +-- so Monday must subtract 0 days, Sunday 6. weekStart :: Day -> Day -weekStart day = addDays (fromIntegral (negate (fromEnum (dayOfWeek day)))) day +weekStart day = + addDays (fromIntegral (negate (fromEnum (dayOfWeek day) - 1))) day -- | Intensity class for the heatmap (hm0 … hm4). heatClass :: Int -> String @@ -297,7 +300,7 @@ renderHeatmap wordsByDay today = nDays = diffDays today startDay + 1 allDays = [addDays i startDay | i <- [0 .. nDays - 1]] weekOf d = fromIntegral (diffDays d startDay `div` 7) :: Int - dowOf d = fromEnum (dayOfWeek d) -- Mon=0..Sun=6 + dowOf d = fromEnum (dayOfWeek d) - 1 -- ISO 1..7 -> Mon=0..Sun=6 svgW = (nWeeks - 1) * step + cellSz svgH = 6 * step + cellSz + hdrH