keiro-dsl-0.15.0.0: src/Keiro/Dsl/WorkspaceDiff.hs
-- | Whole-service evolution findings enriched with workspace source ownership.
--
-- The ordinary differ remains the single authority for compatibility. This module
-- runs it over the two composed 'WorkspaceSpec' graphs and adds only source
-- citations. Consequently, file layout can never manufacture, suppress, or demote
-- a wire finding.
module Keiro.Dsl.WorkspaceDiff
( OwnedSite (..),
WorkspaceChange (..),
WorkspaceMeta (..),
WorkspaceDiffReport,
workspaceDiffReport,
workspaceDiffReportWithSemanticImpact,
workspaceDiffReportWithCoordinationImpact,
workspaceDiffReportWithImpacts,
diffWorkspaces,
renderWorkspaceFinding,
)
where
import Control.Applicative ((<|>))
import Data.Char (isSpace)
import Data.List (find)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.Diff (Change (..), ChangeKind (..), advisoryAt, consumerBuildContext, diffServices, sourceLanguageChange)
import Keiro.Dsl.DiffReport (OwnedSite (..), WorkspaceChange (..), WorkspaceDiffReport, WorkspaceMeta (..), renderFinding, workspaceDiffReport, workspaceDiffReportWithCoordinationImpact, workspaceDiffReportWithImpacts, workspaceDiffReportWithSemanticImpact)
import Keiro.Dsl.FoldFingerprint (FoldSurfaceError)
import Keiro.Dsl.Grammar (Loc (..), Name, Placement (..))
import Keiro.Dsl.LanguageVersion (SourceLanguage (..))
import Keiro.Dsl.Validate (DiagnosticCode (..))
import Keiro.Dsl.Workspace (OwnershipIndex (..), WorkspaceMember (..), WorkspaceSpec (..), checkedWorkspace)
-- | Diff two composed service graphs and cite every participant we can resolve.
diffWorkspaces :: WorkspaceSpec -> WorkspaceSpec -> Either FoldSurfaceError [WorkspaceChange]
diffWorkspaces old new = do
semanticChanges <- diffServices (checkedWorkspace old) (checkedWorkspace new)
pure
( memberLanguageChanges old new
<> map annotate semanticChanges
<> ownershipMoveChanges old new
<> authorityChanges old new
)
where
annotate change =
WorkspaceChange
{ change = change,
declarationSite =
ownedSiteForName new (declarationName kind)
<|> ownedSiteForName old (declarationName kind),
useSites =
[ (path, ownedSiteForName new (pathRoot path) <|> ownedSiteForName old (pathRoot path))
| path <- (.paths) kind
]
}
where
kind = changeKind change
memberLanguageChanges :: WorkspaceSpec -> WorkspaceSpec -> [WorkspaceChange]
memberLanguageChanges old new =
[ WorkspaceChange
{ change = change,
declarationSite = Just (OwnedSite path (sourceLine ((.sourceLanguage) newMember))),
useSites = []
}
| (path, newMember) <- Map.toAscList newByPath,
Just oldMember <- [Map.lookup path oldByPath],
change <- sourceLanguageChange ((.service) new) (T.pack path) ((.sourceLanguage) oldMember) ((.sourceLanguage) newMember)
]
where
oldByPath = Map.fromList [((.path) member, member) | member <- (.members) old]
newByPath = Map.fromList [((.path) member, member) | member <- (.members) new]
sourceLine LegacyUnversioned = 1
sourceLine DeclaredLanguage {languageVersionLoc = Loc lineNumber} = lineNumber
-- | Preserve the existing headline/vector bytes and append indented citations.
renderWorkspaceFinding :: WorkspaceChange -> Text
renderWorkspaceFinding workspaceChange =
T.intercalate "\n" (renderFinding ((.change) workspaceChange) : declarationLine <> useLines)
where
declarationLine = case (.declarationSite) workspaceChange of
Nothing -> []
Just site -> [" declared: " <> renderOwnedSite site]
useLines =
[ " use-site: " <> path <> " (" <> renderOwnedSite site <> ")"
| (path, Just site) <- (.useSites) workspaceChange
]
renderOwnedSite :: OwnedSite -> Text
renderOwnedSite site = T.pack ((.file) site) <> ":" <> T.pack (show ((.line) site))
ownedSiteForName :: WorkspaceSpec -> Name -> Maybe OwnedSite
ownedSiteForName workspace name = do
(_, (file, Loc line)) <- find ((== name) . snd . fst) entries
pure (OwnedSite file line)
where
ownership = (.ownership) workspace
entries = Map.toAscList ((.declarations) ownership) <> Map.toAscList ((.nodes) ownership)
declarationName :: ChangeKind -> Name
declarationName kind
| "mapped-" `T.isPrefixOf` (.facet) kind,
Just mapped <- mappedNameFromSubject ((.subject) kind) =
mapped
| otherwise = (.node) kind
mappedNameFromSubject :: Text -> Maybe Name
mappedNameFromSubject subject =
case T.breakOn " : " subject of
(_, rest)
| not (T.null rest),
let name = T.takeWhile (not . isSpace) (T.drop 3 rest),
not (T.null name) ->
Just name
_ -> Nothing
pathRoot :: Text -> Name
pathRoot = T.takeWhile (\c -> c /= '.' && not (isSpace c))
changeKind :: Change -> ChangeKind
changeKind (Additive kind) = kind
changeKind (Advisory kind) = kind
changeKind (Breaking kind) = kind
ownershipMoveChanges :: WorkspaceSpec -> WorkspaceSpec -> [WorkspaceChange]
ownershipMoveChanges old new =
[ WorkspaceChange
{ change =
advisoryAt
(consumerBuildContext name [])
name
"ownership"
name
OwnershipMoved
( "declaration moved "
<> T.pack oldFile
<> " -> "
<> T.pack newFile
<> "; source ownership changed while wire evolution remains independently classified"
),
declarationSite = Just (OwnedSite newFile ((.unLoc) newLoc)),
useSites = []
}
| (key@(_, name), (oldFile, _)) <- Map.toAscList (ownershipEntries ((.ownership) old)),
Just (newFile, newLoc) <- [Map.lookup key (ownershipEntries ((.ownership) new))],
oldFile /= newFile
]
ownershipEntries :: OwnershipIndex -> Map.Map (Text, Name) (FilePath, Loc)
ownershipEntries ownership = (.declarations) ownership <> (.nodes) ownership
authorityChanges :: WorkspaceSpec -> WorkspaceSpec -> [WorkspaceChange]
authorityChanges old new =
concat
[ changed "service-identity" ((.service) old) ((.service) new) serviceDetail,
changed "context" ((.context) old) ((.context) new) contextDetail,
changed "module-root" (renderModuleRoot ((.moduleRoot) old)) (renderModuleRoot ((.moduleRoot) new)) moduleDetail,
changed "layout" (renderLayout ((.layout) old)) (renderLayout ((.layout) new)) layoutDetail
]
where
changed field before after detail
| before == after = []
| otherwise =
[ WorkspaceChange
{ change =
advisoryAt
(consumerBuildContext ((.service) new) [])
((.service) new)
"workspace-authority"
field
WorkspaceAuthorityChanged
(field <> " changed '" <> before <> "' -> '" <> after <> "'; " <> detail),
declarationSite = Nothing,
useSites = []
}
]
serviceDetail = "scaffold and compatibility history are re-keyed; follow the workspace adoption path"
contextDetail = "generated module namespaces change, and read-model registry/subscription identities may emit separate DerivedIdentityChanged findings"
moduleDetail = "generated module paths change without changing persisted wire identity"
layoutDetail = "generated module placement changes without changing persisted wire identity"
renderModuleRoot :: Maybe Text -> Text
renderModuleRoot = maybe "(default)" id
renderLayout :: Maybe Placement -> Text
renderLayout Nothing = "(default)"
renderLayout (Just GeneratedPrefix) = "prefixed"
renderLayout (Just CollocatedLeaf) = "collocated"