kioku-core 0.6.0.0 → 0.7.0.0
raw patch · 22 files changed
+1042/−352 lines, 22 filesdep ~keirodep ~keiro-coredep ~kioku-apiPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: keiro, keiro-core, kioku-api, kioku-core, kioku-migrations
API changes (from Hackage documentation)
+ Kioku.Memory.EventStream: validateMemoryEventStream :: Either [EventStreamWarning] (ValidatedEventStream (HsPred MemoryRegs MemoryCommand) MemoryRegs MemoryVertex MemoryCommand MemoryEvent)
+ Kioku.Memory.ReadModel: memoriesByNamespaceReadModelBlueprint :: ReadModelBlueprint MemoriesByNamespaceQuery [MemoryRecord]
+ Kioku.Memory.ReadModel: memoriesByNamespaceRowsReadModelBlueprint :: ReadModelBlueprint MemoriesByNamespaceQuery [MemoryRow]
+ Kioku.Memory.ReadModel: memoriesByScopeReadModelBlueprint :: ReadModelBlueprint MemoriesByScopeQuery [MemoryRecord]
+ Kioku.Memory.ReadModel: memoriesByScopeRowsReadModelBlueprint :: ReadModelBlueprint MemoriesByScopeQuery [MemoryRow]
+ Kioku.Memory.ReadModel: memoriesBySessionReadModelBlueprint :: ReadModelBlueprint MemoriesBySessionQuery [MemoryRecord]
+ Kioku.Memory.ReadModel: memoriesBySessionRowsReadModelBlueprint :: ReadModelBlueprint MemoriesBySessionQuery [MemoryRow]
+ Kioku.Memory.ReadModel: memoriesByTypeReadModelBlueprint :: ReadModelBlueprint MemoriesByTypeQuery [MemoryRecord]
+ Kioku.Memory.ReadModel: memoriesByTypeRowsReadModelBlueprint :: ReadModelBlueprint MemoriesByTypeQuery [MemoryRow]
+ Kioku.Memory.ReadModel: memoryByIdReadModelBlueprint :: ReadModelBlueprint MemoryByIdQuery (Maybe MemoryRow)
+ Kioku.Memory.ReadModel: memorySupersessionChainReadModelBlueprint :: ReadModelBlueprint MemorySupersessionChainQuery [MemoryRow]
+ Kioku.ProjectionCatalog: kiokuCatalogFingerprint :: CatalogFingerprint
+ Kioku.ProjectionCatalog: kiokuCatalogInventory :: CatalogInventory
+ Kioku.ProjectionCatalog: kiokuProjectionCatalog :: ValidatedProjectionCatalog
+ Kioku.ProjectionCatalog: memoryProjectionSet :: ProjectionSet MemoryEvent
+ Kioku.ProjectionCatalog: renderKiokuCatalogDiagnostics :: NonEmpty CatalogDiagnostic -> Text
+ Kioku.ProjectionCatalog: sessionProjectionSet :: ProjectionSet SessionEvent
+ Kioku.ProjectionCatalog: validateKiokuProjectionCatalog :: Validation (NonEmpty CatalogDiagnostic) ValidatedProjectionCatalog
+ Kioku.ProjectionCatalog: validateKiokuProjectionCatalogWith :: (ProjectionCatalog -> ProjectionCatalog) -> Validation (NonEmpty CatalogDiagnostic) ValidatedProjectionCatalog
+ Kioku.Session.EventStream: validateSessionEventStream :: Either [EventStreamWarning] (ValidatedEventStream (HsPred SessionRegs SessionCommand) SessionRegs SessionVertex SessionCommand SessionEvent)
+ Kioku.Session.ReadModel: awaitingSessionsByCorrelationKeyReadModelBlueprint :: ReadModelBlueprint AwaitingSessionsByCorrelationKeyQuery [SessionRow]
+ Kioku.Session.ReadModel: sessionByIdReadModelBlueprint :: ReadModelBlueprint SessionByIdQuery (Maybe SessionRow)
+ Kioku.Session.ReadModel: sessionChainReadModelBlueprint :: ReadModelBlueprint SessionChainQuery [SessionRow]
+ Kioku.Session.ReadModel: sessionDelegationChildrenReadModelBlueprint :: ReadModelBlueprint SessionDelegationChildrenQuery [SessionRow]
+ Kioku.Session.ReadModel: sessionsByFocusReadModelBlueprint :: ReadModelBlueprint SessionsByFocusQuery [SessionRow]
+ Kioku.Session.ReadModel: sessionsByNamespaceReadModelBlueprint :: ReadModelBlueprint SessionsByNamespaceQuery [SessionRow]
+ Kioku.Session.ReadModel: sessionsByScopeReadModelBlueprint :: ReadModelBlueprint SessionsByScopeQuery [SessionRow]
+ Kioku.Session.ReadModel: sessionsByStartedRangeReadModelBlueprint :: ReadModelBlueprint SessionsByStartedRangeQuery [SessionRow]
+ Kioku.Session.ReadModel: turnsBySessionReadModelBlueprint :: ReadModelBlueprint TurnsBySessionQuery [TurnRow]
- Kioku.AI.Config: AIConfig :: DistillationConfig -> Map AIFeature DistillationConfig -> Map AIFeature EmbeddingModel -> AIConfig
+ Kioku.AI.Config: AIConfig :: !DistillationConfig -> !Map AIFeature DistillationConfig -> !Map AIFeature EmbeddingModel -> AIConfig
- Kioku.AI.Config: [distillationDefault] :: AIConfig -> DistillationConfig
+ Kioku.AI.Config: [distillationDefault] :: AIConfig -> !DistillationConfig
- Kioku.AI.Config: [embeddingSettings] :: AIConfig -> Map AIFeature EmbeddingModel
+ Kioku.AI.Config: [embeddingSettings] :: AIConfig -> !Map AIFeature EmbeddingModel
- Kioku.AI.Config: [featureOverrides] :: AIConfig -> Map AIFeature DistillationConfig
+ Kioku.AI.Config: [featureOverrides] :: AIConfig -> !Map AIFeature DistillationConfig
- Kioku.AI.Runtime: HostCapabilities :: Maybe ProviderRegistry -> Maybe ProviderRegistry -> Bool -> Maybe (AIFeature -> InteractiveLaunchRequest -> IO (Either AIExecutionError InteractiveLaunchResult)) -> Bool -> HostCapabilities
+ Kioku.AI.Runtime: HostCapabilities :: !Maybe ProviderRegistry -> !Maybe ProviderRegistry -> !Bool -> !Maybe (AIFeature -> InteractiveLaunchRequest -> IO (Either AIExecutionError InteractiveLaunchResult)) -> !Bool -> HostCapabilities
- Kioku.AI.Runtime: [allowEmbeddingAPI] :: HostCapabilities -> Bool
+ Kioku.AI.Runtime: [allowEmbeddingAPI] :: HostCapabilities -> !Bool
- Kioku.AI.Runtime: [allowInteractive] :: HostCapabilities -> Bool
+ Kioku.AI.Runtime: [allowInteractive] :: HostCapabilities -> !Bool
- Kioku.AI.Runtime: [apiRegistry] :: HostCapabilities -> Maybe ProviderRegistry
+ Kioku.AI.Runtime: [apiRegistry] :: HostCapabilities -> !Maybe ProviderRegistry
- Kioku.AI.Runtime: [batchRegistry] :: HostCapabilities -> Maybe ProviderRegistry
+ Kioku.AI.Runtime: [batchRegistry] :: HostCapabilities -> !Maybe ProviderRegistry
- Kioku.AI.Runtime: [launchInteractive] :: HostCapabilities -> Maybe (AIFeature -> InteractiveLaunchRequest -> IO (Either AIExecutionError InteractiveLaunchResult))
+ Kioku.AI.Runtime: [launchInteractive] :: HostCapabilities -> !Maybe (AIFeature -> InteractiveLaunchRequest -> IO (Either AIExecutionError InteractiveLaunchResult))
- Kioku.Distill.Consolidate: ConsolidateInput :: Field "human-readable memory scope label" Text -> ExtractedAtom -> [ExistingMemory] -> ConsolidateInput
+ Kioku.Distill.Consolidate: ConsolidateInput :: !Field "human-readable memory scope label" Text -> !ExtractedAtom -> ![ExistingMemory] -> ConsolidateInput
- Kioku.Distill.Consolidate: ConsolidationDecision :: ConsolidationAction -> [Text] -> Maybe Text -> Field "one concise reason for the decision" Text -> ConsolidationDecision
+ Kioku.Distill.Consolidate: ConsolidationDecision :: !ConsolidationAction -> ![Text] -> !Maybe Text -> !Field "one concise reason for the decision" Text -> ConsolidationDecision
- Kioku.Distill.Consolidate: ExistingMemory :: Field "existing memory identifier" Text -> Field "existing memory type or category" Text -> Field "existing memory content" Text -> Field "existing memory priority where lower is more important" Int -> Field "existing confidence label" Text -> ExistingMemory
+ Kioku.Distill.Consolidate: ExistingMemory :: !Field "existing memory identifier" Text -> !Field "existing memory type or category" Text -> !Field "existing memory content" Text -> !Field "existing memory priority where lower is more important" Int -> !Field "existing confidence label" Text -> ExistingMemory
- Kioku.Distill.Consolidate: [action] :: ConsolidationDecision -> ConsolidationAction
+ Kioku.Distill.Consolidate: [action] :: ConsolidationDecision -> !ConsolidationAction
- Kioku.Distill.Consolidate: [candidate] :: ConsolidateInput -> ExtractedAtom
+ Kioku.Distill.Consolidate: [candidate] :: ConsolidateInput -> !ExtractedAtom
- Kioku.Distill.Consolidate: [confidence] :: ExistingMemory -> Field "existing confidence label" Text
+ Kioku.Distill.Consolidate: [confidence] :: ExistingMemory -> !Field "existing confidence label" Text
- Kioku.Distill.Consolidate: [content] :: ExistingMemory -> Field "existing memory content" Text
+ Kioku.Distill.Consolidate: [content] :: ExistingMemory -> !Field "existing memory content" Text
- Kioku.Distill.Consolidate: [existing] :: ConsolidateInput -> [ExistingMemory]
+ Kioku.Distill.Consolidate: [existing] :: ConsolidateInput -> ![ExistingMemory]
- Kioku.Distill.Consolidate: [memoryId] :: ExistingMemory -> Field "existing memory identifier" Text
+ Kioku.Distill.Consolidate: [memoryId] :: ExistingMemory -> !Field "existing memory identifier" Text
- Kioku.Distill.Consolidate: [memoryType] :: ExistingMemory -> Field "existing memory type or category" Text
+ Kioku.Distill.Consolidate: [memoryType] :: ExistingMemory -> !Field "existing memory type or category" Text
- Kioku.Distill.Consolidate: [priority] :: ExistingMemory -> Field "existing memory priority where lower is more important" Int
+ Kioku.Distill.Consolidate: [priority] :: ExistingMemory -> !Field "existing memory priority where lower is more important" Int
- Kioku.Distill.Consolidate: [rationale] :: ConsolidationDecision -> Field "one concise reason for the decision" Text
+ Kioku.Distill.Consolidate: [rationale] :: ConsolidationDecision -> !Field "one concise reason for the decision" Text
- Kioku.Distill.Consolidate: [resultContent] :: ConsolidationDecision -> Maybe Text
+ Kioku.Distill.Consolidate: [resultContent] :: ConsolidationDecision -> !Maybe Text
- Kioku.Distill.Consolidate: [scopeLabel] :: ConsolidateInput -> Field "human-readable memory scope label" Text
+ Kioku.Distill.Consolidate: [scopeLabel] :: ConsolidateInput -> !Field "human-readable memory scope label" Text
- Kioku.Distill.Consolidate: [targetMemoryIds] :: ConsolidationDecision -> [Text]
+ Kioku.Distill.Consolidate: [targetMemoryIds] :: ConsolidationDecision -> ![Text]
- Kioku.Distill.Extract: ExtractInput :: Field "the session focus, task, or topic" Text -> Field "human-readable memory scope label" Text -> Field "recent turns, notes, or recorded memory evidence" Text -> ExtractInput
+ Kioku.Distill.Extract: ExtractInput :: !Field "the session focus, task, or topic" Text -> !Field "human-readable memory scope label" Text -> !Field "recent turns, notes, or recorded memory evidence" Text -> ExtractInput
- Kioku.Distill.Extract: ExtractedAtom :: Field "one of: fact | pattern | preference | constraint | instruction" Text -> Field "one concise durable memory sentence" Text -> Field "0=always inject; 100=default; larger=lower priority" Int -> Field "one of: high | medium | low" Text -> ExtractedAtom
+ Kioku.Distill.Extract: ExtractedAtom :: !Field "one of: fact | pattern | preference | constraint | instruction" Text -> !Field "one concise durable memory sentence" Text -> !Field "0=always inject; 100=default; larger=lower priority" Int -> !Field "one of: high | medium | low" Text -> ExtractedAtom
- Kioku.Distill.Extract: [atomType] :: ExtractedAtom -> Field "one of: fact | pattern | preference | constraint | instruction" Text
+ Kioku.Distill.Extract: [atomType] :: ExtractedAtom -> !Field "one of: fact | pattern | preference | constraint | instruction" Text
- Kioku.Distill.Extract: [confidence] :: ExtractedAtom -> Field "one of: high | medium | low" Text
+ Kioku.Distill.Extract: [confidence] :: ExtractedAtom -> !Field "one of: high | medium | low" Text
- Kioku.Distill.Extract: [content] :: ExtractedAtom -> Field "one concise durable memory sentence" Text
+ Kioku.Distill.Extract: [content] :: ExtractedAtom -> !Field "one concise durable memory sentence" Text
- Kioku.Distill.Extract: [conversation] :: ExtractInput -> Field "recent turns, notes, or recorded memory evidence" Text
+ Kioku.Distill.Extract: [conversation] :: ExtractInput -> !Field "recent turns, notes, or recorded memory evidence" Text
- Kioku.Distill.Extract: [focus] :: ExtractInput -> Field "the session focus, task, or topic" Text
+ Kioku.Distill.Extract: [focus] :: ExtractInput -> !Field "the session focus, task, or topic" Text
- Kioku.Distill.Extract: [priority] :: ExtractedAtom -> Field "0=always inject; 100=default; larger=lower priority" Int
+ Kioku.Distill.Extract: [priority] :: ExtractedAtom -> !Field "0=always inject; 100=default; larger=lower priority" Int
- Kioku.Distill.Extract: [scopeLabel] :: ExtractInput -> Field "human-readable memory scope label" Text
+ Kioku.Distill.Extract: [scopeLabel] :: ExtractInput -> !Field "human-readable memory scope label" Text
- Kioku.Distill.Persona: PersonaInput :: Field "human label of the scope" Text -> Field "the scene blocks for this scope, newline-joined" Text -> PersonaInput
+ Kioku.Distill.Persona: PersonaInput :: !Field "human label of the scope" Text -> !Field "the scene blocks for this scope, newline-joined" Text -> PersonaInput
- Kioku.Distill.Persona: [scenes] :: PersonaInput -> Field "the scene blocks for this scope, newline-joined" Text
+ Kioku.Distill.Persona: [scenes] :: PersonaInput -> !Field "the scene blocks for this scope, newline-joined" Text
- Kioku.Distill.Persona: [scopeLabel] :: PersonaInput -> Field "human label of the scope" Text
+ Kioku.Distill.Persona: [scopeLabel] :: PersonaInput -> !Field "human label of the scope" Text
- Kioku.Distill.Runtime: TestRunners :: (ExtractInput -> IO (Either ShikumiError ExtractOutput)) -> (ConsolidateInput -> IO (Either ShikumiError ConsolidationDecision)) -> (SceneInput -> IO (Either ShikumiError SceneOutput)) -> (PersonaInput -> IO (Either ShikumiError PersonaOutput)) -> TestRunners
+ Kioku.Distill.Runtime: TestRunners :: !ExtractInput -> IO (Either ShikumiError ExtractOutput) -> !ConsolidateInput -> IO (Either ShikumiError ConsolidationDecision) -> !SceneInput -> IO (Either ShikumiError SceneOutput) -> !PersonaInput -> IO (Either ShikumiError PersonaOutput) -> TestRunners
- Kioku.Distill.Runtime: [runConsolidate] :: TestRunners -> ConsolidateInput -> IO (Either ShikumiError ConsolidationDecision)
+ Kioku.Distill.Runtime: [runConsolidate] :: TestRunners -> !ConsolidateInput -> IO (Either ShikumiError ConsolidationDecision)
- Kioku.Distill.Runtime: [runExtract] :: TestRunners -> ExtractInput -> IO (Either ShikumiError ExtractOutput)
+ Kioku.Distill.Runtime: [runExtract] :: TestRunners -> !ExtractInput -> IO (Either ShikumiError ExtractOutput)
- Kioku.Distill.Runtime: [runPersona] :: TestRunners -> PersonaInput -> IO (Either ShikumiError PersonaOutput)
+ Kioku.Distill.Runtime: [runPersona] :: TestRunners -> !PersonaInput -> IO (Either ShikumiError PersonaOutput)
- Kioku.Distill.Runtime: [runScene] :: TestRunners -> SceneInput -> IO (Either ShikumiError SceneOutput)
+ Kioku.Distill.Runtime: [runScene] :: TestRunners -> !SceneInput -> IO (Either ShikumiError SceneOutput)
- Kioku.Distill.Scene: SceneInput :: Field "human label of the scope" Text -> Field "the active memory atoms in this scope, newline-joined" Text -> SceneInput
+ Kioku.Distill.Scene: SceneInput :: !Field "human label of the scope" Text -> !Field "the active memory atoms in this scope, newline-joined" Text -> SceneInput
- Kioku.Distill.Scene: SceneOutput :: Field "a short scene title, e.g. 'Testing & CI practices'" Text -> Field "a markdown scene block summarizing the atoms as a narrative" Text -> SceneOutput
+ Kioku.Distill.Scene: SceneOutput :: !Field "a short scene title, e.g. 'Testing & CI practices'" Text -> !Field "a markdown scene block summarizing the atoms as a narrative" Text -> SceneOutput
- Kioku.Distill.Scene: [atoms] :: SceneInput -> Field "the active memory atoms in this scope, newline-joined" Text
+ Kioku.Distill.Scene: [atoms] :: SceneInput -> !Field "the active memory atoms in this scope, newline-joined" Text
- Kioku.Distill.Scene: [bodyMd] :: SceneOutput -> Field "a markdown scene block summarizing the atoms as a narrative" Text
+ Kioku.Distill.Scene: [bodyMd] :: SceneOutput -> !Field "a markdown scene block summarizing the atoms as a narrative" Text
- Kioku.Distill.Scene: [scopeLabel] :: SceneInput -> Field "human label of the scope" Text
+ Kioku.Distill.Scene: [scopeLabel] :: SceneInput -> !Field "human label of the scope" Text
- Kioku.Distill.Scene: [title] :: SceneOutput -> Field "a short scene title, e.g. 'Testing & CI practices'" Text
+ Kioku.Distill.Scene: [title] :: SceneOutput -> !Field "a short scene title, e.g. 'Testing & CI practices'" Text
- Kioku.ReadModel: registerKiokuReadModels :: forall (es :: [Effect]). Store :> es => Eff es ()
+ Kioku.ReadModel: registerKiokuReadModels :: forall (es :: [Effect]). Store :> es => Eff es (Either CatalogRegistrationError [GroupRebuildMetadata])
Files
- CHANGELOG.md +26/−0
- kioku-core.cabal +109/−98
- src/Kioku/AI/Config.hs +3/−3
- src/Kioku/AI/Runtime.hs +5/−5
- src/Kioku/App.hs +23/−1
- src/Kioku/Distill/Consolidate.hs +12/−12
- src/Kioku/Distill/Extract.hs +7/−7
- src/Kioku/Distill/Persona.hs +2/−2
- src/Kioku/Distill/Runtime.hs +4/−4
- src/Kioku/Distill/Scene.hs +4/−4
- src/Kioku/Memory.hs +20/−15
- src/Kioku/Memory/EventStream.hs +7/−2
- src/Kioku/Memory/ReadModel.hs +76/−51
- src/Kioku/ProjectionCatalog.hs +247/−0
- src/Kioku/ReadModel.hs +16/−62
- src/Kioku/Recall.hs +11/−11
- src/Kioku/Session.hs +26/−21
- src/Kioku/Session/EventStream.hs +7/−2
- src/Kioku/Session/ReadModel.hs +69/−46
- test/Kioku/ProjectionCatalogSpec.hs +353/−0
- test/Kioku/ReadModelReconcileSpec.hs +13/−6
- test/Main.hs +2/−0
CHANGELOG.md view
@@ -1,5 +1,31 @@ # Changelog +## 0.7.0.0 — 2026-09-18++### Breaking Changes++- Requires `keiro ^>=0.17.0.0` and `keiro-core ^>=0.17.0.0`, excluding the 0.16 compatibility+ line. The exported read-model values now derive from truthful `ReadModelBlueprint` definitions;+ their names, schemas, versions, shape hashes, SQL, and immediate behavior are unchanged.++### Added++- `Kioku.ProjectionCatalog`, which validates and binds two sources, three application projection+ targets, two rebuild groups, two projection owners, and all nineteen query models. Catalog+ registration and migration-time identity reconciliation derive from the same inventory.+- A public `ReadModelBlueprint` value beside every existing memory and session read model.++### Changed++- Queries use `runQueryWithFreshness Immediate` and no longer depend on Keiro's deprecated+ consistency fields or `runQueryWith` compatibility layer.+- `withNoopAppEnv` validates both event streams and the projection catalog, then registers the+ catalog before running user effects. Framework-owned timer writes remain explicit and atomic+ with application projections because Keiro's exclusive target ownership cannot represent a+ shared timer table.+- AI and distillation product records use strict fields, and the package participates in the+ repository Haskell convention ratchet.+ ## 0.6.0.0 — 2026-09-08 ### Breaking Changes
kioku-core.cabal view
@@ -1,36 +1,42 @@-cabal-version: 3.0-name: kioku-core-version: 0.6.0.0-synopsis: Reusable agent memory runtime+cabal-version: 3.0+name: kioku-core+version: 0.7.0.0+synopsis: Reusable agent memory runtime description: Core runtime for kioku. M1 establishes the application effect stack; later milestones add the memory and session aggregates. -license: BSD-3-Clause-license-file: LICENSE-author: Nadeem Bitar-maintainer: nadeem@gmail.com-copyright: 2026 Nadeem Bitar-category: Data-build-type: Simple-tested-with: GHC >=9.12 && <9.13-homepage: https://github.com/shinzui/kioku-bug-reports: https://github.com/shinzui/kioku/issues+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: 2026 Nadeem Bitar+category: Data+build-type: Simple+tested-with: ghc >=9.12 && <9.13+homepage: https://github.com/shinzui/kioku+bug-reports: https://github.com/shinzui/kioku/issues extra-doc-files: CHANGELOG.md source-repository head- type: git+ type: git location: https://github.com/shinzui/kioku.git- subdir: kioku-core+ subdir: kioku-core common warnings ghc-options:- -Wall -Wcompat -Widentities -Wincomplete-record-updates- -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints+ -Wall+ -Wcompat+ -Widentities+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wpartial-fields+ -Wredundant-constraints+ -Werror=deprecations -Werror=incomplete-patterns common shared- default-language: GHC2024+ default-language: GHC2024 default-extensions: BlockArguments DeriveAnyClass@@ -43,8 +49,8 @@ TemplateHaskell library- import: warnings, shared- hs-source-dirs: src+ import: warnings, shared+ hs-source-dirs: src exposed-modules: Kioku.AI.Config Kioku.AI.File@@ -71,6 +77,7 @@ Kioku.Memory.EventStream Kioku.Memory.ReadModel Kioku.Partition+ Kioku.ProjectionCatalog Kioku.ReadModel Kioku.Recall Kioku.Recall.Capability@@ -84,8 +91,7 @@ -- Where Kioku's projections physically live is Kioku's business, so the -- qualified relation names stay internal rather than becoming a public API a -- consumer could pin its own SQL to.- other-modules: Kioku.Database.Schema-+ other-modules: Kioku.Database.Schema -- Works around a GHC 9.12.4 coercion-optimiser bug that makes the *profiled* -- build of Kioku.Distill.L1 panic: --@@ -105,48 +111,48 @@ -- mori://MMZK1526/mmzk-typeid/upstream-issues/mmzk-typeid-kindid-ghc-9-12-4-profiling-coercionkind-panic ghc-prof-options: -fno-opt-coercion build-depends:- , aeson >=2.2 && <2.3- , baikai ^>=0.7.0.0- , baikai-claude ^>=0.7.0.0- , baikai-effectful ^>=0.4.0.1- , baikai-openai ^>=0.7.0.0- , base >=4.21 && <5- , bytestring >=0.11 && <0.13- , containers >=0.6 && <0.8- , contravariant >=1.5 && <1.6- , contravariant-extras >=0.3 && <0.4- , crypton ^>=1.1.4- , directory >=1.3 && <1.4- , effectful >=2.5 && <2.7- , effectful-core >=2.5 && <2.7- , filepath >=1.4 && <1.6- , generic-lens >=2.2 && <2.4- , hasql >=1.6 && <1.11- , hasql-pool >=1.2 && <1.5- , hasql-transaction >=1.0 && <1.3- , hs-opentelemetry-api >=1.0 && <1.1- , keiki ^>=0.9.0.0- , keiro ^>=0.16.0.0- , keiro-core ^>=0.16.0.0- , kioku-api ^>=0.6.0.0- , kiroku-store ^>=0.8.0.0- , lens >=5.2 && <5.4- , mmzk-typeid >=0.7 && <0.8- , shibuya-core ^>=0.9.0.0- , shibuya-kiroku-adapter ^>=0.5.1.1- , shikumi ^>=0.4.0.0- , shikumi-trace ^>=0.3.0.0- , text >=2.1 && <2.2- , time >=1.12 && <1.15- , unix >=2.8.8 && <2.9- , unordered-containers >=0.2 && <0.3- , uuid >=1.3 && <1.4- , vector ^>=0.13.2.0+ aeson >=2.2 && <2.3,+ baikai ^>=0.7.0.0,+ baikai-claude ^>=0.7.0.0,+ baikai-effectful ^>=0.4.0.1,+ baikai-openai ^>=0.7.0.0,+ base >=4.21 && <5,+ bytestring >=0.11 && <0.13,+ containers >=0.6 && <0.8,+ contravariant >=1.5 && <1.6,+ contravariant-extras >=0.3 && <0.4,+ crypton ^>=1.1.4,+ directory >=1.3 && <1.4,+ effectful >=2.5 && <2.7,+ effectful-core >=2.5 && <2.7,+ filepath >=1.4 && <1.6,+ generic-lens >=2.2 && <2.4,+ hasql >=1.6 && <1.11,+ hasql-pool >=1.2 && <1.5,+ hasql-transaction >=1.0 && <1.3,+ hs-opentelemetry-api >=1.0 && <1.1,+ keiki ^>=0.9.0.0,+ keiro ^>=0.17.0.0,+ keiro-core ^>=0.17.0.0,+ kioku-api ^>=0.7.0.0,+ kiroku-store ^>=0.8.0.0,+ lens >=5.2 && <5.4,+ mmzk-typeid >=0.7 && <0.8,+ shibuya-core ^>=0.9.0.0,+ shibuya-kiroku-adapter ^>=0.5.1.1,+ shikumi ^>=0.4.0.0,+ shikumi-trace ^>=0.3.0.0,+ text >=2.1 && <2.2,+ time >=1.12 && <1.15,+ unix >=2.8.8 && <2.9,+ unordered-containers >=0.2 && <0.3,+ uuid >=1.3 && <1.4,+ vector ^>=0.13.2.0, test-suite kioku-test- import: warnings, shared- type: exitcode-stdio-1.0- main-is: Main.hs+ import: warnings, shared+ type: exitcode-stdio-1.0+ main-is: Main.hs other-modules: Kioku.AIRuntimeSpec Kioku.AwaitingSpec@@ -156,6 +162,7 @@ Kioku.IdempotencySpec Kioku.MemorySpaceSpec Kioku.PortfolioAccessSpec+ Kioku.ProjectionCatalogSpec Kioku.ReadModelReconcileSpec Kioku.RecallCompatSpec Kioku.RecallHarness@@ -172,39 +179,43 @@ Kioku.WorkspaceSpec hs-source-dirs: test- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ ghc-options:+ -threaded+ -rtsopts+ -with-rtsopts=-N+ build-depends:- , aeson >=2.2- , async >=2.2 && <2.3- , baikai ^>=0.7.0.0- , base >=4.21 && <5- , bytestring >=0.11- , containers >=0.6- , contravariant >=1.5- , directory >=1.3- , effectful >=2.5- , effectful-core >=2.5- , filepath >=1.4- , generic-lens >=2.2 && <2.4- , hasql >=1.6- , hasql-transaction >=1.0- , keiro ^>=0.16.0.0- , keiro-core ^>=0.16.0.0- , kioku-api ^>=0.6.0.0- , kioku-core ^>=0.6.0.0- , kioku-migrations:test-support ^>=0.6.0.0- , kiroku-store ^>=0.8.0.0- , lens >=5.2- , shibuya-core ^>=0.9.0.0- , shikumi ^>=0.4.0.0- , shikumi-trace ^>=0.3.0.0- , tasty >=1.5- , tasty-expected-failure >=0.12- , tasty-hunit >=0.10- , temporary >=1.3- , text >=2.1- , time >=1.12- , unix >=2.8.8 && <2.9- , unordered-containers >=0.2- , uuid >=1.3- , vector+ aeson >=2.2,+ async >=2.2 && <2.3,+ baikai ^>=0.7.0.0,+ base >=4.21 && <5,+ bytestring >=0.11,+ containers >=0.6,+ contravariant >=1.5,+ directory >=1.3,+ effectful >=2.5,+ effectful-core >=2.5,+ filepath >=1.4,+ generic-lens >=2.2 && <2.4,+ hasql >=1.6,+ hasql-transaction >=1.0,+ keiro ^>=0.17.0.0,+ keiro-core ^>=0.17.0.0,+ kioku-api ^>=0.7.0.0,+ kioku-core ^>=0.7.0.0,+ kioku-migrations:test-support ^>=0.7.0.0,+ kiroku-store ^>=0.8.0.0,+ lens >=5.2,+ shibuya-core ^>=0.9.0.0,+ shikumi ^>=0.4.0.0,+ shikumi-trace ^>=0.3.0.0,+ tasty >=1.5,+ tasty-expected-failure >=0.12,+ tasty-hunit >=0.10,+ temporary >=1.3,+ text >=2.1,+ time >=1.12,+ unix >=2.8.8 && <2.9,+ unordered-containers >=0.2,+ uuid >=1.3,+ vector,
src/Kioku/AI/Config.hs view
@@ -42,9 +42,9 @@ | InteractiveConfig InteractiveProvider InteractiveLaunchRequest data AIConfig = AIConfig- { distillationDefault :: DistillationConfig,- featureOverrides :: Map AIFeature DistillationConfig,- embeddingSettings :: Map AIFeature EmbeddingModel+ { distillationDefault :: !DistillationConfig,+ featureOverrides :: !(Map AIFeature DistillationConfig),+ embeddingSettings :: !(Map AIFeature EmbeddingModel) } disabledAIConfig :: AIConfig
src/Kioku/AI/Runtime.hs view
@@ -39,11 +39,11 @@ -- | Registries grant separate API and batch capabilities. The callback grants a -- fresh interactive session; presence of a terminal or executable grants none. data HostCapabilities = HostCapabilities- { apiRegistry :: Maybe Registry.ProviderRegistry,- batchRegistry :: Maybe Registry.ProviderRegistry,- allowInteractive :: Bool,- launchInteractive :: Maybe (AIFeature -> InteractiveLaunchRequest -> IO (Either AIExecutionError InteractiveLaunchResult)),- allowEmbeddingAPI :: Bool+ { apiRegistry :: !(Maybe Registry.ProviderRegistry),+ batchRegistry :: !(Maybe Registry.ProviderRegistry),+ allowInteractive :: !Bool,+ launchInteractive :: !(Maybe (AIFeature -> InteractiveLaunchRequest -> IO (Either AIExecutionError InteractiveLaunchResult))),+ allowEmbeddingAPI :: !Bool } noHostCapabilities :: HostCapabilities
src/Kioku/App.hs view
@@ -10,11 +10,19 @@ ) where +import Data.Text qualified as Text import Effectful (Eff, IOE, runEff) import Effectful.Error.Static (Error, runErrorNoCallStack)+import Keiro.Projection.Catalog (Validation (..)) import Keiro.Telemetry (KeiroMetrics)+import Kioku.Memory.EventStream (validateMemoryEventStream) import Kioku.Prelude+import Kioku.ProjectionCatalog+ ( renderKiokuCatalogDiagnostics,+ validateKiokuProjectionCatalog,+ ) import Kioku.ReadModel (registerKiokuReadModels)+import Kioku.Session.EventStream (validateSessionEventStream) import Kiroku.Store.Connection (ConnectionSettings) import Kiroku.Store.Effect (Store, runStoreResource) import Kiroku.Store.Effect.Resource (KirokuStoreResource, withKirokuStore)@@ -42,12 +50,26 @@ withNoopAppEnv :: ConnectionSettings -> (AppEnv -> IO a) -> IO a withNoopAppEnv connectionSettings continue = do+ validateRuntimeDefinitions tracer <- noopTracer let env = AppEnv {connectionSettings, tracer, metrics = Nothing} registration <- runAppIO env registerKiokuReadModels case registration of Left err -> fail ("Kioku read-model registration failed: " <> show err)- Right () -> continue env+ Right (Left err) -> fail ("Kioku projection-catalog registration failed: " <> show err)+ Right (Right _) -> continue env++validateRuntimeDefinitions :: IO ()+validateRuntimeDefinitions = do+ case validateMemoryEventStream of+ Left warnings -> fail ("Kioku memory event-stream validation failed: " <> show warnings)+ Right _ -> pure ()+ case validateSessionEventStream of+ Left warnings -> fail ("Kioku session event-stream validation failed: " <> show warnings)+ Right _ -> pure ()+ case validateKiokuProjectionCatalog of+ Failure diagnostics -> fail ("Kioku projection-catalog validation failed: " <> Text.unpack (renderKiokuCatalogDiagnostics diagnostics))+ Success _ -> pure () noopTracer :: IO Tracer noopTracer = do
src/Kioku/Distill/Consolidate.hs view
@@ -30,28 +30,28 @@ deriving anyclass (ToSchema, FromModel) data ExistingMemory = ExistingMemory- { memoryId :: Field "existing memory identifier" Text,- memoryType :: Field "existing memory type or category" Text,- content :: Field "existing memory content" Text,- priority :: Field "existing memory priority where lower is more important" Int,- confidence :: Field "existing confidence label" Text+ { memoryId :: !(Field "existing memory identifier" Text),+ memoryType :: !(Field "existing memory type or category" Text),+ content :: !(Field "existing memory content" Text),+ priority :: !(Field "existing memory priority where lower is more important" Int),+ confidence :: !(Field "existing confidence label" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt) data ConsolidateInput = ConsolidateInput- { scopeLabel :: Field "human-readable memory scope label" Text,- candidate :: ExtractedAtom,- existing :: [ExistingMemory]+ { scopeLabel :: !(Field "human-readable memory scope label" Text),+ candidate :: !ExtractedAtom,+ existing :: ![ExistingMemory] } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt) data ConsolidationDecision = ConsolidationDecision- { action :: ConsolidationAction,- targetMemoryIds :: [Text],- resultContent :: Maybe Text,- rationale :: Field "one concise reason for the decision" Text+ { action :: !ConsolidationAction,+ targetMemoryIds :: ![Text],+ resultContent :: !(Maybe Text),+ rationale :: !(Field "one concise reason for the decision" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt)
src/Kioku/Distill/Extract.hs view
@@ -20,18 +20,18 @@ import Shikumi.Signature (Signature, mkSignature) data ExtractInput = ExtractInput- { focus :: Field "the session focus, task, or topic" Text,- scopeLabel :: Field "human-readable memory scope label" Text,- conversation :: Field "recent turns, notes, or recorded memory evidence" Text+ { focus :: !(Field "the session focus, task, or topic" Text),+ scopeLabel :: !(Field "human-readable memory scope label" Text),+ conversation :: !(Field "recent turns, notes, or recorded memory evidence" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt) data ExtractedAtom = ExtractedAtom- { atomType :: Field "one of: fact | pattern | preference | constraint | instruction" Text,- content :: Field "one concise durable memory sentence" Text,- priority :: Field "0=always inject; 100=default; larger=lower priority" Int,- confidence :: Field "one of: high | medium | low" Text+ { atomType :: !(Field "one of: fact | pattern | preference | constraint | instruction" Text),+ content :: !(Field "one concise durable memory sentence" Text),+ priority :: !(Field "0=always inject; 100=default; larger=lower priority" Int),+ confidence :: !(Field "one of: high | medium | low" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt)
src/Kioku/Distill/Persona.hs view
@@ -18,8 +18,8 @@ import Shikumi.Signature (Signature, mkSignature) data PersonaInput = PersonaInput- { scopeLabel :: Field "human label of the scope" Text,- scenes :: Field "the scene blocks for this scope, newline-joined" Text+ { scopeLabel :: !(Field "human label of the scope" Text),+ scenes :: !(Field "the scene blocks for this scope, newline-joined" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt)
src/Kioku/Distill/Runtime.hs view
@@ -42,10 +42,10 @@ -- | Explicit controlled-runner seam for tests. Production construction accepts -- only a validated AIRuntime; its captured settings cannot be record-updated. data TestRunners = TestRunners- { runExtract :: ExtractInput -> IO (Either ShikumiError ExtractOutput),- runConsolidate :: ConsolidateInput -> IO (Either ShikumiError ConsolidationDecision),- runScene :: SceneInput -> IO (Either ShikumiError SceneOutput),- runPersona :: PersonaInput -> IO (Either ShikumiError PersonaOutput)+ { runExtract :: !(ExtractInput -> IO (Either ShikumiError ExtractOutput)),+ runConsolidate :: !(ConsolidateInput -> IO (Either ShikumiError ConsolidationDecision)),+ runScene :: !(SceneInput -> IO (Either ShikumiError SceneOutput)),+ runPersona :: !(PersonaInput -> IO (Either ShikumiError PersonaOutput)) } testDistillRuntime :: IO DistillRuntime
src/Kioku/Distill/Scene.hs view
@@ -18,15 +18,15 @@ import Shikumi.Signature (Signature, mkSignature) data SceneInput = SceneInput- { scopeLabel :: Field "human label of the scope" Text,- atoms :: Field "the active memory atoms in this scope, newline-joined" Text+ { scopeLabel :: !(Field "human label of the scope" Text),+ atoms :: !(Field "the active memory atoms in this scope, newline-joined" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt) data SceneOutput = SceneOutput- { title :: Field "a short scene title, e.g. 'Testing & CI practices'" Text,- bodyMd :: Field "a markdown scene block summarizing the atoms as a narrative" Text+ { title :: !(Field "a short scene title, e.g. 'Testing & CI practices'" Text),+ bodyMd :: !(Field "a markdown scene block summarizing the atoms as a narrative" Text) } deriving stock (Generic, Eq, Show) deriving anyclass (ToSchema, FromModel, ToPrompt, Validatable)
src/Kioku/Memory.hs view
@@ -62,7 +62,8 @@ import Effectful.Error.Static (Error) import Keiro.Command (CommandError (..), defaultRunCommandOptions) import Keiro.Projection (runCommandWithProjections)-import Keiro.ReadModel (ConsistencyMode (..), ReadModelError, runQueryWith)+import Keiro.Projection.Catalog (typedInlineProjections)+import Keiro.ReadModel (QueryFreshness (Immediate), ReadModelError, runQueryWithFreshness) import Kioku.Api.Access ( MemoryAccessContext, MemoryPermission (..),@@ -93,10 +94,10 @@ memoriesBySessionRowsReadModel, memoriesByTypeRowsReadModel, memoryByIdReadModel,- memoryInlineProjection, memorySupersessionChainReadModel, ) import Kioku.Prelude+import Kioku.ProjectionCatalog (kiokuProjectionCatalog, memoryProjectionSet) import Kiroku.Store.Effect (Store) import Kiroku.Store.Effect.Resource (KirokuStoreResource) import Kiroku.Store.Error (StoreError)@@ -518,9 +519,9 @@ MemoryId -> Eff es (Either ReadModelError (Maybe MemoryRow)) lookupMemory space mid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoryByIdReadModel MemoryByIdQuery {memorySpaceId = space, memoryId = idText mid} @@ -538,9 +539,9 @@ Namespace -> Eff es (Either ReadModelError [MemoryRow]) getActiveRowsInNamespace space (Namespace ns) =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesByNamespaceRowsReadModel MemoriesByNamespaceQuery {memorySpaceId = space, namespace = ns} @@ -550,9 +551,9 @@ MemoryScope -> Eff es (Either ReadModelError [MemoryRow]) getActiveRowsByScope space scope =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesByScopeRowsReadModel MemoriesByScopeQuery { memorySpaceId = space,@@ -567,9 +568,9 @@ SessionId -> Eff es (Either ReadModelError [MemoryRow]) getRowsBySession space sid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesBySessionRowsReadModel MemoriesBySessionQuery {memorySpaceId = space, sessionId = idText sid} @@ -580,9 +581,9 @@ MemoryType -> Eff es (Either ReadModelError [MemoryRow]) getActiveRowsByType space (Namespace ns) memoryType =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesByTypeRowsReadModel MemoriesByTypeQuery { memorySpaceId = space,@@ -596,9 +597,9 @@ MemoryId -> Eff es (Either ReadModelError [MemoryRow]) getSupersessionChain space mid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memorySupersessionChainReadModel MemorySupersessionChainQuery {memorySpaceId = space, memoryId = idText mid} @@ -608,13 +609,17 @@ MemoryCommand -> Eff es (Either MemoryWriteError MemoryId) runMemoryCommand mid cmd = do+ -- Keiro's catalog cannot declare the shared framework-owned timer table as+ -- a Kioku target. Derive Kioku's handler from the catalog, keep the timer+ -- callback explicit, and run both in the append transaction; see+ -- docs/adr/catalog-application-projections-not-framework-timers.md. result <- runCommandWithProjections defaultRunCommandOptions memoryEventStream (memoryStream mid) cmd- [memoryInlineProjection, l2SceneTimerScheduleProjection]+ (typedInlineProjections kiokuProjectionCatalog memoryProjectionSet <> [l2SceneTimerScheduleProjection]) pure $ case result of Left err -> Left (MemoryCommandRejected err)
src/Kioku/Memory/EventStream.hs view
@@ -1,6 +1,7 @@ module Kioku.Memory.EventStream ( MemoryEventStream, memoryEventStream,+ validateMemoryEventStream, memoryCodec, memoryStream, parseMemoryEvent,@@ -15,7 +16,7 @@ import Keiki.Generics (emptyRegFile) import Keiro.Codec (Codec (..), EventType (..)) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))-import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)+import Keiro.EventStream.Validate (EventStreamWarning, ValidatedEventStream, mkEventStream) import Keiro.Stream (Stream) import Keiro.Stream qualified as Stream import Kioku.Id (MemoryId, idText)@@ -30,7 +31,11 @@ memoryEventStream :: ValidatedEventStream (HsPred MemoryRegs MemoryCommand) MemoryRegs MemoryVertex MemoryCommand MemoryEvent memoryEventStream =- mkEventStreamOrThrow "kioku-memory" memoryEventStreamDefinition+ either (error . ("invalid kioku memory event stream: " <>) . show) id validateMemoryEventStream++-- | The explicit startup proof for the hand-written memory stream.+validateMemoryEventStream :: Either [EventStreamWarning] (ValidatedEventStream (HsPred MemoryRegs MemoryCommand) MemoryRegs MemoryVertex MemoryCommand MemoryEvent)+validateMemoryEventStream = mkEventStream "kioku-memory" memoryEventStreamDefinition memoryEventStreamDefinition :: MemoryEventStream memoryEventStreamDefinition =
src/Kioku/Memory/ReadModel.hs view
@@ -14,15 +14,25 @@ MemoriesBySessionQuery (..), MemoriesByTypeQuery (..), MemorySupersessionChainQuery (..),+ memoryByIdReadModelBlueprint, memoryByIdReadModel,+ memoriesByNamespaceReadModelBlueprint, memoriesByNamespaceReadModel,+ memoriesByNamespaceRowsReadModelBlueprint, memoriesByNamespaceRowsReadModel,+ memoriesByScopeReadModelBlueprint, memoriesByScopeReadModel,+ memoriesByScopeRowsReadModelBlueprint, memoriesByScopeRowsReadModel,+ memoriesBySessionReadModelBlueprint, memoriesBySessionReadModel,+ memoriesBySessionRowsReadModelBlueprint, memoriesBySessionRowsReadModel,+ memoriesByTypeReadModelBlueprint, memoriesByTypeReadModel,+ memoriesByTypeRowsReadModelBlueprint, memoriesByTypeRowsReadModel,+ memorySupersessionChainReadModelBlueprint, memorySupersessionChainReadModel, ) where@@ -40,7 +50,12 @@ import Hasql.Statement (Statement, preparable) import Hasql.Transaction qualified as Tx import Keiro.Projection (InlineProjection (..))-import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), StrongScope (..))+import Keiro.ReadModel+ ( QueryCursorAuthority (NoQueryCursor),+ ReadModel,+ ReadModelBlueprint (..),+ immediateReadModel,+ ) import Kioku.Api.Access (MemorySpaceId) import Kioku.Api.Scope (scopeFromColumns, scopeKindText, scopeNamespaceText, scopeRefText) import Kioku.Api.Types (MemoryRecord (..), confidenceToText, memoryTypeToText)@@ -189,142 +204,152 @@ } memoryByIdReadModel :: ReadModel MemoryByIdQuery (Maybe MemoryRow)-memoryByIdReadModel =- ReadModel+memoryByIdReadModel = immediateReadModel memoryByIdReadModelBlueprint++memoryByIdReadModelBlueprint :: ReadModelBlueprint MemoryByIdQuery (Maybe MemoryRow)+memoryByIdReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memory-by-id", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectMemoryByIdStmt } memoriesByNamespaceReadModel :: ReadModel MemoriesByNamespaceQuery [MemoryRecord]-memoriesByNamespaceReadModel =- ReadModel+memoriesByNamespaceReadModel = immediateReadModel memoriesByNamespaceReadModelBlueprint++memoriesByNamespaceReadModelBlueprint :: ReadModelBlueprint MemoriesByNamespaceQuery [MemoryRecord]+memoriesByNamespaceReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memories-by-namespace", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectActiveByNamespaceStmt } memoriesByNamespaceRowsReadModel :: ReadModel MemoriesByNamespaceQuery [MemoryRow]-memoriesByNamespaceRowsReadModel =- ReadModel+memoriesByNamespaceRowsReadModel = immediateReadModel memoriesByNamespaceRowsReadModelBlueprint++memoriesByNamespaceRowsReadModelBlueprint :: ReadModelBlueprint MemoriesByNamespaceQuery [MemoryRow]+memoriesByNamespaceRowsReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memory-rows-by-namespace", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectActiveByNamespaceRowsStmt } memoriesByScopeReadModel :: ReadModel MemoriesByScopeQuery [MemoryRecord]-memoriesByScopeReadModel =- ReadModel+memoriesByScopeReadModel = immediateReadModel memoriesByScopeReadModelBlueprint++memoriesByScopeReadModelBlueprint :: ReadModelBlueprint MemoriesByScopeQuery [MemoryRecord]+memoriesByScopeReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memories-by-scope", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectActiveByScopeStmt } memoriesByScopeRowsReadModel :: ReadModel MemoriesByScopeQuery [MemoryRow]-memoriesByScopeRowsReadModel =- ReadModel+memoriesByScopeRowsReadModel = immediateReadModel memoriesByScopeRowsReadModelBlueprint++memoriesByScopeRowsReadModelBlueprint :: ReadModelBlueprint MemoriesByScopeQuery [MemoryRow]+memoriesByScopeRowsReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memory-rows-by-scope", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectActiveByScopeRowsStmt } memoriesBySessionReadModel :: ReadModel MemoriesBySessionQuery [MemoryRecord]-memoriesBySessionReadModel =- ReadModel+memoriesBySessionReadModel = immediateReadModel memoriesBySessionReadModelBlueprint++memoriesBySessionReadModelBlueprint :: ReadModelBlueprint MemoriesBySessionQuery [MemoryRecord]+memoriesBySessionReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memories-by-session", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectBySessionStmt } memoriesBySessionRowsReadModel :: ReadModel MemoriesBySessionQuery [MemoryRow]-memoriesBySessionRowsReadModel =- ReadModel+memoriesBySessionRowsReadModel = immediateReadModel memoriesBySessionRowsReadModelBlueprint++memoriesBySessionRowsReadModelBlueprint :: ReadModelBlueprint MemoriesBySessionQuery [MemoryRow]+memoriesBySessionRowsReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memory-rows-by-session", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectBySessionRowsStmt } memoriesByTypeReadModel :: ReadModel MemoriesByTypeQuery [MemoryRecord]-memoriesByTypeReadModel =- ReadModel+memoriesByTypeReadModel = immediateReadModel memoriesByTypeReadModelBlueprint++memoriesByTypeReadModelBlueprint :: ReadModelBlueprint MemoriesByTypeQuery [MemoryRecord]+memoriesByTypeReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memories-by-type", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectByTypeStmt } memoriesByTypeRowsReadModel :: ReadModel MemoriesByTypeQuery [MemoryRow]-memoriesByTypeRowsReadModel =- ReadModel+memoriesByTypeRowsReadModel = immediateReadModel memoriesByTypeRowsReadModelBlueprint++memoriesByTypeRowsReadModelBlueprint :: ReadModelBlueprint MemoriesByTypeQuery [MemoryRow]+memoriesByTypeRowsReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memory-rows-by-type", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectByTypeRowsStmt } memorySupersessionChainReadModel :: ReadModel MemorySupersessionChainQuery [MemoryRow]-memorySupersessionChainReadModel =- ReadModel+memorySupersessionChainReadModel = immediateReadModel memorySupersessionChainReadModelBlueprint++memorySupersessionChainReadModelBlueprint :: ReadModelBlueprint MemorySupersessionChainQuery [MemoryRow]+memorySupersessionChainReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-memory-supersession-chain", schema = kiokuSchema, tableName = memoriesRelation,- subscriptionName = "kioku-memory-inline", version = memoryReadModelVersion, shapeHash = memoryReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSupersessionChainStmt }
+ src/Kioku/ProjectionCatalog.hs view
@@ -0,0 +1,247 @@+-- | Kioku's complete application-owned projection inventory.+--+-- The catalog deliberately stops at the @kioku@ schema boundary. Timer+-- scheduling writes Keiro's shared @keiro.keiro_timers@ table and therefore+-- remains an explicitly unmanaged transactional side effect at command call+-- sites; declaring that table here would falsely give one Kioku projection+-- exclusive ownership of framework state.+module Kioku.ProjectionCatalog+ ( kiokuProjectionCatalog,+ validateKiokuProjectionCatalog,+ validateKiokuProjectionCatalogWith,+ memoryProjectionSet,+ sessionProjectionSet,+ kiokuCatalogInventory,+ kiokuCatalogFingerprint,+ renderKiokuCatalogDiagnostics,+ )+where++import Data.List.NonEmpty qualified as NonEmpty+import Data.Text qualified as Text+import Keiro.Projection (InlineProjection (..))+import Keiro.Projection.Catalog+ ( CatalogDiagnostic (..),+ CatalogFingerprint,+ CatalogInventory,+ ClaimSite,+ ProjectionCatalog (..),+ ProjectionDefinition (..),+ ProjectionHandler (..),+ ProjectionReplayPolicy (..),+ ProjectionSet (..),+ QualifiedTable (..),+ QueryModelBinding (..),+ RebuildGroupDeclaration (..),+ RebuildGroupId,+ SomeProjectionSet (..),+ SomeQueryModelBinding (..),+ SourceDeclaration (..),+ SourceId,+ SourceScope (..),+ TargetDeclaration (..),+ TargetId,+ TargetResetPolicy (..),+ ValidatedProjectionCatalog,+ Validation (..),+ catalogFingerprint,+ catalogInventory,+ diagnosticCodeText,+ mkClaimSite,+ mkProjectionId,+ mkQueryModelId,+ mkRebuildGroupId,+ mkSourceId,+ mkTargetId,+ replayAdapterFromCodec,+ validateProjectionCatalog,+ )+import Keiro.ReadModel (ReadModel (..))+import Kioku.Database.Schema (kiokuSchema, memoriesRelation, sessionsRelation, turnsRelation)+import Kioku.Memory.Domain (MemoryEvent)+import Kioku.Memory.EventStream (memoryCodec)+import Kioku.Memory.ReadModel+ ( memoriesByNamespaceReadModel,+ memoriesByNamespaceRowsReadModel,+ memoriesByScopeReadModel,+ memoriesByScopeRowsReadModel,+ memoriesBySessionReadModel,+ memoriesBySessionRowsReadModel,+ memoriesByTypeReadModel,+ memoriesByTypeRowsReadModel,+ memoryByIdReadModel,+ memoryInlineProjection,+ memorySupersessionChainReadModel,+ )+import Kioku.Prelude+import Kioku.Session.Domain (SessionEvent)+import Kioku.Session.EventStream (sessionCodec)+import Kioku.Session.ReadModel+ ( awaitingSessionsByCorrelationKeyReadModel,+ sessionByIdReadModel,+ sessionChainReadModel,+ sessionDelegationChildrenReadModel,+ sessionInlineProjection,+ sessionsByFocusReadModel,+ sessionsByNamespaceReadModel,+ sessionsByScopeReadModel,+ sessionsByStartedRangeReadModel,+ turnsBySessionReadModel,+ )+import Kiroku.Store.Types (CategoryName (..))++memorySourceId, sessionSourceId :: SourceId+memorySourceId = must (mkSourceId "kioku-memory-events")+sessionSourceId = must (mkSourceId "kioku-session-events")++memoriesTargetId, sessionsTargetId, turnsTargetId :: TargetId+memoriesTargetId = must (mkTargetId "kioku-memories")+sessionsTargetId = must (mkTargetId "kioku-sessions")+turnsTargetId = must (mkTargetId "kioku-turns")++memoryGroupId, sessionGroupId :: RebuildGroupId+memoryGroupId = must (mkRebuildGroupId "kioku-memory")+sessionGroupId = must (mkRebuildGroupId "kioku-session")++memoryProjectionSet :: ProjectionSet MemoryEvent+memoryProjectionSet =+ ProjectionSet+ { projectionSource = memorySourceId,+ projectionDefinitions =+ ProjectionDefinition+ { projectionId = must (mkProjectionId "kioku-memory-inline"),+ rebuildGroup = memoryGroupId,+ ownedTargets = memoriesTargetId :| [],+ replayPolicy = Replayable (replayAdapterFromCodec memoryCodec memoryInlineProjection.apply),+ handlers = InlineHandler memoryInlineProjection (site "memory inline handler") :| [],+ claimSite = site "memory projection owner"+ }+ :| [],+ claimSite = site "memory projection set"+ }++sessionProjectionSet :: ProjectionSet SessionEvent+sessionProjectionSet =+ ProjectionSet+ { projectionSource = sessionSourceId,+ projectionDefinitions =+ ProjectionDefinition+ { projectionId = must (mkProjectionId "kioku-session-inline"),+ rebuildGroup = sessionGroupId,+ ownedTargets = sessionsTargetId :| [turnsTargetId],+ replayPolicy = Replayable (replayAdapterFromCodec sessionCodec sessionInlineProjection.apply),+ handlers = InlineHandler sessionInlineProjection (site "session inline handler") :| [],+ claimSite = site "session projection owner"+ }+ :| [],+ claimSite = site "session projection set"+ }++kiokuProjectionCatalogDefinition :: ProjectionCatalog+kiokuProjectionCatalogDefinition =+ ProjectionCatalog+ { sources =+ [ SourceDeclaration memorySourceId (CategorySource (CategoryName "kioku_memory")) "kioku-memory-codec-v1" (site "memory event source"),+ SourceDeclaration sessionSourceId (CategorySource (CategoryName "kioku_session")) "kioku-session-codec-v1" (site "session event source")+ ],+ targets =+ [ TargetDeclaration memoriesTargetId (QualifiedTable kiokuSchema memoriesRelation) ClearBeforeReplay [] (site "memories target"),+ TargetDeclaration sessionsTargetId (QualifiedTable kiokuSchema sessionsRelation) ClearBeforeReplay [] (site "sessions target"),+ TargetDeclaration turnsTargetId (QualifiedTable kiokuSchema turnsRelation) ClearBeforeReplay [sessionsTargetId] (site "turns target")+ ],+ rebuildGroups =+ [ RebuildGroupDeclaration memoryGroupId [memoriesTargetId] [] (site "memory rebuild group"),+ RebuildGroupDeclaration sessionGroupId [sessionsTargetId, turnsTargetId] [] (site "session rebuild group")+ ],+ projectionRevisions = [],+ externalReadContracts = [],+ subscriptions = [],+ dedupKeys = [],+ queryModels = memoryQueryBindings <> sessionQueryBindings,+ projectionSets =+ [ SomeProjectionSet memoryProjectionSet,+ SomeProjectionSet sessionProjectionSet+ ]+ }++memoryQueryBindings :: [SomeQueryModelBinding]+memoryQueryBindings =+ [ binding memoryByIdReadModel memoryGroupId memoriesTargetId,+ binding memoriesByNamespaceReadModel memoryGroupId memoriesTargetId,+ binding memoriesByNamespaceRowsReadModel memoryGroupId memoriesTargetId,+ binding memoriesByScopeReadModel memoryGroupId memoriesTargetId,+ binding memoriesByScopeRowsReadModel memoryGroupId memoriesTargetId,+ binding memoriesBySessionReadModel memoryGroupId memoriesTargetId,+ binding memoriesBySessionRowsReadModel memoryGroupId memoriesTargetId,+ binding memoriesByTypeReadModel memoryGroupId memoriesTargetId,+ binding memoriesByTypeRowsReadModel memoryGroupId memoriesTargetId,+ binding memorySupersessionChainReadModel memoryGroupId memoriesTargetId+ ]++sessionQueryBindings :: [SomeQueryModelBinding]+sessionQueryBindings =+ [ binding sessionByIdReadModel sessionGroupId sessionsTargetId,+ binding sessionsByNamespaceReadModel sessionGroupId sessionsTargetId,+ binding sessionsByScopeReadModel sessionGroupId sessionsTargetId,+ binding sessionsByFocusReadModel sessionGroupId sessionsTargetId,+ binding sessionsByStartedRangeReadModel sessionGroupId sessionsTargetId,+ binding sessionChainReadModel sessionGroupId sessionsTargetId,+ binding sessionDelegationChildrenReadModel sessionGroupId sessionsTargetId,+ binding awaitingSessionsByCorrelationKeyReadModel sessionGroupId sessionsTargetId,+ binding turnsBySessionReadModel sessionGroupId turnsTargetId+ ]++binding :: ReadModel q r -> RebuildGroupId -> TargetId -> SomeQueryModelBinding+binding readModel rebuildGroup observedTarget =+ SomeQueryModelBinding+ QueryModelBinding+ { queryModelId = must (mkQueryModelId readModel.name),+ readModel,+ rebuildGroup,+ observedTargets = [observedTarget],+ claimSite = site ("query model " <> readModel.name)+ }++validateKiokuProjectionCatalog :: Validation (NonEmpty CatalogDiagnostic) ValidatedProjectionCatalog+validateKiokuProjectionCatalog = validateKiokuProjectionCatalogWith id++-- | Validate a deliberately transformed definition. Production code should+-- use 'validateKiokuProjectionCatalog'; this seam exists so tests can prove+-- malformed ownership and fingerprint declarations fail closed without+-- exporting Kioku's unvalidated catalog as a runtime value.+validateKiokuProjectionCatalogWith ::+ (ProjectionCatalog -> ProjectionCatalog) ->+ Validation (NonEmpty CatalogDiagnostic) ValidatedProjectionCatalog+validateKiokuProjectionCatalogWith modifyCatalog =+ validateProjectionCatalog (modifyCatalog kiokuProjectionCatalogDefinition)++kiokuProjectionCatalog :: ValidatedProjectionCatalog+kiokuProjectionCatalog =+ case validateKiokuProjectionCatalog of+ Success catalog -> catalog+ Failure diagnostics ->+ error (Text.unpack (renderKiokuCatalogDiagnostics diagnostics))++kiokuCatalogInventory :: CatalogInventory+kiokuCatalogInventory = catalogInventory kiokuProjectionCatalog++kiokuCatalogFingerprint :: CatalogFingerprint+kiokuCatalogFingerprint = catalogFingerprint kiokuProjectionCatalog++renderKiokuCatalogDiagnostics :: NonEmpty CatalogDiagnostic -> Text+renderKiokuCatalogDiagnostics diagnostics =+ Text.intercalate+ "; "+ [ diagnosticCodeText diagnostic.diagnosticCode+ <> " ("+ <> diagnostic.diagnosticIdentity+ <> "): "+ <> diagnostic.diagnosticMessage+ | diagnostic <- NonEmpty.toList diagnostics+ ]++site :: Text -> ClaimSite+site = must . mkClaimSite++must :: (Show error) => Either error value -> value+must = either (error . show) id
src/Kioku/ReadModel.hs view
@@ -12,9 +12,9 @@ -- 'Keiro.ReadModel.ReadModelStaleSchema'. -- -- 'reconcileReadModelRegistry' repairs those rows to the identity the compiled--- code expects, deriving every name, version, and shape hash from--- 'kiokuReadModelSchemas' — the same 'ReadModel' values the queries use, so the--- registry can never disagree with the code. The @kioku-migrate@ executable runs+-- code expects. 'kiokuReadModelSchemas' is derived from the same validated+-- projection catalog that registers and supplies the queries, so the migration+-- reconciler cannot drift into a second runtime inventory. The @kioku-migrate@ executable runs -- it immediately after applying migrations, which is why a read-model version -- bump needs no hand-written registry SQL. A host that applies migrations as a -- library (by running @Kioku.Migrations.kiokuMigrationPlan@ through pg-migrate)@@ -34,32 +34,11 @@ where import Effectful (Eff, (:>))-import Keiro.ReadModel (ReadModel (..))+import Keiro.Projection.Catalog (CatalogRegistration (..), catalogRegistrations)+import Keiro.ReadModel.Rebuild (CatalogRegistrationError, GroupRebuildMetadata, registerProjectionCatalog) import Keiro.ReadModel.Schema qualified as Schema-import Kioku.Memory.ReadModel- ( memoriesByNamespaceReadModel,- memoriesByNamespaceRowsReadModel,- memoriesByScopeReadModel,- memoriesByScopeRowsReadModel,- memoriesBySessionReadModel,- memoriesBySessionRowsReadModel,- memoriesByTypeReadModel,- memoriesByTypeRowsReadModel,- memoryByIdReadModel,- memorySupersessionChainReadModel,- ) import Kioku.Prelude-import Kioku.Session.ReadModel- ( awaitingSessionsByCorrelationKeyReadModel,- sessionByIdReadModel,- sessionChainReadModel,- sessionDelegationChildrenReadModel,- sessionsByFocusReadModel,- sessionsByNamespaceReadModel,- sessionsByScopeReadModel,- sessionsByStartedRangeReadModel,- turnsBySessionReadModel,- )+import Kioku.ProjectionCatalog (kiokuProjectionCatalog) import Kiroku.Store.Effect (Store) -- | The registry identity of a read model: its logical name plus the schema@@ -71,47 +50,22 @@ } deriving stock (Eq, Show) -schemaOf :: ReadModel q r -> ReadModelSchema-schemaOf rm = ReadModelSchema rm.name rm.version rm.shapeHash+schemaOf :: CatalogRegistration -> ReadModelSchema+schemaOf registration =+ ReadModelSchema registration.registryName registration.version registration.shapeHash -- | Every Kioku read model paired with the schema identity the current code -- expects. Ordered session models first, then memory models. kiokuReadModelSchemas :: [ReadModelSchema]-kiokuReadModelSchemas =- [ schemaOf sessionByIdReadModel,- schemaOf sessionsByNamespaceReadModel,- schemaOf sessionsByScopeReadModel,- schemaOf sessionsByFocusReadModel,- schemaOf sessionsByStartedRangeReadModel,- schemaOf sessionChainReadModel,- schemaOf sessionDelegationChildrenReadModel,- schemaOf awaitingSessionsByCorrelationKeyReadModel,- schemaOf turnsBySessionReadModel,- schemaOf memoryByIdReadModel,- schemaOf memoriesByNamespaceReadModel,- schemaOf memoriesByNamespaceRowsReadModel,- schemaOf memoriesByScopeReadModel,- schemaOf memoriesByScopeRowsReadModel,- schemaOf memoriesBySessionReadModel,- schemaOf memoriesBySessionRowsReadModel,- schemaOf memoriesByTypeReadModel,- schemaOf memoriesByTypeRowsReadModel,- schemaOf memorySupersessionChainReadModel- ]+kiokuReadModelSchemas = schemaOf <$> catalogRegistrations kiokuProjectionCatalog --- | Register every Kioku read model at application startup.+-- | Register Kioku's validated projection catalog at application startup. ----- Keiro 0.3 deliberately stopped registering models on their first query. This--- operation is idempotent and leaves an existing row unchanged, allowing Keiro--- to continue failing closed when its version or shape hash is stale.-registerKiokuReadModels :: (Store :> es) => Eff es ()-registerKiokuReadModels =- forM_ kiokuReadModelSchemas \schema ->- void $- Schema.registerReadModel- schema.readModelName- schema.readModelVersion- schema.readModelShapeHash+-- This registers the query identities, rebuild groups, and persisted catalog+-- fingerprint in one idempotent handshake. A conflicting fingerprint or stale+-- query identity is returned as a typed registration error.+registerKiokuReadModels :: (Store :> es) => Eff es (Either CatalogRegistrationError [GroupRebuildMetadata])+registerKiokuReadModels = registerProjectionCatalog kiokuProjectionCatalog -- | What reconciliation did to one read model's registry row. data ReconcileOutcome
src/Kioku/Recall.hs view
@@ -97,7 +97,7 @@ import Hasql.Encoders qualified as E import Hasql.Statement (Statement, preparable) import Hasql.Transaction qualified as Tx-import Keiro.ReadModel (ConsistencyMode (..), ReadModelError, runQueryWith)+import Keiro.ReadModel (QueryFreshness (Immediate), ReadModelError, runQueryWithFreshness) import Kioku.AI.Config import Kioku.AI.Runtime (AIRuntime, runtimeEmbeddingModel) import Kioku.Api.Access (MemoryAccessContext, MemorySpaceId, memoryContextSpace)@@ -1152,9 +1152,9 @@ MemoryScope -> Eff es (Either ReadModelError [MemoryRecord]) getActiveByScope space scope =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesByScopeReadModel MemoriesByScopeQuery { memorySpaceId = space,@@ -1171,9 +1171,9 @@ Namespace -> Eff es (Either ReadModelError [MemoryRecord]) getActiveInNamespace space (Namespace ns) =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesByNamespaceReadModel MemoriesByNamespaceQuery {memorySpaceId = space, namespace = ns} @@ -1194,9 +1194,9 @@ Eff es (Either ReadModelError (Maybe MemoryRecord)) getById space mid = fmap (fmap (fmap memoryRowToRecord)) $- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoryByIdReadModel MemoryByIdQuery {memorySpaceId = space, memoryId = idText mid} @@ -1206,9 +1206,9 @@ SessionId -> Eff es (Either ReadModelError [MemoryRecord]) getBySession space sid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesBySessionReadModel MemoriesBySessionQuery {memorySpaceId = space, sessionId = idText sid} @@ -1219,9 +1219,9 @@ MemoryType -> Eff es (Either ReadModelError [MemoryRecord]) getByType space (Namespace ns) mt =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate memoriesByTypeReadModel MemoriesByTypeQuery {memorySpaceId = space, namespace = ns, memoryType = memoryTypeToText mt}
src/Kioku/Session.hs view
@@ -56,7 +56,8 @@ import Effectful.Error.Static (Error) import Keiro.Command (CommandError (..), defaultRunCommandOptions) import Keiro.Projection (runCommandWithProjections)-import Keiro.ReadModel (ConsistencyMode (..), ReadModelError, runQueryWith)+import Keiro.Projection.Catalog (typedInlineProjections)+import Keiro.ReadModel (QueryFreshness (Immediate), ReadModelError, runQueryWithFreshness) import Kioku.Api.Access ( MemoryAccessContext, MemoryPermission (..),@@ -72,6 +73,7 @@ import Kioku.Distill.Timer (l1TimerScheduleProjection) import Kioku.Id (SessionId, idText) import Kioku.Prelude+import Kioku.ProjectionCatalog (kiokuProjectionCatalog, sessionProjectionSet) import Kioku.Session.Domain import Kioku.Session.EventStream (sessionEventStream, sessionStream) import Kioku.Session.ReadModel@@ -90,7 +92,6 @@ sessionByIdReadModel, sessionChainReadModel, sessionDelegationChildrenReadModel,- sessionInlineProjection, sessionsByFocusReadModel, sessionsByNamespaceReadModel, sessionsByScopeReadModel,@@ -675,9 +676,9 @@ SessionId -> Eff es (Either ReadModelError (Maybe SessionRow)) getById space sid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionByIdReadModel SessionByIdQuery {memorySpaceId = space, sessionId = idText sid} @@ -688,9 +689,9 @@ Int -> Eff es (Either ReadModelError [SessionRow]) getRecentInNamespace space ns limit =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionsByNamespaceReadModel SessionsByNamespaceQuery {memorySpaceId = space, namespace = namespaceText ns, limit} @@ -700,9 +701,9 @@ MemoryScope -> Eff es (Either ReadModelError [SessionRow]) getByScope space scope =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionsByScopeReadModel SessionsByScopeQuery { memorySpaceId = space,@@ -718,9 +719,9 @@ Text -> Eff es (Either ReadModelError [SessionRow]) getByFocus space ns focus =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionsByFocusReadModel SessionsByFocusQuery {memorySpaceId = space, namespace = namespaceText ns, focus} @@ -732,9 +733,9 @@ UTCTime -> Eff es (Either ReadModelError [SessionRow]) getByStartedRange space ns startedAfter startedBefore =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionsByStartedRangeReadModel SessionsByStartedRangeQuery { memorySpaceId = space,@@ -749,9 +750,9 @@ SessionId -> Eff es (Either ReadModelError [SessionRow]) getChain space sid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionChainReadModel SessionChainQuery {memorySpaceId = space, sessionId = idText sid} @@ -761,9 +762,9 @@ SessionId -> Eff es (Either ReadModelError [SessionRow]) getDelegationChildren space sid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate sessionDelegationChildrenReadModel SessionDelegationChildrenQuery {memorySpaceId = space, parentSessionId = idText sid} @@ -774,9 +775,9 @@ Text -> Eff es (Either ReadModelError [SessionRow]) getAwaitingByCorrelationKey space ns correlationKey =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate awaitingSessionsByCorrelationKeyReadModel AwaitingSessionsByCorrelationKeyQuery { memorySpaceId = space,@@ -793,9 +794,9 @@ SessionId -> Eff es (Either ReadModelError [TurnRow]) getTurns space sid =- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate turnsBySessionReadModel TurnsBySessionQuery {memorySpaceId = space, sessionId = idText sid} @@ -805,13 +806,17 @@ SessionCommand -> Eff es (Either SessionWriteError SessionId) runSessionCommand sid cmd = do+ -- Keiro's catalog cannot declare the shared framework-owned timer table as+ -- a Kioku target. Derive Kioku's handlers from the catalog, keep the timer+ -- callback explicit, and run both in the append transaction; see+ -- docs/adr/catalog-application-projections-not-framework-timers.md. result <- runCommandWithProjections defaultRunCommandOptions sessionEventStream (sessionStream sid) cmd- [sessionInlineProjection, l1TimerScheduleProjection]+ (typedInlineProjections kiokuProjectionCatalog sessionProjectionSet <> [l1TimerScheduleProjection]) pure $ case result of Left err -> Left (SessionCommandRejected err)
src/Kioku/Session/EventStream.hs view
@@ -1,6 +1,7 @@ module Kioku.Session.EventStream ( SessionEventStream, sessionEventStream,+ validateSessionEventStream, sessionCodec, sessionStream, parseSessionEvent,@@ -15,7 +16,7 @@ import Keiki.Generics (emptyRegFile) import Keiro.Codec (Codec (..), EventType (..)) import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))-import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)+import Keiro.EventStream.Validate (EventStreamWarning, ValidatedEventStream, mkEventStream) import Keiro.Stream (Stream) import Keiro.Stream qualified as Stream import Kioku.Id (SessionId, idText)@@ -30,7 +31,11 @@ sessionEventStream :: ValidatedEventStream (HsPred SessionRegs SessionCommand) SessionRegs SessionVertex SessionCommand SessionEvent sessionEventStream =- mkEventStreamOrThrow "kioku-session" sessionEventStreamDefinition+ either (error . ("invalid kioku session event stream: " <>) . show) id validateSessionEventStream++-- | The explicit startup proof for the hand-written session stream.+validateSessionEventStream :: Either [EventStreamWarning] (ValidatedEventStream (HsPred SessionRegs SessionCommand) SessionRegs SessionVertex SessionCommand SessionEvent)+validateSessionEventStream = mkEventStream "kioku-session" sessionEventStreamDefinition sessionEventStreamDefinition :: SessionEventStream sessionEventStreamDefinition =
src/Kioku/Session/ReadModel.hs view
@@ -16,14 +16,23 @@ SessionDelegationChildrenQuery (..), AwaitingSessionsByCorrelationKeyQuery (..), TurnsBySessionQuery (..),+ sessionByIdReadModelBlueprint, sessionByIdReadModel,+ sessionsByNamespaceReadModelBlueprint, sessionsByNamespaceReadModel,+ sessionsByScopeReadModelBlueprint, sessionsByScopeReadModel,+ sessionsByFocusReadModelBlueprint, sessionsByFocusReadModel,+ sessionsByStartedRangeReadModelBlueprint, sessionsByStartedRangeReadModel,+ sessionChainReadModelBlueprint, sessionChainReadModel,+ sessionDelegationChildrenReadModelBlueprint, sessionDelegationChildrenReadModel,+ awaitingSessionsByCorrelationKeyReadModelBlueprint, awaitingSessionsByCorrelationKeyReadModel,+ turnsBySessionReadModelBlueprint, turnsBySessionReadModel, ) where@@ -36,7 +45,12 @@ import Hasql.Statement (Statement, preparable) import Hasql.Transaction qualified as Tx import Keiro.Projection (InlineProjection (..))-import Keiro.ReadModel (ConsistencyMode (..), ReadModel (..), StrongScope (..))+import Keiro.ReadModel+ ( QueryCursorAuthority (NoQueryCursor),+ ReadModel,+ ReadModelBlueprint (..),+ immediateReadModel,+ ) import Kioku.Api.Access (MemorySpaceId) import Kioku.Api.Scope (scopeKindText, scopeNamespaceText, scopeRefText) import Kioku.Database.Schema@@ -294,128 +308,137 @@ } sessionByIdReadModel :: ReadModel SessionByIdQuery (Maybe SessionRow)-sessionByIdReadModel =- ReadModel+sessionByIdReadModel = immediateReadModel sessionByIdReadModelBlueprint++sessionByIdReadModelBlueprint :: ReadModelBlueprint SessionByIdQuery (Maybe SessionRow)+sessionByIdReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-session-by-id", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSessionByIdStmt } sessionsByNamespaceReadModel :: ReadModel SessionsByNamespaceQuery [SessionRow]-sessionsByNamespaceReadModel =- ReadModel+sessionsByNamespaceReadModel = immediateReadModel sessionsByNamespaceReadModelBlueprint++sessionsByNamespaceReadModelBlueprint :: ReadModelBlueprint SessionsByNamespaceQuery [SessionRow]+sessionsByNamespaceReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-sessions-by-namespace", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSessionsByNamespaceStmt } sessionsByScopeReadModel :: ReadModel SessionsByScopeQuery [SessionRow]-sessionsByScopeReadModel =- ReadModel+sessionsByScopeReadModel = immediateReadModel sessionsByScopeReadModelBlueprint++sessionsByScopeReadModelBlueprint :: ReadModelBlueprint SessionsByScopeQuery [SessionRow]+sessionsByScopeReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-sessions-by-scope", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSessionsByScopeStmt } sessionsByFocusReadModel :: ReadModel SessionsByFocusQuery [SessionRow]-sessionsByFocusReadModel =- ReadModel+sessionsByFocusReadModel = immediateReadModel sessionsByFocusReadModelBlueprint++sessionsByFocusReadModelBlueprint :: ReadModelBlueprint SessionsByFocusQuery [SessionRow]+sessionsByFocusReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-sessions-by-focus", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSessionsByFocusStmt } sessionsByStartedRangeReadModel :: ReadModel SessionsByStartedRangeQuery [SessionRow]-sessionsByStartedRangeReadModel =- ReadModel+sessionsByStartedRangeReadModel = immediateReadModel sessionsByStartedRangeReadModelBlueprint++sessionsByStartedRangeReadModelBlueprint :: ReadModelBlueprint SessionsByStartedRangeQuery [SessionRow]+sessionsByStartedRangeReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-sessions-by-started-range", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSessionsByStartedRangeStmt } sessionChainReadModel :: ReadModel SessionChainQuery [SessionRow]-sessionChainReadModel =- ReadModel+sessionChainReadModel = immediateReadModel sessionChainReadModelBlueprint++sessionChainReadModelBlueprint :: ReadModelBlueprint SessionChainQuery [SessionRow]+sessionChainReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-session-chain", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectSessionChainStmt } sessionDelegationChildrenReadModel :: ReadModel SessionDelegationChildrenQuery [SessionRow]-sessionDelegationChildrenReadModel =- ReadModel+sessionDelegationChildrenReadModel = immediateReadModel sessionDelegationChildrenReadModelBlueprint++sessionDelegationChildrenReadModelBlueprint :: ReadModelBlueprint SessionDelegationChildrenQuery [SessionRow]+sessionDelegationChildrenReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-session-delegation-children", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectDelegationChildrenStmt } awaitingSessionsByCorrelationKeyReadModel :: ReadModel AwaitingSessionsByCorrelationKeyQuery [SessionRow]-awaitingSessionsByCorrelationKeyReadModel =- ReadModel+awaitingSessionsByCorrelationKeyReadModel = immediateReadModel awaitingSessionsByCorrelationKeyReadModelBlueprint++awaitingSessionsByCorrelationKeyReadModelBlueprint :: ReadModelBlueprint AwaitingSessionsByCorrelationKeyQuery [SessionRow]+awaitingSessionsByCorrelationKeyReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-sessions-awaiting-by-correlation-key", schema = kiokuSchema, tableName = sessionsRelation,- subscriptionName = "kioku-session-inline", version = sessionReadModelVersion, shapeHash = sessionReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectAwaitingByCorrelationKeyStmt } turnsBySessionReadModel :: ReadModel TurnsBySessionQuery [TurnRow]-turnsBySessionReadModel =- ReadModel+turnsBySessionReadModel = immediateReadModel turnsBySessionReadModelBlueprint++turnsBySessionReadModelBlueprint :: ReadModelBlueprint TurnsBySessionQuery [TurnRow]+turnsBySessionReadModelBlueprint =+ ReadModelBlueprint { name = "kioku-turns-by-session", schema = kiokuSchema, tableName = turnsRelation,- subscriptionName = "kioku-session-inline", version = turnReadModelVersion, shapeHash = turnReadModelShapeHash,- defaultConsistency = Eventual,- strongScope = EntireLog,+ cursorAuthority = NoQueryCursor, query = \q -> Tx.statement q selectTurnsBySessionStmt }
+ test/Kioku/ProjectionCatalogSpec.hs view
@@ -0,0 +1,353 @@+module Kioku.ProjectionCatalogSpec (tests) where++import Data.Foldable (for_)+import Data.List.NonEmpty (NonEmpty (..))+import Data.List.NonEmpty qualified as NonEmpty+import Data.Set qualified as Set+import Data.Time (getCurrentTime)+import Data.Vector qualified as Vector+import Effectful (liftIO)+import Keiro.Command+ ( SqlCommandOutcome (..),+ SqlTransactionDecision (..),+ defaultRunCommandOptions,+ runCommandWithSqlEventsControlled,+ )+import Keiro.Projection (InlineProjection (..))+import Keiro.Projection.Catalog+ ( CatalogDiagnostic (..),+ CatalogDiagnosticCode (..),+ CatalogInventory (..),+ InventoryQueryCursor,+ InventoryQueryFreshness (..),+ InventoryQueryModel (..),+ ProjectionCatalog (..),+ ProjectionDefinition (..),+ ProjectionSet (..),+ SomeProjectionSet (..),+ SourceDeclaration (..),+ Validation (..),+ catalogFingerprintText,+ catalogRegistrations,+ mkClaimSite,+ mkProjectionId,+ mkTargetId,+ typedInlineProjections,+ )+import Keiro.ReadModel+ ( HeadScope (EntireVisibleLog),+ QueryFreshness (WaitForHead),+ ReadModelDefinitionError (ReadModelDefinitionMissingCursor),+ headWaitingReadModel,+ )+import Keiro.ReadModel.Rebuild (registerProjectionCatalog)+import Keiro.Stream qualified as Stream+import Keiro.Timer (lookupTimer)+import Kioku.Api.Scope (MemoryScope (..), Namespace (..))+import Kioku.Api.Types (Confidence (..), MemoryType (..))+import Kioku.App (runAppIO, withNoopAppEnv)+import Kioku.Distill.L2 (l2SceneTimerId, l2SceneTimerScheduleProjection)+import Kioku.Id (genMemoryId, genSessionId, idText)+import Kioku.Memory qualified as Memory+import Kioku.Memory.Domain (MemoryCommand (..), RecordMemoryData (..))+import Kioku.Memory.EventStream (memoryEventStream, memoryStream, validateMemoryEventStream)+import Kioku.Memory.ReadModel (memoryByIdReadModelBlueprint)+import Kioku.Migrations.TestSupport (withKiokuMigratedDatabase)+import Kioku.ProjectionCatalog+ ( kiokuCatalogFingerprint,+ kiokuCatalogInventory,+ kiokuProjectionCatalog,+ memoryProjectionSet,+ sessionProjectionSet,+ validateKiokuProjectionCatalog,+ validateKiokuProjectionCatalogWith,+ )+import Kioku.Session qualified as Session+import Kioku.Session.Domain (StartSessionData (..))+import Kioku.Session.EventStream (validateSessionEventStream)+import Kioku.SpaceFixtures (testActorPrincipal, testContext, testSpace)+import Kiroku.Store.Connection (defaultConnectionSettings)+import Kiroku.Store.Read (readStreamForward)+import Kiroku.Store.Types (StreamVersion (..))+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)++tests :: TestTree+tests =+ testGroup+ "ProjectionCatalog"+ [ testCase "the Kioku catalog has one stable complete inventory" testInventory,+ testCase "typed projection sets select only application-owned handlers" testTypedHandlers,+ testCase "both hand-written event streams validate explicitly" testEventStreams,+ testCase "duplicate target ownership is rejected with a stable diagnostic" testDuplicateOwnership,+ testCase "an unknown owned target is rejected with a stable diagnostic" testUnknownOwnership,+ testCase "a target without its supplier is rejected at validation" testMissingSupplier,+ testCase "a cursorless query model cannot be made waiting" testWaitingWithoutCursor,+ testCase "registered catalog serves immediate memory and session queries" testImmediateQueries,+ testCase "a late projection failure rolls back events, rows, and timers" testProjectionRollback,+ testCase "persisted catalog fingerprint drift refuses registration" testFingerprintDrift+ ]++testInventory :: Assertion+testInventory = do+ case validateKiokuProjectionCatalog of+ Failure diagnostics -> assertFailure ("valid Kioku catalog was rejected: " <> show diagnostics)+ Success _ -> pure ()+ assertEqual "sources" 2 (length kiokuCatalogInventory.inventorySources)+ assertEqual "targets" 3 (length kiokuCatalogInventory.inventoryTargets)+ assertEqual "groups" 2 (length kiokuCatalogInventory.inventoryGroups)+ assertEqual "projection owners" 2 (length kiokuCatalogInventory.inventoryProjections)+ assertEqual "query models" 19 (length kiokuCatalogInventory.inventoryQueryModels)+ assertEqual "registrations" 19 (length (catalogRegistrations kiokuProjectionCatalog))+ assertBool+ "every query is immediate and cursorless"+ ( all+ (\query -> query.freshness == InventoryImmediate && noCursor query.cursor)+ kiokuCatalogInventory.inventoryQueryModels+ )+ assertEqual+ "canonical fingerprint"+ "catalog-v7:54f9ac55d73f40160d87b0ae49c2477a0de860c2e6e1e38df0591a2036d002e4"+ (catalogFingerprintText kiokuCatalogFingerprint)+ where+ noCursor :: Maybe InventoryQueryCursor -> Bool+ noCursor = maybe True (const False)++testTypedHandlers :: Assertion+testTypedHandlers = do+ assertEqual+ "memory handler"+ ["kioku-memory-inline"]+ (map (.name) (typedInlineProjections kiokuProjectionCatalog memoryProjectionSet))+ assertEqual+ "session handler"+ ["kioku-session-inline"]+ (map (.name) (typedInlineProjections kiokuProjectionCatalog sessionProjectionSet))++testEventStreams :: Assertion+testEventStreams = do+ assertBool "memory stream" (isRight validateMemoryEventStream)+ assertBool "session stream" (isRight validateSessionEventStream)++testDuplicateOwnership :: Assertion+testDuplicateOwnership =+ diagnosticsFor addDuplicateMemoryOwner+ `shouldContainCode` TargetWithMultipleOwners+ where+ addDuplicateMemoryOwner catalog =+ catalog+ { projectionSets = catalog.projectionSets <> [SomeProjectionSet duplicateMemoryProjectionSet]+ }+ duplicateMemoryProjectionSet =+ memoryProjectionSet+ { projectionDefinitions = duplicateDefinition :| []+ }+ duplicateDefinition =+ case memoryProjectionSet.projectionDefinitions of+ definition :| _ ->+ definition+ { projectionId = must (mkProjectionId "kioku-memory-inline-duplicate"),+ claimSite = must (mkClaimSite "duplicate memory owner test")+ }++testMissingSupplier :: Assertion+testMissingSupplier =+ diagnosticsFor (\catalog -> catalog {projectionSets = []})+ `shouldContainCode` TargetWithoutOwner++testUnknownOwnership :: Assertion+testUnknownOwnership =+ diagnosticsFor addUnknownTarget+ `shouldContainCode` UnknownTargetReference+ where+ addUnknownTarget catalog =+ catalog+ { projectionSets =+ [ SomeProjectionSet+ memoryProjectionSet+ { projectionDefinitions =+ addTarget <$> memoryProjectionSet.projectionDefinitions+ }+ ]+ <> drop 1 catalog.projectionSets+ }+ addTarget definition =+ definition+ { ownedTargets =+ definition.ownedTargets+ <> (must (mkTargetId "kioku-unknown") :| [])+ }++testWaitingWithoutCursor :: Assertion+testWaitingWithoutCursor =+ case headWaitingReadModel EntireVisibleLog memoryByIdReadModelBlueprint of+ Left err ->+ assertEqual+ "truthful construction refuses waiting without a cursor"+ (ReadModelDefinitionMissingCursor "kioku-memory-by-id" (WaitForHead EntireVisibleLog))+ err+ Right _ -> assertFailure "a cursorless Kioku read model became head-waiting"++testImmediateQueries :: Assertion+testImmediateQueries =+ withKiokuMigratedDatabase \connStr ->+ withNoopAppEnv (defaultConnectionSettings connStr) \env -> do+ result <- runAppIO env do+ now <- liftIO getCurrentTime+ sid <- liftIO genSessionId+ sessionWrite <-+ Session.startWithContext+ testContext+ StartSessionData+ { sessionId = sid,+ memorySpaceId = testSpace,+ actorPrincipal = testActorPrincipal,+ ownerPrincipal = Nothing,+ agentId = "catalog-test",+ focus = "prove immediate catalog queries",+ scope = testScope,+ subjectRef = Nothing,+ previousSessionId = Nothing,+ parentSessionId = Nothing,+ delegationDepth = 0,+ startedAt = now+ }+ liftIO case sessionWrite of+ Left err -> assertFailure ("session command failed: " <> show err)+ Right _ -> pure ()+ sessionRead <- Session.getById testSpace sid+ liftIO case sessionRead of+ Right (Just _) -> pure ()+ other -> assertFailure ("immediate session query failed: " <> show (() <$ other))++ mid <- liftIO genMemoryId+ memoryWrite <-+ Memory.recordWithContext+ testContext+ RecordMemoryData+ { memoryId = mid,+ memorySpaceId = testSpace,+ actorPrincipal = testActorPrincipal,+ ownerPrincipal = Nothing,+ agentId = "catalog-test",+ sessionId = Just sid,+ scope = testScope,+ memoryType = MemoryFact,+ content = "catalog registration is live",+ priority = 50,+ confidence = HighConfidence,+ tags = Set.singleton "catalog",+ supersedes = Nothing,+ recordedAt = now+ }+ liftIO case memoryWrite of+ Left err -> assertFailure ("memory command failed: " <> show err)+ Right _ -> pure ()+ memoryRead <- Memory.getMemoryRowById testSpace mid+ liftIO case memoryRead of+ Right (Just _) -> pure ()+ other -> assertFailure ("immediate memory query failed: " <> show (() <$ other))+ case result of+ Left err -> assertFailure ("store error while exercising catalog queries: " <> show err)+ Right () -> pure ()+ where+ testScope :: MemoryScope+ testScope = ScopeGlobal (Namespace "kioku-catalog-test")++testProjectionRollback :: Assertion+testProjectionRollback =+ withKiokuMigratedDatabase \connStr ->+ withNoopAppEnv (defaultConnectionSettings connStr) \env -> do+ mid <- genMemoryId+ now <- getCurrentTime+ let record =+ RecordMemoryData+ { memoryId = mid,+ memorySpaceId = testSpace,+ actorPrincipal = testActorPrincipal,+ ownerPrincipal = Nothing,+ agentId = "catalog-test",+ sessionId = Nothing,+ scope = testScope,+ memoryType = MemoryFact,+ content = "must roll back",+ priority = 50,+ confidence = HighConfidence,+ tags = Set.singleton "rollback",+ supersedes = Nothing,+ recordedAt = now+ }+ handlers =+ typedInlineProjections kiokuProjectionCatalog memoryProjectionSet+ <> [l2SceneTimerScheduleProjection]+ failed <-+ runAppIO env $+ runCommandWithSqlEventsControlled+ defaultRunCommandOptions+ memoryEventStream+ (memoryStream mid)+ (RecordMemory record)+ ( \pairs _ -> do+ for_ handlers \handler ->+ for_ pairs \(event, recorded) -> handler.apply event recorded+ pure (RollbackSqlTransaction ())+ )+ case failed of+ Left err -> assertFailure ("store error while injecting rollback: " <> show err)+ Right (Left err) -> assertFailure ("command failed before the injected rollback: " <> show err)+ Right (Right (SqlCommandRolledBack ())) -> pure ()+ Right (Right other) -> assertFailure ("injected rollback committed: " <> show other)++ probe <- runAppIO env do+ events <- readStreamForward (Stream.streamName (memoryStream mid)) (StreamVersion 0) 10+ row <- Memory.getMemoryRowById testSpace mid+ timer <- lookupTimer (l2SceneTimerId testSpace testScope (idText mid))+ pure (Vector.length events, row, timer)+ case probe of+ Left err -> assertFailure ("store error while checking rollback: " <> show err)+ Right (eventCount, row, timer) -> do+ assertEqual "event append rolled back" 0 eventCount+ assertEqual "application projection rolled back" (Right Nothing) row+ assertEqual "framework timer rolled back" Nothing timer+ where+ testScope :: MemoryScope+ testScope = ScopeGlobal (Namespace "kioku-catalog-rollback")++testFingerprintDrift :: Assertion+testFingerprintDrift =+ withKiokuMigratedDatabase \connStr ->+ withNoopAppEnv (defaultConnectionSettings connStr) \env ->+ case validateKiokuProjectionCatalogWith driftSources of+ Failure diagnostics -> assertFailure ("drift fixture was structurally invalid: " <> show diagnostics)+ Success drifted -> do+ result <- runAppIO env (registerProjectionCatalog drifted)+ case result of+ Left err -> assertFailure ("store error while checking catalog drift: " <> show err)+ Right (Left _) -> pure ()+ Right (Right _) -> assertFailure "registration accepted a changed persisted catalog fingerprint"+ where+ driftSources catalog =+ catalog+ { sources =+ [ source {codecFingerprint = source.codecFingerprint <> "-drift"}+ | source <- catalog.sources+ ]+ }++diagnosticsFor :: (ProjectionCatalog -> ProjectionCatalog) -> [CatalogDiagnostic]+diagnosticsFor transform =+ case validateKiokuProjectionCatalogWith transform of+ Failure diagnostics -> NonEmpty.toList diagnostics+ Success _ -> []++shouldContainCode :: [CatalogDiagnostic] -> CatalogDiagnosticCode -> Assertion+shouldContainCode diagnostics expected =+ assertBool+ ("expected diagnostic " <> show expected <> ", got " <> show diagnostics)+ (any ((== expected) . (.diagnosticCode)) diagnostics)++isRight :: Either left right -> Bool+isRight = either (const False) (const True)++must :: (Show error) => Either error value -> value+must = either (error . show) id
test/Kioku/ReadModelReconcileSpec.hs view
@@ -26,10 +26,12 @@ import Effectful.Error.Static (Error) import Hasql.Transaction qualified as Tx import Keiro.ReadModel- ( ConsistencyMode (Eventual),- ReadModel (..),+ ( QueryFreshness (Immediate),+ ReadModel,+ ReadModelBlueprint (..), ReadModelError (..),- runQueryWith,+ immediateReadModel,+ runQueryWithFreshness, ) import Kioku.Api.Scope (MemoryScope (..), Namespace (..)) import Kioku.App (AppEffects, runAppIO, withNoopAppEnv)@@ -138,9 +140,9 @@ withApp \sid -> do _ <- reconcileReadModelRegistry result <-- runQueryWith+ runQueryWithFreshness Nothing- Eventual+ Immediate preRelocationSessionByIdReadModel Session.SessionByIdQuery {memorySpaceId = testSpace, sessionId = idText sid} liftIO case result of@@ -156,7 +158,12 @@ -- the identity it carried before the projections moved. preRelocationSessionByIdReadModel :: ReadModel Session.SessionByIdQuery (Maybe Session.SessionRow) preRelocationSessionByIdReadModel =- Session.sessionByIdReadModel {version = 4, shapeHash = "kioku-session-v4"}+ immediateReadModel+ ( Session.sessionByIdReadModelBlueprint+ { version = 4,+ shapeHash = "kioku-session-v4"+ }+ ) -- | A second pass must write nothing. If it reported 'Reconciled' again, the reconciler -- would be rewriting @last_built_at@ on every @just migrate@ — and, worse, would be lying
test/Main.hs view
@@ -8,6 +8,7 @@ import Kioku.IdempotencySpec qualified as IdempotencySpec import Kioku.MemorySpaceSpec qualified as MemorySpaceSpec import Kioku.PortfolioAccessSpec qualified as PortfolioAccessSpec+import Kioku.ProjectionCatalogSpec qualified as ProjectionCatalogSpec import Kioku.ReadModelReconcileSpec qualified as ReadModelReconcileSpec import Kioku.RecallCompatSpec qualified as RecallCompatSpec import Kioku.RecallSpec qualified as RecallSpec@@ -33,6 +34,7 @@ IdempotencySpec.tests, MemorySpaceSpec.tests, PortfolioAccessSpec.tests,+ ProjectionCatalogSpec.tests, ReadModelReconcileSpec.tests, RecallSpec.tests, RecallCompatSpec.tests,