Commit Graph

11 Commits

Author SHA1 Message Date
MechaCat02
5efb068b9f feat(cli): kind-aware shared-collection reconcile + string-or-table manifest (§11.6 docs C4)
Make the declarative collection surface carry a `kind` so docs (and future
kinds) are declarable. Back-compatible: the shipped `collections = ["catalog"]`
form still means kv.

- manifest: `collections` entries are now `CollectionDecl` — an untagged enum of
  a bare string (kv shorthand) or a `{ name, kind }` table (kind ∈ kv/docs).
  `Manifest::collections()` normalizes to `(name, kind)` pairs; build_bundle
  emits `[{name, kind}]`. Still `[group]`-only. Unit test covers the
  string-or-table mix + app rejection.
- apply: `Bundle.collections: Vec<CollectionSpec>` ({name, kind=default "kv"});
  `CurrentState.collections: Vec<(name,kind)>` via list_all_for_owner;
  `diff_collections` keys each change by name with `detail = kind` and identity
  `(LOWER(name), kind)` (so a kv and a docs collection of the same name are
  distinct); reconcile reads the kind from `detail`; state_token folds
  `coll|{kind}|{name}`; validate_bundle rejects unknown kinds + dups by
  (name,kind); `collection_report`/CollectionInfo + `pic collections ls` gain a
  kind column.

Existing kv journeys + nearest-wins still green (the bare-string form normalizes
to kind=kv end to end).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:04:43 +02:00
MechaCat02
0973344515 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>
2026-06-29 22:26:43 +02:00
MechaCat02
529725ebb6 fix(cli): reject misplaced/unknown manifest keys (§5.5 review)
`extension_points` is an `[app]`/`[group]` node key. Placed at top level
(before any table header) TOML reads it as a root key — and `Manifest`
silently dropped unknown root keys, the same silent-drop class that bit in
C5 (a key absorbed under `[group]` and discarded). Add
`deny_unknown_fields` to `Manifest`, `ManifestApp`, and `ManifestGroup` so a
misplaced or typo'd key is a hard parse error instead of a no-op apply.
Regression test covers top-level placement, an in-`[app]` typo, and the
correct node-key form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:09:57 +02:00
MechaCat02
a049397bb0 test(cli): extension-point journeys + node-key manifest fix + docs (§5.5 C5)
- Move the manifest `extension_points` from a top-level key to an `[app]`/
  `[group]` node key (`ManifestApp`/`ManifestGroup` + a `Manifest::extension_points()`
  accessor). A bare top-level array placed after the node header was silently
  re-nested by TOML into that table and dropped; as a node key it parses
  unambiguously wherever it sits. build_bundle/pull/init updated.
- Journeys (live server + Postgres): a group default body resolves for an
  inheriting app; the app provides its own module and OVERRIDES the EP (the
  inverse of the Phase 4b sealed import); `extension-points ls` shows the
  provider; a body-less EP with no provider fails `pic plan`.
- Re-bless the schema snapshot (new `extension_points` table).
- Docs: §5.5 marked complete (extension points ) + CLAUDE.md current-focus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:53:18 +02:00
MechaCat02
e62e073970 feat(apply): declarative extension_points reconcile (§5.5 C3)
Thread extension-point markers through the manifest and the apply engine —
a name-only resource modeled on `secrets` (shape) with `vars`-style
create/prune write behavior.

- manifest: top-level `extension_points = ["theme", …]` (allowed on app and
  group; overlay stays base-only via deny_unknown_fields); `build_bundle`
  emits the names.
- apply_service: `Bundle.extension_points`, `CurrentState.extension_point_names`,
  `Plan.extension_points` (+ is_noop), `diff_extension_points`
  (declared→Create / live-undeclared→Delete / else NoOp), `load_current`
  loads them, `reconcile_node_tx` inserts on Create + deletes on prune,
  `state_token_with_names` folds in `ep|<name>`, `validate_bundle` rejects
  duplicates + reserved names, `ApplyReport` gains created/deleted counts.
- CLI client + plan/apply rendering: `extension_points` in PlanDto/NodePlanDto/
  ApplyReportDto, an `extension_point` row group in `pic plan`, a count in the
  apply summary.

