test(cli): shared-docs journey + docs (§11.6 docs C5)

End-to-end docs journey: a group declares a kv AND a docs collection via the
string-or-table manifest; an authenticated app A docs::shared_collection(
"articles").create(#{...}), app B finds it back across the subtree (exercising
the shared find DSL); a sibling-subtree app gets CollectionNotShared;
collections ls shows both kinds; re-apply NoOp. Full journey suite 116/116.

Docs: groups-and-project-tool §11.6 (KV+docs slices shipped; files/topics/queue
deferred), sdk-shape.md (docs::shared_collection + string-or-table), CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-30 19:09:41 +02:00
parent 5efb068b9f
commit a00454e5de
4 changed files with 164 additions and 17 deletions

View File

@@ -1044,24 +1044,31 @@ Resolved items now live inline next to their topic. What genuinely remains:
real shared-scope authz model. Optionally, trigger/route **templates** (§4.5) if cardinality
demands.
> **Shipped — §11.6 KV slice (full shared read/write).** A group declares a collection
> group-shared (`[group]` manifest `collections = ["catalog"]`, owner-polymorphic marker table
> `0052_group_collections`, `kind='kv'`); the data lives in `0053_group_kv_entries`, keyed by the
> owning `group_id` (NOT app — a shared row belongs to the group). Scripts read/write via the
> **explicit** `kv::shared_collection("catalog")` handle (a distinct `GroupKvHandle`; `shared`
> alone is a Rhai reserved word, hence `shared_collection`). The service resolves the owning group
> from `cx.app_id`'s ancestor chain (nearest-group-wins) — **that walk is the isolation boundary**:
> a foreign app's chain never contains the owning group, so the name returns `CollectionNotShared`.
> **Trust model:** reads are open to any subtree script (anonymous public HTTP included — the
> declaration *is* the grant); **writes require an authenticated editor+** on the owning group
> (`GroupKvWrite`, fails closed on an anonymous principal). CASCADE on group delete; an app delete
> leaves the shared data. Live- + journey-validated (app A writes, app B reads; a sibling-subtree
> app gets `CollectionNotShared`).
> **Shipped — §11.6 KV + DOCS slices (full shared read/write).** A group declares a collection
> group-shared (`[group]` manifest `collections = [...]`, owner-polymorphic marker table
> `0052_group_collections` with a `kind` discriminator); the data lives in a per-kind store keyed by
> the owning `group_id` (NOT app — a shared row belongs to the group): `0053_group_kv_entries`
> (`kind='kv'`) and `0054_group_docs` (`kind='docs'`, the queryable-JSON store). Scripts read/write
> via the **explicit** `kv::shared_collection("catalog")` / `docs::shared_collection("articles")`
> handles (distinct `GroupKvHandle`/`GroupDocsHandle`; `shared` alone is a Rhai reserved word, hence
> `shared_collection`). The service resolves the owning group from `cx.app_id`'s ancestor chain
> **filtered by kind**, nearest-group-wins — **that walk is the isolation boundary**: a foreign
> app's chain never contains the owning group, so the name returns `CollectionNotShared`; a `kv` and
> a `docs` collection of the same name are distinct stores. **Trust model:** reads are open to any
> subtree script (anonymous public HTTP included — the declaration *is* the grant); **writes require
> an authenticated editor+** on the owning group (`GroupKvWrite`/`GroupDocsWrite`, fail closed on an
> anonymous principal). The docs slice reuses the `docs_filter` DSL — `build_find_query` was
> generalized on its owner column (`docs`/`app_id` vs `group_docs`/`group_id`), both compile-time
> literals, so the find SQL has one source. Declarative authoring uses the **string-or-table** form:
> `collections = ["catalog", { name = "articles", kind = "docs" }]` (bare string = `kv`). CASCADE on
> group delete; an app delete leaves the shared data. Live- + journey-validated for both kinds (app
> A writes, app B reads/finds; a sibling-subtree app gets `CollectionNotShared`).
>
> **Deferred (documented gaps):** write-triggers/events on shared collections (the "group trigger
> has no app to watch" problem); docs/files/topics/queue shared collections (the `kind` column
> generalizes); per-group total-size quotas + write-rate limits; CAS/`set_if`; app-declared
> collections. Multi-node tree-apply leans on the runtime backstop for no-op edges, as elsewhere.
> has no app to watch" problem); **files/topics/queue** shared collections (the `kind` column
> generalizes — files needs group-keyed disk paths); per-group total-size quotas + write-rate
> limits; CAS/`set_if`; app-declared collections. Multi-node tree-apply leans on the runtime
> backstop for no-op edges, as elsewhere.
### 11.1 Re-sequencing review (post-Phase-3)

View File

@@ -99,6 +99,15 @@ script. **Reads** are open to any subtree script (including anonymous
public endpoints — declaring the collection *is* the grant); **writes**
require an authenticated principal with editor+ on the owning group.
**Docs too.** The same handle exists for the queryable-JSON store:
`docs::shared_collection("articles")` returns a shared-docs handle with the
full `create`/`get`/`find`/`find_one`/`update`/`delete`/`list` surface (same
trust model + isolation boundary as the KV one). A `[group]` declares a
collection's store with a kind: `collections = ["catalog", { name =
"articles", kind = "docs" }]` — a bare string is `kv`, a `{ name, kind }`
table sets it. A `kv` and a `docs` collection of the same name are distinct
stores. (Files/topics/queue shared collections are not yet implemented.)
## Error convention
- **Throw on failure.** `widgets.set("k", "v")` throws a Rhai runtime