Commit Graph

35 Commits

Author SHA1 Message Date
MechaCat02
b8b047368e feat(realtime): external SSE subscription for group shared topics (§11.6 D2 / Track A M6)
Shared TOPICS fanned out only to in-cluster trigger handlers; external clients
could not subscribe (per-app topics already can). Add SSE for shared topics.

- RealtimeBroadcaster gains a parallel (group_id, topic) channel map:
  subscribe_group / publish_group / drop_group_topic (default no-ops so
  NoopRealtimeBroadcaster + test doubles are untouched). InProcessBroadcaster
  implements them with a second map; GC + channel_count span both.
- Route GET /realtime/shared/topics/{topic}: Host->app dispatch (as the per-app
  route), then RealtimeAuthority::authorize_subscribe_shared resolves the OWNING
  GROUP from the app's chain (kind=topic, root segment). Reads-open model — the
  resolution IS the authorization, consistent with in-script shared reads; a
  foreign-subtree app never resolves (404, the isolation boundary). No principal
  machinery needed.
- GroupPubsubServiceImpl::with_realtime bridges a shared-topic publish to the
  owning-group channel (best-effort) after the durable trigger fan-out.
- Host wires the broadcaster into the group pubsub service + the collection
  resolver into the authority.

Auth-model note: chose reads-open (subtree app's Host is the grant) over
"authenticated principal + GroupKvRead" — it's both simpler and faithful to how
shared-collection reads already work. Pinned by realtime broadcaster group-map
tests, realtime_api shared-route tests (404 + stream), and
group_pubsub_service::publish_bridges_to_the_group_broadcaster. No migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:33:39 +02:00
MechaCat02
c06a9e801e feat(apply): make the per-env approval gate hermetic (§3 M3 / Track A M1)
The per-env approval policy was applier-supplied — a hand-crafted request
that omitted `project.environments` was ungated, and flipping a gate to
`confirm = false` in the same request un-gated it. Persist the policy
server-side and enforce against `persisted ∪ declared`.

- Migration 0067: `project_environments (project_id, env_name, confirm)`,
  CASCADE on the project. Written declaratively (delete-then-insert) inside
  `upsert_project_tx`, same tx as the project row + node claim.
- `ProjectRepository::get_environments_by_slug` (read side) +
  `ApplyService::effective_env_policy` union the persisted policy with the
  request's declared one (confirm if EITHER says so — monotonic).
- `env_gate_check` now evaluates the effective policy; the three handlers load
  it before the gate. `plan.approvals_required` is the effective gated set, so
  CI sees a persisted gate even when the manifest omits it.
- The union rule closes both bypasses: an omitted policy still trips a
  persisted gate, and an ungating apply must itself pass the gate (the
  declarative replace then takes effect next time) — TOCTOU-safe.

Pinned by env_approval::{persisted_policy_gates_a_request_that_omits_it,
flipping_a_gate_to_false_in_the_same_request_still_gates} +
projects_repo::get_environments_by_slug_returns_persisted_policy. Schema golden
reblessed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:31:15 +02:00
MechaCat02
4b4b3148b7 feat(apply): server-side enforcement of per-env approval gating (§3 M3 / §4.2)
The per-env approval gate (`[project.environments]`, `confirm = true`) was
client-side only — the policy was `#[serde(skip_serializing)]`, so a patched or
older CLI applying to a confirm-required env bypassed it entirely. This makes
the gate server-enforced, admin-gated, and audited (the deferred §4.2 piece).

- Wire: `ProjectDecl` gains `environments` (the CLI stops skip_serializing it and
  sends the selected `env` + the `--approve` set on the apply request). The
  server RE-DERIVES the gate from the request (`env_gate_check`): a
  confirm-required env not in `approved_envs` → 409 `ApprovalRequired`.
- Admin gate: an approved override additionally requires `AppAdmin`/`GroupAdmin`
  on EVERY declared node — a step up from the editor write caps an ordinary
  apply needs (reusing the admin caps per §4.2) — and emits a `tracing::info!`
  audit line. Enforced on all three handlers (single app, single group, tree);
  the check runs before any tx, so a denial (403) precedes any mutation.
- Token: the env policy folds into the TREE bound-plan token (a policy edit
  between plan and apply trips StateMoved). The single-node token is left
  unchanged (a bare live-state hash), so plan/apply still match without threading
  a project into it.
- CLI: single-node `apply`/`plan` now resolve the GOVERNING project (own block
  else nearest ancestor's) so a leaf apply carries the root's policy; `plan`
  surfaces `approvals_required`. Client-side `require_env_approval` fast-fail
  kept as UX.

Adapts the earlier feat/ownership-and-approval M5 (654e387) to main's DTOs; main
addresses tree group nodes by slug only, so the admin loop mirrors authz_tree
(app: resolve_app_id fail-closed; group: get_by_slug, skip to-create).

Threat-model note (honest scope, in the doc + code comments): this closes the
patched/older-CLI bypass and admin-gates + audits the override, but is NOT a
hermetic boundary against a hand-crafted request that OMITS the policy — the
policy is applier-supplied, not persisted server state. Full closure (persist a
`project_environments` source of truth + a server-determined env) is deferred.

Review (adversarial pass): fixed a MEDIUM (single-node `plan` didn't resolve the
governing project, so a leaf plan under-reported the gate `apply` enforces) and a
LOW (duplicate clippy attr); corrected overstated "can't bypass" wording.

Tests: ProjectDecl gate unit test; env_approval journeys
`server_enforces_the_gate_against_a_non_stock_client` (raw request → 409 without
approval, admin-approved → 200) and `approving_a_gated_apply_requires_admin`
(editor + --approve → 403). 417 lib tests + 33 CLI journeys + workspace clippy
-D warnings + fmt clean. No migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:01:04 +02:00
MechaCat02
87292fc4e9 refactor(apply): split reconcile_group_structure_tx create/reparent branches
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 19m24s
CI / Dashboard — check (push) Successful in 9m46s
Review #4 (LOW, cleanup). `reconcile_group_structure_tx` carried a
`#[allow(clippy::too_many_lines)]` over a ~145-line body doing create +
reparent + attach/RBAC/lock in one loop. Extract the create branch
(`create_group_node_tx`) and the divergence-resolution branch
(`reparent_diverged_group_tx`) into helper methods, threading the shared
attach-ceiling / principal / mode context through a small `ReconcileCtx` — the
loop is now thin enough to drop the allow. Also improve the child-before-parent
error in `resolve_declared_parent` to hint that group nodes must be ordered
parents-first (only a hand-rolled bundle hits it; the CLI already sorts).

No behavior change. Documents the Tier-1 review follow-ups in the design doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:49:18 +02:00
MechaCat02
e2bfe633a7 test/docs(cli): §3 M3 per-env approval journey; Tier 1 complete
`tests/env_approval.rs`: a `[project.environments]` gating `production` →
`pic apply --env production` refused (names --approve); `--yes` alone does not
bypass; `--approve production` applies; an un-gated `staging` applies freely.
Design doc §3 records M3 shipped and the §6/§3 group-tree Tier 1 (M1 create ·
M2 divergence/reparent · M3 per-env approval) COMPLETE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:28:28 +02:00
MechaCat02
b8eced9d91 test/docs(apply): §6 M2 structural-divergence journey + design note
`tests/structural_divergence.rs`: a repo that nests a group under a different
parent than the server → `pic plan` shows it `diverged`; a bare apply is
refused (422); `--force-local-structure` reparents to the manifest shape;
`--adopt-server-structure` keeps the server placement. Design doc §6 records M2
shipped, leaving only per-env approval gating deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:23:10 +02:00
MechaCat02
dcc387c345 test/docs(apply): §6 M1 group-create journey + design note
`tests/group_create.rs`: a nested `[group]` tree with no server groups yet →
`pic apply --dir` creates both (parent from directory nesting), claims them for
the `[project]`, and re-applies as a no-op; a member with only editor (no
group-admin) is refused and leaves nothing behind. Design doc §6 records M1
shipped (parent-by-nesting, Phase-0 create-in-tx, claim, RBAC + attach ceiling)
with reparent/divergence still deferred to M2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:07:12 +02:00
MechaCat02
f673922d89 feat(cli): pic projects ls + §7 M3/track-complete docs
§7 M3 (part 3) — the ownership track becomes inspectable, completing M1–M3.

- server: GET /api/v1/admin/projects (projects_api) lists every project + its
  owned-group count (projects repo list_with_counts, one GROUP BY); behind the
  /admin middleware, like the groups list.
- CLI: pic projects ls (cmds/projects.rs + client projects_list); the
  apply_ownership plan-preview journey now also asserts projects ls (plat owns
  exactly 1 group; teamb listed).
- docs: design §7 + CLAUDE.md record M3 shipped and the whole §7 multi-repo
  ownership track (M1 claim · M2 attach ceiling · M3 preview + projects ls)
  COMPLETE; deferred = structural-divergence + declarative tree shape + per-env
  approval gating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:26:10 +02:00
MechaCat02
5a820d7262 test/docs(ownership): attach-ceiling journey + M2 status
The apply_ownership journey gains an attach-ceiling case: a group node below
the attach point applies; the attach point itself and a sibling subtree are
both refused (422, message names the attach point). Design doc §7 + CLAUDE.md
record M2 shipped and re-point 'Next' at M3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:57:57 +02:00
MechaCat02
5bd72956b1 test/docs(ownership): §7 apply_ownership journey + design/CLAUDE updates
End-to-end coverage of the M1 ownership claim through the real CLI + server:
claim-on-first-apply + owner column, idempotent re-apply by the owner, a
second project refused (409, naming the owner), --takeover by an admin,
app-inheritance from the nearest claimed ancestor (owning project ok; foreign
or absent project refused), and the capability gate — a member with only an
editor GROUP role can reconcile but is refused --takeover (needs group-admin),
with ownership unchanged after the failed takeover.

- tests/apply_ownership.rs (registered in cli.rs); a grant_group_membership
  helper in tests/common/member.rs (group-level, mirroring the app one).
- design doc §7 gains an M1-shipped status note; CLAUDE.md records §7 M1 and
  re-points 'Next' at M2 (attach ceiling) + M3 (blast-radius / pic projects ls).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:49:31 +02:00
MechaCat02
c361048022 test/docs(shared-queues): journey + docs (D3.3)
CLI journey (shared_queues): authoring + `pic triggers ls --group` shared
column + the two validation rejections (undeclared queue collection; shared
on an app), mirroring the shared_topics/shared_triggers norm; the live
competing-consumer + dispatcher behaviour is pinned by the deterministic
manager-core tests. Docs: CLAUDE.md + design doc §11.6 record D3 as shipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:35:40 +02:00
MechaCat02
19ba6dbfb4 test/docs(shared-topics): dispatch test + journey + docs (D2)
Deterministic manager-core test (tests/shared_topics.rs) proves the
namespace boundary both ways: a shared publish fans out only to the
group's shared pubsub trigger (not a per-app or non-shared group
template), and a per-app publish never hits the shared trigger. CLI
journey (shared_topics) covers authoring + `pic triggers ls --group`
shared column + the two validation rejections (undeclared topic; shared
on an app), mirroring the shared_triggers norm. Docs: CLAUDE.md + design
doc §11.6 record D1 + D2 as shipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:54:32 +02:00
MechaCat02
186b6f1dcc docs(stateful-templates): group email materialization is shipped (M5.5)
Update the design doc §4.5 and CLAUDE.md current-focus: email joins
cron+queue as a materialized stateful template via the shared-group-secret
model; drop the email deferral and the stale "email rejected on a group"
note; retarget "Next" to multi-node / multi-repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:31:23 +02:00
MechaCat02
aa5bb3e8cc feat(stateful-templates): journey + docs + concurrency fix; email deferred (M5.6)
- 0063: partial unique index on (app_id, materialized_from) + ON CONFLICT DO
  NOTHING in the reconciler, so a materialized copy is idempotent under
  concurrent reconciles (two parallel app-creates no longer duplicate a copy).
- stateful_templates journey: a group cron template + a descendant app → a
  materialized cron copy in `pic triggers ls --app`; re-apply is a NoOp.
- docs (§4.5 + CLAUDE.md): cron+queue materialization implemented; group EMAIL
  templates deferred (per-descendant inbound-secret reseal needs the master key
  threaded through the hooks + a secret-ref schema) and cleanly rejected at
  apply for now, alongside a `materialized` ls column.

Completes the v1.2 near-term batch (M1-M5, cron+queue); group email is the one
scoped follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:54:34 +02:00
MechaCat02
9a4b83dfcf feat(shared-triggers): visibility + tests + docs (M2.5)
- `shared` column in `pic triggers ls --group` (TriggerTemplateInfo + DTO +
  report + renderer).
- manager-core/tests/shared_triggers.rs: a shared write matches the group's
  shared trigger and NOT a same-named per-app trigger; a per-app write matches
  the app trigger and NOT the shared one (the `shared` flag is the boundary).
- shared_triggers journey: declarative authoring applies, ls --group shows
  shared, an undeclared-collection shared trigger + an app shared trigger are
  rejected.
- docs: §11.6 + CLAUDE.md move shared-collection triggers from Deferred to
  implemented.

Completes M2. (Pre-existing async-dispatcher timing flakes pass in isolation.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:05:30 +02:00
MechaCat02
16267cec06 feat(suppress): group-suppress warnings + ls + journey + docs (M1.5)
- dangling_suppress_warnings takes an ApplyOwner and walks the group's own
  chain (GROUP_CHAIN_LEVELS_CTE) for a group node; runs for both owners.
- GET /groups/{id}/suppressions + `pic suppress ls --group` (client + cmd +
  main.rs; --app/--group mutually exclusive).
- suppress journey: a child group declines a parent template for its subtree;
  suppress ls --group shows the marker. Docs §4.5 + CLAUDE.md move group-level
  suppression from Deferred to implemented.

Completes M1. (An unrelated pre-existing email_inbound dispatch-timing flake
passes in isolation.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:16:22 +02:00
MechaCat02
bfa4b48930 feat(sealed): sealed journey + docs (§11 tail M5)
- tests/sealed.rs: a group declares a sealed [[routes]] template; a descendant
  that suppresses it still serves the path, apply warns "sealed — no effect",
  routes ls --group shows sealed=true, and sealing an [app] route is rejected.
- docs: §4.5 trust-model callout + CLAUDE.md move `sealed` from Deferred to
  implemented — group templates are advisory-by-default *unless* sealed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:42:18 +02:00
MechaCat02
3df37b2b85 docs(suppress): flag advisory-by-default trust model; test dangling-suppress warning (§11 tail review)
Two review findings.

1. Per-app opt-out changes the group-template guarantee from "runs on every
   descendant" to "runs unless the descendant declines" — a footgun for
   compliance hooks (an audit/security trigger a tenant can silently opt out
   of). There is no non-suppressible flag. Document this in design §4.5 +
   CLAUDE.md, and record the deferred mitigation: a `sealed`/`mandatory`
   group-template marker the trigger anti-join + route filter skip (cheap —
   both filters are centralized).

2. The dangling-suppress warning path had no coverage. The suppress journey
   now applies a `[suppress] triggers=["does-not-exist"]`, asserts the apply
   still succeeds and the report warns "no effect".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:59:20 +02:00
MechaCat02
b74e1a401b feat(suppress): suppression journey + docs (§11 tail S5)
- Journey tests/suppress.rs: a group declares a `[[routes]]` template; a
  descendant app applies `[suppress] routes=["/ghello"]` → stops serving it
  (via `routes match`), a sibling still serves it, `pic suppress ls --app`
  shows it, re-apply is a NoOp; pruning the block re-inherits. The
  trigger-side filter + isolation are pinned at the repo layer by
  template_suppression.rs.
- Docs: design §4.5 (per-app opt-out closing the deferred gap for both
  templates — coarse-by-reference, inheritance-only, the two consumption
  points; group-level suppress still deferred), CLAUDE.md current-focus.

Full journey suite 121/121; workspace tests 34 suites/0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:05:06 +02:00
MechaCat02
b048daa700 refactor(routes): drop prod-dead compile_routes; document disabled-shadow (§11 tail review)
Two review findings.

1. After R3 every production rebuild path uses `compile_effective_routes`;
   `compile_routes` (the old app-only compile) was called only by its own
   two unit tests, and its "runs in build_app" doc was stale — so the
   lenient-skip + disabled-drop regression guards no longer covered the
   live path. Remove `compile_routes` + its re-export, fold the lenient-skip
   rationale onto `compile_one`, and port both guards to
   `compile_effective_routes` (app-only rows are just depth-0 effective
   rows). Add a third guard pinning nearest-owner shadowing at the pure-
   compile layer. Fix the stale `compile_routes` mention in apply_service.

2. Document the disabled + inherited semantic: the `enabled` filter runs
   before the shadow check, so a disabled own-route does not claim its
   binding and an enabled ancestor template at the same path falls through
   ("disabled = absent", §4.3). Recorded on `compile_effective_routes` and
   in design §4.5 — the corollary (can't 404 an inherited route by
   disabling a same-path own route) points at the deferred per-app opt-out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:14:09 +02:00
MechaCat02
a977ff6a32 feat(cli): pic routes ls --group + group-route journey + docs (§11 tail R5)
Visibility + end-to-end coverage + docs for group route templates.

- ApplyService::route_report(Group) → GET /api/v1/admin/groups/{id}/routes
  (viewer-tier GroupScriptsRead), surfacing a group's own route templates
  (method, host, path, handler script, dispatch, enabled).
- CLI: `pic routes ls --group <g>` (the script-id positional now conflicts
  with --group), client `group_routes_list` + RouteTemplateDto.
- Journey `group_routes.rs`: a group declares a `[[routes]]` template
  binding a group handler; a DESCENDANT app serves it (via `routes match`
  against the live RouteTable), a SIBLING-subtree app does not, `routes ls
  --group` shows it, re-apply is a NoOp. Shadowing + isolation are
  additionally pinned at the repo layer by group_route_templates.rs.
- Update the manifest unit test: a [group] now accepts route templates and
  rejects only the stateful trigger kinds.
- Docs: design §4.5 (the live route-template model + full-live
  invalidation + nearest-wins shadowing + deferrals), CLAUDE.md focus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:03:59 +02:00
MechaCat02
c492a08775 feat(cli): pic triggers ls --group + group-template journeys + docs (§11 tail T4)
- apply_service: trigger_report(group) → TriggerTemplateInfo
  (kind/target/script/enabled); resolve_inherited_targets_for(Group) now
  surfaces the group's OWN endpoint scripts so a template's handler
  validates (fixes "binds to unknown script" when the handler is a
  pre-existing group script, not declared in the same manifest).
- apply_api: GET /groups/{id}/triggers (GroupScriptsRead).
- CLI: `pic triggers ls --group <g>` (--app/--group mutually exclusive)
  + the client method + DTO.
- tests/group_trigger_templates.rs (manager-core, live DB): the chain
  union matches a descendant app's kv insert against the group template
  and NOT a sibling subtree — the isolation boundary, deterministic.
- tests/group_triggers.rs (journey): apply a kv template, ls --group
  shows it, re-apply NoOp, cron-on-group rejected.
- docs: design §4.5 (live-event-kinds decision + deferrals), CLAUDE.md.

Full journey suite 119/119; workspace tests 0 failures; clippy -D clean;
schema unchanged (blessed in T1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:02:03 +02:00
MechaCat02
9c6d690792 test(cli): shared-files journey + .gitignore + docs (§11.6 files C5)
- collections.rs: shared_files_collection_is_read_write_across_the_subtree
  — a group declares kv+docs+files in one string-or-table manifest; authed
  app A creates a blob via files::shared_collection("assets").create(...),
  app B lists + gets the SAME bytes back across the subtree, a
  sibling-subtree app gets CollectionNotShared, collections ls shows all
  three kinds. Live-verified the bytes land under
  files/groups/<group_id>/assets/<id[0:2]>/<id>.
- .gitignore: ignore /crates/picloud-cli/data (the CLI journey harness
  spawns the server from that dir; the files journey is the first CLI test
  to write blobs).
- docs: design §11.6 (KV+DOCS+FILES shipped; topics/queue still deferred),
  sdk-shape (files::shared_collection), CLAUDE.md current-focus.

Full journey suite 117/117; schema blessed (55 migrations); workspace
clippy -D clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:56:47 +02:00
MechaCat02
a00454e5de 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>
2026-06-30 19:09:41 +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
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
52da8a8704 test(cli): group-module lexical-resolution journeys + Phase 4b docs (C5)
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 17m58s
CI / Dashboard — check (push) Successful in 9m45s
Two journeys against a live server + Postgres:
- `group_module_is_lexically_sealed_under_inheritance`: a group module +
  a group endpoint that imports it, inherited by an app — proves the §5.5
  trust boundary (a leaf's same-named module does NOT shadow the inherited
  endpoint's import) and app-origin CoW (an app endpoint's import resolves
  the app's module).
- `dangling_import_is_rejected_by_plan`: a manifest script importing a
  non-existent module is a `pic plan` error.

Docs: mark §5.5 residual resolved + §11 Phase 4b  in the design doc;
update CLAUDE.md current-focus (extension points are the remaining §5.5
piece).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:39:08 +02:00
MechaCat02
3ef3038eb7 test(cli): project-tree journeys + Phase 5 docs
Phase 5 C5. Three `pic ... --dir` tree journeys:
  * a group + a nested app apply atomically as one tree (the app's route binds
    the group's same-apply `shared` script); the group node's script is
    created; re-plan is all-noop (idempotent),
  * one invalid node aborts the whole tree — the valid group node's script is
    NOT created (true all-or-nothing),
  * a `pic groups reparent` between `pic plan --dir` and `pic apply --dir` trips
    the bound-plan (StateMoved) check via the tree's structure version.

Docs: `groups-and-project-tool.md` §11.5 status ( shipped — single-repo
nested tree apply; multi-repo single-owner + per-env approval gating deferred
per §11.1) + CLAUDE.md current-focus.

Gates: fmt, clippy -D warnings, cargo test --workspace, check-versioning (50
migrations — Phase 5 added none), schema snapshot unchanged, full journey suite
108/108. (Pre-existing flake `queue_e2e::queue_receive_acks_on_success` — a
racy immediate `count==0` after the async ack; flaky at the pre-Phase-5
baseline too, unrelated to this work.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 22:43:20 +02:00
MechaCat02
9598db149b docs: record Phase 4-lite (group-owned scripts) as shipped, live-resolved
Mark §11.4  shipped with the live-resolution decision (no body
materialization / invalidation fan-out, mirroring Phase 3's config), the
shipped surface, and the deliberate Phase-4b deferrals (group modules + lexical
import resolver, invoke-by-id, live auto-rebinding). Note the sharp edge:
routes.script_id ON DELETE CASCADE means deleting a group script drops
descendant routes. Update the §5.1 materialization box and CLAUDE.md
(current-focus + the data-model invariant now covers group-owned scripts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:44:51 +02:00
MechaCat02
b46667c309 docs: reconcile the groups/project-tool initiative into the roadmap
The groups/project-tool work had no home in the blueprint's version
scheme, two "Phase N" numbering schemes overlapped, and CLAUDE.md's
"current focus" was stale (claimed v1.1.0 SDK foundation; we're at v1.1.9
with the SDK line complete). Reconcile all three:

* blueprint §12 Phase 5 (v1.2, already titled "Advanced Workflows &
  Hierarchies") — split into a Hierarchies track (the groups + project
  tool initiative, with §11 Phases 1–3 marked shipped) and a Workflows
  track, with sequencing flagged as an open call. This gives the work an
  explicit version home: the Hierarchies half of v1.2.
* design doc header — Status Draft → Active, "scheduled as the Hierarchies
  track of v1.2", and an explicit warning that §11's local Phase 1–6
  numbering is distinct from the blueprint product-phase numbering.
* design doc §11.1 — a post-Phase-3 re-sequencing review: resolve Phase 4
  scripts LIVE (drop the fan-out-invalidation sub-project, per the Phase 3
  result); consider a "Phase 5-lite" (declarative apply of vars/secrets +
  app scripts) before the hard group-scripts work; and force the two
  deferred decisions (trigger/route templates vs tenant cardinality;
  multi-repo ownership vs a single-repo start).
* CLAUDE.md — refresh "current focus" to v1.2 Hierarchies, and correct the
  now-broken "every v1.1+ table is app_id NOT NULL" invariant: the new
  group-inheritable config tables (vars, secrets) use a polymorphic owner
  (group_id XOR app_id), not app_id NOT NULL.

Doc-only; no code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:12:25 +02:00
MechaCat02
ca360d84cb docs(design): record Phase 3 shipped + the live-resolution decision
Phase 3 (group-inherited config) is implemented, so fold the outcome back
into the design doc (per its own "before it drifts" note) — and flag the
one deliberate divergence from the plan.

§5.1 / §11.3 / §12 prescribed a *materialized* per-app effective view with
a generation counter and fan-out invalidation. We shipped **live,
resolve-on-read inheritance** instead: one recursive CTE per
`vars::get`/`secrets::get`/`config/effective`, no cache. The org tree is
small and there was no pre-existing config cache to fight, so this is
cheap and sidesteps §10's unsolved invalidation SLA.

Records, mirroring the Phase 1/2 status blocks:
* a "Status (Phase 3):  shipped" block in §11.3 with the full surface
  (0048/0049 schema incl. `apps.environment`, the §3 resolver, vars SDK +
  CRUD, group-secret AAD + inherited read, the two secret gates, masked
  `config/effective`, `--explain`) and the live-vs-materialized rationale;
* §5.1 runtime-model bullet annotated as the deferred (Phase-4 script
  inheritance) design, not Phase-3 runtime;
* §10 "invalidation SLA" and "inherited-membership revocation lag" marked
  moot/resolved — no cache means immediate propagation;
* §12 resolver contract + the header framing updated to live resolution.

Doc-only; no code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:53:58 +02:00
MechaCat02
49c4fb41ce docs(design): mark Phase 2 (groups) shipped + record decisions/deferrals
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:16:29 +02:00
MechaCat02
695987d6b7 docs(design): record async disabled-execution + cross-app backstops
Update §4.3: the fire-time `enabled` re-check is shipped and test-backed on
BOTH async arms (the unified outbox `!resolved.active` gate and the queue
arm's fresh-script nack), with the specific regression tests cited, plus the
same-app backstop now present on every async build path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:06:52 +02:00
MechaCat02
5e62f4acfe docs(design): mark Phase 1 shipped + record decisions/deferrals
Fold the implementation outcome back into the project-tool spec (§11) as
the design asks. Records what landed (init/pull/config/plan/apply/prune,
env overlays, enabled lifecycle, trigger name+backfill, content-fingerprint
bound-plan, apply lock) and the deliberate deferrals: tree-structure
version → groups; vars/--explain → Phase 3; trigger upsert-by-name + the
dashboard enabled toggle → tracked follow-ups beyond the §11 bullets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:20:32 +02:00
MechaCat02
c600177fd6 docs(design): groups & declarative project-tool spec
The architecture spec for the full vision — a declarative project tool
(picloud.toml + pull/plan/apply/prune) and a server-side nested-groups
hierarchy with live-resolve inheritance. Reviewed across several passes
for consistency, gaps, and contradictions; resolutions are folded in
beside the topics they settle.

This commit lands the spec only. The first slice of implementation (the
single-app reconcile foundation) follows; groups, env-scoping, and the
`enabled` toggle are later milestones called out in the doc.

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