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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ The `state` engine is the extreme end of this: **stdlib-only**, with no injected
IO at all. The IO modules carry the heavier seams via injection, but follow the
same rule. Defaults are no-ops, nothing third-party is forced on the consumer.

This "stdlib-only" guarantee is about the library you import: the `state` engine
and its seams pull in nothing third-party. The standalone `crucible` CLI is a
leaf tool, not a library, and is the one exception — it embeds a pure-Go
(WebAssembly) Graphviz **only** for `render -format svg|png`, so you can render
images without installing Graphviz. That convenience lives entirely in the CLI
binary; it never enters the `state` engine or any module you import. See
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) for attribution.

## Documentation

Guides, concepts, the food-delivery example, and the generated API reference live
Expand Down
78 changes: 78 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Third-Party Notices

The `crucible` command-line tool (`cmd/crucible`) embeds third-party software so
that `crucible render -format svg|png` can produce images without requiring an
external Graphviz installation. The crucible `state` engine and its seams do
**not** depend on any of the software listed here; these notices apply only to
the CLI binary.

This file is informational. It is **not** a substitute for the licenses shipped
with each dependency, which remain authoritative.

---

## github.com/goccy/go-graphviz

Pure-Go bindings that run Graphviz compiled to WebAssembly via
[wazero](https://github.com/tetratelabs/wazero). Used by the CLI to render DOT
to SVG/PNG in-process.

- License: MIT
- Project: https://github.com/goccy/go-graphviz

```
MIT License

Copyright (c) 2020 Masaaki Goshima

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

---

## Graphviz (bundled as WebAssembly by go-graphviz)

`go-graphviz` embeds the [Graphviz](https://graphviz.org/) graph-layout and
rendering engine compiled to WebAssembly. The distributed `crucible` binary
therefore contains Graphviz.

- License: Eclipse Public License, Version 1.0 (EPL-1.0)
- Project: https://graphviz.org/
- License text: https://graphviz.org/license/ (and the `LICENSE` shipped within
the go-graphviz module under `vendor`/embedded WebAssembly assets)

Graphviz is distributed under the Eclipse Public License, Version 1.0. The full
license text is available at https://www.eclipse.org/legal/epl-v10.html. The
program source for the embedded Graphviz is available from the Graphviz project
at https://gitlab.com/graphviz/graphviz.

---

## Transitive image-encoding dependencies

`go-graphviz` pulls in pure-Go image-encoding libraries used during PNG/JPEG
rendering. Their licenses are reproduced in their respective module directories
in the Go module cache and in `cmd/crucible/go.sum`:

- `github.com/tetratelabs/wazero` — Apache License 2.0 (WebAssembly runtime)
- `github.com/disintegration/imaging` — MIT
- `github.com/fogleman/gg` — MIT
- `github.com/golang/freetype` — FreeType License / GNU GPL (dual)
- `github.com/flopp/go-findfont` — MIT
- `golang.org/x/image`, `golang.org/x/text` — BSD-3-Clause
5 changes: 5 additions & 0 deletions cmd/crucible/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ versioned independently of the `state` module.

### Added

- `render -format svg|png` renders the machine directly to a themed image via an
embedded, pure-Go (WebAssembly) Graphviz — no external Graphviz install is
required. The image carries the Crucible brand palette. `-o file` writes the
output to a file instead of stdout (the norm for binary `png`); `mermaid` and
`dot` output is unchanged.
- `lint -format` selects the output format: `text` (default), `json`, or
`sarif` (SARIF 2.1.0) for machine-readable CI ingestion.
- `diff -format` selects `text` (default) or `json` output.
Expand Down
14 changes: 10 additions & 4 deletions cmd/crucible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ physical location unless the IR was read from stdin (`-`).
### render

```
crucible render <ir.json> [-format mermaid|dot]
crucible render <ir.json> [-format mermaid|dot|svg|png] [-o outfile]
```

Renders the machine as a Mermaid `stateDiagram-v2` (the default) or as Graphviz
DOT. Output is text. For an SVG, pipe the DOT through Graphviz (`crucible render
m.json -format dot | dot -Tsvg`); native SVG rendering is a future addition.
Renders the machine as a Mermaid `stateDiagram-v2` (the default), Graphviz DOT,
or a themed `svg`/`png` image. The `svg` and `png` formats are rendered directly
by an embedded, pure-Go (WebAssembly) Graphviz, so **no external Graphviz
install is required** and the image carries the Crucible brand palette.

`-o` writes the output to a file instead of stdout; it is the norm for `png`,
whose bytes are binary. `mermaid`/`dot` remain text and stream to stdout
unchanged (the historical `crucible render m.json -format dot | dot -Tsvg`
pipeline still works for callers who prefer their own Graphviz).

### diff

Expand Down
47 changes: 40 additions & 7 deletions cmd/crucible/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"

"github.com/stablekernel/crucible/gen"
"github.com/stablekernel/crucible/state"
"github.com/stablekernel/crucible/state/analysis"
"github.com/stablekernel/crucible/state/evolution"
)
Expand Down Expand Up @@ -59,22 +60,28 @@ func runLint(args []string, stdout, stderr io.Writer) int {
return exitOK
}

// runRender loads an IR, assembles it with stub behaviors, and prints the
// machine diagram. -format selects mermaid (the default) or dot. SVG output is
// not produced here; pipe the dot text through Graphviz for an image.
// runRender loads an IR, assembles it with stub behaviors, and emits the
// machine diagram. -format selects mermaid (the default), dot, svg, or png. The
// svg and png formats are rendered directly via an embedded (pure-Go, WASM)
// Graphviz — no external `dot` install is required — and carry the Crucible
// brand theme. Image bytes go to -o when set, otherwise to stdout; png in
// particular is binary, so -o is the norm.
func runRender(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("render", flag.ContinueOnError)
fs.SetOutput(stderr)
format := fs.String("format", "mermaid", "diagram format: mermaid or dot")
format := fs.String("format", "mermaid", "diagram format: mermaid, dot, svg, or png")
out := fs.String("o", "", "output file (default: stdout)")
if err := fs.Parse(reorderArgs(args)); err != nil {
return exitUsage
}
if fs.NArg() != 1 {
emitln(stderr, "usage: crucible render <ir.json> [-format mermaid|dot]")
emitln(stderr, "usage: crucible render <ir.json> [-format mermaid|dot|svg|png] [-o outfile]")
return exitUsage
}
if *format != "mermaid" && *format != "dot" {
emitf(stderr, "crucible render: unknown -format %q (want mermaid or dot)\n", *format)
switch *format {
case "mermaid", "dot", "svg", "png":
default:
emitf(stderr, "crucible render: unknown -format %q (want mermaid, dot, svg, or png)\n", *format)
return exitUsage
}

Expand All @@ -90,6 +97,10 @@ func runRender(args []string, stdout, stderr io.Writer) int {
}

switch *format {
case "svg":
return renderImageToOutput(m, formatSVG, *out, stdout, stderr)
case "png":
return renderImageToOutput(m, formatPNG, *out, stdout, stderr)
case "dot":
emit(stdout, m.ToDOT())
default:
Expand All @@ -98,6 +109,28 @@ func runRender(args []string, stdout, stderr io.Writer) int {
return exitOK
}

// renderImageToOutput renders the machine to image bytes and writes them either
// to the named file or to stdout. The bytes are binary, so they bypass the
// emit* helpers (which append newlines and would corrupt a PNG). It returns the
// process exit code.
func renderImageToOutput[S comparable, E comparable, C any](m *state.Machine[S, E, C], format imageFormat, out string, stdout, stderr io.Writer) int {
img, err := renderImage(m, format)
if err != nil {
emitf(stderr, "crucible render: %v\n", err)
return exitError
}
if out == "" {
_, err = stdout.Write(img)
} else {
err = os.WriteFile(out, img, 0o644)
}
if err != nil {
emitf(stderr, "crucible render: write output: %v\n", err)
return exitError
}
return exitOK
}

// runDiff loads two serialized IRs, classifies the changes between them, and
// prints the recommended semver bump along with the breaking and additive
// changes split apart.
Expand Down
18 changes: 12 additions & 6 deletions cmd/crucible/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ func TestRender(t *testing.T) {
}

func TestRender_UnknownFormat(t *testing.T) {
code, _, errOut := runCmd("render", "testdata/clean.json", "-format", "svg")
if code != exitUsage {
t.Fatalf("exit = %d, want %d", code, exitUsage)
}
if !strings.Contains(errOut, "unknown -format") {
t.Fatalf("stderr missing format error: %s", errOut)
// jpg is explicitly out of scope and zzz is nonsense; both are usage errors.
// svg/png are now valid (see render_image_test.go).
for _, format := range []string{"jpg", "zzz"} {
t.Run(format, func(t *testing.T) {
code, _, errOut := runCmd("render", "testdata/clean.json", "-format", format)
if code != exitUsage {
t.Fatalf("exit = %d, want %d", code, exitUsage)
}
if !strings.Contains(errOut, "unknown -format") {
t.Fatalf("stderr missing format error: %s", errOut)
}
})
}
}

Expand Down
14 changes: 13 additions & 1 deletion cmd/crucible/go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
module github.com/stablekernel/crucible/cmd/crucible

go 1.25
go 1.25.0

require (
github.com/goccy/go-graphviz v0.2.10
github.com/stablekernel/crucible/gen v0.1.0
github.com/stablekernel/crucible/state v1.0.0
)

require (
github.com/disintegration/imaging v1.6.2 // indirect
github.com/flopp/go-findfont v0.1.0 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.35.0 // indirect
)
24 changes: 24 additions & 0 deletions cmd/crucible/go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/goccy/go-graphviz v0.2.10 h1:jHu/1I0Iw0xIzzYk96Ous/ZeuD11Rt2oW8juHdIE30g=
github.com/goccy/go-graphviz v0.2.10/go.mod h1:LRlMnNmY17QbN6fLnvOzY7g0rXQjLKAhzxeTHbEUM6w=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/stablekernel/crucible/gen v0.1.0 h1:IdQlLIsoIYMlUgWeZiY3pzTBvxe5wBZhpPaZ02xW/NY=
github.com/stablekernel/crucible/gen v0.1.0/go.mod h1:rl6qLJ21rp9B/nMYiAM0sbSgi+2wE0NXP+7+p/rc1RE=
github.com/stablekernel/crucible/state v1.0.0 h1:YjhEM3vqqHptq33j+zZlYgSa4FupAFrDgAUUPfLwx68=
github.com/stablekernel/crucible/state v1.0.0/go.mod h1:GU2LNVI+FJrdii+UNZYqfwcH219dCKO+TkX6KRE/Fys=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
2 changes: 1 addition & 1 deletion cmd/crucible/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Usage:

Commands:
lint <ir.json> [-format f] run static analysis; -format text (default), json, or sarif
render <ir.json> [-format f] render the machine as mermaid (default) or dot
render <ir.json> [-format f] [-o file] render as mermaid (default), dot, svg, or png (svg/png embed Graphviz; no external install)
diff <old.json> <new.json> [-format f] [-exit-code] classify changes and recommend a semver bump
validate <ir.json> confirm the IR loads and assembles
eject <ir.json> [-package p] [-o f] generate typed Go behavior stubs
Expand Down
Loading