Skip to content

examples: where the 42k lines of application code go, and the four building blocks that would remove most of it #793

Description

@Yaraslaut

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

  1. The QML bridge (8,496 lines) — largest, least application-specific,
    and the erased form already exists in the wire layer.
  2. Validation (153 sites) — smallest change, possibly just discoverability.
  3. Error vocabulary (17 definitions, 276 throws) — mechanical, but
    constrained by invariant 3.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions