You can't write good code without getters.
The reason why isn't because getters don't break encapsulation, they do. It isn't because getters don't tempt people to not bother following OOP which would have them put methods with the data they act on. They do. No, you need getters because of boundaries.
The ideas of encapsulation and keeping methods together with the data they act on simply don't work when you run into a boundary that keeps you from moving a method and so forces you to move data.
It's really that simple. If you use getters when there is no boundary you end up having no real objects. Everything starts to tend to the procedural. Which works as well as it ever did.
True OOP isn't something you can spread everywhere. It only works within those boundaries.
Those boundaries aren't razor thin. They have code in them. That code can't be OOP. It can't be functional either. No this code has our ideals stripped from it so it can deal with harsh reality.
Michael Feathers called this code fascia after that white connective tissue that holds sections of an orange together.
This is a wonderful way to think about it. It explains why it's ok to have both kinds of code in the same code base. Without this perspective many new programmers cling to their ideals hard, then have their hearts broken and give up on these ideals when they hit their first boundary.
The ideals only work in their proper place. Don't give up on them just because they don't work everywhere. Use them where they work. That place is the juicy part that the fascia protects.
A simple example of a boundary is a collection. This holds something and has no idea what it is. How could a collection designer possibly move the behavioral functionality of the held object into the collection when they have no idea what it's going to be holding? You can't. You're up against a boundary. Which is why collections have getters.
Now if you did know, you could move that behavior, and avoid moving state. When you do know, you should. You just don't always know.
Some people just call this being pragmatic. And it is. But it's nice to know why we have to be pragmatic.
You've expressed that you don't want to hear semantic arguments and seem to be advocating putting "sensible getters" everywhere. You're asking for this idea to be challenged. I think I can show the idea has problems with the way you've framed it. But it also think I know where you're coming from because I've been there.
If you want getters everywhere look at Python. There is no private keyword. Yet Python does OOP just fine. How? They use a semantic trick. They name anything meant to be private with a leading underscore. You're even allowed to read from it provided you take responsibility for doing so. "We're all adults here", they often say.
So what's the difference between that and just putting getters on everything in Java or C#? Sorry but it's semantics. Pythons underscore convention clearly signals to you that you're poking around behind the employees only door. Slap getters on everything and you loose that signal. With reflection you could have stripped off the private anyway and still not have lost the semantic signal. There simply isn't a structural argument to be made here.
So what we're left with is the job of deciding where to hang the "employees only" sign. What should be considered private? You call that "sensible getters". As I've said, the best justification for a getter is a boundary that forces us away from our ideals. That shouldn't result in getters on everything. When it does result in a getter you should consider moving the behavior further into the juicy bit where you can protect it.
This separation has given rise to a few terms. A Data Transfer Object or DTO, holds no behavior. The only methods are getters and sometimes setters, sometimes a constructor. This name is unfortunate because it's not a true object at all. The getters and setters are really just debugging code that give you a place to set a breakpoint. If it wasn't for that need they'd just be a pile of public fields. In C++ we used to call them structs. The only difference they had from a C++ class was they defaulted to public.
DTO's are nice because you can throw them over a boundary wall and keep your other methods safely in a nice juicy behavior object. A true object. With no getters to violate it's encapsulation. My behavior objects may eat DTO's by using them as Parameter Objects. Sometimes I have to make a defensive copy of it to prevent shared mutable state. I don't spread mutable DTO's around inside the juicy part within the boundary. I encapsulate them. I hide them. And when I finally run into a new boundary I spin up a new DTO and throw it over the wall thus making it someone else's problem.
But you want to provide getters that express identity. Well congrats you've found a boundry. Entities have an identity that goes beyond their reference. That is, beyond their memory address. So it has to be stored somewhere. And something has to be able to refer to this thing by it's identity. A getter that expresses identity is perfectly reasonable. A pile of code that uses that getter to make decisions that the Entity could have made itself is not.
In the end it's not the existence of getters that is wrong. They are far better than public fields. What's bad is when they are used to pretend you're being Object Oriented when you're not. Getters are good. Being Object Oriented is good. Getters are not Object Oriented. Use getters to carve out a safe place to be Object Oriented.