Merge the arbitration kernel study (#229)
docs/plans/20-two-artifacts-and-neither-is-spring.md. No Go changed. The thesis holds for the routing cascade, querySources and the pre-route ladder, and is refuted for reach selection: ChannelsFor is a total pure function that returns several winners, so nothing claims and nothing loses. The digestion tick is not the odd seam out but the one already done right, and the proposal is that the other three come to look like it. The strongest finding is not the shape. The three structural holes that make a route untrustworthy are written three times for three consumers with three return types. internal/claim is built and tested and has no callers. (V-585)
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
# Two artifacts, and neither one is Spring
|
||||
|
||||
Proposal. V-585. Related umbrella V-558, and the design collected in
|
||||
`docs/plans/19-dialogue-arbitration.md`.
|
||||
|
||||
## Verdict
|
||||
|
||||
**Thesis one holds for four seams and fails for one.** Four are one shape: the routing
|
||||
cascade, the query source chain, the pre-route resolver ladder, the digestion tick. Reach
|
||||
selection is not. It maps severity and presence to a set of channels. It has no claimants and
|
||||
no losers.
|
||||
|
||||
**Thesis two holds.** An arbitration kernel is a package and a convention inside one program.
|
||||
It is not a framework. A framework whose only client is the codebase it came from is that
|
||||
codebase with more ceremony.
|
||||
|
||||
**The answer is two artifacts of different sizes.** One package inside Maven, built from
|
||||
`internal/claim` and `internal/decision`. Both already exist and neither is wired. One small
|
||||
library across the four services, holding the correlation id, the headers, the timeout policy
|
||||
and the named gap. Neither is Spring. The daemons must not get a third.
|
||||
|
||||
**The strongest finding is the duplication, not the shape.** Three structural holes make a
|
||||
route untrustworthy. They are written out by hand in three files, for three consumers, with
|
||||
three return types. `gateLLMDecision` flattens them to a float. `vetoOf` re-derives them as a
|
||||
sentence. `thinReason` re-derives them again as a trace string. That is what having no common
|
||||
unit costs, and it is countable in lines rather than in taste.
|
||||
|
||||
## The five seams, tested
|
||||
|
||||
### 1. The routing cascade. The shape, ordered by hand.
|
||||
|
||||
The claimant is `router.Grammar` (`internal/router/stage0.go:19`). It holds a name, a regex
|
||||
and a `Build` that may still decline. A claim is a regex match plus `ok` from `Build`.
|
||||
|
||||
Ordering is the append order in `buildRouter` (`cmd/mavend/voicewire.go:384`). Twelve appends.
|
||||
Each one carries a comment arguing its position against its neighbours. First match wins at
|
||||
confidence 1.0.
|
||||
|
||||
Below stage 0 the two engines are alternatives, not rivals. The classifier runs only when the
|
||||
router is nil or errored. Inside the classifier the order is cosine score, and the top three
|
||||
are recorded.
|
||||
|
||||
Losers are recorded and change nothing. `noteGrammarOutcomes`
|
||||
(`internal/router/decisiontrace.go:56`) separates a grammar that did not match from one whose
|
||||
`Build` declined. Everything past the winner is marked `NeverAsked`.
|
||||
|
||||
### 2. The query source chain. The shape, ordered by hand, with a boundary in it.
|
||||
|
||||
The claimant is `querySource` (`cmd/mavend/actions_query.go:45`), a name and one function
|
||||
returning `(string, bool)`. Twenty-four of them sit in one slice literal, walked in order. The
|
||||
comment on the slice says the order is load-bearing. It is right.
|
||||
|
||||
This seam carries something the others do not. The personal boundary at line 139 is a stop,
|
||||
not an answer. Everything above it reads the owner's data. Everything below reads the world. A
|
||||
question about him that reaches the boundary ends there.
|
||||
|
||||
### 3. The pre-route resolver ladder. The shape, and the one that hurts.
|
||||
|
||||
Seven rungs, each returning `(reply string, handled bool)`. Ordering is the order of the `if`
|
||||
statements in `runTurn` (`cmd/mavend/voice.go:258`). The roster in `preRouteLadder`
|
||||
(`cmd/mavend/decisiontrace.go:33`) is kept by hand, and its own comment admits nothing
|
||||
enforces the correspondence.
|
||||
|
||||
The recurring bug lives here. A rung claims before the utterance is routed. So the claimant
|
||||
with the earliest and strongest trigger is the one that knows least about what was said. V-560
|
||||
fixed half of it. It computes the route once, before the ladder, and lets the clarify resolver
|
||||
read it. The other rungs still decide without reading it.
|
||||
|
||||
### 4. The digestion tick. The shape, and the only one already done right.
|
||||
|
||||
This corrects the brief. `loop.Tick` (`internal/loop/loop.go:85`) is not a first-to-claim
|
||||
walk. It is an arbitration with a declared comparator.
|
||||
|
||||
- The claimant is `loop.Rule`. Its `Predicate` says whether it wants the turn.
|
||||
- The gate is separate from the claim. `Gate` (`loop.go:21`) checks snooze, cooldown, quiet
|
||||
hours, calendar busy, presence and missing data. `ExplainGate` names which one blocked.
|
||||
- The comparator is data, not position. Max severity wins, and ties break on name.
|
||||
`DefaultRules` states outright that slice order is not load-bearing.
|
||||
- Losers are recorded with what they lost to. `ExplainTick` (`internal/loop/explain.go:86`)
|
||||
fills `LostTo`, and rewrites the previous best when a higher severity displaces it.
|
||||
- Losers get a second life. `DigestEligible` (`loop.go:129`) decides which suppressed
|
||||
candidates are bundled for later. It refuses cooldown and snooze, because neither is
|
||||
restraint.
|
||||
|
||||
Every property the kernel wants already exists here, on five rules. The kernel argues that the
|
||||
other three seams should look like this one. It does not need a new idea.
|
||||
|
||||
### 5. Reach selection. Not the shape.
|
||||
|
||||
`ChannelsFor` (`internal/delivery/channel.go:73`) takes severity and presence and returns a
|
||||
slice of channels. Nothing claims. Nothing passes. Nothing loses. Every channel in the
|
||||
returned slice sends, so there is not even one winner.
|
||||
|
||||
Naming the sinks claimants would be the forced abstraction. It would also hide the property
|
||||
this table has and the ladders lack. It is total, it is pure, and every cell is covered by
|
||||
`TestChannelsForEveryTableCell`.
|
||||
|
||||
One thing in the dispatcher does re-decide. `ErrVoiceNoSession` means the presence guess was
|
||||
wrong, so the remaining channel list is replaced with the away table
|
||||
(`internal/delivery/dispatcher.go:188`). That is a retry on new evidence, not a contest. Leave
|
||||
it alone.
|
||||
|
||||
### What else has the shape
|
||||
|
||||
`fillMatchedSlots` (`internal/router/router.go:212`) arbitrates per slot. A matched value
|
||||
always wins, and the extractor fills only what was left empty. That is the coverage-first rule
|
||||
the kernel proposes, written once for four slots.
|
||||
|
||||
`bestRecall` and the topic veto pick between a fact and a note by score with a margin. That
|
||||
one is a real score comparison, and it should stay one.
|
||||
|
||||
## The abstractions
|
||||
|
||||
Three, not four. The straw man had `Claimant`, `Claim`, `Arbiter` and `Record`. Drop
|
||||
`Claimant`.
|
||||
|
||||
**`claim.Claim`, evidence rather than a verdict.** It exists at `internal/claim/claim.go`,
|
||||
built and tested, imported by one function that nothing calls. It carries `Consumed` and
|
||||
`Unexplained` for coverage, an ordinal `Band`, and a `Veto` string that keeps the reason a
|
||||
float threw away. Coverage is compared before band. That is the fix for the Rome failure,
|
||||
where a parked reminder ate the whole utterance while explaining none of it.
|
||||
|
||||
**`decision.Record`, the trace.** It exists at `internal/decision/decision.go` and it is wired
|
||||
everywhere. It separates won, declined, lost on score, thinned, merged and never asked. The
|
||||
last one is the valuable one. A claimant that never looked reads identically to one that looked and
|
||||
passed. That is what hardcoded order hides.
|
||||
|
||||
**`Arbiter`, the thing that does not exist.** One function. It takes a set of claims and a
|
||||
comparator, returns a winner, and notes the rest. `loop.Tick` is that function, specialised to
|
||||
rules. Generalising it is the proposal.
|
||||
|
||||
**Against a `Claimant` interface.** Every seam already rejected one, for the same reason.
|
||||
`querySource` is a struct of one function because the sources are methods on one handler with
|
||||
no state. An interface would mean one empty type per source. `confirmResolver` is the same
|
||||
shape, and `loop.Rule` is a struct with a closure. An interface would buy a shared name and
|
||||
cost twenty-four empty types. The claimants stay what they are. Each seam builds `claim.Claim`
|
||||
values at its own edge, which is what `router.ClaimOf` already does.
|
||||
|
||||
## What ordering becomes
|
||||
|
||||
Ordering becomes a comparator plus a rank, and the rank is data.
|
||||
|
||||
Today ordering is position in a slice, and position is invisible in the record. Add a rung to
|
||||
`runTurn`, forget `preRouteLadder`, and the rung vanishes from the trace. The roster's own
|
||||
comment admits nothing enforces it.
|
||||
|
||||
The proposal is smaller than a dependency graph. A claimant declares a rank. The arbiter sorts
|
||||
by coverage, then band, then rank. Rank breaks the tie that evidence cannot break.
|
||||
|
||||
**A dependency graph is the wrong tool.** The real constraints are pairwise and local. Day
|
||||
plan before calendar. Praxis before the capture marker. Narrative last. A graph turns those
|
||||
into edges and then needs a topological sort whose output nobody can read. The twelve comments
|
||||
in `buildRouter` would become twelve edges with the arguments deleted. Keep the arguments.
|
||||
|
||||
**Two claimants at the same rank must be an error, caught at wiring time.** Not at turn time.
|
||||
The registry is built once at boot, so a duplicate rank is a boot failure naming both
|
||||
claimants. Falling back to slice order on a tie would restore the invisible ordering the
|
||||
kernel exists to remove.
|
||||
|
||||
**The roster stops being hand-kept.** A claimant registered with the arbiter is on the roster
|
||||
by construction. That deletes the `preRouteLadder` failure mode outright.
|
||||
|
||||
## What it buys
|
||||
|
||||
**It deletes three copies of one test.** The three structural holes appear in
|
||||
`gateLLMDecision` (`internal/router/router.go:271`), in `vetoOf`
|
||||
(`internal/router/claim.go:101`), and in `thinReason` (`internal/router/decisiontrace.go:28`).
|
||||
Three files, three return types, one rule. A fourth consumer would write it a fourth time.
|
||||
With a claim carrying `Veto`, the rule is written once. The float, the sentence and the trace
|
||||
string all derive from it. This one is worth the work on its own.
|
||||
|
||||
**It makes the recurring bug expressible.** Rome, V-567 and V-577 are one defect. The claimant
|
||||
that knows least holds the earliest trigger. Coverage-first arbitration states the fix once,
|
||||
in `MoreSpecificThan`. A parked clarify explaining zero tokens of "какая сейчас погода в
|
||||
Риме?" loses to a weather claim explaining all of them. Nobody has to encode that a parked
|
||||
clarify is less trustworthy than a grammar.
|
||||
|
||||
**The limit.** The kernel prevents the class only where the losing claimant
|
||||
computes low coverage. Rome, V-567 and V-577 all qualify. Each is a stateful claimant
|
||||
swallowing an utterance it explains none of. A claimant that matches a substring does explain
|
||||
those tokens, and coverage does not catch it. V-567's substring match is that case from the
|
||||
other side. Coverage there has to be measured against the whole utterance rather than the
|
||||
matched span. `claimSpans` already does that, in the safe direction. So the kernel
|
||||
prevents most of the class and describes the rest. Claiming more would be dishonest.
|
||||
|
||||
**It makes contention countable.** Today it is not. The 91-case fixture draws two stage-0
|
||||
grammars exactly once, at `ru-query-019`, and both route the same intent. Nobody knows whether
|
||||
contention is rare or whether the fixture omits it. An arbiter that sees every claim can
|
||||
count.
|
||||
|
||||
**What it does not buy.** No accuracy point comes from this alone. Every number in
|
||||
`docs/evals/2026-08-05-routing-resident-model.md` is reachable without it. The kernel is a
|
||||
place to put the fix, not the fix.
|
||||
|
||||
## What it costs
|
||||
|
||||
Every seam rewritten is a chance to break a measured number.
|
||||
|
||||
Re-measure the 91-case routing fixture. The baseline is 75.8% full and 80.2% intent-only at
|
||||
p50 1.19s, in `docs/evals/2026-08-05-routing-resident-model.md`. Judge against the classifier
|
||||
and the resident model, because those are what always answer.
|
||||
|
||||
Re-measure Praxis reach. The baseline is 27/30 overall, 11/12 Praxis and 5/5 lifecycle, in
|
||||
`docs/evals/2026-08-05-praxis-reach.md`. The Praxis grammars are the only path to Praxis, so a
|
||||
reordering that demotes them costs every point.
|
||||
|
||||
Re-run the nine-scenario interleave probe in `docs/evals/2026-08-06-claimant-interleave.md`.
|
||||
Six of nine pass today. That probe measures exactly what this proposal is for.
|
||||
|
||||
Re-run the dialogue contract tests from V-563. They are whole multi-turn traces, and the only
|
||||
tests that cover the ladder as a ladder.
|
||||
|
||||
Latency is the cheap part. A stage-0 query costs 3.7µs. One claim per claimant adds two slices
|
||||
and a token split, on a path whose p50 is over a second.
|
||||
|
||||
The real cost is the arguments. Twenty-three comments across `buildRouter` and `querySources`
|
||||
explain why each entry sits where it does. A migration that turns them into rank integers and
|
||||
drops the prose destroys the only documentation the ordering has.
|
||||
|
||||
## Migration order
|
||||
|
||||
**Step one, no behaviour change. Delete the duplication.** Make `vetoOf` the single definition
|
||||
of the three structural holes. Have `gateLLMDecision` and `thinReason` read it. One rule,
|
||||
three consumers, no new abstraction. Re-measure the routing fixture and nothing else. This is
|
||||
worth landing whether or not the rest does.
|
||||
|
||||
**Step two, the proof. Arbitrate the pre-route ladder.** Smallest seam, seven rungs, and the
|
||||
one with the measured defect. Each resolver returns a claim instead of a bool. The arbiter
|
||||
compares coverage, then band, then rank. The roster comes from the registry. The proof is the
|
||||
interleave probe reaching nine of nine with the routing fixture unmoved.
|
||||
|
||||
**Step three, the query source chain.** Twenty-four sources, most of which already compute a
|
||||
match span. The personal boundary does not become a ranked claimant. It stays a hard stop, and
|
||||
the arbiter runs above it and below it separately. Re-measure Praxis reach and the search and
|
||||
Kiwix fallback.
|
||||
|
||||
**Step four, stage 0, or not at all.** Twelve grammar groups whose order encodes twelve
|
||||
arguments, scoring 20/20 on the fixture. Most to lose, least to gain. Defer it until steps two
|
||||
and three have sat in the deploy long enough to break something.
|
||||
|
||||
**`loop.Tick` moves last or never.** It already has the comparator, the gate with reasons, the
|
||||
loser trace and the loser rescue. Rewriting it to call a generic arbiter risks the digest path
|
||||
to gain a shared name.
|
||||
|
||||
## What must not be in the kernel
|
||||
|
||||
**Authorization.** This is the hard line. CLAUDE.md is explicit that LLM output is not
|
||||
authorization, and that a confirmation binds capability id, target entity, arguments,
|
||||
requester and expiry (`cmd/mavend/confirm.go`). A generic arbiter turns many opinions into
|
||||
one winner. That is the wrong shape for a binding. Make confirm a claimant with a rank and a band, and a claim that scored higher could take the
|
||||
turn from it. The binding would be softened into a comparison. Confirm may report to the record. It must not compete in the
|
||||
arbiter.
|
||||
|
||||
The same rule covers the Hexis path. Free text never reaches a mutating call, and resolution
|
||||
happens against Nexus. Neither is a contest, so neither is arbitration.
|
||||
|
||||
**The personal boundary.** Same reason, different currency. The boundary is not the most
|
||||
specific claimant. It is a stop. A boundary that can lose to a higher-coverage claim is not a
|
||||
boundary. The failure is the owner's notes reaching a search engine.
|
||||
|
||||
**Confidence as a float.** The band exists because the measurement said a calibrated float is
|
||||
not available. The classifier scores 62% correct below its median and 62% above, over a spread
|
||||
0.083 wide. Its top-two margin has a p50 of 0.009
|
||||
(`docs/plans/19-dialogue-arbitration.md`). A kernel with a `Score float64` on the claim invites
|
||||
every claimant to invent one. When V-546 lands a softmax head with a calibrated probability,
|
||||
that number is read beside the bands, not squeezed inside them.
|
||||
|
||||
**Slot extraction and validation.** `fillMatchedSlots` runs after a winner exists, and slot
|
||||
validation against the action schema is V-562. Both ask whether a claim is well formed. Neither
|
||||
asks which claim wins.
|
||||
|
||||
## The second artifact: the ecosystem client
|
||||
|
||||
The kernel is Maven's alone. The one thing here with plural clients by construction is the
|
||||
contract between the four services. Maven implements its side of it twice and a half.
|
||||
|
||||
### Is the contract uniform today? No.
|
||||
|
||||
Nexus and Praxis share one implementation. `ecosystemHTTP` (`cmd/mavend/ecosystem.go:62`) is
|
||||
embedded in both, so both get the same 10 second timeout, the same `setHeaders`, the same
|
||||
typed `ecosystemError`, and the same correlation key. Hexis is a separate client in another
|
||||
repository, vendored at `vendor/github.com/kami/hexis/pkg/client`, and it agrees on some of
|
||||
that and not the rest.
|
||||
|
||||
Eleven divergences. Four of them are defects rather than style.
|
||||
|
||||
**A Hexis 401 is spoken as an outage.** Nexus and Praxis return `*ecosystemError` with
|
||||
`Unauthorized()`, `ContractMismatch()` and `Unreachable()` classifiers
|
||||
(`cmd/mavend/ecosystem.go:144`). Hexis returns `fmt.Errorf` strings
|
||||
(`hexis/pkg/client/client.go:157`). So `unauthorizedEcosystemError`
|
||||
(`cmd/mavend/ecosystem_acts.go:549`) does `errors.As` and always gets false for Hexis. The
|
||||
owner hears "Hexis is down" when the truth is that Hexis refused the credential. The comment
|
||||
at `ecosystem_acts.go:32` says that conflation must not happen.
|
||||
|
||||
**The Hexis discovery hop is uncorrelated.** Hexis carries its own context key
|
||||
(`client.go:81`), invisible to Maven's. The bridge is a manual second stamp at
|
||||
`ecosystem.go:546`. `discoverCapabilities` (`ecosystem.go:527`) does not do it, and discovery
|
||||
runs before execute, so that call goes out with no correlation id. The doc comment above it
|
||||
claims the opposite.
|
||||
|
||||
**Causation is computed and never sent.** `causationID` is derived at
|
||||
`ecosystem_acts.go:771`. Hexis supports `X-Causation-ID` (`client.go:147`). Nothing passes it.
|
||||
|
||||
**A Praxis failure names no service.** `ecosystemGap` (`ecosystem_acts.go:36`) is the shared
|
||||
named-gap helper, and Nexus and Hexis call it. Praxis returns per-verb Russian strings instead
|
||||
(`ecosystem_acts.go:62`, `:73`, `:84`, `:95`). There is no `servicePraxis` constant. A Praxis
|
||||
outage and a Praxis 403 both say "не получилось", with the service unnamed.
|
||||
|
||||
The rest are real but smaller. Hexis sends no `X-Requested-By: maven` and no `Accept` header,
|
||||
so Hexis cannot attribute a read call to Maven at all. Its timeout is 30 seconds against
|
||||
Maven's 10, which Maven cannot change from here. Its success predicate is `>= 400` where the
|
||||
shared client uses `!= 200`. `withToken` is duplicated verbatim per client because the
|
||||
embedded struct cannot return the concrete type. The version constant `"v1"` is defined twice,
|
||||
in two repositories, with nothing keeping the two equal.
|
||||
|
||||
### Would a shared library have plural clients?
|
||||
|
||||
**Partly verifiable, and the honest answer is that two of the three cannot be checked from
|
||||
here.**
|
||||
|
||||
Hexis is verified. It is a Go module consumed through a `replace` directive, and it already
|
||||
publishes a Go client library that Maven imports. That is an existence proof that the pattern
|
||||
works for one of them.
|
||||
|
||||
Praxis is suggested and not proven. Two comments reference Go paths in its repository
|
||||
(`cmd/mavend/factenrichment.go:4`, `cmd/mavend/ecosystem.go:365`). Nothing here compiles
|
||||
against it.
|
||||
|
||||
Nexus is unverifiable from this repository. There is an HTTP base URL and some JSON shapes,
|
||||
and no language signal at all.
|
||||
|
||||
So the claim that a shared library would have plural clients rests on one confirmed adopter
|
||||
and two assumptions. Do not present it as settled. The cheaper test is to fix Maven's side first,
|
||||
in Maven. Offer the package outward once it has proven itself on one caller.
|
||||
|
||||
### What belongs in it
|
||||
|
||||
The straw man is right, with one addition.
|
||||
|
||||
- The correlation id, minted once per action, with one context key that all clients read. The
|
||||
two-key split is the cause of the uncorrelated discovery hop.
|
||||
- Causation, since one of the three already supports it and the value is already computed.
|
||||
- The version header and `X-Requested-By`.
|
||||
- The timeout policy, as one number rather than 10 in one repository and 30 in another.
|
||||
- A typed error with the three classifiers, so a refused credential never speaks as an outage.
|
||||
- The named gap shape, so no client invents its own vocabulary the way Praxis did.
|
||||
|
||||
### What must stay out
|
||||
|
||||
**Authorization, for the second time and the same reason.** The confirmation binding is
|
||||
Maven's. It binds capability id, target entity, arguments, requester and expiry, and it lives
|
||||
in `cmd/mavend/confirm.go`. A shared client that offered a policy hook would invite each
|
||||
service to supply its own, and the binding would become configuration. Free text never reaches
|
||||
a mutating Hexis call, and entity resolution stays in Nexus. Neither belongs in a transport
|
||||
library.
|
||||
|
||||
Retry stays out too, or nearly. The only retry Maven has is Nexus enrichment at the worker
|
||||
layer, with backoff from one minute to one hour (`cmd/mavend/factenrichment.go:49`). It is
|
||||
there because enrichment is a background job with no listener. A turn cannot retry, because
|
||||
the owner is standing there. A transport-level retry in a shared library would hide a
|
||||
second budget behind a turn that already has a name for failing.
|
||||
|
||||
## The daemons must not get a framework
|
||||
|
||||
The instinct is right, and half the work is already done in a way that shows why.
|
||||
|
||||
Transport is shared and abstracted. `ipc.Dial` (`internal/ipc/client.go:94`) and `ipc.Listen`
|
||||
(`internal/ipc/server.go:202`) both go through `netaddr`. A bare path is a unix socket with
|
||||
`SO_PEERCRED` identity. A `tcp://host:port?token=...` address binds a network listener with a
|
||||
mandatory token. Callers pass a string and never branch on scheme. That seam made the
|
||||
workstation offload a deployment rather than a build. It is a library, it has nine clients in
|
||||
this repository, and it earns its weight.
|
||||
|
||||
Startup is not shared, and should not be. There are twelve binaries under `cmd/`, each with a
|
||||
hand-written `main`, and no common lifecycle package. `cmd/mavend/main.go` runs 830 lines. The
|
||||
reason is visible in it. The daemon can boot **locked**, with no store at all, and wire its
|
||||
components later from inside an unlock handler. Thirteen subsystem pointers are pre-declared
|
||||
nil (`main.go:236`) and filled on one of two paths. The whole graph is built a second time
|
||||
inside the unlock path (`main.go:478`). A container owning object lifecycle would have to model
|
||||
a graph whose nodes do not exist at boot and may never exist.
|
||||
|
||||
The deeper reason is a design property. Every daemon degrades alone, and every ecosystem
|
||||
client is nil unless configured. A wiring framework's job is to fail loudly when a dependency
|
||||
is missing. Maven needs the opposite: a missing dependency is a named gap in one answer and a
|
||||
working daemon everywhere else. An abstraction over startup would trade that property for a
|
||||
shorter `main`.
|
||||
|
||||
The duplicated locked and unlocked wiring in `mavend` is a real defect and worth fixing. Fix it
|
||||
by extracting one function in that file. That is not a framework.
|
||||
|
||||
## The honest comparison
|
||||
|
||||
Spring and Django own object lifecycle and request handling for applications they have never
|
||||
seen. That is where the weight is paid for. The abstraction is general because the clients are
|
||||
unknown.
|
||||
|
||||
Maven is one application, on one box, with one user. Its clients are known, there are nine of
|
||||
them, and they are in this repository. A framework here would be an abstraction with a census.
|
||||
|
||||
So the right unit is a shared package and a convention. That is a smaller and more defensible
|
||||
claim. The evidence for its size is that both halves already exist in that form, and nobody
|
||||
called them a framework. `internal/ipc` is a shared package and a convention, and it carried
|
||||
the daemons off the box. `internal/decision` is a shared package and a convention, and it made
|
||||
the losers readable in one release. `internal/claim` is the third, written and waiting for a
|
||||
caller.
|
||||
|
||||
The thing to disagree with: **the answer is two artifacts of different sizes. A package inside
|
||||
Maven, a library across the four services, and neither one is Spring.**
|
||||
Reference in New Issue
Block a user