@@ 1107,13 1107,55 @@ CREATE TABLE digest_mark (
);
```
-Projects are deliberately absent: a project is a saved filter, so it is a name
-plus a space-id list, and adding it before there are several spaces to filter
-would be speculative. It arrives with the meta-project in Phase 2.
+```sql
+CREATE TABLE project (
+ id SERIAL PRIMARY KEY,
+ owner TEXT NOT NULL,
+ name TEXT NOT NULL,
+ created TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE (owner, name)
+);
+
+-- Membership is a SET: the composite key both dedupes and indexes project->space.
+CREATE TABLE project_space (
+ project_id INTEGER NOT NULL REFERENCES project(id) ON DELETE CASCADE,
+ space_id INTEGER NOT NULL REFERENCES space(id) ON DELETE CASCADE,
+ PRIMARY KEY (project_id, space_id)
+);
+CREATE INDEX ix_project_space_space ON project_space (space_id);
+```
-`comment` is likewise absent — inline comments are post-v1, and the anchoring
-model (`doc_id`, heading path, block index, block hash) should be settled by
-building the review UI before it is committed to a schema.
+**The meta-project is not a row.** `~owner/+everything` resolves to an implicit
+"all spaces" filter, and the name is reserved so nothing can create a row that
+shadows it. A stored `+everything` would need a **sync job** appending every
+newly created space — which the Project section explicitly rules out — and that
+job's single failure mode is a meta-project silently missing a space, which is
+invisible. It also must not be renameable or deletable: a corpus-wide view is not
+an object with a lifecycle. Consequence to remember: a project listing cannot
+show the meta-project, and any UI must add it by hand.
+
+**Empty-means-nothing, and the polarity trap that follows.** A freshly created
+project has no members and must select **nothing**; the corpus-wide filter is a
+distinct `All` flag, not an empty list. This is the opposite polarity from the
+search layer, where an empty space list means *every* space:
+
+> **Wiring a project to a search query by passing its member list straight
+> through inverts the meaning of an empty project — from "nothing" to "the entire
+> corpus".** A silent scope inversion, invisible in any test that only uses
+> non-empty projects.
+
+Documented on both types today; the durable fix is for the query to take the
+filter type itself rather than a bare slice, so the mistake stops being
+expressible. That change is deferred only because the read surfaces were being
+built against the current signature at the time.
+
+The meta-project is **corpus-wide, not viewer-scoped**. With no visibility levels
+and no ACL rows in v1 there is no "spaces the caller may read" to scope it to;
+that phrase implies machinery that does not exist.
+
+`comment` remains absent — inline comments are post-v1, and the anchoring model
+(`doc_id`, heading path, block index, block hash) should be settled by building
+the review UI before it is committed to a schema.
## Implementation plan
@@ 1259,9 1301,10 @@ degrading, which is the point of listing them.
- **Naming.** `spec.sr.ht` / `spec.srht.bigb.es` follows the
named-by-function pattern of the two siblings. `docs.sr.ht` collides
conceptually with upstream `man.sr.ht`.
-- **Project URL namespace.** `~user/+project` distinguishes projects from spaces
- (`~user/space`) in one character; alternatives are `/projects/~user/name` or
- reusing hub.sr.ht's own namespace.
+- ~~**Project URL namespace.**~~ **Settled: `~user/+project`**, one character
+ distinguishing a project from a space (`~user/space`), with `+everything`
+ reserved for the meta-project. Isolated in the ref parser, so changing it later
+ is one function.
- **Port.** compare.sr.ht is on 5090, dolt.sr.ht on 5306–5308. 5091 is free and
is what the config block above assumes.
- **MCP transport.** Streamable HTTP on the same chi router (`/mcp`) keeps it to