packages feed

shikumi-okf-0.1.0.0: src/Shikumi/Okf/Render.hs

-- | Render a documented program into the Markdown body of its OKF concept.
--
-- The body has up to four parts: a heading, the author's prose description, a
-- declared-interface section (free-text input/output summaries), and a structure
-- section derived from the program value itself. The structure section is
-- best-effort: a typed @Predict@ pipeline yields a constructor tree and the
-- input/output field names of each model call, whereas an opaque @Embed@ program
-- (the shape an agent runtime produces) has no inspectable internals, so we say so
-- plainly rather than emit an empty section. The declared-interface fields are
-- what carry the documentary weight in that opaque case.
module Shikumi.Okf.Render
  ( renderProgramBody,
  )
where

import Data.List (intercalate)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Shikumi.Okf.Types (ProgramDoc (..), SomeProgram (..))
import Shikumi.Program
  ( NodeFields (..),
    ProgramShape (..),
    nodeFieldsIndexed,
    nodeInstructionsIndexed,
    programShape,
  )

-- | The full Markdown body for one program, with a trailing newline.
renderProgramBody :: ProgramDoc -> Text
renderProgramBody doc =
  T.unlines (intercalate [""] sections)
  where
    sections :: [[Text]]
    sections =
      filter
        (not . null)
        [ ["# " <> fromMaybe (name doc) (title doc)],
          maybe [] pure (description doc),
          interfaceSection doc,
          structureSection (program doc)
        ]

-- | The declared-interface section, present only when the author supplied at
-- least one of the free-text input/output summaries.
interfaceSection :: ProgramDoc -> [Text]
interfaceSection doc =
  case (declaredInputs doc, declaredOutputs doc) of
    (Nothing, Nothing) -> []
    (mi, mo) ->
      ["## Interface", ""]
        <> maybe [] (\t -> ["- Input: " <> t]) mi
        <> maybe [] (\t -> ["- Output: " <> t]) mo

-- | The structure section, derived from the program's run-free shape. A
-- metadata-only doc ('Nothing') has no program to reflect, so it says so.
structureSection :: Maybe SomeProgram -> [Text]
structureSection Nothing =
  ["## Structure", "", "Documented from metadata only; program structure is not available."]
structureSection (Just (SomeProgram p)) =
  ["## Structure", ""] <> bodyLines
  where
    bodyLines = case programShape p of
      ShapeEmbed ->
        ["Opaque embedded program (no inspectable internal structure)."]
      sh ->
        renderShapeTree 0 sh
          <> modelCalls (zip (nodeFieldsIndexed p) (nodeInstructionsIndexed p))

-- | Render the constructor tree as a nested Markdown bullet list. Each level of
-- nesting is indented two spaces.
renderShapeTree :: Int -> ProgramShape -> [Text]
renderShapeTree depth shape = case shape of
  ShapePredict label -> [bullet ("Predict — outputs: " <> label)]
  ShapeCompose a b -> bullet "Compose" : descend a <> descend b
  ShapeFMap a -> bullet "FMap (pure post-processing)" : descend a
  ShapeMap width a -> bullet ("Map (concurrency width " <> tshow width <> ")") : descend a
  ShapeParallel a b -> bullet "Parallel" : descend a <> descend b
  ShapeRetry n a -> bullet ("Retry (up to " <> tshow n <> " attempts)") : descend a
  ShapeRetryWhen n a -> bullet ("RetryWhen (up to " <> tshow n <> " attempts)") : descend a
  ShapeValidate a -> bullet "Validate" : descend a
  ShapeMajorityVote k _ a -> bullet ("MajorityVote (k=" <> tshow k <> ")") : descend a
  ShapeEnsemble as -> bullet "Ensemble" : concatMap descend as
  ShapeEmbed -> [bullet "Embed (opaque effectful program)"]
  where
    bullet t = T.replicate (depth * 2) " " <> "- " <> t
    descend = renderShapeTree (depth + 1)

-- | A "Model calls" subsection listing, for each @Predict@ node in order, its
-- input and output field names and its signature instruction (as a nested bullet).
-- Omitted when the program has no model calls.
modelCalls :: [(NodeFields, Text)] -> [Text]
modelCalls [] = []
modelCalls calls =
  "" : "### Model calls" : "" : concat (zipWith entry [1 :: Int ..] calls)
  where
    entry idx (nf, instruction) =
      [ "- "
          <> tshow idx
          <> ". inputs ("
          <> commaSep (inputFieldNames nf)
          <> ") -> outputs ("
          <> commaSep (outputFieldNames nf)
          <> ")",
        "  - Instruction: " <> instruction
      ]

commaSep :: [Text] -> Text
commaSep = T.intercalate ", "

tshow :: (Show a) => a -> Text
tshow = T.pack . show