mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 23:56:29 +03:00
* Fix truncated slug collisions Prepared with AI assistance under maintainer-authorized automation. * Preserve legacy long-slug reads Prepared with AI assistance under maintainer-authorized automation. * Harden legacy slug compatibility Require target metadata before reading collision-prone legacy brief and critique paths. Add regressions for two long targets with the same pre-hash suffix.\n\nPrepared with AI assistance. * Keep explicit access to legacy critiques Allow identity-less pre-hash snapshots to be read by their exact legacy slug while keeping path and URL fallback identity-gated. Document the compatibility boundary and extend collision coverage.\n\nPrepared with AI assistance.
402 lines
15 KiB
Rust
402 lines
15 KiB
Rust
//! JS: lib/surface-briefs.mjs
|
|
|
|
use crate::jsp;
|
|
use crate::target_slug::{legacy_slug_from_target, slug_from_target};
|
|
use crate::url;
|
|
use crate::util::{exists, js_trim, read_dir_names, safe_read};
|
|
use serde_json::{Map, Value};
|
|
|
|
pub const SURFACE_BRIEF_VERSION: u32 = 1;
|
|
|
|
pub fn get_surface_brief_dir(project_root: &str) -> String {
|
|
jsp::join(&[project_root, ".impeccable", "surfaces"])
|
|
}
|
|
|
|
fn normalize_route_target(route: &str) -> Option<String> {
|
|
if !route.starts_with('/') || route.contains("..") {
|
|
return None;
|
|
}
|
|
let cut = route.split(|c| c == '?' || c == '#').next().unwrap_or("");
|
|
// collapse //+ -> /
|
|
let mut collapsed = String::with_capacity(cut.len());
|
|
let mut prev_slash = false;
|
|
for c in cut.chars() {
|
|
if c == '/' {
|
|
if !prev_slash {
|
|
collapsed.push('/');
|
|
}
|
|
prev_slash = true;
|
|
} else {
|
|
prev_slash = false;
|
|
collapsed.push(c);
|
|
}
|
|
}
|
|
// strip one trailing '/'
|
|
let stripped = collapsed.strip_suffix('/').unwrap_or(&collapsed);
|
|
let normalized = if stripped.is_empty() { "/" } else { stripped };
|
|
Some(format!("route:{}", normalized))
|
|
}
|
|
|
|
/// JS: normalizeSurfaceTarget(target, { projectRoot })
|
|
pub fn normalize_surface_target(target: Option<&str>, project_root: &str) -> Option<String> {
|
|
let target = target?;
|
|
let trimmed = js_trim(target);
|
|
if trimmed.is_empty() {
|
|
return None;
|
|
}
|
|
let lower = trimmed.to_ascii_lowercase();
|
|
if lower.starts_with("http://") || lower.starts_with("https://") {
|
|
let mut u = url::parse(trimmed)?;
|
|
u.hash.clear();
|
|
u.search.clear();
|
|
let s = u.to_string();
|
|
let s = s.strip_suffix('/').unwrap_or(&s).to_string();
|
|
return Some(if s.is_empty() { u.origin() } else { s });
|
|
}
|
|
if lower.starts_with("route:") {
|
|
let idx = trimmed.find(':').unwrap();
|
|
return normalize_route_target(js_trim(&trimmed[idx + 1..]));
|
|
}
|
|
if trimmed == "/" {
|
|
return normalize_route_target(trimmed);
|
|
}
|
|
if trimmed.starts_with('/') {
|
|
let absolute = jsp::resolve(project_root, &[trimmed]);
|
|
let rel = jsp::relative(project_root, project_root, &absolute);
|
|
let is_project_file = !rel.is_empty() && !rel.starts_with("..") && !jsp::is_absolute(&rel);
|
|
if !is_project_file && !exists(&absolute) {
|
|
return normalize_route_target(trimmed);
|
|
}
|
|
}
|
|
let abs = if jsp::is_absolute(trimmed) { trimmed.to_string() } else { jsp::resolve(project_root, &[trimmed]) };
|
|
let rel = jsp::relative(project_root, project_root, &abs);
|
|
if rel.is_empty() || rel == "." || rel.starts_with("..") || jsp::is_absolute(&rel) {
|
|
return None;
|
|
}
|
|
Some(jsp::to_posix(&rel))
|
|
}
|
|
|
|
/// JS: surfaceBriefPathForTarget
|
|
pub fn surface_brief_path_for_target(target: Option<&str>, project_root: &str) -> Option<String> {
|
|
let normalized = normalize_surface_target(target, project_root)?;
|
|
let slug_input = match normalized.strip_prefix("route:") {
|
|
Some(rest) => format!("route{}", rest),
|
|
None => normalized.clone(),
|
|
};
|
|
let slug = slug_from_target(Some(&slug_input), project_root)?;
|
|
Some(jsp::join(&[&get_surface_brief_dir(project_root), &format!("{}.md", slug)]))
|
|
}
|
|
|
|
fn legacy_surface_brief_path_for_target(target: Option<&str>, project_root: &str) -> Option<String> {
|
|
let normalized = normalize_surface_target(target, project_root)?;
|
|
let slug_input = match normalized.strip_prefix("route:") {
|
|
Some(rest) => format!("route{}", rest),
|
|
None => normalized.clone(),
|
|
};
|
|
let slug = legacy_slug_from_target(Some(&slug_input), project_root)?;
|
|
Some(jsp::join(&[&get_surface_brief_dir(project_root), &format!("{}.md", slug)]))
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SurfaceBrief {
|
|
pub path: Option<String>,
|
|
pub text: String,
|
|
pub body: String,
|
|
pub meta: Map<String, Value>,
|
|
pub slug: Option<String>,
|
|
pub primary_target: Option<String>,
|
|
pub related_targets: Vec<String>,
|
|
pub targets: Vec<String>,
|
|
}
|
|
|
|
fn is_json_start(raw: &str) -> bool {
|
|
raw.starts_with('[') || raw.starts_with('{') || raw.starts_with('"')
|
|
}
|
|
|
|
fn is_json_scalar(raw: &str) -> bool {
|
|
if raw == "true" || raw == "false" || raw == "null" {
|
|
return true;
|
|
}
|
|
// -?\d+(\.\d+)?
|
|
let s = raw.strip_prefix('-').unwrap_or(raw);
|
|
let mut parts = s.splitn(2, '.');
|
|
let int = parts.next().unwrap_or("");
|
|
if int.is_empty() || !int.chars().all(|c| c.is_ascii_digit()) {
|
|
return false;
|
|
}
|
|
match parts.next() {
|
|
None => true,
|
|
Some(f) => !f.is_empty() && f.chars().all(|c| c.is_ascii_digit()),
|
|
}
|
|
}
|
|
|
|
/// Split frontmatter per /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/ .
|
|
/// Returns (inner, match_len).
|
|
pub fn split_frontmatter(text: &str) -> Option<(String, usize)> {
|
|
let after_open = if let Some(r) = text.strip_prefix("---\r\n") {
|
|
(r, 5)
|
|
} else if let Some(r) = text.strip_prefix("---\n") {
|
|
(r, 4)
|
|
} else {
|
|
return None;
|
|
};
|
|
let (rest, open_len) = after_open;
|
|
// find the earliest "\r?\n---" followed by "\r?\n" or end
|
|
let bytes = rest.as_bytes();
|
|
let mut i = 0;
|
|
while i < bytes.len() {
|
|
if bytes[i] == b'\n' && rest[i + 1..].starts_with("---") {
|
|
let inner_end = if i > 0 && bytes[i - 1] == b'\r' { i - 1 } else { i };
|
|
let after = &rest[i + 4..];
|
|
let tail_len = if after.starts_with("\r\n") {
|
|
Some(2)
|
|
} else if after.starts_with('\n') {
|
|
Some(1)
|
|
} else if after.is_empty() {
|
|
Some(0)
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(t) = tail_len {
|
|
let inner = rest[..inner_end].to_string();
|
|
return Some((inner, open_len + i + 4 + t));
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// JS: parseSurfaceBrief(text, filePath)
|
|
pub fn parse_surface_brief(text: &str, file_path: Option<&str>) -> SurfaceBrief {
|
|
let mut meta = Map::new();
|
|
let fm = split_frontmatter(text);
|
|
if let Some((inner, _)) = &fm {
|
|
for line in inner.split('\n') {
|
|
let line = line.strip_suffix('\r').unwrap_or(line);
|
|
let Some(colon) = line.find(':') else { continue };
|
|
let key = js_trim(&line[..colon]);
|
|
let raw = js_trim(&line[colon + 1..]);
|
|
if key.is_empty() {
|
|
continue;
|
|
}
|
|
if is_json_start(raw) || is_json_scalar(raw) {
|
|
if let Ok(v) = serde_json::from_str::<Value>(raw) {
|
|
meta.insert(key.to_string(), v);
|
|
continue;
|
|
}
|
|
}
|
|
let mut v = raw;
|
|
// /^['"]|['"]$/g : strip one leading and one trailing quote
|
|
if v.starts_with('\'') || v.starts_with('"') {
|
|
v = &v[1..];
|
|
}
|
|
if v.ends_with('\'') || v.ends_with('"') {
|
|
v = &v[..v.len() - 1];
|
|
}
|
|
meta.insert(key.to_string(), Value::String(v.to_string()));
|
|
}
|
|
}
|
|
let primary_target = meta.get("primary_target").and_then(|v| v.as_str()).map(|s| s.to_string());
|
|
let related_targets: Vec<String> = meta
|
|
.get("related_targets")
|
|
.and_then(|v| v.as_array())
|
|
.map(|a| a.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
|
|
.unwrap_or_default();
|
|
let body = match &fm {
|
|
Some((_, len)) => js_trim(&text[*len..]).to_string(),
|
|
None => js_trim(text).to_string(),
|
|
};
|
|
let slug = match meta.get("slug").and_then(|v| v.as_str()) {
|
|
Some(s) => Some(s.to_string()),
|
|
None => file_path.map(|p| jsp::basename_ext(p, ".md")),
|
|
};
|
|
let mut targets: Vec<String> = Vec::new();
|
|
if let Some(p) = &primary_target {
|
|
if !p.is_empty() {
|
|
targets.push(p.clone());
|
|
}
|
|
}
|
|
for r in &related_targets {
|
|
if !r.is_empty() {
|
|
targets.push(r.clone());
|
|
}
|
|
}
|
|
SurfaceBrief {
|
|
path: file_path.map(|s| s.to_string()),
|
|
text: text.to_string(),
|
|
body,
|
|
meta,
|
|
slug,
|
|
primary_target,
|
|
related_targets,
|
|
targets,
|
|
}
|
|
}
|
|
|
|
/// JS: listSurfaceBriefs
|
|
pub fn list_surface_briefs(project_root: &str) -> Vec<SurfaceBrief> {
|
|
let dir = get_surface_brief_dir(project_root);
|
|
let Some(names) = read_dir_names(&dir) else { return vec![] };
|
|
let mut names: Vec<String> = names.into_iter().filter(|n| n.ends_with(".md")).collect();
|
|
names.sort();
|
|
names
|
|
.into_iter()
|
|
.filter_map(|name| {
|
|
let fp = jsp::join(&[&dir, &name]);
|
|
safe_read(&fp).map(|t| parse_surface_brief(&t, Some(&fp)))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub struct SurfaceResolution {
|
|
pub brief: Option<SurfaceBrief>,
|
|
pub candidates: Vec<SurfaceBrief>,
|
|
pub reason: &'static str,
|
|
}
|
|
|
|
/// JS: resolveSurfaceBrief(projectRoot, target)
|
|
pub fn resolve_surface_brief(project_root: &str, target: Option<&str>) -> SurfaceResolution {
|
|
let briefs = list_surface_briefs(project_root);
|
|
let Some(target) = target.filter(|t| !t.is_empty()) else {
|
|
let n = briefs.len();
|
|
return SurfaceResolution {
|
|
brief: if n == 1 { Some(briefs[0].clone()) } else { None },
|
|
reason: if n == 1 { "only-brief" } else if n > 1 { "ambiguous" } else { "none" },
|
|
candidates: briefs,
|
|
};
|
|
};
|
|
let Some(normalized) = normalize_surface_target(Some(target), project_root) else {
|
|
return SurfaceResolution { brief: None, candidates: briefs, reason: "invalid-target" };
|
|
};
|
|
let exact_path = surface_brief_path_for_target(Some(&normalized), project_root);
|
|
let legacy_path = legacy_surface_brief_path_for_target(Some(&normalized), project_root);
|
|
let exact = briefs
|
|
.iter()
|
|
.find(|b| b.path == exact_path && (b.targets.is_empty() || b.targets.contains(&normalized)))
|
|
.or_else(|| {
|
|
// Pre-hash long slugs can collide because they contain only the
|
|
// target suffix. Require the legacy brief's metadata to prove it
|
|
// belongs to this target before accepting that compatibility path.
|
|
briefs.iter().find(|b| b.path == legacy_path && b.targets.contains(&normalized))
|
|
});
|
|
if let Some(exact) = exact {
|
|
return SurfaceResolution { brief: Some(exact.clone()), candidates: briefs, reason: "slug" };
|
|
}
|
|
let mapped: Vec<SurfaceBrief> = briefs.iter().filter(|b| b.targets.contains(&normalized)).cloned().collect();
|
|
let n = mapped.len();
|
|
SurfaceResolution {
|
|
brief: if n == 1 { Some(mapped[0].clone()) } else { None },
|
|
candidates: if n > 1 { mapped } else { briefs },
|
|
reason: if n == 1 { "mapping" } else if n > 1 { "ambiguous-target" } else { "not-found" },
|
|
}
|
|
}
|
|
|
|
/// JS: writeSurfaceBrief
|
|
pub fn write_surface_brief(
|
|
project_root: &str,
|
|
primary_target: &str,
|
|
related_targets: &[String],
|
|
body: &str,
|
|
) -> Result<String, String> {
|
|
let normalized_primary = normalize_surface_target(Some(primary_target), project_root)
|
|
.ok_or_else(|| "surface brief requires a concrete project-relative primary target or URL".to_string())?;
|
|
let mut related: Vec<String> = Vec::new();
|
|
for t in related_targets {
|
|
if let Some(n) = normalize_surface_target(Some(t), project_root) {
|
|
if n != normalized_primary && !related.contains(&n) {
|
|
related.push(n);
|
|
}
|
|
}
|
|
}
|
|
let slug = slug_from_target(Some(&normalized_primary), project_root);
|
|
let file_path = surface_brief_path_for_target(Some(&normalized_primary), project_root)
|
|
.ok_or_else(|| "surface brief requires a concrete project-relative primary target or URL".to_string())?;
|
|
let _ = std::fs::create_dir_all(jsp::dirname(&file_path));
|
|
let frontmatter = [
|
|
"---".to_string(),
|
|
format!("version: {}", SURFACE_BRIEF_VERSION),
|
|
format!("slug: {}", serde_json::to_string(&slug).unwrap()),
|
|
format!("primary_target: {}", serde_json::to_string(&normalized_primary).unwrap()),
|
|
format!("related_targets: {}", serde_json::to_string(&related).unwrap()),
|
|
"---".to_string(),
|
|
]
|
|
.join("\n");
|
|
let content = format!("{}\n\n{}\n", frontmatter, js_trim(body));
|
|
std::fs::write(&file_path, content).map_err(|e| e.to_string())?;
|
|
Ok(file_path)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
static TMP_SEQ: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
#[test]
|
|
fn resolves_pre_hash_long_slug_briefs() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"impeccable-surface-legacy-{}-{}",
|
|
std::process::id(),
|
|
TMP_SEQ.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
let root = root.to_string_lossy().into_owned();
|
|
let target = "src/a-very-long-directory-structure-with-many-segments/component-name.tsx";
|
|
let legacy = legacy_surface_brief_path_for_target(Some(target), &root).unwrap();
|
|
let current = surface_brief_path_for_target(Some(target), &root).unwrap();
|
|
assert_ne!(legacy, current);
|
|
|
|
std::fs::create_dir_all(get_surface_brief_dir(&root)).unwrap();
|
|
let normalized = normalize_surface_target(Some(target), &root).unwrap();
|
|
let legacy_body = format!(
|
|
"---\nprimary_target: {}\n---\n# Legacy brief\n",
|
|
serde_json::to_string(&normalized).unwrap()
|
|
);
|
|
std::fs::write(&legacy, legacy_body).unwrap();
|
|
|
|
let resolved = resolve_surface_brief(&root, Some(target));
|
|
assert_eq!(resolved.reason, "slug");
|
|
assert_eq!(resolved.brief.unwrap().path.as_deref(), Some(legacy.as_str()));
|
|
|
|
std::fs::write(¤t, "# Current brief\n").unwrap();
|
|
let resolved = resolve_surface_brief(&root, Some(target));
|
|
assert_eq!(resolved.brief.unwrap().path.as_deref(), Some(current.as_str()));
|
|
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unmapped_pre_hash_slug_collisions() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"impeccable-surface-legacy-collision-{}-{}",
|
|
std::process::id(),
|
|
TMP_SEQ.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
let root = root.to_string_lossy().into_owned();
|
|
let target = "src/first-prefix-that-is-long-enough/a-very-long-shared-tail/component-name.tsx";
|
|
let collision = "src/second-prefix-that-is-long-enough/a-very-long-shared-tail/component-name.tsx";
|
|
let legacy = legacy_surface_brief_path_for_target(Some(target), &root).unwrap();
|
|
assert_eq!(legacy, legacy_surface_brief_path_for_target(Some(collision), &root).unwrap());
|
|
assert_ne!(
|
|
surface_brief_path_for_target(Some(target), &root),
|
|
surface_brief_path_for_target(Some(collision), &root)
|
|
);
|
|
|
|
std::fs::create_dir_all(get_surface_brief_dir(&root)).unwrap();
|
|
std::fs::write(&legacy, "# Unmapped legacy brief\n").unwrap();
|
|
assert_eq!(resolve_surface_brief(&root, Some(target)).reason, "not-found");
|
|
|
|
let normalized = normalize_surface_target(Some(target), &root).unwrap();
|
|
let legacy_body = format!(
|
|
"---\nprimary_target: {}\n---\n# Mapped legacy brief\n",
|
|
serde_json::to_string(&normalized).unwrap()
|
|
);
|
|
std::fs::write(&legacy, legacy_body).unwrap();
|
|
assert_eq!(resolve_surface_brief(&root, Some(target)).reason, "slug");
|
|
assert_eq!(resolve_surface_brief(&root, Some(collision)).reason, "not-found");
|
|
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
}
|
|
}
|