pull sets it empty for now (wired to the read endpoint in C4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:22:05 +02:00
MechaCat02
da03fda1d6 feat(cli): [group] manifest + pic plan/apply for a group node (Phase 5 C2)
Phase 5 C2. A `picloud.toml` can now declare a `[group]` node (instead of
`[app]`): the group's own scripts + `[vars]` (+ secret names). `pic plan` /
`pic apply` reconcile it via the C1 group endpoints — the same plan/apply/
bound-token flow as an app, routed by node kind.

  * `Manifest` is now app-XOR-group: `app`/`group` are both optional with an
    exactly-one check in `parse`, plus a group-node guard that rejects
    `[[routes]]`/`[[triggers]]` (those bind to an app). Accessors `slug()` /
    `is_group()` replace the hardcoded `manifest.app.slug`.
  * `client`: `plan_node`/`apply_node` take a `NodeKind { App | Group }` that
    selects the `apps` vs `groups` API path; the old `plan`/`apply` wrappers
    are gone (callers pass the kind).
  * `pic plan`/`apply` detect the node kind from the manifest and label output
    `group`/`app`; `pic config --effective` cleanly rejects a group manifest
    (effective config is an app's inherited view).

Live-validated: a `[group]` manifest with a script + var → `pic plan` (script
+ var creates) → `pic apply` (group-owned) → idempotent re-plan noop →
`pic scripts ls --group` shows it. Manifest unit test for group parse +
app-only-block rejection; init/pull/plan/apply/overlay journeys all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:57:58 +02:00
MechaCat02
34dcae98a2 feat(cli): declare app vars in the manifest + plan/apply them
Adds a `[vars]` block to `picloud.toml` (key → TOML value), reconciled by
`pic plan`/`pic apply` alongside scripts/routes/triggers/secrets. Unlike
`[secrets]` (name-only), var values live inline since they aren't secret.
The overlay (`picloud.<env>.toml`) may carry `[vars]`; overlay keys win
per key (the env-specific value of an env-agnostic default).

`build_bundle` serializes the vars to JSON for the wire (TOML round-trips
via serde); `pic plan` shows a `var` row group and `pic apply` reports
`vars +c ~u -d`. The `PlanDto`/`ApplyReportDto` gain the matching fields.
`pic pull` carries an empty `[vars]` for now (export lands next). Adds a
round-trip + overlay-precedence unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:40:36 +02:00
MechaCat02
345f265062 fix(cli): env-aware config, pull clobber guard, strict overlays, 409 hint
Four review fixes on the `pic` surface:

- `config --effective` gains `--env`: it now resolves through the same
  overlay as `plan`/`apply`, so the masked report targets the app those
  commands act on instead of silently reading the base slug's secrets.
- `pull` gains `--force` and refuses to overwrite an existing
  `picloud.toml` (and colliding `scripts/*.rhai`) without it — fail-fast
  before any network call or write, mirroring `init`.
- Overlays (`ManifestOverlay`/`OverlayApp`) get `deny_unknown_fields`: a
  `[[scripts]]`/`[[routes]]`/`[[triggers]]` table or a typo'd key in an
  overlay now errors loudly instead of being silently dropped. +unit test.
- `apply` 409 (StateMoved — the only 409 the endpoint emits) surfaces an
  actionable hint (re-run `pic plan`, or `--force`) instead of a bare
  `HTTP 409`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:06:45 +02:00
MechaCat02
2ba476aac8 feat(cli): env-scoped overlays — --env merges picloud.<env>.toml
The §4.1 base+overlay model for the single-app world: a base `picloud.toml`
holds the shared scripts/routes/triggers; a sparse `picloud.<env>.toml`
carries the per-environment slug (+ extra secret names). `pic plan/apply
--env <E>` merges the overlay onto the base before diffing/applying, so an
environment deploys to its own app ("an environment is an app", §2)
without duplicating the shared definitions.

Merge is sparse: overlay `app.slug`/`name` replace the base's; secret
names union. Scripts/routes/triggers always come from the base. Rich
per-key `vars` resolution stays Phase 3.

Tested: unit tests for the merge + overlay-path derivation; a journey
proving `apply --env staging` deploys to the staging app and leaves the
base app untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:58:04 +02:00
MechaCat02
55cf995eda feat(enabled): scripts/routes enabled column + declarative data path
Phase-1 `enabled` three-state lifecycle (§4.3), data-model half. Triggers
already carried `enabled`; this adds it to scripts and routes and threads
it through the declarative project tool. Runtime honoring (disabled route
404 / script non-invocable / dispatcher fire-time re-check) is the next
commit — this change only stores and reconciles the flag.

- Migration 0045: `enabled BOOLEAN NOT NULL DEFAULT TRUE` on scripts +
  routes. Default true ⇒ no behavior change on migrate.
- `Script`/`Route` (shared) gain `enabled` (serde default true via the new
  `picloud_shared::default_true`); repos' SELECT/INSERT/UPDATE SQL, row
  structs, `NewScript`/`NewRoute`/`ScriptPatch` all carry it.
- Apply diff treats `enabled` as a declarative field (omitted ⇒ active):
  `script_update_reason` + `diff_routes` detect a toggle as an Update, and
  the create/update/insert paths persist it. The bound-plan `state_token`
  re-includes script/route `enabled` (removed in the earlier review fix
  precisely because the diff didn't key on it yet — now it does).
- CLI manifest model + `build_bundle` + `pull` round-trip `enabled`
  (serialized only when false; omitted ⇒ active). `pic init` scaffold and
  the interactive script/route create paths default active.

Tested: manager-core lib 365 (incl. enabled diff + token sensitivity) +
cli bins 31 (incl. manifest skip-serialize) green; clippy -D warnings
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:36:40 +02:00
MechaCat02
3b650a2b14 feat: declarative project-tool foundation (pull/plan/apply/prune)
Add a server-side, atomic, declarative reconcile loop for a single app —
the foundation of the project-tool design. Developers describe an app's
scripts, routes, triggers, and secret-names in `picloud.toml`, then
`pic pull / plan / apply [--prune]` to converge live state to the manifest.

Server (manager-core):
- apply_service: a pure diff engine (compute_diff) shared by plan and
  apply, plus an ApplyService that composes the existing per-repo writes
  into ONE Postgres transaction. Identity keys mirror the DB UNIQUE
  constraints (script=lower(name); route=(method,host_kind,host,
  path_kind,path); trigger=per-kind semantic tuple; secret=name).
  Apply takes a per-app advisory lock, recomputes the diff in-tx, applies
  scripts -> routes -> triggers, prunes dependents-first, commits, then
  refreshes the route table once post-commit.
- apply_api: POST /apps/{id}/plan (AppRead) and /apps/{id}/apply.
  Apply requires the per-kind write caps the bundle exercises (all three
  when --prune), plus AppSecretsRead when it binds an email trigger.
- tx-accepting repo siblings (insert/update/delete *_tx) so the existing
  create/update/delete delegate to one SQL definition each.
- email triggers reference an inbound secret by NAME; the value is
  resolved, decrypted (AAD-bound), and re-sealed server-side at apply —
  it never travels in the manifest.

CLI (picloud-cli):
- manifest.rs (picloud.toml model), client plan/apply, and the pull/plan/
  apply commands. pull rejects filesystem-unsafe script names up front.

Safety properties enforced and tested:
- idempotent: a freshly-pulled manifest re-applies as all-NoOp.
- atomic: a mid-bundle failure rolls back with nothing written.
- routes delete-before-insert so a freed binding is reusable in one apply.
- queue one-consumer invariant held inside the shared tx.
- email triggers are never pruned, and a script that still owns an
  email/dead-letter trigger can't be pruned (the FK cascade would destroy
  the sealed secret) — refused with a pointer to `pic triggers rm`.
- plan and apply agree on unset email-secret references.

No migration: the existing schema's UNIQUE constraints serve as identity
keys. Groups, env-scoping, and the `enabled` toggle are later milestones.

Tested: manager-core lib (360) + CLI bins (27) + 8 project-tool journeys
(pull/plan/apply/prune/email+queue), all green; clippy -D warnings clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:52:21 +02:00