Skip to content

v0.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 01 Aug 12:08
· 5 commits to main since this release
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