KgwariC4 Architecture
03

Kgwari Sharedkgwari-shared · TypeScript · Changesets · GitHub Packages

npm workspaces Changesets GitHub Packages SemVer contracts ×17 domains

3A — ContextAn internal package registry

Not a runtime system — a versioned contract surface published to GitHub Packages and consumed by both application repositories at build time.

sh-context diagram
PlantUML source
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml

LAYOUT_WITH_LEGEND()
HIDE_STEREOTYPE()
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "Inter"
skinparam shadowing false

title kgwari-shared — System Context

Person(maintainer, "Maintainer", "Authors changesets · merges the Version Packages PR")
Person_Ext(consumerDev, "Consumer Developer", "Frontend / backend engineer")

System(shared, "kgwari-shared", "Private TypeScript monorepo\nThree packages under @edwardseshoka:\ncontracts · foundation · samples\nThe places capability and its Google adapter\nwere removed — nothing consumed them")

System_Ext(fe, "kgwari-frontend-app", "Consumes contracts · foundation · samples")
System_Ext(be, "kgwari-backend-app", "Consumes contracts · foundation · samples")
System_Ext(ghpkg, "GitHub Packages", "Private npm registry under the @edwardseshoka scope")
System_Ext(gha, "GitHub Actions", "Changesets release workflow")
System_Ext(docs, "kgwari-docs", "Package layering doc — the rule the dependency direction follows")

Rel(maintainer, shared, "Commits + changesets")
Rel(shared, gha, "PR validation & release workflows", "on push / merge")
Rel(gha, ghpkg, "Publishes only changed packages", "npm publish")
Rel(consumerDev, fe, "Develops")
Rel(consumerDev, be, "Develops")
Rel_Back(ghpkg, fe, "Installs from", "npm install")
Rel_Back(ghpkg, be, "Installs from", "npm install")
Rel_U(docs, shared, "Governs layering", "review-time")

@enduml

3B — ContainerThree packages, and the two that left

Contracts and foundation are the wire and the abstractions; samples is the shared content pool. The capabilities and adapters pair shown at the last revision is gone — the places domain and its Google adapter were removed once nothing consumed them, and neither application repository names them today.

sh-container diagram
PlantUML source
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

LAYOUT_WITH_LEGEND()
HIDE_STEREOTYPE()
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "Inter"
skinparam shadowing false

title kgwari-shared — Containers (three independently versioned packages)

System_Boundary(shared, "kgwari-shared (npm workspaces)") {

  Container(foundation, "@edwardseshoka/foundation", "v3.3.0 · zero deps", "Result<T,E> · UseCase / NoInputUseCase\nMapper<TIn,TOut> · Validator\nparseMoneyAmount — no floating point, no Intl,\nso the server re-validates with the client's parser")

  Container(contracts, "@edwardseshoka/contracts", "v10.0.0", "Wire DTOs for seventeen domains:\ncatalog · cellar · collections · contributions\ndiscover · editorial · events · lenses · media\nmember · money · provenance · search · social\ntext · trust · vocabulary")

  Container(samples, "@edwardseshoka/samples", "v14.0.0", "Endpoint-shaped sample content per feature —\nthe corpus both apps develop against")

  Container(cs, "Changesets", "Tooling", "Per-PR semver intent")
  Container(release, "release workflow", "GitHub Actions", "Builds -> publishes only what changed")
}

note right of contracts
  **Six new top-level domains since the last revision**,
  and two of them exist because a concept escaped the
  feature that discovered it. `money` moved out of
  `catalog` and `text` out of `search` — each the moment
  a second feature needed the same shape. Leaving them
  where they were born is how every other feature ends
  up declaring its own.
end note

System_Ext(ghpkg, "GitHub Packages", "@edwardseshoka scope")
System_Ext(fe, "kgwari-frontend-app")
System_Ext(be, "kgwari-backend-app")

Rel(samples, contracts, "depends on", "workspace:*")
Rel(contracts, foundation, "depends on", "workspace:*")
Rel(cs, shared, "tracks bumps per package")
Rel(release, cs, "Applies versions via")
Rel(release, ghpkg, "Publishes changed packages")
Rel_Back(ghpkg, fe, "Installs")
Rel_Back(ghpkg, be, "Installs")

@enduml

3C — ComponentWhat a wire may not carry: text and money

Two modules that exist because a concept escaped the feature that discovered it. Both encode the same rule from opposite directions: the server never sends a composed sentence, a formatted number or a formatted date. A display field is one of three declared sources, and an amount is integer minor units plus a currency code — never a symbol, never a float.

sh-component diagram
PlantUML source
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml

LAYOUT_WITH_LEGEND()
HIDE_STEREOTYPE()
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "Inter"
skinparam shadowing false

title kgwari-shared — Component view of `contracts/text` and `contracts/money`: what a wire may not carry

