I will say, based on what you've shared, you and I appear to have fairly similar approaches / philosophies to design - which I don't often find. I also agree this thread is way deeper than I planned, but it's a topic I really enjoy so here's another reply. :)

Regarding the choices I listed above I'm firmly in the Static Typing / SQL / TypeScript / Fixed Schema / "has-a" / make-the-impossible-unrepresentable camps. Which, excluding the last one, I believe aligns with your views of the word. (I do also agree that a statically / optionally typed clojure would probably be my preference. And I haven't spent much/any time yet with clojure's spec). I mainly listed them with alternates as those alternates are valid choices at times even if they aren't my choice and I think I can defend why those I listed are better (and it seems you agree). But the other reason I listed them is that ECS clearly falls in with the dynamically typed / NoSQL / ... camps. It is structural typing (or duck typing). But if I go that route how do I enforce that my modeling is correct? Or in a computer game example, if I model my chat messages as entities, how do I ensure that a chat message can never "hold a grenade launcher" via a component? Clearly an error in modeling. And if the answer is "write good code", then that's fine, but it's definitely the dynamically typed approach to solving the problem which isn't my usual preference.

For me that's an important part of modeling - making invalid state unrepresentable. So if I were actually going with ECS (or ECA), I'd actually go ahead and build a type system on top of it. I'd build a meta-component which is a name of a type and it would specify via statements (using your terminology) this type named X must have these components and can optionally have these other components. Any other components by default it can't have. A type (specified by the type meta-component) would be a collection of components. And entity must have an entry in the type component specifying which other components it can and must have. But then it brings the next question, can a type refer to other types? If I declare a type A that has 30 components and a type B that has 20, and I now want to declare a type C, do I have to list out all 50 components? Or can I say type C has all of Type A's and Type B's components? Once I've now decided to include these types that reference types (or higher order / higher-kinded types), and I include the most basic two ways of combining them sum/product, or enum/struct or tuple/discriminatedUnion or which ever names you want to call them by and I have a full support for ADTs I'd be happy. But then the pragmatist in me would ask now that you've added a fully structured type system on top of your ECS/ECA have you now lost all of the flexibility that ECA gave you? I don't know the answer to that. At this point, we've reached thought exercise for me so I'd have to do it and see. But It would be very tempted to explore to to answer those questions and decide do I stick with ECS or include typing on top to better ensure my system adheres to my business domain constraints.

So jumping back up the stack a bit, the C2 Payroll list is a great real-world list of the style of things that people often ignore. However, I think things go deeper than that. That list is a collection of condition action pairs. If an entity has this situation then treat it like the following. That is a big part of of business logic. But it ignores the other part. Which entities are allowed to have which situations. For example I want to ensure that in my system a non-union worker can never have the UPGWA old-timer pay. Funnily enough that's actually one of the things I was playing around with in clojure from concepts of Out of the Tar Pit.

> After being sick for 3 days (when your pay comes from your normal account), you go on disability, where your pay continues but comes from another account. After N days you only get 60% of your pay, unless you come back and go out on a different disability.

You see, for this hard to describe constraint I see the same problem as you do with it. But my approach was the exact opposite. Defining constraints in the data layer is great - but only a subset of constraints can be easily defined in a relational data layer. Your approach was push everything to the application layer. My approach was add support for way more complex constraints in the data layer.

I'd model the payroll constraint above as: have your only base table be statements of employee X was sick on Y day. Have a derived table / sql view that takes the last 3 days of entries and sees if it was 3 days ago, if so in an 'out of the tar pit' derived-data sense or a Functional Reactive Programming sense enable a field on this derived table that says they are now on disability. Another set of code or table that says 'if that field is enabled then reference the alternate account'. To do this you need support for so many more constraints than basic SQL support now. So that was my approach build in support all of these complex row level, multi-row, and full table constraints into your data system (or just above it) so that your domain language could be expressed as constraints on either you base statement table, or constraints on your derived data / materialized view tables. And yup with each insert, modify, delete in an FRP/trigger style all of your relevant derived tables would be updated and their constraints checked. It turned out that once you start deriving data the constraints aren't too complicated, lots of map, filter, reduce and the rest across your table rows (yes you could express the same things in SQL but I was playing with clojure and the functional approach here seemed cleaner). But this project never really made it past 1 weekend of playing around. It was promising, but I will also say it was solving a slightly different problem which is "how do you get state out of code?" - and that was via dynamically updating derived data (similar enough to FRP).

So all in all, if I'm understanding you correctly, you and I may both be seeing the same problem and approached it slightly differently. But I would agree it is a area where there is a lot of potential for improving software quality and modeling. But maybe you went a different way with your ECA and building relational types on top of it? I don't quite follow that final paragraph you wrote about arithmetic, but will think it over some more.