Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
# Hexis

ZIO / Scala 3 SDK for [TypeSafe System One](https://docs.typesafe.ai/introduction) (Jev).
State and questions go in. Structured answers come out.

Send a state and typed questions. Get structured answers, probabilities, and confidence.
Compose those answers in your code.

Cross-built for JVM, Scala.js, and Scala Native. HTTP is [heddle](https://github.com/early-effect/heddle) 0.4.0+.

Install, including the current version: [earlyeffect.rocks/hexis](https://www.earlyeffect.rocks/hexis/install.html).
Docs: [earlyeffect.rocks/hexis](https://www.earlyeffect.rocks/hexis/)

## Quick start

Expand Down
10 changes: 8 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,14 @@ lazy val docs = project
specularBuildMain := "hexis.docs.BuildSite",
specularMetaProject := Some(LocalProject("hexis")),
specularArtifactKind := "library",
specularSiteDirectory := (ThisBuild / baseDirectory).value / "target" / "site",
specularDisplayVersion := stripCi,
specularSiteDirectory := (ThisBuild / baseDirectory).value / "target" / "site",
// Do not strip -ci first. sbt-dynver-ci names a distance build 0.1.1-ci, and stripping
// that suffix yields 0.1.1, which is not the published coordinate. A raw version that
// contains -ci, +, or SNAPSHOT advertises the last Central tag.
specularDisplayVersion := { raw =>
val distance = raw.contains("-ci") || raw.contains("+") || raw.contains("SNAPSHOT")
if raw.nonEmpty && !distance then raw else "0.1.0"
},
)

addCommandAlias("docsPreview", "~docs/specularPreview")
Expand Down
40 changes: 26 additions & 14 deletions docs/src/test/scala/hexis/docs/BuildSite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,48 @@ import earlyeffect.docs.EarlyEffectTheme
import specular.site.*
import zio.*

import java.nio.file.Path
import java.nio.file.{Files, Path, StandardCopyOption}

object BuildSite extends DocsSite:

def pages =
/** The guide. The front page is not in this list, so it is not a second sidebar item. */
private val guide: Vector[specular.DocPage] =
Vector(Overview.doc, Install.doc, Quickstart.doc, Transport.doc, Patterns.doc, Errors.doc, Cookbooks.doc)

override def site: SiteModel =
val m = meta
val branded = EarlyEffectTheme.brand(super.site)
/** Not a nav item. The sidebar is the guide; this page is the front. */
private val frontPage: specular.DocPage = Front.doc

def pages: Vector[specular.DocPage] = frontPage +: guide

override def site(settings: DocsSettings): SiteModel =
val branded = EarlyEffectTheme.brand(super.site(settings))
branded.copy(
summaryMarkdown = Some(
"""**Hexis** is a ZIO SDK for TypeSafe System One. Typed questions, structured
answers, probabilities you can branch on. HTTP is heddle on JVM, JS, and Native.
"""
),
nav = Some(NavModel.flat(guide)),
pages = pages,
summaryMarkdown = None,
installSnippets = Vector(CodeSnippet("Install", Install.coordinate)),
brand = Some(
Brand(
name = m.title.getOrElse("hexis"),
name = settings.meta.displayTitle,
links = Vector(EarlyEffectTheme.github("https://github.com/early-effect/hexis")),
)
),
installSnippets = Vector(CodeSnippet("Install", Install.coordinate)),
)
end site

override def layers: ZLayer[Any, Nothing, SiteBuilder] =
EarlyEffectTheme.layers

override def afterBuild(out: Path, result: SiteOutput): Task[Unit] =
override def afterBuild(out: Path, result: SiteOutput): IO[SiteError, Unit] =
val _ = result
EarlyEffectTheme.writeLogo(out)
// Specular always writes index.html as a summary plus a second copy of the nav.
// The front DocPage is the index. Copy it over that file. Asset paths stay site-relative.
val front = out.resolve(s"${frontPage.slug}.html")
val index = out.resolve("index.html")
val promote =
ZIO.attemptBlockingIO(Files.copy(front, index, StandardCopyOption.REPLACE_EXISTING)).mapError { err =>
SiteError.WriteFailed(index, err)
}
promote *> EarlyEffectTheme.writeLogo(out)
end afterBuild
end BuildSite
18 changes: 18 additions & 0 deletions docs/src/test/scala/hexis/docs/DocsVersion.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hexis.docs

/** Install coordinate. Judge the raw build version. Do not trust a version after `-ci` was stripped. */
object DocsVersion:

/** Last published tag. */
val published = "0.1.0"

/** A dynver distance (`-ci`, `+`, `SNAPSHOT`) is the next line. Anything else is advertised as written. */
def advertise(raw: String): String =
val trimmed = raw.trim
val distance = trimmed.contains("-ci") || trimmed.contains("+") || trimmed.contains("SNAPSHOT")
if trimmed.nonEmpty && !distance then trimmed else published

/** `specular.meta.version` is the raw build version. Absent in unit tests, so those use [[published]]. */
def version: String =
sys.props.get("specular.meta.version").map(advertise).getOrElse(published)
end DocsVersion
149 changes: 149 additions & 0 deletions docs/src/test/scala/hexis/docs/Front.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package hexis.docs

import hexis.*
import mermoid.ascent.MermoidAscent
import mermoid.{
ContainerFit,
Diagram,
LayoutConfig,
Mermaid,
RenderConfig,
ResponsiveConfig,
SequenceConfig,
SequenceModel,
Viewport,
}
import specular.*
import specular.ziotest.DocSpecSuite
import zio.test.*

/** The site front. Specular still emits a summary index; [[BuildSite]] copies this page over it.
*
* Not a sidebar item.
*/
object Front extends DocSpecSuite:

/** Caller, SystemOne, Transport, then System One. Transport is the client dependency. The remote is
* `POST /v1/systemone`. There is no broker.
*/
private val evaluate =
Mermaid("""sequenceDiagram
| participant Caller
| participant SystemOne
| participant Transport
| participant API as System One
| Caller->>SystemOne: state,<br/>questions
| SystemOne->>Transport: POST<br/>/v1/systemone
| Transport->>API: Http<br/>Request
| API-->>Transport: answers
| Transport-->>SystemOne: Http<br/>Response
| SystemOne-->>Caller: structured<br/>answers
|""".stripMargin)

/** Phone column is about 390px. Fit scales down to 0.72, then the column scrolls. It does not crop the scene. */
private val shown =
RenderConfig(
layout = LayoutConfig(padding = 8, fontSize = 15, edgeLabelFontSize = 13, lineHeight = 18),
sequence = SequenceConfig(
actorMinWidth = 48,
actorPadH = 6,
actorHeight = 32,
columnGap = 12,
rowPitch = 34,
headerGap = 8,
footerGap = 10,
),
responsive = ResponsiveConfig(fit = ContainerFit.ToWidth(0.72)),
)

private val phone = Some(Viewport(390))

def doc = page("State and questions go in. Structured answers come out.")(
illustration(MermoidAscent.diagram(evaluate, shown, phone)).assert { ui =>
val text = ui.toString
assertTrue(
text.contains("mermoid-root"),
text.contains("mermoid-fit"),
text.contains("Caller"),
text.contains("SystemOne"),
text.contains("Transport"),
text.contains("System One"),
text.contains("state,"),
text.contains("questions"),
text.contains("/v1/systemone"),
text.contains("Http"),
text.contains("Request"),
text.contains("Response"),
text.contains("structured"),
text.contains("answers"),
)
},
section("Install")(
md"""
```scala
${Install.coordinate}
```
"""
),
section("SystemOne.evaluate")(
md"""
`SystemOne.evaluate` is the client. The state and the questions go in. A structured answer comes out.
""",
cite(
SystemOne.evaluate(_: String, _: Question[Boolean], _: Option[Model])(using
_: ToState[String],
_: Ask.Aux[Question[Boolean], Answer[Boolean]],
)
),
),
)

override def spec =
suite("Front")(
super.spec,
test("install coordinate is 0.1.0") {
val version = DocsVersion.version
assertTrue(
version == DocsVersion.published,
Install.coordinate.contains(version),
Install.coordinate.contains("%% \"hexis\""),
Install.coordinate.contains("%%% \"hexis\""),
!Install.coordinate.contains("0.1.1"),
!Install.coordinate.contains("-ci"),
!Install.coordinate.contains("SNAPSHOT"),
!Install.coordinate.contains("<version>"),
DocsVersion.advertise("0.1.1-ci") == DocsVersion.published,
DocsVersion.advertise("0.1.1-SNAPSHOT") == DocsVersion.published,
DocsVersion.advertise("0.1.0+4-abcdef") == DocsVersion.published,
DocsVersion.advertise("0.1.0") == "0.1.0",
)
},
test("the figure is evaluate, and fit keeps it inside a phone column") {
val order = evaluate.diagram match
case Diagram.Sequence(stmts) => SequenceModel.participantOrder(stmts).map(_.value)
case _ => Nil
val ui = MermoidAscent.diagram(evaluate, shown, phone)
val width = sceneWidth(ui)
// 0.72 is the fit floor. A 390px phone has room for about 360px of figure after page padding.
val fits = width.exists(w => w > 0 && w * 0.72 <= 360)
assertTrue(
order == List("Caller", "SystemOne", "Transport", "API"),
evaluate.source.contains("sequenceDiagram"),
!evaluate.source.contains("broker"),
fits,
).label(s"scene width $width")
},
)

private def sceneWidth(ui: ascent.ast.UI[Any]): Option[Double] =
ui match
case element: ascent.ast.UI.Element[?] =>
val here = element.attrs.collectFirst {
case ascent.ast.Attr.StaticAttr("data-mermoid-width", ascent.domtypes.AttrValue.Str(raw)) =>
raw.toDoubleOption
}.flatten
here.orElse(element.children.iterator.map(sceneWidth).collectFirst { case Some(w) => w })
case ascent.ast.UI.Fragment(children) =>
children.iterator.map(sceneWidth).collectFirst { case Some(w) => w }
case _ => None
end Front
6 changes: 2 additions & 4 deletions docs/src/test/scala/hexis/docs/Install.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package hexis.docs

import specular.*
import specular.site.ProjectMeta
import specular.ziotest.DocSpecSuite
import zio.test.*

object Install extends DocSpecSuite:

/** Site builds pass `-Dspecular.meta.*`. Doc tests load this page without them. */
def docsVersion: String =
ProjectMeta.fromSystemProperties.map(_.docsVersion).getOrElse("0.0.0")
/** Site builds pass `-Dspecular.meta.version`. Doc tests load this page without it. */
def docsVersion: String = DocsVersion.version

def coordinate: String =
s"""libraryDependencies += "rocks.earlyeffect" %% "hexis" % "$docsVersion"
Expand Down
41 changes: 35 additions & 6 deletions project/ZipxVersions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,50 @@ object MyVersions extends ZipxVersions:
val scalaJavaTime = Lib("io.github.cquiroz", "scala-java-time", "2.7.0")
val scalaJavaTimeTzdb = scalaJavaTime.mod("scala-java-time-tzdb")

val specular = Lib("rocks.earlyeffect", "specular-core", "0.17.0")
val specular = Lib("rocks.earlyeffect", "specular-core", "0.20.0")
val specularZioTest = specular.mod("specular-zio-test").test
val specularTheme = specular.mod("early-effect-docs-theme").test
val ascentJs = Lib("rocks.earlyeffect", "ascent-js", "0.7.4")
val ascentCss = ascentJs.mod("ascent-css")

/** Specular 0.20 depends on heddle 0.9. The published client stays on 0.4, so docs do not take that edge. */
val specularSite = specular
.mod("specular-site")
.excluding(
ZipxExclude.org("rocks.earlyeffect", "heddle_3"),
ZipxExclude.org("rocks.earlyeffect", "ascent-preview_3"),
)
.test
val specularTheme = specular
.mod("early-effect-docs-theme")
.excluding(
ZipxExclude.org("rocks.earlyeffect", "heddle_3"),
ZipxExclude.org("rocks.earlyeffect", "ascent-preview_3"),
)
.test

/** Docs figures only. The published hexis artifact does not select this. */
val mermoidAscent = Lib("rocks.earlyeffect", "mermoid-ascent", "0.2.0")

/** What mermoid-ascent 0.2.0 and specular 0.20 resolve. No docs client, so ascent-js stays off this classpath. */
val ascentCore = Lib("rocks.earlyeffect", "ascent-core", "0.10.1")
val ascentCss = ascentCore.mod("ascent-css")
val ascentHtml = Lib("rocks.earlyeffect", "ascent-html", "0.10.0")

val scalafmt = Plugin("org.scalameta", "sbt-scalafmt", "2.6.2")
val scalajs = Plugin("org.scala-js", "sbt-scalajs", "1.22.0")
val scalaNative = Plugin("org.scala-native", "sbt-scala-native", "0.5.12")
val specularPlugin = Plugin("rocks.earlyeffect", "sbt-specular", "0.17.0")
val specularPlugin = Plugin("rocks.earlyeffect", "sbt-specular", "0.20.0")
val sbtReload = Plugin("com.jamesward", "sbt-reload", "0.0.7")

def coreLib = library(zio, zioStreams, zioJson, heddle)
def coreTest = library(zioTest, zioTestSbt)
def docsTest = library(specularZioTest, specularTheme, ascentCss)
def docsTest = library(
specularZioTest,
specularSite,
specularTheme,
mermoidAscent.test,
ascentCore,
ascentCss,
ascentHtml,
)
def javaTime = library(scalaJavaTime, scalaJavaTimeTzdb)
def jsRuntime = javaTime

Expand Down
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ addSbtPlugin("rocks.earlyeffect" % "sbt-zipx" % "0.18.0")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12")
addSbtPlugin("rocks.earlyeffect" % "sbt-specular" % "0.17.0")
addSbtPlugin("rocks.earlyeffect" % "sbt-specular" % "0.20.0")
addSbtPlugin("com.jamesward" % "sbt-reload" % "0.0.7")
Loading