feat(cli): collections manifest + ls + journeys; rename to kv::shared_collection (§11.6 C5)

Finish the §11.6 KV slice: declarative authoring, read-only inspection, the
runtime journey, and docs — plus a forced SDK-name fix.

- SDK name: `shared` is a Rhai RESERVED keyword, so `kv::shared(...)` is a parse
  error. Renamed the handle constructor to `kv::shared_collection(...)`
  (keeps the user's "shared" intent; unambiguous). Updated everywhere.
- CLI manifest: `collections = [...]` on `[group]` only (ManifestApp has no such
  field + deny_unknown_fields → an app manifest carrying it is a hard error);
  Manifest::collections() accessor; build_bundle emits it.
- plan/apply: a `collection` row group in `pic plan`; created/deleted counts in
  the apply summary; collections threaded through PlanDto/NodePlanDto/
  ApplyReportDto.
- read-only `pic collections ls --group` → GET /admin/groups/{id}/collections
  (viewer+), backed by ApplyService::collection_report + CollectionInfo.
- journey: a group declares `catalog`; app A writes, app B (same subtree) reads
  it back; a sibling-subtree app gets CollectionNotShared; `collections ls` +
  re-apply NoOp. Full suite 114/114.
- docs: groups-and-project-tool §11.6 (KV-only MVP shipped + deferrals),
  sdk-shape.md (the shared-collection handle + trust model), CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-29 22:26:43 +02:00
parent b79d8ef47d
commit 0973344515
19 changed files with 398 additions and 13 deletions

View File

@@ -74,6 +74,31 @@ collection, id)` for docs. Collections are **mandatory**, not optional
— even single-collection apps name their collection. The service layer
rejects requests with empty collection names.
### Shared group collections (§11.6)
`kv::collection(name)` is **app-private** — scoped to `cx.app_id`, the
isolation boundary. A separate, explicit handle reaches a **group-shared**
KV collection that every app in a group's subtree reads (and an
authenticated editor+ writes):
```rhai
let cat = kv::shared_collection("catalog"); // resolves the nearest
cat.set("sku-1", #{ price: 9 }); // ancestor group that
let p = cat.get("sku-1"); // declares "catalog"
```
The name is deliberately distinct from `kv::collection` — a script opts
into shared scope explicitly (implicit shadowing would silently redirect a
write across apps). (`kv::shared` would be ideal but `shared` is a Rhai
reserved word, hence `shared_collection`.) The owning group is resolved
from `cx.app_id`'s ancestor chain — that walk is the isolation boundary;
an app outside the owning group's subtree gets a "not a shared collection"
error, never another subtree's data. A collection is made shared
declaratively (`[group]` manifest `collections = [...]`), never from a
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.
## Error convention
- **Throw on failure.** `widgets.set("k", "v")` throws a Rhai runtime