Container_Boundary(text, "@edwardseshoka/contracts/text — three sources, never a bare string") {
  Component(canonical, "CanonicalText", "{ source, text }", "A proper noun, identical everywhere —\n'Meerlust Estate', 'Rubicon', a certificate number.\nNobody renders it; nothing translates it.\n**Must never be stemmed by the index.**")
  Component(chrome, "ChromeText", "{ source, key }", "A closed enum — a verdict, a member status,\na business persona. The server sends the KEY\nand never the word, so it reads in every locale\nwith no translation pipeline at all.")
  Component(negotiated, "NegotiatedText", "{ source, text, languageTag }", "Server-localised content — an exonym, a title.\nThe languageTag is what makes a FALLBACK VISIBLE:\nwithout it a French member reading English\ncannot be told that it fell back.")
  Component(union, "LocalizedText", "Discriminated union", "The three, and nothing else.\nA display field typed as `string` is the bug\nthis type exists to make unspellable.")
  Component(measurement, "Measurement · YearRange", "TS", "Value plus unit, never a formatted quantity")
}

Container_Boundary(money, "@edwardseshoka/contracts/money — the third axis") {
  Component(moneyC, "MoneyContract", "{ amountMinorUnits, currency }", "Integer minor units — 89500, not 895.\nThe field was once `amount: number` meaning rands\nwhile the formatter expected cents, and R895\nrendered as R8,95")
  Component(currency, "CurrencyCode", "Closed union", "ZAR · EUR · GBP · CHF · CAD · USD\n**No symbol field** — CAD is '$' in Canada\nand 'CA$' elsewhere; a stored symbol\ndestroys that disambiguation")
  Component(priceKind, "PriceKind", "listed | paid | valuation", "Three concepts that must never share a field.\nA field called `price` invites all three, after\nwhich a distributor's change silently rewrites\nwhat a member remembers paying")
  Component(transacted, "TransactedMoneyContract", "MoneyContract + asOf", "Pins a historical amount in time")
}

Container_Boundary(foundation, "@edwardseshoka/foundation — the only dependency contracts has") {
  Component(parse, "parseMoneyAmount / toEditableAmount", "Pure fns", "Members type '895,50'; the contract stores 89500.\n**No floating point** — parseFloat('19.99') * 100\nis 1998.9999999999998. **No Intl**, so it imports\ninto domain and backend code alike, and the server\nre-validates with the same function that parsed")
  Component(result, "Result<T, E> · UseCase · Mapper", "TS", "The vocabulary both apps' layers are built from")
}

System_Ext(features, "search · catalog · cellar · …", "Every feature that renders text or money")
System_Ext(clientEdge, "The presentation edge", "Where, and only where, a string is composed,\na number grouped and a date formatted")

Rel(canonical, union, "member of")
Rel(chrome, union, "member of")
Rel(negotiated, union, "member of")
Rel(features, union, "Every display field is typed as")
Rel(features, moneyC, "Every amount is typed as")
Rel(moneyC, currency, "Scale derived from, never stored")
Rel(moneyC, priceKind, "Disambiguated by")
Rel(transacted, moneyC, "extends")
Rel(moneyC, parse, "Round-trips through")
Rel(union, clientEdge, "Rendered by")
Rel(moneyC, clientEdge, "Formatted by")
Rel(measurement, clientEdge, "Formatted by")
Rel(features, result, "Built from")

note bottom of text
  **These types were introduced for search, and were never
  a search concept.** The moment a wine record carries
  ninety fields, a register of tasting words and an estate's
  own essay, the same three-way distinction governs all of
  them — so they moved out of `search`, the way `money`
  moved out of `catalog`. Left where it was born, every
  other feature declares its own.

  The rule underneath all of it: **the server never sends a
  composed sentence, a formatted number or a formatted
  date.** A server that sends "214 readings" has hardcoded
  English plural rules; one that sends "14.21 %" has
  hardcoded a decimal separator that is a comma across most
  of Europe.
end note

@enduml
Technical Summary

A private monorepo of three independently versioned packages under the @edwardseshoka scope: foundation (Result, UseCase, Mapper, validators and the money parser — zero dependencies), contracts (wire DTOs across seventeen domains, depending only on foundation), and samples (endpoint-shaped sample content per feature). It has shrunk since the last revision rather than grown: the capabilities and adapters split was removed when the places domain stopped being consumed, which is the healthier outcome for a shared package — an unused published dependency is a version everyone still has to resolve.

What grew instead is contracts, and the interesting part is where the growth came from. Two of the newest top-level domains exist because a concept outgrew the feature that invented it: money moved out of catalog and text out of search, each at the moment a second feature needed the same shape. Left where they were born, every other feature declares its own — which is exactly the drift the published boundary exists to prevent.

Versioning is driven by Changesets, so every pull request that touches a published package declares its own semver bump, and merging the generated “Version Packages” pull request publishes only what actually changed. The samples package earns its keep twice over: the backend seeds its dev tables from it and the frontend builds its App Doubles from it, which is what keeps local development from quietly disagreeing with the deployed system.