feat(orchestrator): append — apply_append_context_to_plan + status-bar filter

This commit is contained in:
Fini 2026-05-23 07:11:25 +08:00
parent 25b3b8bf39
commit efcb345faa
2 changed files with 314 additions and 0 deletions

View file

@ -0,0 +1,313 @@
//! Append-to-document mode — `apply_append_context_to_plan`.
//!
//! Faithful port of `apps/web/src/services/ai/orchestrator-append.ts`.
//! Mutates an [`OrchestratorPlan`] in-place when an [`AppendContext`] is
//! present; returns an [`AppendPlanResult`] that `run.rs` uses to skip
//! root-frame insertion and status-bar scaffold (Task B2).
#![allow(dead_code)] // callers land in Task B2
use crate::plan::OrchestratorPlan;
use crate::types::AppendContext;
/// Result of applying append context to a plan.
///
/// Port of the three-field return in `orchestrator-append.ts`.
/// `plan` is mutated in-place; these flags tell `run.rs` what to skip.
pub(crate) struct AppendPlanResult {
pub skip_root_insertion: bool,
pub skip_status_bar: bool,
}
/// Returns `true` when `id + " " + label` (lowercased) matches any of the
/// status-bar keyword alternatives.
///
/// Port of `STATUS_BAR_SUBTASK_RE = /(status\s*bar|status_bar|status-bar|system chrome|系统栏|状态栏)/i`.
/// We avoid the `regex` crate: the only non-literal in the original is `\s*`
/// between "status" and "bar". We cover that with two literals:
/// `"status bar"` (single space) and `"statusbar"` (zero spaces).
fn is_status_bar_subtask(id: &str, label: &str) -> bool {
let haystack = format!("{id} {label}").to_lowercase();
haystack.contains("status bar")
|| haystack.contains("statusbar")
|| haystack.contains("status_bar")
|| haystack.contains("status-bar")
|| haystack.contains("system chrome")
|| haystack.contains("系统栏")
|| haystack.contains("状态栏")
}
/// Mutates `plan` according to `append` and returns skip-flags for `run.rs`.
///
/// - `None` → returns `{ false, false }`, plan unmutated.
/// - `Some(a)`:
/// - `plan.root_frame.id` ← `a.target_parent_id`
/// - `plan.root_frame.width` ← `a.target_width`
/// - status-bar subtasks filtered out
/// - every remaining subtask's `existing_section_labels` ← `Some(a.existing_section_labels.clone())`
/// - returns `{ skip_root_insertion: true, skip_status_bar: true }`
///
/// Port of `applyAppendContextToPlan` in `orchestrator-append.ts`.
pub(crate) fn apply_append_context_to_plan(
plan: &mut OrchestratorPlan,
append: Option<&AppendContext>,
) -> AppendPlanResult {
let Some(a) = append else {
return AppendPlanResult {
skip_root_insertion: false,
skip_status_bar: false,
};
};
plan.root_frame.id = a.target_parent_id.clone();
plan.root_frame.width = a.target_width;
plan.subtasks
.retain(|st| !is_status_bar_subtask(&st.id, &st.label));
for st in &mut plan.subtasks {
st.existing_section_labels = Some(a.existing_section_labels.clone());
}
AppendPlanResult {
skip_root_insertion: true,
skip_status_bar: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plan::{OrchestratorPlan, Region, RootFrameSpec, Subtask};
fn make_plan(subtask_ids_labels: &[(&str, &str)]) -> OrchestratorPlan {
OrchestratorPlan {
root_frame: RootFrameSpec {
id: "original-root".into(),
name: "Design".into(),
width: 1200.0,
height: 800.0,
layout: Some("vertical".into()),
gap: None,
padding: None,
fill: None,
},
subtasks: subtask_ids_labels
.iter()
.map(|(id, label)| Subtask {
id: id.to_string(),
label: label.to_string(),
region: Region {
width: 1200.0,
height: 400.0,
},
id_prefix: id.to_string(),
parent_frame_id: None,
elements: None,
screen: None,
generated_root_id: None,
existing_section_labels: None,
})
.collect(),
style_guide_name: None,
}
}
fn make_ctx(id: &str, width: f64, labels: &[&str]) -> AppendContext {
AppendContext {
target_parent_id: id.into(),
target_width: width,
existing_section_labels: labels.iter().map(|s| s.to_string()).collect(),
is_mobile: false,
}
}
// ── None case ─────────────────────────────────────────────────────────────
/// None → returns { false, false }, plan unmutated.
#[test]
fn none_returns_false_flags_and_no_mutation() {
let mut plan = make_plan(&[("hero", "Hero"), ("features", "Features")]);
let original_id = plan.root_frame.id.clone();
let original_width = plan.root_frame.width;
let original_subtask_count = plan.subtasks.len();
let result = apply_append_context_to_plan(&mut plan, None);
assert!(!result.skip_root_insertion);
assert!(!result.skip_status_bar);
assert_eq!(plan.root_frame.id, original_id, "root id must be unmutated");
assert_eq!(
plan.root_frame.width, original_width,
"root width must be unmutated"
);
assert_eq!(
plan.subtasks.len(),
original_subtask_count,
"subtask count must be unmutated"
);
assert!(
plan.subtasks
.iter()
.all(|st| st.existing_section_labels.is_none()),
"existing_section_labels must remain None"
);
}
// ── Some case: basic mutation ──────────────────────────────────────────────
/// Some(ctx) → returns { true, true }, root_frame repointed, labels propagated.
#[test]
fn some_returns_true_flags_and_mutates_plan() {
let mut plan = make_plan(&[("hero", "Hero"), ("pricing", "Pricing")]);
let ctx = make_ctx("existing-frame-abc", 390.0, &["Hero", "About"]);
let result = apply_append_context_to_plan(&mut plan, Some(&ctx));
assert!(result.skip_root_insertion);
assert!(result.skip_status_bar);
assert_eq!(plan.root_frame.id, "existing-frame-abc");
assert_eq!(plan.root_frame.width, 390.0);
}
/// Each remaining subtask gets existing_section_labels = Some(ctx labels).
#[test]
fn some_propagates_existing_section_labels_to_all_remaining_subtasks() {
let mut plan = make_plan(&[("hero", "Hero"), ("pricing", "Pricing")]);
let ctx = make_ctx("frame-1", 1200.0, &["Hero", "Pricing"]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
for st in &plan.subtasks {
let labels = st.existing_section_labels.as_ref().expect("labels set");
assert_eq!(labels, &vec!["Hero", "Pricing"]);
}
}
// ── Status-bar filter ─────────────────────────────────────────────────────
/// "status-bar" in id is matched and filtered.
#[test]
fn status_bar_subtask_filtered_by_id_hyphen() {
let mut plan = make_plan(&[("status-bar", "Top Area"), ("hero", "Hero")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "hero");
}
/// "Status Bar" in label (case-insensitive) is matched and filtered.
#[test]
fn status_bar_subtask_filtered_by_label_case_insensitive() {
let mut plan = make_plan(&[("chrome", "Status Bar"), ("hero", "Hero")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "hero");
}
/// CJK 状态栏 in label is matched.
#[test]
fn status_bar_subtask_filtered_by_cjk_zhuangtailan() {
let mut plan = make_plan(&[("chrome", "状态栏"), ("hero", "Hero")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "hero");
}
/// CJK 系统栏 in label is matched.
#[test]
fn status_bar_subtask_filtered_by_cjk_xitonglan() {
let mut plan = make_plan(&[("chrome", "系统栏"), ("content", "Content")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "content");
}
/// "system chrome" in label is matched.
#[test]
fn status_bar_subtask_filtered_by_system_chrome() {
let mut plan = make_plan(&[("top-bar", "System Chrome"), ("footer", "Footer")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "footer");
}
/// "statusbar" (zero space) in label is matched (covers `\s*` zero-space case).
#[test]
fn status_bar_subtask_filtered_by_statusbar_no_space() {
let mut plan = make_plan(&[("s", "statusbar"), ("hero", "Hero")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "hero");
}
/// "status_bar" (underscore) in id is matched.
#[test]
fn status_bar_subtask_filtered_by_underscore() {
let mut plan = make_plan(&[("status_bar", "Chrome"), ("hero", "Hero")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 1);
assert_eq!(plan.subtasks[0].id, "hero");
}
/// "Status Mention" should NOT match (no false positive).
#[test]
fn status_mention_is_not_filtered() {
let mut plan = make_plan(&[("status-mention", "Status Mention"), ("hero", "Hero")]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
// Neither subtask matches the status-bar pattern
assert_eq!(plan.subtasks.len(), 2);
}
/// Multiple status-bar subtasks all get filtered.
#[test]
fn multiple_status_bar_subtasks_all_filtered() {
let mut plan = make_plan(&[
("status-bar", "Status Bar"),
("hero", "Hero"),
("system-chrome", "System Chrome"),
("footer", "Footer"),
]);
let ctx = make_ctx("frame-1", 390.0, &[]);
apply_append_context_to_plan(&mut plan, Some(&ctx));
assert_eq!(plan.subtasks.len(), 2);
let ids: Vec<_> = plan.subtasks.iter().map(|st| st.id.as_str()).collect();
assert!(ids.contains(&"hero"));
assert!(ids.contains(&"footer"));
}
// ── Full integration: all mutations together ───────────────────────────────
/// Full append scenario: root repointed, status-bar filtered, labels propagated.
#[test]
fn full_append_scenario() {
let mut plan = make_plan(&[
("status-bar", "Status Bar"),
("hero", "Hero"),
("pricing", "Pricing"),
]);
let ctx = make_ctx("target-frame-xyz", 390.0, &["Nav", "Hero"]);
let result = apply_append_context_to_plan(&mut plan, Some(&ctx));
assert!(result.skip_root_insertion);
assert!(result.skip_status_bar);
assert_eq!(plan.root_frame.id, "target-frame-xyz");
assert_eq!(plan.root_frame.width, 390.0);
// status-bar filtered out; 2 remaining
assert_eq!(plan.subtasks.len(), 2);
let ids: Vec<_> = plan.subtasks.iter().map(|st| st.id.as_str()).collect();
assert!(ids.contains(&"hero"));
assert!(ids.contains(&"pricing"));
// labels propagated to all remaining
for st in &plan.subtasks {
let labels = st.existing_section_labels.as_ref().unwrap();
assert_eq!(labels, &vec!["Nav", "Hero"]);
}
}
}

View file

@ -24,6 +24,7 @@ pub mod timeouts;
pub mod types;
pub mod variables;
pub mod append;
pub mod cleanup;
pub mod concurrent;
pub mod prompt;