diff --git a/.changepacks/changepack_log_h1Z8bhReFSk91gTrAJyLF.json b/.changepacks/changepack_log_h1Z8bhReFSk91gTrAJyLF.json new file mode 100644 index 00000000..55412d65 --- /dev/null +++ b/.changepacks/changepack_log_h1Z8bhReFSk91gTrAJyLF.json @@ -0,0 +1,7 @@ +{ + "changes": { + "Cargo.toml": "Patch" + }, + "note": "Preserve caller sizing and absolute rectangle geometry, emit route-owned pages, and require functional E2E plus native Figma visual evidence for completion. Teach split-screen concurrent desktop work with isolated project windows, inputs and visual-test viewports.", + "date": "2026-11-05T16:51:25.331465500Z" +} diff --git a/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs b/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs index e5493888..edfa1582 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/component/render_node.rs @@ -109,6 +109,32 @@ fn render_node( let attributes = properties.iter().map(|(name, value)| { format!(" {name}={{{}}}", serde_json::to_string(value).unwrap()) }).collect::(); + // A reference only passes variant props. Its master component cannot + // reproduce an instance's parent allocation by itself. Keep that loss + // actionable instead of treating a named reference as layout evidence. + let mut placement_props = Vec::new(); + layout::push_layout_props(snapshot, node, "Box", &mut placement_props, + context.root_layout, depth == 0, context.width_policy.as_ref(), false); + let placement_props: serde_json::Map = placement_props.into_iter() + .filter(|(name, _)| matches!(name.as_str(), "w" | "h" | "boxSize" | "minW" | "maxW" | "minH" | "maxH" | "flex" | "flexShrink" | "alignSelf" | "gridColumn" | "gridRow")) + .map(|(name, PropValue::String(value))| (name, serde_json::Value::String(value))).collect(); + if !placement_props.is_empty() { + context.diagnostics.push(Diagnostic { + code: "DEVUP_CODEGEN_INSTANCE_PLACEMENT_CONTRACT".into(), + node_id: Some(node.id.clone()), + severity: Some(DiagnosticSeverity::Warning), + fidelity_impact: Some(devup_mcp_figma::FidelityImpact::Approximated), + message: "Component reference does not apply the instance's caller sizing; preserve its root placement contract during assembly.".into(), + details: Some(serde_json::json!({ + "component":component,"parentId":view.string("parentId"), + "requiredRootProps":placement_props, + "sourceSize":{"width":view.number("width"),"height":view.number("height")}, + "appliedToReference":false, + "nextAction":"Apply these props to the actual component root through typed layout props or inline the captured instance. Do not add unsupported props to a component or assume maxW allocates width. Verify the assembled parent/child bounds alongside the Figma reference visual test." + })), + ..Diagnostic::default() + }); + } let content = main_property.unwrap_or_else(|| format!("<{component}{attributes} />")); let expression = if let Some(visible) = visible_property { format!("{{{visible} && {content}}}") diff --git a/crates/devup-mcp-devup-ui/src/codegen/evidence.rs b/crates/devup-mcp-devup-ui/src/codegen/evidence.rs index bd03d452..65541efb 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/evidence.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/evidence.rs @@ -316,6 +316,13 @@ pub(super) fn placement_contract( requirements .push(include_str!("../text/evidence-placement_contract-adba181a.md").to_owned()); } + if parent.is_none() + && ["layoutSizingHorizontal", "layoutSizingVertical"] + .iter() + .any(|axis| view.string(axis) == Some("FILL")) + { + requirements.push("This detached FILL root needs its original parent's layout direction and allocation. maxW/maxH alone do not allocate space. Restore the caller's flex/grid sizing from instanceOverrides or collect the containing parent; verify the assembled component in the browser.".into()); + } if vertical_fill && (embedded || !view.number("height").is_some_and(|h| { diff --git a/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs b/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs index 08030b58..e08c6a6c 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/layout/layout_properties.rs @@ -24,7 +24,7 @@ pub(super) fn push_layout_props( .child_ids() .any(|child| child == node.id) }); - let is_root = snapshot.roots.iter().any(|root| root == &node.id); + let is_root = is_render_root || snapshot.roots.iter().any(|root| root == &node.id); // The parent of a collected root sits outside the collected subtree, so it // cannot be looked up and the node's recorded parent type is the only // account of it. Without that fallback a screen read as having no parent at @@ -98,6 +98,15 @@ pub(super) fn push_layout_props( } else if let Some((w, h)) = fixed_native_ellipse_size(&view, component, is_asset) { width = Some(px(w)); height = Some(px(h)); + } else if let Some((w, h)) = fixed_native_rectangle_size(&view, component, is_asset) { + // Keep the existing full-parent overlay relationship on matching + // axes. A smaller, intrinsically empty shape needs its own extent. + let spans = |axis: &str, extent: f64| parent.is_some_and(|p| { + let p = p.typed_view(); + p.field_error(axis).is_none() && p.number(axis) == Some(extent) + }); + width = Some(if spans("width", w) { "100%".into() } else { px(w) }); + height = Some(if spans("height", h) { "100%".into() } else { px(h) }); } else if component == "Text" { width = own_width.map(px); height = Some("100%".to_owned()); @@ -308,6 +317,12 @@ pub(super) fn push_layout_props( } } + // A selected FILL root can have a FIXED opposite axis, so it may have + // taken the mixed-sizing branch before reaching the root branch above. + if is_render_root && !embedded_root && !absolute && !is_page_root && fill_w && width.is_none() { + width = Some("100%".into()); + } + // A canvas with positioned children is their coordinate system, not just // a responsive page measurement. Preserve each FIXED axis even on a page // or section root. Embedded mode explicitly delegates dimensions to host. @@ -686,7 +701,8 @@ pub(super) fn push_layout_props( && let (Some(own), Some(room)) = (view.number(size), inner_extent(parent, size, near, far)) && own + 0.5 < room - && a_wider_box_would_show(&view, across_is_horizontal) + && (view.node_type() == "INSTANCE" + || a_wider_box_would_show(&view, across_is_horizontal)) { string_prop(props, "alignSelf", "flex-start"); } diff --git a/crates/devup-mcp-devup-ui/src/codegen/layout/solid_ellipse.rs b/crates/devup-mcp-devup-ui/src/codegen/layout/solid_ellipse.rs index 2cc6b663..6a954916 100644 --- a/crates/devup-mcp-devup-ui/src/codegen/layout/solid_ellipse.rs +++ b/crates/devup-mcp-devup-ui/src/codegen/layout/solid_ellipse.rs @@ -26,3 +26,22 @@ fn fixed_native_ellipse_size(view: &TypedNode<'_>, component: &str, is_asset: bo let (width, height) = (view.number("width")?, view.number("height")?); (width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0).then_some((width, height)) } + +/// Rectangles have no intrinsic content either. Figma's shape API may omit +/// layoutSizing*, unlike auto-layout frames; error-free measured axes and +/// non-stretch constraints are enough to retain their native fixed geometry. +fn fixed_native_rectangle_size(view: &TypedNode<'_>, component: &str, is_asset: bool) -> Option<(f64, f64)> { + if is_asset || component != "Box" || view.node_type() != "RECTANGLE" + || view.string("layoutPositioning") != Some("ABSOLUTE") + || view.child_ids().next().is_some() + || ["width", "height", "layoutPositioning", "layoutSizingHorizontal", "layoutSizingVertical", "constraints"] + .iter().any(|field| view.field_error(field).is_some()) + || ["layoutSizingHorizontal", "layoutSizingVertical"].iter() + .any(|field| view.string(field).is_some_and(|sizing| sizing != "FIXED")) + || ["horizontal", "vertical"].iter().any(|axis| view.value("constraints") + .and_then(|constraints| constraints.get(axis)).and_then(Value::as_str) + .is_some_and(|constraint| matches!(constraint, "STRETCH" | "SCALE"))) + { return None; } + let (width, height) = (view.number("width")?, view.number("height")?); + (width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0).then_some((width, height)) +} diff --git a/crates/devup-mcp-devup-ui/tests/fixed_absolute_ellipses.rs b/crates/devup-mcp-devup-ui/tests/fixed_absolute_ellipses.rs index 13953461..9d6ea9a1 100644 --- a/crates/devup-mcp-devup-ui/tests/fixed_absolute_ellipses.rs +++ b/crates/devup-mcp-devup-ui/tests/fixed_absolute_ellipses.rs @@ -88,7 +88,7 @@ fn fluid_unknown_and_unreadable_geometry_do_not_gain_fixed_shape_sizes() { } #[test] -fn relative_ellipse_and_other_shape_paths_keep_their_existing_sizing() { +fn relative_ellipses_keep_their_path_and_absolute_rectangles_preserve_measured_axes() { for positioning in [json!("AUTO"), json!({"$unsupported":"unknown"})] { let mut snapshot = source("I44:2832;44:3209"); snapshot @@ -107,5 +107,5 @@ fn relative_ellipse_and_other_shape_paths_keep_their_existing_sizing() { .unwrap() .node_type = "RECTANGLE".into(); let output = render(&snapshot); - assert!(!output.contains("boxSize=\"48px\""), "{output}"); + assert!(output.contains("boxSize=\"48px\""), "{output}"); } diff --git a/crates/devup-mcp-devup-ui/tests/fixtures/prime-placement.json b/crates/devup-mcp-devup-ui/tests/fixtures/prime-placement.json new file mode 100644 index 00000000..2994055a --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/fixtures/prime-placement.json @@ -0,0 +1,132 @@ +{ + "fileKey": "prime-placement-feedback", + "roots": [ + "row" + ], + "diagnostics": [], + "nodes": { + "row": { + "id": "row", + "type": "FRAME", + "fields": { + "name": "RenderFixture", + "parentType": "FRAME", + "width": 980, + "height": 250, + "layoutMode": "HORIZONTAL", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "HUG", + "itemSpacing": 40, + "childrenIds": [ + "title", + "card" + ], + "inferredAutoLayout": { + "layoutMode": "HORIZONTAL", + "itemSpacing": 40 + } + } + }, + "title": { + "id": "title", + "type": "INSTANCE", + "fields": { + "name": "StepTitle", + "parentId": "row", + "width": 420, + "height": 94, + "maxWidth": 420, + "layoutMode": "VERTICAL", + "layoutSizingHorizontal": "FILL", + "layoutSizingVertical": "HUG", + "itemSpacing": 12, + "childrenIds": [ + "title-content" + ], + "inferredAutoLayout": { + "layoutMode": "VERTICAL", + "itemSpacing": 12 + } + } + }, + "title-content": { + "id": "title-content", + "type": "FRAME", + "fields": { + "parentId": "title", + "width": 120, + "height": 94, + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "fills": [ + { + "type": "SOLID", + "color": { + "r": 0.1, + "g": 0.1, + "b": 0.1 + } + } + ] + } + }, + "card": { + "id": "card", + "type": "FRAME", + "fields": { + "name": "StepCard", + "parentId": "row", + "width": 520, + "height": 218, + "layoutMode": "VERTICAL", + "layoutSizingHorizontal": "FIXED", + "layoutSizingVertical": "FIXED", + "layoutPositioning": "AUTO", + "childrenIds": [ + "marker" + ], + "fills": [ + { + "type": "SOLID", + "color": { + "r": 0.97, + "g": 0.97, + "b": 0.98 + } + } + ], + "inferredAutoLayout": { + "layoutMode": "VERTICAL" + } + } + }, + "marker": { + "id": "marker", + "type": "RECTANGLE", + "fields": { + "name": "Rectangle 37", + "parentId": "card", + "width": 16, + "height": 16, + "x": -48, + "y": 32, + "layoutPositioning": "ABSOLUTE", + "constraints": { + "horizontal": "MIN", + "vertical": "MIN" + }, + "cornerRadius": 1000, + "fills": [ + { + "type": "SOLID", + "color": { + "r": 0.3176470588235294, + "g": 0.23529411764705882, + "b": 0.9333333333333333 + } + } + ] + } + } + } +} diff --git a/crates/devup-mcp-devup-ui/tests/prime_placement_feedback.rs b/crates/devup-mcp-devup-ui/tests/prime_placement_feedback.rs new file mode 100644 index 00000000..4815b9a5 --- /dev/null +++ b/crates/devup-mcp-devup-ui/tests/prime_placement_feedback.rs @@ -0,0 +1,152 @@ +use devup_mcp_devup_ui::codegen::{CodegenOptions, generate_component}; +use devup_mcp_figma::Snapshot; +use serde_json::json; + +fn snapshot() -> Snapshot { + serde_json::from_str(include_str!("fixtures/prime-placement.json")).unwrap() +} + +#[test] +fn absolute_rectangle_retains_both_measured_axes_inside_a_frame() { + let output = generate_component( + &snapshot(), + "card", + &CodegenOptions { + inline_instances: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(output.tsx.contains("boxSize=\"16px\""), "{}", output.tsx); + assert!(!output.tsx.contains("h=\"100%\""), "{}", output.tsx); +} + +#[test] +fn detached_render_root_retains_fill_even_when_snapshot_roots_are_different() { + let mut snapshot = snapshot(); + snapshot.nodes.remove("row"); + snapshot.roots = vec!["card".into()]; + let output = generate_component( + &snapshot, + "title", + &CodegenOptions { + inline_instances: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(output.tsx.contains("w=\"100%\""), "{}", output.tsx); + assert!(output.tsx.contains("maxW=\"420px\""), "{}", output.tsx); + let contract = output + .diagnostics + .iter() + .find(|d| d.code == "DEVUP_CODEGEN_PLACEMENT_CONTRACT") + .unwrap(); + assert!( + !contract.details.as_ref().unwrap()["hostRequirements"] + .as_array() + .unwrap() + .is_empty() + ); +} + +#[test] +fn component_reference_reports_the_parent_allocation_that_it_cannot_apply() { + let output = generate_component(&snapshot(), "row", &CodegenOptions::default()).unwrap(); + let contract = output + .diagnostics + .iter() + .find(|d| { + d.code == "DEVUP_CODEGEN_INSTANCE_PLACEMENT_CONTRACT" + && d.node_id.as_deref() == Some("title") + }) + .expect("reference must disclose unapplied caller sizing"); + let details = contract.details.as_ref().unwrap(); + assert_eq!(details["requiredRootProps"]["flex"], "1"); + assert_eq!(details["requiredRootProps"]["maxW"], "420px"); + assert_eq!(details["sourceSize"], json!({"width":420.0,"height":94.0})); + assert_eq!( + contract.fidelity_impact(), + devup_mcp_figma::FidelityImpact::Approximated + ); +} + +#[test] +fn mixed_axis_detached_fill_is_preserved_without_a_maximum() { + let mut snapshot = snapshot(); + snapshot.nodes.remove("row"); + snapshot.roots = vec!["card".into()]; + let title = snapshot.nodes.get_mut("title").unwrap(); + title + .fields + .insert("maxWidth".into(), serde_json::Value::Null); + title + .fields + .insert("layoutSizingVertical".into(), json!("FIXED")); + let output = generate_component( + &snapshot, + "title", + &CodegenOptions { + inline_instances: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(output.tsx.contains("w=\"100%\""), "{}", output.tsx); + assert!(output.tsx.contains("h=\"94px\""), "{}", output.tsx); +} + +#[test] +fn rectangle_repair_does_not_override_fluid_constraints_or_failed_reads() { + for (field, value, read_error) in [ + ( + "constraints", + json!({"horizontal":"STRETCH","vertical":"MIN"}), + false, + ), + ("layoutSizingHorizontal", json!("FILL"), false), + ("width", json!(16), true), + ] { + let mut snapshot = snapshot(); + let marker = snapshot.nodes.get_mut("marker").unwrap(); + if read_error { + marker + .field_errors + .insert(field.into(), "unavailable".into()); + } else { + marker.fields.insert(field.into(), value); + } + let output = generate_component( + &snapshot, + "card", + &CodegenOptions { + inline_instances: true, + ..Default::default() + }, + ) + .unwrap(); + assert!( + !output.tsx.contains("boxSize=\"16px\""), + "{field}: {}", + output.tsx + ); + } +} + +#[test] +fn full_parent_rectangle_keeps_its_existing_overlay_relationship() { + let mut snapshot = snapshot(); + let marker = snapshot.nodes.get_mut("marker").unwrap(); + marker.fields.insert("width".into(), json!(520)); + marker.fields.insert("height".into(), json!(218)); + let output = generate_component( + &snapshot, + "card", + &CodegenOptions { + inline_instances: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(output.tsx.contains("boxSize=\"100%\""), "{}", output.tsx); +} diff --git a/crates/devup-mcp/src/server/completion.rs b/crates/devup-mcp/src/server/completion.rs index 0d20450b..954d1a79 100644 --- a/crates/devup-mcp/src/server/completion.rs +++ b/crates/devup-mcp/src/server/completion.rs @@ -24,6 +24,11 @@ pub struct CompletionInput { pub audits: Vec, #[serde(default)] pub check_receipt_path: Option, + /// Missing consumer-only renderer fields, keyed by the ingest comparison ID. + /// Actual PNG, reference, threshold and observed capture context stay bound + /// to the audit. Completion reruns comparisons; caller verdicts are not used. + #[serde(default)] + pub visual_environments: BTreeMap, #[serde(default)] pub data_plan_path: Option, #[serde(default)] @@ -56,6 +61,48 @@ fn is_motion(candidate: &Value) -> bool { ) ) } + +async fn verify_visual( + comparison: &Value, + environment: Option<&super::visual_compare::VisualEnvironment>, + policy: &OutputPolicy, + artifacts: &ArtifactStore, +) -> Value { + let mut args = comparison["call"]["arguments"].clone(); + // A local file, including the implementation's own screenshot, cannot be + // supplied as a replacement baseline to the completion gate. + if args["reference"]["artifactId"].as_str().is_none_or(|id| id.starts_with('<')) + || args["reference"].get("path").is_some() + || comparison["blockers"].as_array().is_some_and(|v| !v.is_empty()) + { + return json!({"id":comparison["id"],"verdict":"inconclusive","reason":"A valid Figma artifact and compatible capture are required."}); + } + if let Some(environment) = environment { + let observed = args["environment"].clone(); + args["environment"] = serde_json::to_value(environment).unwrap_or(Value::Null); + if let Some(observed) = observed.as_object() { + for (key, value) in observed { + args["environment"][key] = value.clone(); + } + } + } + let result = match serde_json::from_value::(args) { + Ok(input) => super::visual_compare::compare(input, policy, artifacts).await, + Err(_) => return json!({"id":comparison["id"],"verdict":"inconclusive","reason":"Invalid visual comparison input."}), + }; + match result { + Ok(result) => { + let reference_node = result["reference"]["nodeId"].as_str() + .or_else(|| result["reference"]["target"]["nodeId"].as_str()); + let bound = comparison["screenshot"]["sha256"].as_str().is_some() + && comparison["screenshot"]["sha256"] == result["actual"]["sha256"] + && reference_node.is_some() + && reference_node == comparison["nodeId"].as_str(); + json!({"id":comparison["id"],"routePath":comparison["routePath"],"viewport":comparison["viewport"],"class":comparison["class"],"evidenceBound":bound,"result":result,"verdict":if bound {result["verdict"].clone()} else {json!("inconclusive")}}) + }, + Err(error) => json!({"id":comparison["id"],"verdict":"inconclusive","reason":error.message}), + } +} fn complete_inventory_for(plan: &Value, record: &Value, scope: Option<&str>) -> bool { super::behavior_contracts::verified_coverage_inventory(plan).is_some() && plan["scopeNodeIds"] @@ -304,6 +351,7 @@ pub(super) async fn evaluate( || input.widths.len() > 8 || input.widths.iter().any(|w| *w == 0 || *w > 4096) || input.audits.len() > 60 + || input.visual_environments.len() > 1000 || input.behavior_plan_paths.len() > 1000 || input.routes.len() > 1000 || input.static_bindings.len() > 10000 @@ -392,7 +440,7 @@ pub(super) async fn evaluate( && r["schemaVersion"] == 1 }); let mut missing_checks = Vec::new(); - for required in ["build", "lint", "typecheck", "test"] { + for required in ["build", "lint", "typecheck", "test", "e2e"] { let passing = checked && checks.as_ref().is_some_and(|r| { values(r, "checks").any(|c| { @@ -413,6 +461,9 @@ pub(super) async fn evaluate( routes.extend(values(&plan, "routes").filter_map(|r| r["url"].as_str().map(str::to_owned))); } let mut covered = BTreeSet::new(); + let mut required_visual = BTreeSet::new(); + let mut visual_covered = BTreeSet::new(); + let mut visual_results = Vec::new(); let mut motion_covered = BTreeSet::new(); let mut audit_results = Vec::new(); let mut audit_ok = !input.audits.is_empty(); @@ -438,10 +489,27 @@ pub(super) async fn evaluate( && ingested["integrity"]["planFileHashMatches"] == true; audit_ok &= pass; if pass { + for comparison in values(&ingested, "visualComparisons") { + let result = verify_visual(comparison, + comparison["id"].as_str().and_then(|id| input.visual_environments.get(id)), + policy, artifacts).await; + let visual_pass = result["verdict"] == "pass"; + audit_ok &= visual_pass; + if visual_pass && comparison["class"] == "page-visual" + && let (Some(route), Some(width)) = (comparison["routePath"].as_str(), comparison["viewport"]["width"].as_u64()) + { + visual_covered.insert((route.to_owned(), width as u32)); + } + visual_results.push(result); + } for screen in values(&plan["plan"], "screens") { if let Some(route) = screen["routePath"].as_str() { for viewport in values(&plan["plan"], "viewports") { if let Some(width) = viewport["width"].as_u64() { + let drawn = screen["figma"]["drawnWidths"].as_array(); + if drawn.is_none_or(|v| v.is_empty() || v.iter().any(|w| w.as_u64() == Some(width))) { + required_visual.insert((route.to_owned(), width as u32)); + } let kinds = values(screen, "checks") .filter(|check| { check["widths"].as_array().is_none_or(|v| { @@ -480,11 +548,14 @@ pub(super) async fn evaluate( .difference(&motion_covered) .cloned() .collect::>(); + let missing_visual = required_visual.difference(&visual_covered).cloned().collect::>(); let browser_verified = implemented && audit_ok && !routes.is_empty() && missing_routes.is_empty() - && missing_motion.is_empty(); + && missing_motion.is_empty() + && !required_visual.is_empty() + && missing_visual.is_empty(); let mut data = json!({"sourceMatched":false,"apiVerified":false,"dbVerified":false,"coveredBindingIds":[]}); if let (Some(plan_path), Some(receipt_path)) = (&input.data_plan_path, &input.data_receipt_path) { @@ -537,8 +608,8 @@ pub(super) async fn evaluate( json!({"kind":"devup-completion-receipt","schemaVersion":1,"sourceDigest":source_digest,"sourceFileCount":files.len(),"sourceScope":"project-source-and-public-assets","stage":stage,"done":data_verified,"reportAllowed":data_verified, "stages":{"generated":generated,"implemented":implemented,"browserVerified":browser_verified,"dataVerified":data_verified}, "requiredMotionIds":required_motion,"requiredBindingIds":required_bindings,"staticBindingDecisions":input.static_bindings, - "blockers":{"incompleteBehaviorCoverage":truncated,"missingChecks":missing_checks,"missingRoutesOrWidths":missing_routes,"missingMotion":missing_motion,"missingBindings":missing_bindings}, - "audits":audit_results,"dataEvidence":data,"dataVerificationScope":if required_bindings.is_empty(){"not-required"}else{"isolated-database-snapshots"},"input":input, + "blockers":{"incompleteBehaviorCoverage":truncated,"missingChecks":missing_checks,"missingRoutesOrWidths":missing_routes,"missingVisualComparisons":missing_visual,"missingMotion":missing_motion,"missingBindings":missing_bindings}, + "audits":audit_results,"visualResults":visual_results,"dataEvidence":data,"dataVerificationScope":if required_bindings.is_empty(){"not-required"}else{"isolated-database-snapshots"},"input":input, "trust":"Command exits, browser measurements and SQL rows are executor-observed; source digests, plan/report bindings and referenced artifact hashes are independently rechecked. A generated export or a caller success boolean never establishes application completion."}), ) } diff --git a/crates/devup-mcp/src/server/completion_tests.rs b/crates/devup-mcp/src/server/completion_tests.rs index bac95e81..4931d4e9 100644 --- a/crates/devup-mcp/src/server/completion_tests.rs +++ b/crates/devup-mcp/src/server/completion_tests.rs @@ -2,6 +2,37 @@ use super::*; fn snapshot(metric: bool) -> devup_mcp_figma::Snapshot { serde_json::from_value(json!({"fileKey":"completion-fixture","roots":["root"],"nodes":if metric{json!({"root":{"id":"root","type":"FRAME","fields":{"name":"Statistics","childrenIds":["metric"]}},"metric":{"id":"metric","type":"TEXT","fields":{"name":"Metric","characters":"3 patents"}}})}else{json!({"root":{"id":"root","type":"FRAME","fields":{"name":"Home","childrenIds":[]}}})},"diagnostics":[]})).unwrap() } + +#[tokio::test] +async fn completion_recomputes_pixels_and_rejects_local_baselines_and_missing_environment() { + // Synthetic PNGs exercise the evidence gate, not real design fidelity. + use base64::{Engine as _, engine::general_purpose::STANDARD}; + let (root, policy, _) = fixture(); + let store = ArtifactStore::default(); + let white = STANDARD.decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg==").unwrap(); + let black = STANDARD.decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGD4DwABBAEAX+XDSwAAAABJRU5ErkJggg==").unwrap(); + let mut payload = super::super::artifacts::w3_tests::payload(); + payload.reference_png = Some(devup_mcp_figma::ReferencePng { + mime_type:"image/png".into(),data_base64:STANDARD.encode(&white), + byte_length:white.len(),sha256:hash(&white),note:None, + }); + let artifact = store.insert(super::super::artifacts::ArtifactRequestKey::from_collection(&super::super::artifacts::w3_tests::request()),payload).await.unwrap(); + let actual = root.join(".devup/actual.png"); + std::fs::write(&actual,&white).unwrap(); + let observed = json!({"schemaVersion":1,"actualPng":actual,"viewport":{"width":1,"height":1,"deviceScaleFactor":1},"captureContext":{"innerWidth":1,"innerHeight":1,"devicePixelRatio":1,"visualViewportScale":1,"rootCssZoom":1,"bodyCssZoom":1},"environmentStatus":"valid","renderer":{"name":"playwright-chromium","version":"test"},"os":"test","locale":"en-US","timezone":"UTC"}); + let env = serde_json::from_value(json!({"tsxResource":{"uri":"devup://artifact/test/tsx","sha256":"a".repeat(64)},"themePath":"devup.json","assetDirectory":"public/images","devupUiVersion":"test","fontManifest":[{"family":"Test","sha256":"b".repeat(64)}]})).unwrap(); + let mut comparison = json!({"id":"home-page-1","nodeId":"1:1","screenshot":{"sha256":hash(&white)},"class":"page-visual","routePath":"/","viewport":{"width":1},"blockers":[],"call":{"arguments":{"actual":actual,"reference":{"artifactId":artifact.artifact_id},"threshold":0,"environment":observed}}}); + assert_eq!(verify_visual(&comparison,Some(&env),&policy,&store).await["verdict"],"pass"); + assert_eq!(verify_visual(&comparison,None,&policy,&store).await["verdict"],"inconclusive"); + std::fs::write(&actual,&black).unwrap(); + assert_eq!(verify_visual(&comparison,Some(&env),&policy,&store).await["verdict"],"inconclusive"); + comparison["screenshot"]["sha256"] = json!(hash(&black)); + assert_eq!(verify_visual(&comparison,Some(&env),&policy,&store).await["verdict"],"fail"); + comparison["call"]["arguments"]["reference"] = json!({"path":actual}); + assert_eq!(verify_visual(&comparison,Some(&env),&policy,&store).await["verdict"],"inconclusive"); + drop(artifact);drop(store);drop(policy); + std::fs::remove_dir_all(root).unwrap(); +} fn signed_plan() -> Value { let mut result = json!({"artifactId":"fixture-artifact","nodeId":"root"}); super::super::behavior_contracts::attach(&mut result, &snapshot(false)); @@ -32,6 +63,7 @@ fn fixture() -> (std::path::PathBuf, OutputPolicy, CompletionInput) { project_root: root.to_string_lossy().into_owned(), audits: vec![], check_receipt_path: None, + visual_environments: BTreeMap::new(), data_plan_path: None, data_receipt_path: None, static_bindings: BTreeMap::new(), @@ -53,7 +85,7 @@ async fn generated_source_never_implies_application_completion() { .as_array() .unwrap() .len(), - 4 + 5 ); drop(policy); std::fs::remove_dir_all(root).unwrap(); @@ -62,7 +94,7 @@ async fn generated_source_never_implies_application_completion() { async fn build_receipts_bind_current_source_and_cannot_replace_browser_or_data_proof() { let (root, policy, mut input) = fixture(); let (digest, _) = source_digest(&root, &policy).unwrap(); - let commands = ["build", "lint", "typecheck", "test"] + let commands = ["build", "lint", "typecheck", "test", "e2e"] .map(|name| json!({"name":name,"command":["bun","run",name],"exitCode":0})); let checks = json!({"kind":"devup-project-check-receipt","schemaVersion":1,"sourceDigest":digest,"sourceChanged":false,"checks":commands}); let path = root.join(".devup/checks.json"); @@ -250,11 +282,11 @@ async fn unsigned_inventory_is_rebuilt_only_from_a_retained_capture() { } #[tokio::test] -async fn bound_synthetic_receipts_advance_stages_and_source_changes_revoke_completion() { +async fn source_bound_static_checks_cannot_replace_figma_visuals() { let (root, policy, mut input) = fixture(); let store = ArtifactStore::default(); let (digest, _) = source_digest(&root, &policy).unwrap(); - let commands = ["build", "lint", "typecheck", "test"] + let commands = ["build", "lint", "typecheck", "test", "e2e"] .map(|name| json!({"name":name,"command":["bun","run",name],"exitCode":0})); let checks = root.join(".devup/checks.json"); std::fs::write(&checks,serde_json::to_vec(&json!({"kind":"devup-project-check-receipt","schemaVersion":1,"sourceDigest":digest,"sourceChanged":false,"checks":commands})).unwrap()).unwrap(); @@ -293,12 +325,34 @@ async fn bound_synthetic_receipts_advance_stages_and_source_changes_revoke_compl report_path: report_path.to_string_lossy().into(), }); let result = write(&input, &policy, &store, None).await.unwrap(); + assert_eq!(result["stage"], "implemented", "{result}"); + assert_eq!(result["blockers"]["missingVisualComparisons"], json!([["/",360]])); + // Once both the observed E2E command and a native Figma comparison exist, + // the same source-bound evidence can advance all completion stages. + use base64::{Engine as _, engine::general_purpose::STANDARD}; + let mut payload = super::super::artifacts::w3_tests::payload(); + payload.reference_png = Some(devup_mcp_figma::ReferencePng { + mime_type: "image/png".into(), data_base64: STANDARD.encode(&png), + byte_length: png.len(), sha256: hash(&png), note: None, + }); + let artifact = store.insert(super::super::artifacts::ArtifactRequestKey::from_collection(&super::super::artifacts::w3_tests::request()), payload).await.unwrap(); + let mut planned: Value = serde_json::from_slice(&std::fs::read(&plan_path).unwrap()).unwrap(); + planned["plan"]["screens"][0]["figma"] = json!({"nodeId":"root","drawnWidths":[360],"frames":[{"width":360,"height":100,"nodeId":"1:1","artifactId":artifact.artifact_id}]}); + let rewritten = super::super::runtime_audit::create_plan(serde_json::from_value(planned["plan"].clone()).unwrap(), &policy, Some(&plan_path.to_string_lossy()), None, &store).await.unwrap(); + let mut report = report; + report["planId"] = rewritten["planId"].clone(); + report["planSha256"] = json!(hash(&std::fs::read(&plan_path).unwrap())); + report["runs"][0]["screens"][0]["captureContext"] = json!({"innerWidth":360,"innerHeight":100,"devicePixelRatio":1,"visualViewportScale":1,"rootCssZoom":1,"bodyCssZoom":1}); + report["executor"] = json!({"browser":{"name":"chromium","version":"test"}}); + report["environment"] = json!({"os":"test","locale":"en-US","timezone":"UTC"}); + std::fs::write(&report_path, report.to_string()).unwrap(); + let ingested = super::super::runtime_audit::ingest_with_artifacts(&policy, &plan_path.to_string_lossy(), &report_path.to_string_lossy(), &store).await.unwrap(); + let comparison_id = ingested["visualComparisons"][0]["id"].as_str().unwrap().to_owned(); + input.visual_environments.insert(comparison_id, serde_json::from_value(json!({"tsxResource":{"uri":"devup://artifact/test/tsx","sha256":"a".repeat(64)},"themePath":"devup.json","assetDirectory":"public/images","devupUiVersion":"test","fontManifest":[{"family":"Test","sha256":"b".repeat(64)}]})).unwrap()); + let result = write(&input, &policy, &store, None).await.unwrap(); assert_eq!(result["stage"], "data-verified", "{result}"); + assert_eq!(refresh(&input.project_root, &policy, &store).await.unwrap()["done"], true); assert_eq!(result["dataVerificationScope"], "not-required"); - assert_eq!( - refresh(&input.project_root, &policy, &store).await.unwrap()["done"], - true - ); // A syntactically present but unsigned empty inventory cannot remove all // data/motion obligations and turn a generated fixture into a done project. let record_path = root.join(".devup/exports/home.json"); diff --git a/crates/devup-mcp/src/server/development_contract.rs b/crates/devup-mcp/src/server/development_contract.rs index 71236bae..f3c4a33d 100644 --- a/crates/devup-mcp/src/server/development_contract.rs +++ b/crates/devup-mcp/src/server/development_contract.rs @@ -7,13 +7,17 @@ const RULES: &str = include_str!("skills/devfive-frontend/references/delivery-qu pub(super) fn attach(result: &mut Value) { if !result.is_object() { return; } result["developmentContract"] = json!({ - "version": 1, + "version": 2, "guide": "devup://guide/usage#behavior-and-data-contracts", "installedReference": "devfive-frontend/references/delivery-quality.md", "alwaysApplies": true, "requirements": [ "pure-tsx-sidecar-provenance", "jsx-theme-tokens-and-axis-shorthands", - "single-responsive-tree-and-korean-word-boundaries", "persistent-server-layout", + "single-responsive-tree-and-source-text-wrapping", "persistent-server-layout", + "route-owned-page-body", "devup-api-only", + "stop-affected-api-gaps", + "preserve-instance-sizing-and-paint", + "paired-e2e-and-native-figma-visuals", "semantic-native-controls-and-complete-pseudo-states", "variant-palette-consistency", "single-source-url-content-selection", "viewport-footer-shell", "motion-candidates-and-safe-progressive-enhancement", "canonical-semantic-assets", @@ -23,7 +27,7 @@ pub(super) fn attach(result: &mut Value) { "computed-style-and-animation-intermediate-state", "changed-empty-and-failing-api", "all-public-assets-and-persisted-url-references"], "staticExportProvesApplicationBehavior": false, - "nextAction": "Apply this contract and behaviorPlan when wiring generated TSX. Resolve each required binding and test its runtime behavior; report unverified work explicitly." + "nextAction": "Read the linked rules, apply behaviorPlan, fix and rerun functional E2E plus native Figma visuals before claiming completion." }); } @@ -36,6 +40,11 @@ mod tests { attach(&mut result); assert_eq!(result["developmentContract"]["alwaysApplies"], true); assert_eq!(result["developmentContract"]["staticExportProvesApplicationBehavior"], false); + assert_eq!(result["developmentContract"]["version"], 2); + let requirements = result["developmentContract"]["requirements"].as_array().unwrap(); + for required in ["route-owned-page-body", "devup-api-only", "stop-affected-api-gaps", "paired-e2e-and-native-figma-visuals"] { + assert!(requirements.contains(&json!(required))); + } for term in ["keep-all", "grayscale", "layout.tsx", "count", "footer", "DB", "aria-current", "sidecar"] { assert!(RULES.contains(term), "missing delivery policy: {term}"); } diff --git a/crates/devup-mcp/src/server/page_scaffold.rs b/crates/devup-mcp/src/server/page_scaffold.rs index c399ca13..1091f57b 100644 --- a/crates/devup-mcp/src/server/page_scaffold.rs +++ b/crates/devup-mcp/src/server/page_scaffold.rs @@ -167,7 +167,8 @@ pub(crate) fn attach( let code = carrier.get("responsiveTsx").or_else(|| carrier.get("tsx")).and_then(Value::as_str).ok_or_else(|| { invalid("pageScaffold has no generated screen; inspect projection failures.") })?; - // The existing generator emits one named function. Do not split its JSX or infer hooks. + // Keep the actual generated body in the route. Renaming its export does + // not split JSX, introduce a proxy component or infer client boundaries. let name = code .lines() .find_map(|line| line.strip_prefix("export function ")) @@ -179,39 +180,17 @@ pub(crate) fn attach( } else { format!("{name}Page") }; - let page_name = if page_name == name { - format!("{name}ShellPage") - } else { - page_name - }; - let relative = Path::new("components/pages") - .join(route) - .join(format!("{name}.tsx")); - let import = format!( - "{}{}", - "../".repeat(2 + route.split('/').filter(|s| !s.is_empty()).count()), - relative - .to_string_lossy() - .replace('\\', "/") - .trim_end_matches(".tsx") - ); - let page = format!( - "import {{ {name} }} from \"{import}\";\n\nexport default function {page_name}() {{\n return <{name} />;\n}}\n" + let page = code.replacen( + &format!("export function {name}("), + &format!("export default function {page_name}("), 1, ); + devup_mcp_devup_ui::validation::validate_tsx(&page)?; let mut files = Vec::new(); let mut planned = Vec::new(); - for (suffix, path, content) in [ - ( - "page", - root.join("src/app").join(route).join("page.tsx"), - page, - ), - ("component", root.join(relative), code.to_owned()), - ] { - let target = policy.resolve(&path.to_string_lossy())?; - files.push(json!({"path":target.display_path(),"content":content,"encoding":"utf8"})); - planned.push((format!("{key}:{suffix}"), target, content.into_bytes())); - } + let path = root.join("src/app").join(route).join("page.tsx"); + let target = policy.resolve(&path.to_string_lossy())?; + files.push(json!({"path":target.display_path(),"content":page,"encoding":"utf8"})); + planned.push((format!("{key}:page"), target, page.into_bytes())); let mut unavailable = Vec::new(); if let Some(manifest) = manifest { for asset in &manifest.assets { @@ -249,7 +228,7 @@ pub(crate) fn attach( "files":files,"unavailableAssets":unavailable, "unresolvedDecisions":[ {"kind":"client-boundary","reason":"State, events and browser API ownership are not in Figma; a human or validator must choose any client boundary."}, - {"kind":"component-decomposition","reason":"Further component boundaries and reuse require application context; only the page shell and one screen were emitted."} + {"kind":"component-decomposition","reason":"Further component boundaries and reuse require application context; the actual screen body was emitted in app/**/page.tsx; components/pages is for page-local partials only."} ]})); if let Some(output) = carrier .get_mut("outputResults") @@ -285,7 +264,6 @@ pub(crate) fn committed_paths(result: &Map, written: &mut Map Result { + // A temp-wide policy allowed parallel fixtures to write the same sibling + // .devup/public-assets.json. Confine self-contained public fixtures so the + // production registry guard selects their own private metadata directory. + let policy_root = match &operation { + PendingOperation::Export { asset_public_root: Some(root), output_paths, asset_output_paths, .. } + if root.is_dir() && output_paths.values().chain(asset_output_paths.values()) + .all(|path| std::path::Path::new(path).starts_with(root)) => root.clone(), + _ => std::env::temp_dir(), + }; let store = ArtifactStore::default(); let key = ArtifactRequestKey::from_collection(&CollectionRequest::new( payload.target.clone(), @@ -673,7 +682,7 @@ async fn project( &artifact.payload, "fixture", &artifact, - &OutputPolicy::from_roots(vec![std::env::temp_dir()]).unwrap(), + &OutputPolicy::from_roots(vec![policy_root]).unwrap(), &store, ) .await diff --git a/crates/devup-mcp/src/server/projection/w1_regressions_w1_hidden_asset_is_warning_and_requested_failure_is_par.rs b/crates/devup-mcp/src/server/projection/w1_regressions_w1_hidden_asset_is_warning_and_requested_failure_is_par.rs index 5113e616..2ad2d675 100644 --- a/crates/devup-mcp/src/server/projection/w1_regressions_w1_hidden_asset_is_warning_and_requested_failure_is_par.rs +++ b/crates/devup-mcp/src/server/projection/w1_regressions_w1_hidden_asset_is_warning_and_requested_failure_is_par.rs @@ -467,7 +467,7 @@ async fn w7_scaffold_assets_match_public_paths_and_preview_has_no_asset_writes() let preview_files = preview["pageScaffold"]["files"].as_array().unwrap(); assert_eq!( preview_files.len(), - 3, + 2, "identical captured bytes reuse one asset" ); if let PendingOperation::Export { @@ -482,11 +482,14 @@ async fn w7_scaffold_assets_match_public_paths_and_preview_has_no_asset_writes() assert_eq!(preview_files, files); let code = files .iter() - .find(|f| f["path"].as_str().unwrap().contains("components")) + .find(|f| f["path"].as_str().unwrap().ends_with("page.tsx")) .unwrap()["content"] .as_str() .unwrap(); assert!(code.contains("/icons/actual.png"), "{code}"); + assert!(code.contains("export default function"), "{code}"); + assert!(!code.contains("components/pages"), "{code}"); + assert!(!root.join("components/pages").exists()); for file in files { let expected = if file["encoding"] == "base64" { STANDARD.decode(file["content"].as_str().unwrap()).unwrap() @@ -540,7 +543,7 @@ async fn w7_frame_scaffold_uses_frame_output_key_and_rejects_multi_route_guess() .as_array() .unwrap() .len(), - 2 + 1 ); assert!( value["outputPathResults"]["supportedKeys"] diff --git a/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs b/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs index 3d4466dd..298402c9 100644 --- a/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs +++ b/crates/devup-mcp/src/server/screen_plan_tests/responsive_contract.rs @@ -27,7 +27,10 @@ fn responsive_scaffold_uses_the_merged_module_instead_of_the_first_frame() { super::super::projection::page_scaffold::attach(&mut result, &super::super::projection::page_scaffold::PageScaffoldOptions { route: "/".into(), write: false, overwrite: false }, &paths, None, &BTreeMap::new(), &fixture.policy(), false).unwrap(); - assert!(array(&result["pageScaffold"]["files"]).iter().any(|file| file["content"] == code)); + let files = array(&result["pageScaffold"]["files"]); + assert_eq!(files.len(), 1); + assert_eq!(files[0]["content"], code.replacen("export function Main(", "export default function MainPage(", 1)); + assert!(files[0]["path"].as_str().unwrap().ends_with("page.tsx")); } #[test] diff --git a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md index 27baa43d..7a6e0096 100644 --- a/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devfive-frontend/SKILL.md @@ -14,6 +14,49 @@ description: | coding convention, DevFive, puzzle-fit, girok-space --- +## Concurrent projects and split-screen desktop + +Another running project is not a desktop-wide lock. When authorized projects +need GUI work, use OS split-screen with separate visible Figma/browser windows +and continue them side by side. Keep each project's workspace, terminal, server +port and browser context distinct. Do not close, minimize or replace another +project's working window, server or Bridge plugin to make room. + +Before each input or capture, verify the target window identity and bounds, +plus its URL/port or Figma file/page. Recheck coordinates after tiling. Coordinate +only conflicting mouse/keyboard input in short batches, not the entire project. +A real tool lock or permission error blocks its affected operation; report that +exact error, respect the lock and continue independent work. Do not invent a +single-project restriction or claim to bypass a tool-enforced lock. + +Keep functional E2E and Figma visual tests paired. Desktop pane size does not +replace the agreed CSS viewport, DPR or zoom. If the required capture viewport +cannot fit onscreen, use an independent automated browser context at that size; +do not change application layout, CSS zoom or thresholds to make a pane match. + +## Required page, API and verification boundaries + +- Route pages must contain their actual page structure and data preparation in + `app/**/page.tsx`. `components/pages//` contains page-local partials only. + Never delegate the entire page to one component, including through artificial + wrappers, fragments, providers or a shared header around a page proxy. +- Every application API call must use devup-api, including server reads, server + actions and uploads. Native `fetch`, another HTTP client and custom transports + are not substitutes. Before reporting a missing capability, use DCU to update + the relevant dependencies, reinstall, verify the resolved versions, and rerun + a minimal reproduction. Preserve the real API/schema contract, including null. + If the required capability is still missing, document a concrete devup-api + feature request and stop the affected implementation. Continue independent + authorized page work. Posting externally requires explicit authorization. +- Run functional E2E and Figma reference visual tests together for UI changes. + Do not create expected screenshots from the implementation under test. Cover + every changed section at desktop/mobile widths and captured error, selected, + expanded and disabled states. A passing initial screenshot is insufficient. + Keep the actual, Figma reference, diff, threshold and renderer/font/zoom context. +- Diagnosis and feedback files do not complete an authorized fix. Apply the fix, + rerun both kinds of browser verification, and report any remaining scope. + A missing API, reference or render environment stays explicitly unverified. + # DevFive Frontend Coding Conventions Before wiring generated screens, read [delivery-quality](references/delivery-quality.md). @@ -503,7 +546,9 @@ export function Button({ variant, children }: ButtonProps) { import { Suspense } from 'react' import { Header } from '@/components/layout/Header' -import { MyPageContent } from '@/components/pages/my-page/MyPageContent' +import { ProfileCard } from '@/components/pages/my-page/ProfileCard' +import { ActivityList } from '@/components/pages/my-page/ActivityList' +import { VStack, Text } from '@devup-ui/react' import { Loading } from '@/components/common/Loading' export default function MyPagePage() { // Name must end with Page @@ -511,7 +556,11 @@ export default function MyPagePage() { // Name must end with Page
}> - + + Recent activity + + + ) @@ -558,15 +607,20 @@ export function useToastContext() { ## API Integration ```tsx -// Server Component - direct fetch (recommended) -async function getData() { - const res = await fetch('https://api.example.com/data') - return res.json() -} - -export default async function DataPage() { - const data = await getData() - return +// Server Component - configured, generated devup-api client +import { api } from '@/api' +import { VStack, Text } from '@devup-ui/react' +import { ItemList } from '@/components/pages/items/ItemList' + +export default async function ItemsPage() { + const result = await api.get('/items') + if (result.isError) throw new Error('Unable to load items') + return ( + + Items + + + ) } // Client Component - react-query @@ -579,7 +633,7 @@ export function DataList() { if (isPending) return if (isError) return - return + return } ``` diff --git a/crates/devup-mcp/src/server/skills/devfive-frontend/references/critical-rules.md b/crates/devup-mcp/src/server/skills/devfive-frontend/references/critical-rules.md index 20ff0b62..e5915c0d 100644 --- a/crates/devup-mcp/src/server/skills/devfive-frontend/references/critical-rules.md +++ b/crates/devup-mcp/src/server/skills/devfive-frontend/references/critical-rules.md @@ -154,7 +154,8 @@ export default function LoginPage() { ... } // app/(auth)/dashboard/page.tsx import { Header } from '@/components/layout/Header' import { Sidebar } from '@/components/layout/Sidebar' -import { DashboardContent } from '@/components/pages/dashboard/DashboardContent' +import { DashboardSummary } from '@/components/pages/dashboard/DashboardSummary' +import { RecentActivity } from '@/components/pages/dashboard/RecentActivity' export default function DashboardPage() { return ( @@ -162,7 +163,11 @@ export default function DashboardPage() { {/* Server Component */}
{/* Server Component */} - {/* Server Component */} + + Dashboard + + + ) diff --git a/crates/devup-mcp/src/server/skills/devfive-frontend/references/delivery-quality.md b/crates/devup-mcp/src/server/skills/devfive-frontend/references/delivery-quality.md index ce0dffe5..af97a864 100644 --- a/crates/devup-mcp/src/server/skills/devfive-frontend/references/delivery-quality.md +++ b/crates/devup-mcp/src/server/skills/devfive-frontend/references/delivery-quality.md @@ -20,8 +20,10 @@ binary ships this document in its usage resource and skill installation. Do not invent tokens from matching numbers. Collapse equal `pl/pr` to `px`, `pt/pb` to `py`, and matching margin pairs, only when precedence is unchanged. Verify computed styles at each breakpoint after changing token syntax. -- Korean sentences use `wordBreak="keep-all"` and normal wrapping. Do not fix - word splitting with `nowrap`; handle long URLs or single tokens explicitly. +- Preserve Figma line breaks and measured wrapping at each captured width. + Do not apply `wordBreak="keep-all"` solely because text is Korean. Explicit + line breaks, font metrics and available width take precedence over a language + heuristic; verify automatic wrapping against the native Figma reference. ## Shared server shells and usable controls @@ -136,3 +138,91 @@ does not establish runtime completion. Report verified and unverified scopes. - Use devup_assets inventory/plan/apply/rollback for public assets and exact source/seed references. Anonymous exports remain private and withhold public/code writes until semantically named. Persisted URL SQL is an artifact requiring separate execution evidence. - Run devup_runtime_audit completion after real build/lint/typecheck/tests and bound browser/data audits. Generated source, wired implementation, browser verification and actual DB verification are distinct stages. Source changes invalidate stale receipts. Read the completion section in devup://guide/usage for the exact input and runner commands. - The repository CI runs the complete browser/production App Router consumer gate on Linux through its existing checks.test.mjs entry. Rust fixture/visual binaries are prepared once before browser timers start; source fingerprints prevent stale binary reuse. + +## Required page, API and verification boundaries + +- Route pages must contain their actual page structure and data preparation in + `app/**/page.tsx`. `components/pages//` contains page-local partials only. + Never delegate the entire page to one component, including through artificial + wrappers, fragments, providers or a shared header around a page proxy. +- Every application API call must use devup-api, including server reads, server + actions and uploads. Native `fetch`, another HTTP client and custom transports + are not substitutes. Before reporting a missing capability, use DCU to update + the relevant dependencies, reinstall, verify the resolved versions, and rerun + a minimal reproduction. Preserve the real API/schema contract, including null. + If the required capability is still missing, document a concrete devup-api + feature request and stop the affected implementation. Continue independent + authorized page work. Posting externally requires explicit authorization. +- Run functional E2E and Figma reference visual tests together for UI changes. + Do not create expected screenshots from the implementation under test. Cover + every changed section at desktop/mobile widths and captured error, selected, + expanded and disabled states. A passing initial screenshot is insufficient. + Keep the actual, Figma reference, diff, threshold and renderer/font/zoom context. +- Diagnosis and feedback files do not complete an authorized fix. Apply the fix, + rerun both kinds of browser verification, and report any remaining scope. + A missing API, reference or render environment stays explicitly unverified. + +## Assembly must preserve visible geometry + +- A detached component needs its caller allocation as well as its internal + styles. Apply `instanceOverrides.generatedProps` and + `DEVUP_CODEGEN_INSTANCE_PLACEMENT_CONTRACT.requiredRootProps` to the actual + typed component root, or inline the captured instance. `maxW` alone does not + allocate FILL width. Do not silently discard root props when replacing a + reference with a separately exported component. Measure both siblings in their + assembled parent (Prime: 420px title, 40px gap, 520px cards in a 980px row). +- Preserve painted empty nodes. A childless absolute Box may draw a progress + rule, dot, background, border, mask, image or effect. Never hide/delete it + using only its position, width, tag or lack of text. Check paint, visibility, + opacity and captured source identity first. Measure tiny decorations too. +- Semantic `h1`/`h2`/`h3`, native inputs and buttons can change browser defaults. + Verify/reset computed margins, appearance and box sizing. Test input bounds, + focus and ARIA before and after validation messages. Shared button wrappers + must preserve the caller's width and alignment in every usage. +- Compare blur/crop/blend against the composed backdrop; a negative z-index or + isolation can change multiply into white. Preserve intentional baked effects + explicitly and retain pixel evidence for lossy image/absolute fallbacks. +- Resolve assets by source node/paint/asset identity, never the first matching + placeholder filename. Keep collisions blocked. Validate tokens against the + final assembled JSX, including instance exports, rather than the page alone. + Distinguish hidden library masters from visible instances and overrides. +- Source/AST preservation, compiler compatibility, behavior, geometry and pixels + are separate checks. Run the installed Devup UI transform plus real build; + do not cast away compiler failures. Diagnose generated-CSS HMR reloads with + server logs and a production render before changing unrelated page geometry. +- Large resources need bounded chunk/hash recovery; asset acquisition, public + path projection and actual file writes are separate evidence. Check path, + bytes, digest and blocked-write reason; do not treat a response as a saved file. +- Intentional backend-dependent design exceptions (such as unavailable CAPTCHA) + must be listed separately from unrelated layout regressions. Do not invent a + working control or claim API persistence from local state. + +## Dependency and delivery evidence + +Verify latest resolved package versions and a minimal reproducer before declaring +an upstream capability missing. Use the exact CI runtime, restore global mocks, +check exit status and required coverage, and verify remote checks after pushing. +PR creation, merge and release publication are distinct outcomes. For release +work use a clean fixture, the actual package archive paths and archive contents +(name, version and internal dependencies). Never accept stale tarballs, dry-run +output or a retry of a pinned old release source as proof of a current release. + +## Concurrent projects and split-screen desktop + +Another running project is not a desktop-wide lock. When authorized projects +need GUI work, use OS split-screen with separate visible Figma/browser windows +and continue them side by side. Keep each project's workspace, terminal, server +port and browser context distinct. Do not close, minimize or replace another +project's working window, server or Bridge plugin to make room. + +Before each input or capture, verify the target window identity and bounds, +plus its URL/port or Figma file/page. Recheck coordinates after tiling. Coordinate +only conflicting mouse/keyboard input in short batches, not the entire project. +A real tool lock or permission error blocks its affected operation; report that +exact error, respect the lock and continue independent work. Do not invent a +single-project restriction or claim to bypass a tool-enforced lock. + +Keep functional E2E and Figma visual tests paired. Desktop pane size does not +replace the agreed CSS viewport, DPR or zoom. If the required capture viewport +cannot fit onscreen, use an independent automated browser context at that size; +do not change application layout, CSS zoom or thresholds to make a pane match. diff --git a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md index 7bc7d976..dfa9a1f9 100644 --- a/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup-implementation-plan/SKILL.md @@ -113,6 +113,49 @@ items; never claim completion from split or command generation alone. +## Concurrent projects and split-screen desktop + +Another running project is not a desktop-wide lock. When authorized projects +need GUI work, use OS split-screen with separate visible Figma/browser windows +and continue them side by side. Keep each project's workspace, terminal, server +port and browser context distinct. Do not close, minimize or replace another +project's working window, server or Bridge plugin to make room. + +Before each input or capture, verify the target window identity and bounds, +plus its URL/port or Figma file/page. Recheck coordinates after tiling. Coordinate +only conflicting mouse/keyboard input in short batches, not the entire project. +A real tool lock or permission error blocks its affected operation; report that +exact error, respect the lock and continue independent work. Do not invent a +single-project restriction or claim to bypass a tool-enforced lock. + +Keep functional E2E and Figma visual tests paired. Desktop pane size does not +replace the agreed CSS viewport, DPR or zoom. If the required capture viewport +cannot fit onscreen, use an independent automated browser context at that size; +do not change application layout, CSS zoom or thresholds to make a pane match. + +## Required page, API and verification boundaries + +- Route pages must contain their actual page structure and data preparation in + `app/**/page.tsx`. `components/pages//` contains page-local partials only. + Never delegate the entire page to one component, including through artificial + wrappers, fragments, providers or a shared header around a page proxy. +- Every application API call must use devup-api, including server reads, server + actions and uploads. Native `fetch`, another HTTP client and custom transports + are not substitutes. Before reporting a missing capability, use DCU to update + the relevant dependencies, reinstall, verify the resolved versions, and rerun + a minimal reproduction. Preserve the real API/schema contract, including null. + If the required capability is still missing, document a concrete devup-api + feature request and stop the affected implementation. Continue independent + authorized page work. Posting externally requires explicit authorization. +- Run functional E2E and Figma reference visual tests together for UI changes. + Do not create expected screenshots from the implementation under test. Cover + every changed section at desktop/mobile widths and captured error, selected, + expanded and disabled states. A passing initial screenshot is insufficient. + Keep the actual, Figma reference, diff, threshold and renderer/font/zoom context. +- Diagnosis and feedback files do not complete an authorized fix. Apply the fix, + rerun both kinds of browser verification, and report any remaining scope. + A missing API, reference or render environment stays explicitly unverified. + # Screen plan first, confirm once, then implement A design handed over as "implement this" is several screens, and each one has diff --git a/crates/devup-mcp/src/server/skills/devup/SKILL.md b/crates/devup-mcp/src/server/skills/devup/SKILL.md index 0813548c..26fa227b 100644 --- a/crates/devup-mcp/src/server/skills/devup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/devup/SKILL.md @@ -102,6 +102,49 @@ items; never claim completion from split or command generation alone. +## Concurrent projects and split-screen desktop + +Another running project is not a desktop-wide lock. When authorized projects +need GUI work, use OS split-screen with separate visible Figma/browser windows +and continue them side by side. Keep each project's workspace, terminal, server +port and browser context distinct. Do not close, minimize or replace another +project's working window, server or Bridge plugin to make room. + +Before each input or capture, verify the target window identity and bounds, +plus its URL/port or Figma file/page. Recheck coordinates after tiling. Coordinate +only conflicting mouse/keyboard input in short batches, not the entire project. +A real tool lock or permission error blocks its affected operation; report that +exact error, respect the lock and continue independent work. Do not invent a +single-project restriction or claim to bypass a tool-enforced lock. + +Keep functional E2E and Figma visual tests paired. Desktop pane size does not +replace the agreed CSS viewport, DPR or zoom. If the required capture viewport +cannot fit onscreen, use an independent automated browser context at that size; +do not change application layout, CSS zoom or thresholds to make a pane match. + +## Required page, API and verification boundaries + +- Route pages must contain their actual page structure and data preparation in + `app/**/page.tsx`. `components/pages//` contains page-local partials only. + Never delegate the entire page to one component, including through artificial + wrappers, fragments, providers or a shared header around a page proxy. +- Every application API call must use devup-api, including server reads, server + actions and uploads. Native `fetch`, another HTTP client and custom transports + are not substitutes. Before reporting a missing capability, use DCU to update + the relevant dependencies, reinstall, verify the resolved versions, and rerun + a minimal reproduction. Preserve the real API/schema contract, including null. + If the required capability is still missing, document a concrete devup-api + feature request and stop the affected implementation. Continue independent + authorized page work. Posting externally requires explicit authorization. +- Run functional E2E and Figma reference visual tests together for UI changes. + Do not create expected screenshots from the implementation under test. Cover + every changed section at desktop/mobile widths and captured error, selected, + expanded and disabled states. A passing initial screenshot is insufficient. + Keep the actual, Figma reference, diff, threshold and renderer/font/zoom context. +- Diagnosis and feedback files do not complete an authorized fix. Apply the fix, + rerun both kinds of browser verification, and report any remaining scope. + A missing API, reference or render environment stays explicitly unverified. + # /devup [sources] [goal] Arguments: $ARGUMENTS - optional Figma links, Jira keys, spec URLs/files and a diff --git a/crates/devup-mcp/src/server/skills/figma-bridge-setup/SKILL.md b/crates/devup-mcp/src/server/skills/figma-bridge-setup/SKILL.md index 3bef2137..f0e64572 100644 --- a/crates/devup-mcp/src/server/skills/figma-bridge-setup/SKILL.md +++ b/crates/devup-mcp/src/server/skills/figma-bridge-setup/SKILL.md @@ -11,6 +11,25 @@ process inspection; use computer use for desktop menus, tabs and file dialogs. If computer use is unavailable, report the exact blocked step. Don't claim a connection from a running process alone. +## Concurrent projects and split-screen desktop + +Another running project is not a desktop-wide lock. Use OS split-screen with +separate visible Figma/browser windows; keep other projects, servers and plugins +running. Give each project its own workspace, port and browser context. Verify +the target window identity/bounds and file/page before each input or capture; +coordinate only conflicting mouse/keyboard input in short batches. + +For several attached Bridge files, supply the intended file URL/identity on +reads and verify its page/selection and a small node read. Do not rely on the +single-plugin selection shortcut. A real tool lock, permission error or ambiguous +file identity blocks that operation only: report the exact error, respect the +restriction and continue independent work. Never infer a global project lock +or stop another project merely because its window is already in use. + +Keep browser visual captures at their agreed CSS viewport, DPR and zoom. Use +an independent automated context when the reference viewport cannot fit in a +desktop pane; do not alter layout or zoom to force a comparison. + 1. Call `devup_figma_auth` with `action: "status"`. Read `paths.bridge`, its host PID and commit/buildId, and `server.commit`/`server.buildId`. A matching version alone doesn't prove a matching build. Missing host identity is @@ -23,9 +42,10 @@ connection from a running process alone. `brew install --cask figma`, or https://www.figma.com/downloads/. Direct means `devup_figma_auth` login and the real Figma URL. If no question tool exists, leave the choice unresolved and report the limitation. -3. Launch Figma, foreground its window and open the supplied file URL in the - desktop app. With computer use, click the target file tab, navigate to the - target page and select the intended frame/Section. Don't guess a file key. +3. Launch Figma and open the supplied file URL in the desktop app. If another + project uses Figma, open a separate window and tile them with OS split-screen. + Focus only the intended window for its input batch. Click its file tab, + navigate to the target page and select the intended frame/Section. Don't guess a file key. Let the user handle any sign-in, OTP or permission prompt you can't complete. 4. If Devup Bridge isn't registered, locate the actual downloaded repository's `plugin/manifest.json` and `plugin/dist/` bundle. In Figma choose @@ -33,8 +53,9 @@ connection from a running process alone. manifest. Don't invent a local path or rebuild an unchanged shipped bundle. Run **Plugins > Development > Devup Bridge** in the target file and leave its window open. -5. If `attachedFiles[].inView` is false, use computer use to restore and - foreground Figma, then click that file's tab. Keep it visible during reads. +5. If `attachedFiles[].inView` is false, restore the intended Figma window in + its split-screen pane, then click that file's tab. Keep it visible during + reads without hiding another project's window. Call status again and verify the file, current page, selection and `inView`. 6. For a mismatched/legacy relay host, inspect the reported PID, executable path and owning client before changing anything. Restart/update that client's diff --git a/crates/devup-mcp/src/server/skills/manifest.json b/crates/devup-mcp/src/server/skills/manifest.json index b685871e..20e874ac 100644 --- a/crates/devup-mcp/src/server/skills/manifest.json +++ b/crates/devup-mcp/src/server/skills/manifest.json @@ -13,8 +13,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 5866, - "sha256": "e16941efe87d21f466868fd7112c0298728f80360b5aa1b80c97a6cd8c29f649" + "bytes": 7128, + "sha256": "5333792035e3c2bc1b00f91195687914801980f9015fb5f316c69b1834b1cd06" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/figma-bridge-setup", @@ -51,13 +51,13 @@ "documents": [ { "path": "SKILL.md", - "bytes": 27157, - "sha256": "9088379e6d51fedde1f6178f78e09cb6e7a36854b4ab98c6f1078db05d059445" + "bytes": 30455, + "sha256": "21ba16688d97ff24a6a5d2352af5ad611a9ad5813c9e9d4301f07b64ac38648d" }, { "path": "references/critical-rules.md", - "bytes": 57175, - "sha256": "3b7694f5db108407e0b4e3736cc3018b4c72951596eb1e4feab3123a3a4d3549" + "bytes": 57345, + "sha256": "35b3c66d5d6ddc6911085003cb9d5f795f7c9456c97714c4ae40323a7c42cc33" }, { "path": "references/anti-patterns.md", @@ -76,8 +76,8 @@ }, { "path": "references/delivery-quality.md", - "bytes": 9896, - "sha256": "4cfd9bfd755a88853421de5fddf6f8f564dda8c19b0549c9a03707f17a919428" + "bytes": 15985, + "sha256": "aaadb68606fe7aac6d3cdc52bf27c80f95d5de6c23ed7ed49ad2d0b4057d451e" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devfive-frontend", @@ -94,8 +94,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 42556, - "sha256": "cb6ec7010a91502e0d432a359db3267e2e56d24ec60177d4765f64f99250c20d" + "bytes": 45398, + "sha256": "90ced638e1f3ad023bf991bc69872fae18933431cb3f5302f023cb716156bbe4" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup-implementation-plan", @@ -112,8 +112,8 @@ "documents": [ { "path": "SKILL.md", - "bytes": 21804, - "sha256": "f4e8601dff53896f9cecbaea7c0a8d7391443984f18829639b8ada74053dc540" + "bytes": 24646, + "sha256": "41e42e8aa489c4d8b51fcfbe02fa6cc53a72dfdab2713cd284b656666c41c58d" } ], "sourceUrl": "https://github.com/dev-five-git/devup-mcp/blob/HEAD/crates/devup-mcp/src/server/skills/devup", diff --git a/crates/devup-mcp/src/server/skills/tests.rs b/crates/devup-mcp/src/server/skills/tests.rs index 14ba7ac8..24670c15 100644 --- a/crates/devup-mcp/src/server/skills/tests.rs +++ b/crates/devup-mcp/src/server/skills/tests.rs @@ -5,6 +5,18 @@ mod tests { use super::*; use sha2::{Digest, Sha256}; + #[test] + fn prime_feedback_boundaries_are_carried_in_every_frontend_entry_point() { + for name in ["devup", "devup-implementation-plan", "devfive-frontend"] { + let text = EMBEDDED.iter().find(|(key, _)| *key == name).unwrap().1[0].1; + for required in ["app/**/page.tsx", "page-local partials", "Every application API call must use devup-api", "stop the affected implementation", "Continue independent", "functional E2E and Figma reference visual tests together", "Do not create expected screenshots from the implementation under test"] { + assert!(text.contains(required), "{name}: {required}"); + } + assert!(!text.contains("direct fetch (recommended)")); + assert!(!text.contains("")); + } + } + #[test] fn formatter_ownership_warning_is_carried_in_every_frontend_workflow() { for name in ["devup", "devup-implementation-plan", "devfive-frontend"] { diff --git a/crates/devup-mcp/src/server/text/completion.md b/crates/devup-mcp/src/server/text/completion.md index 4b760fb0..5090d059 100644 --- a/crates/devup-mcp/src/server/text/completion.md +++ b/crates/devup-mcp/src/server/text/completion.md @@ -15,11 +15,13 @@ in the browser plan, or pass the explicitly agreed required widths to both. `motionRuntime` and `motionBindings` outputs. Fill semantic selectors and real API values, then mount the shared controller once in the persistent layout. Generated controller code is executable; unresolved bindings remain explicit. -3. Run actual build, lint, typecheck and test commands through the emitted +3. Run actual build, lint, typecheck, unit test and functional E2E commands through the emitted `record-project-checks.mjs`, passing a JSON config with `projectRoot` and `checks: [{name:"build",command:["bun","run","build"]}, ...]`. The resulting `.devup/checks.json` records exits against a complete source/public asset digest; failed commands or source changes invalidate the implementation stage. + Use separate `test` and `e2e` names; a unit test receipt cannot replace the + functional E2E run. Run E2E with Figma reference visual tests for UI changes. 4. Create browser plans with explicit `projectRoot` so the server computes `plan.sourceDigest` from current files. Screen-plan `auditPlan` also binds this digest automatically. The runner independently hashes `plan.projectRoot` @@ -31,6 +33,14 @@ in the browser plan, or pass the explicitly agreed required widths to both. behavior candidate ID on the applicable planned viewport/screen/check index and observed intermediate behavior. Selected paint alone does not prove a sliding indicator or a complete step cycle. + Include the exact Figma frame artifact for every drawn width. Completion + reruns the ingested `visualComparisons` against cached native Figma PNGs; + it never accepts a caller verdict or a path to an implementation baseline. + Missing, failed or inconclusive comparisons block browser completion. + Supply consumer-only renderer fields in `visualEnvironments`, keyed by + comparison ID (tsxResource, themePath, assetDirectory, devupUiVersion and + fontManifest). Observed viewport, capture context, renderer and actual PNG + stay bound to the audited run. Retain actual/reference/diff and thresholds. 5. Operational data uses a typed `data-binding` check with actual OpenAPI/model anchors, exact exported binding IDs (`nodeId::role`), explicit aggregate and landing/list/detail selectors. The runner requires an isolated disposable DB, @@ -42,7 +52,7 @@ in the browser plan, or pass the explicitly agreed required widths to both. SQL is a reviewable artifact, not proof that a live migration was applied. 6. Call action `completion` with `completion: {projectRoot, routes, widths, checkReceiptPath, audits:[{planPath,reportPath}], dataPlanPath, - dataReceiptPath, behaviorPlanPaths}`. For inferred presentation values that are + dataReceiptPath, behaviorPlanPaths, visualEnvironments}`. For inferred presentation values that are explicitly static, record an individual `staticBindings` explanation keyed by binding ID. A blanket success boolean cannot waive required evidence. diff --git a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md index 2099b3c4..67d6f82a 100644 --- a/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md +++ b/crates/devup-mcp/src/server/text/guide-GUIDE-c682b0c5.md @@ -1,5 +1,27 @@ # Using devup-mcp +## Concurrent projects and split-screen desktop + +Another running project is not a desktop-wide lock. When authorized projects +need GUI work, use OS split-screen with separate visible Figma/browser windows +and continue them side by side. Keep each project's workspace, terminal, server +port and browser context distinct. Do not close, minimize or replace another +project's working window, server or Bridge plugin to make room. + +Before each input or capture, verify the target window identity and bounds, +plus its URL/port or Figma file/page. Recheck coordinates after tiling. Coordinate +only conflicting mouse/keyboard input in short batches, not the entire project. +A real tool lock or permission error blocks its affected operation; report that +exact error, respect the lock and continue independent work. Do not invent a +single-project restriction or claim to bypass a tool-enforced lock. + +Keep functional E2E and Figma visual tests paired. Desktop pane size does not +replace the agreed CSS viewport, DPR or zoom. If the required capture viewport +cannot fit onscreen, use an independent automated browser context at that size; +do not change application layout, CSS zoom or thresholds to make a pane match. + +## Design and implementation + When replacing an inlined instance with a shared component, carry over every `instanceOverrides` entry through a new variant or prop, or explicitly report it as unresolved. `DEVUP_CODEGEN_INSTANCE_OVERRIDE` is a required review item; structured instanceOverrides sidecars retain its evidence without metadata comments in TSX. A missing captured value or master is unresolved, never evidence that an override is safe to discard. For a confirmed URL with multiple drawn widths, `responsiveTsx` is the deliverable. Use all sibling frameIds in width order from screen-plan status or `responsiveRequired.nextAction`. Never start from single-width `tsx` or `componentTsx` for that page or its sections. Only wire links, events and data; never change layout values, collapse responsive arrays, or add fixed heights or layout wrappers to approximate the design. `generatedExport.generatedDrift` reports heuristic layout changes against recorded generated styles with source paths and confidence; missing mappings are unverified, not a pass. diff --git a/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md b/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md index 0f0e72d7..dbbb6243 100644 --- a/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md +++ b/crates/devup-mcp/src/server/text/guide-INSTRUCTIONS-49bd7ec6.md @@ -1,6 +1,7 @@ Build identity: use server.commit/buildId, not version; reconnect if stale. server.updateAvailable unknown never blocks. 1. devup-mcp is the primary Figma-to-code source. Connection: bridge -> direct; call devup_figma_auth status first, one Bridge needs no login or url. -2b. /devup starts devup_project_plan intake for any project/feature/fix/refactor. Read Jira/spec/Figma and devup_project_context first; ask every round up front, draft ERD, confirm, then split work units. devup_screen_plan handles the Figma branch. Load devup_skills and devup-implementation-plan; after confirm never wait: defer and continue, assume at the end. +2a. Another running project is not a desktop lock; use OS split-screen. +2b. /devup: devup_project_plan intake; read Jira/spec/Figma and devup_project_context, ask all rounds up front, draft ERD, confirm, split. devup_screen_plan handles Figma. Load devup_skills and devup-implementation-plan; after confirm defer, continue, assume at the end. 3. For Figma implementation use devup_figma_export tsx, the deliverable. TSX links/events need wiring: resolve interactiveCandidates, devup_ui_validate, report wired/tested counts. deliverable.isFinal describes export readiness. Audit every width and exact layout. Backend completion needs devup_stack_diff zero drift. 4. Request only outputs you read. Before your second call read devup://guide/usage for sizing, verification, batching, assets and delivery. 5. After confirm, extract COMPONENT_SET/COMPONENT first with componentTsx into components/common|layout; record mainComponent id -> file in .devup/component-map.json. Assemble generated sections (frame-root children); audit via file mappings, not DOM Figma attributes. Never hand-write TSX from Figma nodes. Report unsupported exports. diff --git a/crates/devup-mcp/src/server/validation.rs b/crates/devup-mcp/src/server/validation.rs index 78ecb4ca..53fdaa14 100644 --- a/crates/devup-mcp/src/server/validation.rs +++ b/crates/devup-mcp/src/server/validation.rs @@ -225,6 +225,14 @@ pub(super) fn validate_outputs(outputs: &[String], debug: bool) -> Result<(), De )); } if !debug && DIAGNOSIS_OUTPUTS.contains(&output.as_str()) { + let mut recovery_outputs = outputs.to_vec(); + // A debug toggle alone led to a second projection error. Include + // source-map code and used-only theme dependencies in one retry. + for required in ["tsx", "sourceMap", "devupJson"] { + if !recovery_outputs.iter().any(|output| output == required) { + recovery_outputs.push(required.into()); + } + } return Err(DevupError::with_details( ErrorCode::DevupInvalidInput, format!( @@ -233,8 +241,9 @@ pub(super) fn validate_outputs(outputs: &[String], debug: bool) -> Result<(), De ), false, json!({"stage":"preflight","nextAction":{"tool":"devup_figma_export", - "arguments":{"debug":true}, - "how":"Repeat the same call with debug: true. Pair rawSnapshot with sourceMap, and reuse cache.artifactId for later projections instead of re-collecting."}}), + "preserveOtherArguments":true, + "arguments":{"debug":true,"outputs":recovery_outputs}, + "how":"Merge these arguments into the original call, preserving its target and theme options. This recipe includes sourceMap's code output and used-only devupJson dependencies. Reuse cache.artifactId for later projections instead of re-collecting."}}), )); } } @@ -653,6 +662,18 @@ pub(super) fn public_asset_url( mod p3_tests { use super::*; #[test] + fn diagnosis_retry_includes_code_map_and_theme_dependencies() { + let error = validate_outputs(&["rawSnapshot".into()], false).unwrap_err(); + let action = &error.details["nextAction"]; + assert_eq!(action["preserveOtherArguments"], true); + assert_eq!(action["arguments"]["debug"], true); + let outputs: Vec = serde_json::from_value(action["arguments"]["outputs"].clone()).unwrap(); + for dependency in ["rawSnapshot", "tsx", "sourceMap", "devupJson"] { + assert!(outputs.iter().any(|output| output == dependency)); + } + validate_outputs(&outputs, true).unwrap(); + } + #[test] fn webp_options_are_validated_and_paths_use_the_actual_codec_extension() { let base = json!({"assetId":"sample:fills:0","format":"webp","quality":0,"lossless":true,"outputPath":"images/sample.png"}); let parsed = diff --git a/crates/devup-mcp/tests/export_project_theme.rs b/crates/devup-mcp/tests/export_project_theme.rs index fd09381d..1cfde997 100644 --- a/crates/devup-mcp/tests/export_project_theme.rs +++ b/crates/devup-mcp/tests/export_project_theme.rs @@ -136,7 +136,11 @@ async fn export(arguments: Value) -> anyhow::Result { /// A project whose `devup.json` names this colour something else entirely. fn project_naming_it_brand() -> anyhow::Result { - let root = std::env::temp_dir().join(format!("devup-f3-{}", std::process::id())); + let root = std::env::temp_dir().join(format!( + "devup-f3-{}-{}", + std::process::id(), + rand::random::() + )); fs::create_dir_all(&root)?; fs::write(root.join("package.json"), r#"{"name":"fixture"}"#)?; fs::write( diff --git a/crates/devup-mcp/tests/page_scaffold.rs b/crates/devup-mcp/tests/page_scaffold.rs index 28dc1079..4c665edd 100644 --- a/crates/devup-mcp/tests/page_scaffold.rs +++ b/crates/devup-mcp/tests/page_scaffold.rs @@ -178,7 +178,7 @@ fn files(value: &Value) -> &Vec { async fn w7_preview_is_default_and_writes_nothing() { let dir = temp_dir().unwrap(); let value = export(dir.path(), args(dir.path())).await; - assert_eq!(files(&value).len(), 2); + assert_eq!(files(&value).len(), 1); assert_eq!(value["pageScaffold"]["mode"], "preview"); assert_eq!( value["outputPathResults"]["pathKinds"]["pageScaffold"], @@ -194,7 +194,7 @@ async fn w7_explicit_write_matches_manifest_exactly() { let mut input = args(dir.path()); input["pageScaffold"]["write"] = json!(true); let value = export(dir.path(), input).await; - assert_eq!(files(&value).len(), 2); + assert_eq!(files(&value).len(), 1); for file in files(&value) { assert_eq!( std::fs::read_to_string(file["path"].as_str().unwrap()).unwrap(), @@ -203,10 +203,7 @@ async fn w7_explicit_write_matches_manifest_exactly() { } assert_eq!( written_files(dir.path()), - std::collections::BTreeSet::from([ - dir.path().join("src/app/proofread/page.tsx"), - dir.path().join("components/pages/proofread/Proofread.tsx"), - ]) + std::collections::BTreeSet::from([dir.path().join("src/app/proofread/page.tsx"),]) ); assert_eq!(value["pageScaffold"]["collisionPolicy"], "refuse-existing"); } @@ -251,7 +248,7 @@ async fn w7_out_of_root_refused_before_writes() { assert_eq!(std::fs::read_dir(outside.path()).unwrap().count(), 0); } #[tokio::test] -async fn w7_page_shell_and_named_screen_have_no_inferred_client_boundary() { +async fn w7_route_owns_the_screen_body_without_an_inferred_client_boundary() { let dir = temp_dir().unwrap(); let value = export(dir.path(), args(dir.path())).await; let files = files(&value); @@ -265,17 +262,12 @@ async fn w7_page_shell_and_named_screen_have_no_inferred_client_boundary() { .unwrap() .contains("export default function ProofreadPage()") ); - let screen = files - .iter() - .find(|f| f["path"].as_str().unwrap().ends_with("Proofread.tsx")) - .unwrap(); - assert!(!screen["path"].as_str().unwrap().contains("src/app")); - assert!( - screen["content"] - .as_str() - .unwrap() - .contains("export function Proofread()") - ); + assert_eq!(files.len(), 1); + let source = page["content"].as_str().unwrap(); + assert!(!source.contains("components/pages"), "{source}"); + assert!(!source.contains("return "), "{source}"); + assert!(source.contains("return ("), "{source}"); + devup_mcp_devup_ui::validation::validate_tsx(source).unwrap(); for file in files { assert!(!file["content"].as_str().unwrap().contains("use client")); } @@ -308,7 +300,7 @@ async fn w7_unsupported_output_key_is_reported_without_writing() { let mut input = args(dir.path()); input["outputPaths"]["frame:unknown:pageScaffold"] = json!(dir.path().join("ignored")); let value = export(dir.path(), input).await; - assert_eq!(files(&value).len(), 2); + assert_eq!(files(&value).len(), 1); assert!( value["outputPathResults"]["supportedKeys"] .as_array() @@ -401,7 +393,7 @@ async fn w7_explicit_overwrite_preserves_unrelated_files() { input["pageScaffold"]["write"] = json!(true); input["pageScaffold"]["overwrite"] = json!(true); let value = export(dir.path(), input).await; - assert_eq!(files(&value).len(), 2); + assert_eq!(files(&value).len(), 1); assert!( std::fs::read_to_string(page) .unwrap() diff --git a/fixtures/devup-figma-plugin/README.md b/fixtures/devup-figma-plugin/README.md index 18f32d71..017abd2a 100644 --- a/fixtures/devup-figma-plugin/README.md +++ b/fixtures/devup-figma-plugin/README.md @@ -99,3 +99,15 @@ of one component-usage inspection comment. Source case JSON, upstream identity and coverage inventory are unchanged. Only the affected snapshot checksums are updated; complete outputs still compare byte for byte. These changes do not assert new pixel measurements of every pinned upstream screen. + +### Prime absolute rectangle feedback (2026-11-06) + +Codegen goldens 022?025 now retain measured absolute RECTANGLE dimensions +rather than an omitted width and parent-relative height. These are the four +synthetic small-shape cases previously kept for plugin byte parity. The native +Figma Prime marker crop and compiled-browser regression demonstrate the same +primitive failure (16x16 becoming 0x218). This is not new pixel evidence for +every legacy fixture. Full-parent overlays (021 and 191), fluid constraints and +read-error cases retain their previous policy. Original case JSON and upstream +identity are unchanged; only four reviewed code snapshots and their +LF-normalized checksums change. No expected image is regenerated from code. diff --git a/fixtures/devup-figma-plugin/manifest.json b/fixtures/devup-figma-plugin/manifest.json index fd48fcfc..50b608c4 100644 --- a/fixtures/devup-figma-plugin/manifest.json +++ b/fixtures/devup-figma-plugin/manifest.json @@ -1231,19 +1231,19 @@ }, { "path": "snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap", - "sha256": "1e9cf4f1ed045a2e2411279abb83315028201abcbaaa2eeb2df8a1d35bf4bebd" + "sha256": "1752faf19cbee0254706da9d368461b78ddbb64c9b3ae651b89d97944d5c9c56" }, { "path": "snapshots/codegen/upstream-codegen-023-040bff1c77.snap", - "sha256": "980347130fb753feac9278651bba8bfc002b4b1b588997b9665d0089fba05192" + "sha256": "da4001530e249858050047e6e9a3abbd2437304a063d7168e211e671ef9f447d" }, { "path": "snapshots/codegen/upstream-codegen-024-eb534f354c.snap", - "sha256": "738294a3c5fd591d2dc56a88d3204b04e37dbf2600ebfe223f06a17608b086f0" + "sha256": "fbe25cc08357f50a3d7fac3cb02661b86f385324ddb8ff2b5e172e83f860efdc" }, { "path": "snapshots/codegen/upstream-codegen-025-74cf3c3463.snap", - "sha256": "23250ae95d18e95ca234104942591abe21aacd42810a2052fb2829439b37ebd7" + "sha256": "852c759c651cfa39f31794bf8140dea5fa22573a0ce3706029d985a7bcb386ab" }, { "path": "snapshots/codegen/upstream-codegen-026-e6df42a1c0.snap", diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap index 64d578a4..5cc7a5b9 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-022-8dce1fe15f.snap @@ -4,5 +4,11 @@ expression: actual --- " - + " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap index 253540df..930313b5 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-023-040bff1c77.snap @@ -5,7 +5,7 @@ expression: actual " - + " diff --git a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap index 5bc85737..932f8b1c 100644 --- a/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap +++ b/fixtures/devup-figma-plugin/snapshots/codegen/upstream-codegen-025-74cf3c3463.snap @@ -5,7 +5,7 @@ expression: actual " 12||checks.some(check=>{ - if(!['build','lint','typecheck','test'].includes(check.name)||names.has(check.name)) return true; + if(!['build','lint','typecheck','test','e2e'].includes(check.name)||names.has(check.name)) return true; names.add(check.name); return !Array.isArray(check.command)||!check.command.length||check.command.length>64||check.command.some(arg=>typeof arg!=='string'||arg.includes('\0')); - })) throw new Error('Checks require unique build/lint/typecheck/test names and explicit command argv arrays'); + })) throw new Error('Checks require unique build/lint/typecheck/test/e2e names and explicit command argv arrays'); const before=await sourceDigest(root),results=[]; for(const check of checks){ const startedAt=new Date().toISOString();let exitCode=0,stderr=''; diff --git a/harness/runtime-audit/test/codegen-layout.test.mjs b/harness/runtime-audit/test/codegen-layout.test.mjs index 000f6d29..bae01b46 100644 --- a/harness/runtime-audit/test/codegen-layout.test.mjs +++ b/harness/runtime-audit/test/codegen-layout.test.mjs @@ -1,7 +1,9 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import {preparedBinaries,renderFixture} from '../prepared-binaries.mjs'; -await preparedBinaries(); +const binaries=await preparedBinaries(); +import {spawnSync} from 'node:child_process'; +import {createHash} from 'node:crypto'; import {mkdir, mkdtemp, writeFile, readFile, rm} from 'node:fs/promises'; import {dirname, join, resolve} from 'node:path'; import {fileURLToPath} from 'node:url'; @@ -25,14 +27,55 @@ async function rendered(snapshot, verify, { responsive = false, assets = [], res await writeFile(join(dir,'main.tsx'),`${reset?"import {resetCss} from '@devup-ui/reset-css';resetCss();":''}import {createRoot} from 'react-dom/client';import {RenderFixture} from './Generated';createRoot(document.getElementById('root')).render();`); const config={root:dir,configFile:false,logLevel:'error',plugins:[DevupUI()],build:{outDir:'dist'},preview:{host:'127.0.0.1',port:0}}; await build(config); server=await preview(config); browser=await chromium.launch(); - await verify(browser,`http://127.0.0.1:${server.httpServer.address().port}`); + await verify(browser,`http://127.0.0.1:${server.httpServer.address().port}`,dir); } finally { if(browser) await browser.close(); if(server) await new Promise((done)=>server.httpServer.close(done)); - await rm(dir,{recursive:true,force:true}); + if(!process.env.DEVUP_KEEP_RENDER_EVIDENCE) await rm(dir,{recursive:true,force:true}); } } +test('Prime caller allocation and absolute marker survive compilation and match the native Figma marker', {timeout:360000}, async()=> { + const reference=join(harness,'fixtures/prime-timeline-marker-reference.png'); + const provenance=JSON.parse(await readFile(join(harness,'fixtures/prime-timeline-marker-reference.json'),'utf8')); + assert.equal(createHash('sha256').update(await readFile(reference)).digest('hex'),provenance.cropSha256); + await rendered(join(repo,'crates/devup-mcp-devup-ui/tests/fixtures/prime-placement.json'),async(browser,url,dir)=> { + const page=await browser.newPage({viewport:{width:1440,height:900},deviceScaleFactor:1}); + await page.goto(url);await page.evaluate(()=>document.fonts.ready); + const title=page.locator('[data-figma-node-id="title"]'); + const card=page.locator('[data-figma-node-id="card"]'); + const marker=page.locator('[data-figma-node-id="marker"]'); + for(const width of [1440,980,1440]) { + await page.setViewportSize({width,height:900}); + const boxes=await Promise.all([title,card,marker].map(element=>element.boundingBox())); + assert.deepEqual(boxes.map(({width,height})=>({width,height})),[{width:420,height:94},{width:520,height:218},{width:16,height:16}]); + assert.equal(boxes[1].x-boxes[0].x-boxes[0].width,40); + assert.equal(boxes[2].x-boxes[1].x,-48); + } + const actual=join(dir,'prime-marker-actual.png'),diff=join(dir,'prime-marker-diff.png'); + const comparisonArgs=['--max-changed-ratio',String(provenance.comparison.maxChangedRatio),'--channel-tolerance',String(provenance.comparison.channelTolerance)]; + await marker.screenshot({path:actual}); + const compared=spawnSync(binaries.visual,['compare','--reference',reference,'--actual',actual,'--diff',diff,...comparisonArgs],{encoding:'utf8',windowsHide:true}); + assert.equal(compared.status,0,compared.stdout+compared.stderr); + await marker.evaluate(element=>element.style.backgroundColor='#00ff00'); + const wrongColor=join(dir,'prime-marker-wrong-color.png'); + await marker.screenshot({path:wrongColor}); + const wrong=spawnSync(binaries.visual,['compare','--reference',reference,'--actual',wrongColor,...comparisonArgs],{encoding:'utf8',windowsHide:true}); + assert.equal(wrong.status,1,'wrong marker paint must fail the same Figma comparison'); + await marker.evaluate(element=>{element.style.backgroundColor='#513cee';element.style.borderRadius='0';}); + const wrongShape=join(dir,'prime-marker-wrong-shape.png'); + await marker.screenshot({path:wrongShape}); + const square=spawnSync(binaries.visual,['compare','--reference',reference,'--actual',wrongShape,...comparisonArgs],{encoding:'utf8',windowsHide:true}); + assert.equal(square.status,1,'wrong shape must fail despite the edge antialiasing tolerance'); + await writeFile(join(dir,'prime-marker-visual-report.json'),JSON.stringify({ + provenance,paths:{reference,actual,diff,wrongColor,wrongShape}, + actualSha256:createHash('sha256').update(await readFile(actual)).digest('hex'), + correct:JSON.parse(compared.stdout),wrongColor:JSON.parse(wrong.stdout),wrongShape:JSON.parse(square.stdout), + },null,2)); + await page.close(); + },{reset:true}); +}); + test('generated staggered grid keeps drawn geometry and reads 01 02 03 04 at 390', {timeout:360000}, async()=> { await rendered(join(repo,'crates/devup-mcp-devup-ui/tests/fixtures/staggered-sequence.json'), async(browser,url)=> { for(const width of [390,1100]) { diff --git a/harness/runtime-audit/test/project-checks.test.mjs b/harness/runtime-audit/test/project-checks.test.mjs index 01016d36..583515b6 100644 --- a/harness/runtime-audit/test/project-checks.test.mjs +++ b/harness/runtime-audit/test/project-checks.test.mjs @@ -17,3 +17,16 @@ test('project command evidence records real failure and rejects stale source',as await assert.rejects(recordProjectChecks({projectRoot:dir,checks:[{name:'build',command:[]}]})); }finally{await rm(dir,{recursive:true,force:true});} }); + +test('functional e2e command has separate observed evidence from unit tests',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'devup-e2e-receipt-')); + try{ + await writeFile(join(dir,'package.json'),'{}'); + const receipt=await recordProjectChecks({projectRoot:dir,checks:[ + {name:'test',command:[process.execPath,'-e','process.exit(0)']}, + {name:'e2e',command:[process.execPath,'-e','process.exit(9)']}, + ]}); + assert.deepEqual(receipt.checks.map(({name,exitCode})=>({name,exitCode})),[{name:'test',exitCode:0},{name:'e2e',exitCode:9}]); + assert.equal(receipt.sourceChanged,false); + }finally{await rm(dir,{recursive:true,force:true});} +}); diff --git a/reports/prime-feedback-2026-11.md b/reports/prime-feedback-2026-11.md new file mode 100644 index 00000000..c7bd9e7a --- /dev/null +++ b/reports/prime-feedback-2026-11.md @@ -0,0 +1,77 @@ +# Prime feedback audit + +Reviewed the application's `.feedback/001` through `031` against main +`0c6073e` (MCP 0.20.0). The records mix generator defects, integration mistakes +and resolved upstream API/release problems. This change does not claim to fix +all three categories by changing the generator. + +| Feedback | Disposition in this change | +| --- | --- | +| 001 | Completion requires separately observed functional `e2e` command evidence and reruns Figma artifact visual comparisons. Missing/inconclusive comparisons cannot complete the application. | +| 002 | Delivery rules require checking/resetting native heading margins after semantic conversion. No claim of an upstream compiler fix. | +| 003 | Selected FILL roots retain width even with a fixed opposite axis and absent maximum. Missing parent allocation remains explicit. | +| 004 | Delivery rules preserve caller width/alignment through shared control wrappers. | +| 005 | Backdrop/blend/isolation review is required; the existing image policy is retained. This PR does not add a general blend compositor. | +| 006 | Keep source node/paint identity and collision refusal; do not choose the first ambiguous filename. Existing registry behavior is retained. | +| 007 | Validate theme tokens against the final assembly, including independently exported instances. Existing token evidence is retained. | +| 008 | Preserve the distinction between hidden masters and visible instances. Existing visibility/boolean override handling is retained. | +| 009 | The browser regression runs the installed Devup UI compiler and a production build. The reported polymorphic Link transform is an external compiler compatibility issue, not repaired here. | +| 010 | Retain existing bounded resource delivery/chunk recovery; the assembly checklist calls for hashes and bounded reads. | +| 011 | Keep acquisition, public projection and actual file writes distinct; preserve blocked-write evidence and collision guards. | +| 012 | Completion no longer promotes static runtime checks without native Figma visual comparisons. Geometry/source checks remain separate from pixels. | +| 013 | Retain generated-CSS reload diagnosis and production verification guidance. This PR does not change a Vite watcher. | +| 014 | Require bounds, focus and ARIA checks before/after validation state changes; a present input alone is insufficient. | +| 015 | List intentional backend-dependent design exceptions separately; prohibit pretend working controls or success state. | +| 016 | Remove the blanket Korean keep-all instruction. Source line breaks and measured wrapping take precedence. | +| 017 | Diagnosis preflight supplies debug, TSX, sourceMap and devupJson in one retry while explicitly preserving the original target/options. | +| 018 | `pageScaffold` emits the actual generated body directly into `src/app/**/page.tsx`, with no whole-screen component/proxy. Correct all three workflow entry points and contradictory examples. | +| 019 | Every application request, including server reads/actions/uploads, must use devup-api. Remove the direct-fetch example; record concrete gaps and stop affected work while continuing independent work. | +| 020 | Resolved upstream devup-api generator issue; no duplicate MCP transport workaround. | +| 021 | Require DCU update, reinstall, resolved-version verification and a minimal reproduction before declaring an API capability missing. | +| 022 | Require exact CI runtime, mock restoration, exit/coverage checks and remote CI verification. | +| 023 | Upstream release configuration issue; require actual package-local archive/path verification. | +| 024 | Upstream release retry issue; distinguish current source from an old pinned release source. | +| 025 | Require clean release fixtures and checking actual archive identity/dependencies; stale tarballs or dry runs are insufficient. | +| 026 | Resolution record for published devup-api and the application's migration; no new MCP defect. | +| 027 | Retain detached FILL width; expose unapplied reference root sizing as an approximated placement contract. Preserve instance HUG cross-axis alignment. Consumers still must apply the typed caller contract or inline the instance. | +| 028 | Measured native absolute rectangles retain both axes, including shapes with omitted layoutSizing fields. Read errors and fluid/stretch/scale constraints do not receive this repair; matching full-parent overlay axes retain their previous percentage relationship. | +| 029 | Explicitly prohibit hiding painted empty absolute nodes by a broad cleanup heuristic. The reported destructive postprocessor belongs to the application, not this MCP source. | +| 030 | Every workflow requires completing an authorized fix and rerunning functional/Figma visual checks; recording a diagnosis is not completion. | +| 031 | Another active project is not a desktop-wide lock. Use OS split-screen and separate windows/ports/browser contexts; coordinate only contended input, respect actual tool locks and preserve capture viewport/DPR/zoom. Ship this in bootstrap instructions, the guide and Bridge/frontend skills. | + +## Regression evidence and limits + +- `prime_placement_feedback.rs` covers detached and mixed-axis FILL roots, + missing parent contracts, unapplied instance allocation, fixed rectangle + dimensions, failed reads and fluid constraints. The initial three regressions + failed on unmodified main before the fixes. +- The browser regression compiles production code and measures the assembled + 420px title / 40px gap / 520px card, 94px title height and 16px marker through + resizing. It compares marker paint to a cropped native Figma reference and + deliberately changes its colour to prove that the same comparison fails. + The reference manifest records the original Figma section, original PNG hash, + exact crop and crop hash. It is not a whole-page Prime visual baseline. +- Completion tests exercise missing visual evidence, missing renderer fields, + changed PNG bytes, substituted local baselines, passing native comparisons, + current source binding and revocation after source/evidence changes. Synthetic + unit PNGs test the gate; they are not presented as real Figma design evidence. +- Page scaffold integration tests cover preview/write behavior and asset paths + with the route-owned body. Skill digests and all frontend entry points are + checked so the delivered binary installs the corrected instructions. + +Full-page blend fidelity, framework watcher behavior, final application token +assembly and downstream API/release publication still require their own exact +application/runtime evidence. Passing this PR does not assert that every +screen or external package has been fixed. + +The earlier ChondroGen generator default that infers keep-all from Korean text +is still present. Item 016 changes the workflow instruction here; replacing +that generator policy needs a separate source-line/native-reference regression +and must preserve explicit user/theme policies. It is not claimed resolved by +the marker crop or the page ownership changes in this PR. + +The user confirmed that split-screen desktop work is possible. Item 031 is a +shipped instruction change; this update does not claim a new live GUI +concurrency experiment. Existing skill-install/digest and bootstrap/guide +delivery checks verify that the rule reaches clients. The 1,500-byte bootstrap +instruction limit is retained.