feat(modules): no-provider plan check + read-only extension-points ls (§5.5 C4)

- apply: `check_extension_points_provided` (app nodes) — every EP visible on
  the app's chain (its own + inherited) must have a provider: a same-apply
  module, or one resolvable up-chain (override or default body). Else a clean
  `pic plan` error instead of a runtime failure. Single-node only (the tree
  path leans on the runtime backstop, like the dangling-import check).
- read-only surface: `extension_point_report` + `ExtensionPointInfo`;
  `GET /{apps|groups}/{id}/extension-points` (AppRead / GroupScriptsRead);
  `pic extension-points ls --app|--group` showing declared-vs-inherited + the
  resolved provider (`app override` / `inherited default` / `unset`).
- `pic pull` round-trips an app's OWN declared EPs (filtered `declared_here`),
  so pull→plan stays idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-29 20:31:14 +02:00
parent e62e073970
commit 82b4579317
8 changed files with 307 additions and 8 deletions

View File

@@ -1305,6 +1305,35 @@ impl NodeKind {
}
}
/// One row of the §5.5 extension-point report.
#[derive(Debug, Deserialize)]
pub struct ExtensionPointInfoDto {
pub name: String,
#[serde(default)]
pub declared_here: bool,
#[serde(default)]
pub provider: Option<String>,
}
impl Client {
/// `GET /api/v1/admin/{apps|groups}/{ident}/extension-points`.
pub async fn extension_points_list(
&self,
kind: NodeKind,
ident: &str,
) -> Result<Vec<ExtensionPointInfoDto>> {
let ident = seg(ident);
let resp = self
.request(
Method::GET,
&format!("/api/v1/admin/{}/{ident}/extension-points", kind.path()),
)
.send()
.await?;
decode(resp).await
}
}
// ---------- DTOs (CLI-local, wire-shape-matched) ----------
/// Response of `POST .../plan`: per-resource diffs grouped by kind.