Skip to content

Releases: vonkernel/hexwright

Release list

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 01 Aug 12:08
4930b29

Before you upgrade

Violation counts can fall, and unlike 0.2.0 that is not an improvement.

0.2.0 removed verdicts for references that did not exist — a type named in a comment or a string. This release removes verdicts for references that do exist, because they live inside a companion object and a companion is no longer read as part of the type around it.

Measured against 0.2.0, on a DTO whose companion maps from an Entity:

0.2.0:  IncidentView -> MediaIncident: DTO exposes Entity
0.3.0:  (none)

The IncidentView type never touched the Entity — its companion did, so the subject of that arrow was wrong. But the file really is coupled to the Entity, and that is now out of scope.

A real problem can disappear this way. A domain entity whose companion holds fun fromRow(row: JpaRow) was reported as a layer back-reference and no longer is.

If your count drops, look at what went before accepting it:

npx hexwright@0.2.0 check --repo . --src server > before.txt
npx hexwright@0.3.0 check --repo . --src server > after.txt
diff before.txt after.txt

Anything in that diff came from a companion. Mapping belongs in the adapter in this architecture, so most of it should be noise — but read it once rather than assume.

The graph also gains edges, which affects more people than the above. Aggregates that appeared connected to nothing now have their references drawn, so a committed graph.tsv will diff even where no violation moved.

Added — a reference held through an identifier

A type keeping another aggregate's id rather than the aggregate itself — MediaItem holding a BlobId, not a Blob — is referencing that aggregate, and referencing it the deliberately weaker way: no object graph to traverse, no shared transaction.

That was previously drawn as a dependency on the id, and since an id is a value class in the domain model the edge was folded away by default. The aggregate could end up connected to nothing at all — in the model this came from, Blob had five outgoing edges, every one to a value type, and sat in the picture as an island while the code plainly pointed at it.

It is now drawn as REFERENCES, at the aggregate, styled lighter than a direct dependency. It never counts as a violation: pointing at another context by id is how you avoid coupling to it, and reporting that would penalise the design that got it right. Cross-domain still marks where one runs.

Which id belongs to which aggregate is a project convention, so the profile says how to read it:

identity:
  from: property        # the type declaring `val id: T` owns T
  property: id

property is the default because it is structural. The alternative, stripping an Id suffix, is wrong on the first real case it met: MediaId identifies MediaItem, and Media does not exist. from: suffix is available for projects whose aggregates do not carry their own id.

A custom profile needs this block added before identifiers resolve. Without it nothing resolves and the graph is what 0.2.0 produced, so nobody gets a silent change they did not opt into. The bundled profile has it.

Fixed — a companion object is not the type around it

A companion object was read as part of the class enclosing it, so everything it named counted as a reference from that class.

The loudest form was a supertype appearing to depend on its own subtype. A sealed root keeps the factory for its variants in the companion:

sealed class MediaItem(…) {
    companion object {
        fun create(type: MediaType, …): MediaItem = when (type) {
            MediaType.PHOTO -> Photo(…)
            MediaType.VIDEO -> Video(…)
        }
    }
}

which produced MediaItem → Photo and MediaItem → Video alongside Photo → MediaItem. The dependency direction of an inheritance relationship is defined — the subtype knows the supertype. What referenced Photo was MediaItem.Companion, a separate object with its own type.

The same mistake, quieter: a value class wrapping a UUID appeared to depend on an id generator, because its companion's new() calls one.

The discriminator is inside the companion, not is a subtype — the same factory written as an ordinary member produces the same edge, and there it is correct.

Also

The added / modified legend keys and the identifier filter both say what they mean now; the web UI's identifier checkbox is Show value-type edges, which is what it has always done.

Known rough edge

REFERENCES and EXTENDS render as similar dotted curves and are not easy to tell apart, and the curve on a long span sweeps across the picture. Shipped as is.


