What the seven ladder rungs actually write, measured, so that the framework can
be given the building blocks that remove most of it. Tests are excluded —
this is only the code needed to run an application.
Where the 42,473 lines go
rung src+include gui_lib qml total
pastebin 1806 608 274 2688
polls 2479 1075 730 4284
bookmarks 4037 1280 680 5997
kanban 5492 2664 1607 9763
ledger 6103 1810 553 8466
lims 4283 1059 599 5941
crm 5334 0 0 5334
TOTAL 29534 8496 4443 42473
Repeated shapes, counted across all seven:
140 execute() overloads 185 BRIDGE_REGISTER_* sites
153 validate() definitions 160 journal / log calls
130 action.validate() call sites 117 transaction sites
105 role and permission checks 17 error-class definitions
276 throw sites of those errors 22 *_qml_bridge files
26 presenter files
1. Validation — 153 written, 1 derived
Classifying the 103 validate() bodies short enough to capture as one
expression:
|
count |
|
vacuous — return true |
29 |
validates nothing |
pure required-ness — !x.empty(), id.hasValue(), n > 0 |
63 |
restates the schema |
| genuine business rules |
11 |
4 of which are !name.empty() && name.size() <= kMax… |
89% either do nothing or restate something already computed.
schemaJson<A>() derives required from the action type — a member is
required unless it is std::optional — so those 63 bodies re-state by hand
what the framework computes from the same type in the same process. Four of the
remaining eleven are length bounds, i.e. minLength/maxLength, so the
irreducible set is nearer seven.
The helper already exists and is documented. include/morph/forms/forms.hpp:114
gives it in its own example:
[[nodiscard]] bool validate() const { return morph::forms::allRequiredEngaged(*this); }
Across 153 definitions, one action calls it. So this is not a missing
primitive — it is an undiscoverable one, and that has a far cheaper fix than a
new API. Rule that in or out first.
2. The QML bridge — 8,496 lines of hand-written type erasure
22 *_qml_bridge files and 26 presenters across six rungs. kanban's board
bridge alone is 1,389 lines. What they do is mechanical: turn typed model
results into QVariantMap / QVariantList properties, and typed calls into
Q_INVOKABLEs.
The clearest evidence that it is mechanical — this exact signature is
hand-written seven times, once per rung:
Q_INVOKABLE void submitIfValid(const QString& actionType, const QString& bodyJson);
An action name and a JSON body. That is precisely the erased form the wire
layer already speaks, and schemaJson<A>() already describes the payload. Six
rungs then hand-convert results back into QVariant containers — 189 mentions
across the six.
This is the largest single block in the corpus, and the least
application-specific.
3. Error hierarchies — the same 2–4 types, seven times
pastebin PastebinError : runtime_error ValidationError : PastebinError
polls PollsError : runtime_error ValidationError : PollsError
bookmarks BookmarksError : runtime_error ValidationError : BookmarksError
kanban KanbanError : runtime_error ValidationError : KanbanError
ledger LedgerError : runtime_error ValidationError, VersionConflict, EmptyPrincipalError
lims LimsError : runtime_error ValidationError, EmptyPrincipalError
crm CrmError : runtime_error ValidationError, EmptyPrincipalError
17 definitions, 276 throw sites. Every rung reinvents "validation failed",
"not found", "forbidden", "conflict" — the same four outcomes a framework
mediating GUI and backend has to transport anyway. EmptyPrincipalError
appears independently in three.
Note the constraint this must respect: invariant 3 — the value type reports
the fact, the layer decides the policy. A shared error vocabulary is a
transport concern; what a rung does about a conflict stays the rung's.
4. The handler frame — ~1,000 lines of identical opening and closing
Nearly every one of the 140 execute() overloads is: principal check →
validate() with a hand-written throw → DataMapper → transaction → mutate →
commit → journal → return. At roughly seven lines of frame that is ~1,000
lines whose shape is identical across seven independently written rungs.
Two parts of it are already tracked and must not be re-litigated here:
- the post-commit half — a throw after the commit reporting a durable
success as a failure — is morph#789, confirmed in three rungs;
IReplayLedger was already promoted for exactly this reason, after five
rungs hand-wrote the same idempotency table.
What is untracked is the opening: auth, validate, throw. That is where the
29 vacuous validate() bodies live, and it is narrower than "a handler seam".
A measured negative, worth recording
Field-by-field copying is not the problem. The row.x = action.x and
.x = y shuttling between DTOs, records and results is 514 lines of 29,534 —
about 1.7%. I expected it to dominate. An auto-mapping or auto-DTO primitive
would solve a problem this corpus does not have, and building one would be
effort spent against a measurement nobody took.
Priority, by size and by how application-specific the work is
- The QML bridge (8,496 lines) — largest, least application-specific,
and the erased form already exists in the wire layer.
- Validation (153 sites) — smallest change, possibly just discoverability.
- Error vocabulary (17 definitions, 276 throws) — mechanical, but
constrained by invariant 3.
- The handler opening (~1,000 lines) — after morph#789 settles, since
they touch the same function.
Verification status
Measured on f0176034, over
examples/{pastebin,polls,bookmarks,kanban,ledger,lims,crm}, excluding every
tests/ directory. Every count above is a grep over that corpus and is
reproducible.
Not verified:
- The 50
validate() bodies too long to capture as one expression were not
classified. If they skew toward genuine rules the 89% falls; if vacuous, it
rises. The figure is of single-expression bodies and is not projected onto
all 153.
- That a generic QML bridge is feasible. The rungs' bridges also carry
offline queue depth, dead-letter counts, sync status and per-rung
Q_PROPERTY sets. How much survives generalisation is unmeasured, and the
8,496 figure is the whole layer, not the removable part.
- That the error hierarchies can be shared without violating invariant 3.
- Whether any rung depends on
validate() being called for a side effect.
What must not happen
- Do not delete
validate(). Seven bodies carry real rules. The target is
that an action with no rules of its own writes nothing — not that the hook
disappears.
- Do not build the DTO mapper. See the measured negative.
- Do not fold morph#789 into this. Same function, different defect.
What would change the verdict
Close as fixed when a new rung of comparable scope can be written in materially
fewer lines, measured the same way — that is the only number that settles
whether the building blocks landed.
Re-open, or re-scope, if classifying the 50 long validate() bodies moves the
89% enough to change the priority order above.
What the seven ladder rungs actually write, measured, so that the framework can
be given the building blocks that remove most of it. Tests are excluded —
this is only the code needed to run an application.
Where the 42,473 lines go
Repeated shapes, counted across all seven:
1. Validation — 153 written, 1 derived
Classifying the 103
validate()bodies short enough to capture as oneexpression:
return true!x.empty(),id.hasValue(),n > 0!name.empty() && name.size() <= kMax…89% either do nothing or restate something already computed.
schemaJson<A>()derivesrequiredfrom the action type — a member isrequired unless it is
std::optional— so those 63 bodies re-state by handwhat the framework computes from the same type in the same process. Four of the
remaining eleven are length bounds, i.e.
minLength/maxLength, so theirreducible set is nearer seven.
The helper already exists and is documented.
include/morph/forms/forms.hpp:114gives it in its own example:
Across 153 definitions, one action calls it. So this is not a missing
primitive — it is an undiscoverable one, and that has a far cheaper fix than a
new API. Rule that in or out first.
2. The QML bridge — 8,496 lines of hand-written type erasure
22
*_qml_bridgefiles and 26 presenters across six rungs. kanban's boardbridge alone is 1,389 lines. What they do is mechanical: turn typed model
results into
QVariantMap/QVariantListproperties, and typed calls intoQ_INVOKABLEs.The clearest evidence that it is mechanical — this exact signature is
hand-written seven times, once per rung:
An action name and a JSON body. That is precisely the erased form the wire
layer already speaks, and
schemaJson<A>()already describes the payload. Sixrungs then hand-convert results back into
QVariantcontainers — 189 mentionsacross the six.
This is the largest single block in the corpus, and the least
application-specific.
3. Error hierarchies — the same 2–4 types, seven times
17 definitions, 276 throw sites. Every rung reinvents "validation failed",
"not found", "forbidden", "conflict" — the same four outcomes a framework
mediating GUI and backend has to transport anyway.
EmptyPrincipalErrorappears independently in three.
Note the constraint this must respect: invariant 3 — the value type reports
the fact, the layer decides the policy. A shared error vocabulary is a
transport concern; what a rung does about a conflict stays the rung's.
4. The handler frame — ~1,000 lines of identical opening and closing
Nearly every one of the 140
execute()overloads is: principal check →validate()with a hand-written throw →DataMapper→ transaction → mutate →commit → journal → return. At roughly seven lines of frame that is ~1,000
lines whose shape is identical across seven independently written rungs.
Two parts of it are already tracked and must not be re-litigated here:
success as a failure — is morph#789, confirmed in three rungs;
IReplayLedgerwas already promoted for exactly this reason, after fiverungs hand-wrote the same idempotency table.
What is untracked is the opening: auth, validate, throw. That is where the
29 vacuous
validate()bodies live, and it is narrower than "a handler seam".A measured negative, worth recording
Field-by-field copying is not the problem. The
row.x = action.xand.x = yshuttling between DTOs, records and results is 514 lines of 29,534 —about 1.7%. I expected it to dominate. An auto-mapping or auto-DTO primitive
would solve a problem this corpus does not have, and building one would be
effort spent against a measurement nobody took.
Priority, by size and by how application-specific the work is
and the erased form already exists in the wire layer.
constrained by invariant 3.
they touch the same function.
Verification status
Measured on
f0176034, overexamples/{pastebin,polls,bookmarks,kanban,ledger,lims,crm}, excluding everytests/directory. Every count above is a grep over that corpus and isreproducible.
Not verified:
validate()bodies too long to capture as one expression were notclassified. If they skew toward genuine rules the 89% falls; if vacuous, it
rises. The figure is of single-expression bodies and is not projected onto
all 153.
offline queue depth, dead-letter counts, sync status and per-rung
Q_PROPERTYsets. How much survives generalisation is unmeasured, and the8,496 figure is the whole layer, not the removable part.
validate()being called for a side effect.What must not happen
validate(). Seven bodies carry real rules. The target isthat an action with no rules of its own writes nothing — not that the hook
disappears.
What would change the verdict
Close as fixed when a new rung of comparable scope can be written in materially
fewer lines, measured the same way — that is the only number that settles
whether the building blocks landed.
Re-open, or re-scope, if classifying the 50 long
validate()bodies moves the89% enough to change the priority order above.