diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 61fc06628bd36462c53daeb80738c1f7b1c7d408..2978a94c7c30b26affca908b9c6ff09e1ab5f1b2 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1107,13 +1107,55 @@ seen_at TIMESTAMPTZ NOT NULL ); ``` -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 @@ - **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