What's Changed

  • feat: draw an id-based reference at the aggregate, not at the identifier by @hagyutae in #18
  • fix: a companion object's references are not the enclosing type's by @hagyutae in #19
  • release: v0.3.0 by @hagyutae in #20

Full Changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 31 Jul 11:41
20d51ed

Before you upgrade

Boundary verdicts move. Two false positives are fixed, so a repository can report fewer violations on 0.2.0 than it did on 0.1.0 with nothing in it having changed. A gate that was passing keeps passing; one that was failing may stop, and if it does, 0.1.0 was wrong rather than 0.2.0 being lenient.

Nothing was removed or renamed, and no existing exit code changed.

Fixed — verdicts that were never real

A top-level declaration was read as part of the type above it (#4). A declaration's body ran to the next class, interface or object, so anything between two of them was absorbed upward and every type it named became a reference from that declaration:

data class IncidentListFilter(val status: String?)

typealias IncidentSlice = List<MediaIncident>   // ← counted against the DTO above

interface MediaIncidentRepository { … }

That reported IncidentListFilter → MediaIncident DTO exposes Entity against a type whose fields never mention it. The alias was one instance — file-level fun, val and const val all did the same, so the fix is at the body boundary rather than in typealias handling.

Aliases are now resolved rather than dropped, including one imported from another file. Removing a false positive by losing the reference underneath it would only have traded the error for a quieter one.

A type named inside a string counted as a reference (#7). Comments were stripped; string literals were not:

fun describe() = "no MediaIncident on this page"   // ← counted as a reference

Log lines, exception text, serialization keys and SQL all name types routinely. String literals are now blanked along with comments — except the code inside a ${…} template, which is real code and still counted.

Added — drawing the state a branch started from

A PR that removes violations was only half describable: the after picture was one command, the before picture took a git worktree and a throwaway commit (#3).

hexwright render --repo . --base origin/main --at base --image before.png
hexwright render --repo . --base origin/main            --image after.png
  • --at base draws the types the delta covers as they stood before the branch. Delta styling is dropped — nothing in a picture of the past can be added — and violations stay red, which is the question a before picture exists to answer.
  • --view violations keeps only the breaches and the types they run through. A state view: what is wrong now, not what changed. Unlike the web UI toggle it is not narrowed by component, because a violation with one end hidden reads as no violation at all.

The two images line up (#10) — same coordinates, same domain colours, same canvas — so they can be flipped between, not just set side by side. Both commands work that out on their own; there is no flag to remember and no order to run them in.

Changed

  • render treats an empty picture as an answer. No violations, or a purely additive branch with no before state, print what happened and exit 0. A non-zero exit there would break the script that renders both halves.
  • The added / modified legend keys now appear only on images that can contain them. They were previously drawn on every render, including views produced without a base.
  • Dev dependencies moved off versions carrying a critical and a high advisory.

Also

Releases are now published from CI on a tag push, with provenance attested through npm trusted publishing — no long-lived token exists. v0.1.0 has been tagged after the fact, so both releases now point at the commit they were cut from.


What's Changed

  • style: use string literals where no interpolation is needed by @hagyutae in #5
  • build(deps): Bump vite and vitest by @dependabot[bot] in #2
  • fix: stop attributing top-level constructs to the declaration above by @hagyutae in #6
  • fix: a type named in a string literal is not a reference by @hagyutae in #8
  • feat: draw the before state — a violations view, and rendering at the base by @hagyutae in #9
  • test: remove the temp directory the render test creates by @hagyutae in #11
  • fix: lay a before/after pair out from both halves so it lines up by @hagyutae in #12
  • ci: publish from a release tag, and check pull requests on both supported nodes by @hagyutae in #13
  • release: v0.2.0 by @hagyutae in #14
  • ci: publish through trusted publishing instead of a token by @hagyutae in #15

Full Changelog: v0.1.0...v0.2.0