Yes, and this is the most commonly used workflow these days with jj (the "squash workflow"). You have a top commit, which is also your working directory, and you make changes freely. To "stage" something, you squash it down into the next commit (all changes, or interactively selected changes with -i aka --interactive).
This generalizes to using a whole stack of "stages", by doing ´squash --into´ to select the patch to put the changes into if it's not just the next one down.
Oh, I missed this part. I think jj is better here in at least one scenario.
Specifically, I believe the scenario you're talking about is:
change file1
build, producing binary.out
squash the change down (leaving your working copy unmodified)
rebuild
If the squash updates the timestamp on file1, then the rebuild will redo the compilation steps that use file1 as input.
When I test it out, it looks like doing a whole-file squash with jj does not update the timestamp. Hm... I guess even a partial squash doesn't update the current contents, let me try that too... yes, again jj does not touch the file nor update its timestamp.
So it looks like it does do what you want, if I'm understanding things correctly.
This generalizes to using a whole stack of "stages", by doing ´squash --into´ to select the patch to put the changes into if it's not just the next one down.