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>
This commit is contained in:
MechaCat02
2026-06-29 20:22:05 +02:00
parent 8f966783fe
commit e62e073970
7 changed files with 165 additions and 3 deletions

View File

@@ -84,6 +84,13 @@ pub async fn run(
"+{} ~{} -{}",
report.vars_created, report.vars_updated, report.vars_deleted
),
)
.field(
"extension_points",
format!(
"+{} -{}",
report.extension_points_created, report.extension_points_deleted
),
);
for w in &report.warnings {
block.field("warning", w.clone());
@@ -151,6 +158,13 @@ pub async fn run_tree(
"+{} ~{} -{}",
report.vars_created, report.vars_updated, report.vars_deleted
),
)
.field(
"extension_points",
format!(
"+{} -{}",
report.extension_points_created, report.extension_points_deleted
),
);
for w in &report.warnings {
block.field("warning", w.clone());

View File

@@ -139,6 +139,7 @@ fn scaffold_manifest(slug: &str, name: &str) -> Manifest {
triggers: crate::manifest::ManifestTriggers::default(),
secrets: crate::manifest::ManifestSecrets::default(),
vars: std::collections::BTreeMap::new(),
extension_points: Vec::new(),
}
}

View File

@@ -64,12 +64,13 @@ fn render_tree(plan: &TreePlanDto, mode: OutputMode) {
let mut table = Table::new(["node", "kind", "op", "resource", "detail"]);
for n in &plan.nodes {
let node = format!("{}:{}", n.kind, n.slug);
let groups: [(&str, &Vec<ChangeDto>); 5] = [
let groups: [(&str, &Vec<ChangeDto>); 6] = [
("script", &n.scripts),
("route", &n.routes),
("trigger", &n.triggers),
("secret", &n.secrets),
("var", &n.vars),
("extension_point", &n.extension_points),
];
for (rk, changes) in groups {
for c in changes {
@@ -160,6 +161,7 @@ pub fn build_bundle(manifest: &Manifest, base_dir: &Path) -> Result<Value> {
"triggers": triggers,
"secrets": manifest.secrets.names,
"vars": vars,
"extension_points": manifest.extension_points,
}))
}
@@ -174,12 +176,13 @@ fn tagged(kind: &str, spec: impl Serialize) -> Result<Value> {
fn render(plan: &PlanDto, mode: OutputMode) {
let mut table = Table::new(["kind", "op", "resource", "detail"]);
let groups: [(&str, &Vec<ChangeDto>); 5] = [
let groups: [(&str, &Vec<ChangeDto>); 6] = [
("script", &plan.scripts),
("route", &plan.routes),
("trigger", &plan.triggers),
("secret", &plan.secrets),
("var", &plan.vars),
("extension_point", &plan.extension_points),
];
for (kind, changes) in groups {
for c in changes {

View File

@@ -224,6 +224,8 @@ pub async fn run(app_ident: &str, dir: &Path, force: bool, mode: OutputMode) ->
names: secrets.iter().map(|s| s.name.clone()).collect(),
},
vars: manifest_vars,
// Wired to the read endpoint in C4 so pull→plan round-trips EPs.
extension_points: Vec::new(),
};
std::fs::write(&manifest_path, manifest.to_toml()?)