Root-cause option for the fanout/contract-coupling predicament: keel-style projection #31
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The v0.5.0 fanout campaign cleared all 24 file-length faults but stalled on 5
fanoutfaults. Liberte ratified per-directory scoped limits (server=12,store=16, service=20, tests/service=11) plus a regroup of
web/src/components(ruling:
.task/resources/liberte-v050-fanout-ruling.md). That closes theadoption, but it treats symptoms. Reading
../keel(PerishLab/keel — a "datamodel description engine") surfaced what looks like the root cause and a
structural cure. This issue records the investigation so we can decide whether
to open a design round on it. No code proposed here — strategic direction.
The predicament, restated
Three of the five stuck directories looked like distinct problems; they are one:
santi-api/src/server(12) — contract-load-bearing. utoipa derives eachOpenAPI operation
tagfrom the handler's module path (super::effects:: effect_status→ tag"super::effects"). Moving a handler changes the tag,which moves the exported OpenAPI digest (proven: a trial reorg turned the
contract test red, reverted). The private source layout is a public
contract surface.
santi-core/src/store(16) andsanti-core/src/service(20) —deliberately flat: one hand-written module per domain concern
(budget/compact/effects/errors/turns/window/…), each bolting a method group
onto the single
SantiStore/Servicetype.Common root: santi hand-writes per-domain and per-endpoint code, so the
module layout carries domain identity. Once identity lives in the layout it
(a) sprawls (fanout) and (b) leaks into contracts (utoipa tags). Splitting
files and ratifying fanout limits are symptom management.
What keel does differently
keel's thesis (README): business code defines resources, fields, and
relations only; control fields and control capabilities stay in the engine;
HTTP and store adaptors are projections of the engine surface, not a business
authoring API. Concretely:
serve.rs::app()is 6 route templates, all parameterized by{unit}/{id}/{bond}/{tie}, all generic overW: Wire. The handlers(
list/create/one/edit/remove/attach/…) are uniform and dispatchto the engine by
{unit}. Adding a resource adds zero routes and zerohandlers — the same 6 templates,
{unit}just resolves to a new table.adapt/http.rs::paths(plan)enumerates concrete routes from the resourcegraph (
plan.units()). The public surface = fixed verb set × declared#[resource]structs. The contract is derived from the declared model;source module organization projects nothing into it.
face.rs(Core/Face/Tx) is one uniform verb surface (put/set/live/query/ask/end/tie/ties/cut/flow) with identity (
Who::{Sudo,Op,Anon}) and cachebuilt in.
macro/lib.rsexpands#[resource]into a plan contribution.Wireseam, one copy of the logic, SQL dialect the only per-store difference
(
docs/conform.md).Why this dissolves all three symptoms
Remove the identity-bearing hand-written code and the layout stops carrying
identity:
no module path can reach an OpenAPI tag. The contract becomes a canonical
projection of the resource graph, decoupled from file layout. The fanout law
can never again collide with contract stability.
becomes resource declarations + one engine + one Wire seam. Not split or
regrouped: replaced.
So the choice we framed to Liberte ("regroup vs ratify a limit") is
symptom-level; keel offers cutting the root.
Honest fit / gap / risk
possibly the domain model.
send_strand/drive_strand, the window doorbell, signature-verified webhookingress, SSE streams, self-upgrade — stateful workflows and streams, not
resource+relation. keel today is resource-CRUD + query DSL + tie/cut +
capability + trigger, still staging S0→S6 (cache/estate unlanded). santi's
command/stream endpoints have no direct home in a pure projection.
(web panel, CLI). keel projects a different contract shape (uniform REST +
/queryDSL, no association reads) and emits no OpenAPI today. Migrationis a contract break → W-C-style staged rollout + Liberte rounds.
Betting the production edge on it is a real commitment.
v0.5.0 adoption now; keel is a longer-horizon bet, not a substitute.
A second signal worth recording
keel's own
negentropy.tomlsets onlyblock=4/path=4— nofilelimit,no
fanoutlimit — whileface.rsis 1086 lines in a flat ~15-entrysrc/,pinned v0.4.0. The same author, in a contemporaneous active project,
deliberately values cohesion (one conceptual surface = one file / one flat
namespace) over the 300-line / 10-entry caps. That reframes our fanout question:
the honest answer for flat aggregates may not be "ratify per-directory limits"
but "reconsider whether
file/fanoutbelong in santi's constitution at all,"with keel as the precedent. (Note: the operator is separately clearing keel's
own negentropy debt, so keel's config may evolve.)
Recommendation
Two legs, not one:
(web regroup, then pin flip + scoped fanout stanzas). Ready now.
projection — starting with the genuinely resource-shaped parts (souls,
webhooks, possibly strands-as-resource), keeping a hand-written layer for
command/stream endpoints, and facing head-on whether keel's projected
contract replaces utoipa and whether
file/fanoutthen leave theconstitution.
References
.task/resources/liberte-v050-fanout-{proposal, ruling}.mdPerishLab/keel(git.perish.top); key files read —crates/keel/src/{serve.rs,adapt/http.rs,face.rs},crates/macro/src/lib.rs,docs/conform.md, README.Misfiled — this is feedback for keel, not a santi decision. Refiling on PerishLab/keel and closing here.