Separate bugs reports for sure, Eli, but I first thought it useful to discuss holistically to illustrate. Let's consider this bug report to be solely about project.

Dmitry, I think the following patch to project--write-project-list might suffice to avoid this in the future:

diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el
index f2a27ff91dd..f9b3b8891bc 100644
--- a/lisp/progmodes/project.el
+++ b/lisp/progmodes/project.el
@@ -1824,7 +1824,7 @@ project--read-project-list
                (lambda (elem)
                  (let ((name (car elem)))
                    (list (if (file-remote-p name) name
-                           (abbreviate-file-name name)))))
+                           (file-name-as-directory (abbreviate-file-name name))))))
                (condition-case nil
                    (read (current-buffer))
                  (end-of-file

This causes /Users/xxx to appear as /Users/xxx/ in the project file and then project--read-project-list produces ~/ rather than ~ for which file-name-directory produces nil.

(defun project--read-project-list ()
...
                           (file-name-as-directory (abbreviate-file-name name))))))

Note: I did not consider the tramp/remote file cases in this analysis.

On Sat, Feb 1, 2025 at 3:50 AM Eli Zaretskii <eliz@gnu.org> wrote:
> From: Ship Mints <shipmints@gmail.com>
> Date: Fri, 31 Jan 2025 19:10:47 -0500
>
> The behaviors below can be seen on 29, 30, 31 (I don't have an earlier Emacs to test but these issues
> probably are long-standing). Let's discuss these as I suspect there may be historical reasons for these
> behaviors that I'm unaware of, or maybe these are legitimate, even if old. It's also possible that I'm
> misunderstanding something.

Thanks, but please don't mix separate issues in the same bug report.
If you think file-name-directory has a bug (I disagree), please report
a separate bug against file-name-directory.  If you think file-equal-p
should accept nil as its argument (I'm not sure I will agree, but
maybe you will convince), please report a separate feature-request bug
about that function.

> Issue 3:
>
> project-forget-projects-under crashes when `project-list-file' contains "/home/username", which appears in
> my remembered project list.

It doesn't crash, it signals an error.  Crashing means the entire
Emacs session goes down in flames.

> (defun project--read-project-list ()
> ...
>    (abbreviate-file-name name) ; converts "/home/username" to "~"
>
> (defun project-forget-projects-under ()
> ...
>       (dolist (proj (project-known-project-roots))
>         (when (file-equal-p (file-name-directory proj) dir) ; <--- boom on nil
>
> I'm happy to submit patches for any or all of these including guarding project-forget-projects-under to check
> for nil and/or amending project--read-project-list to convert "~" to "~/" as workarounds, with each submitted
> against a discrete bug number.

I think this should be solved in project.el, but let's hear what
Dmitry (CC'ed) thinks.