Fix truncated surface-brief slug collisions (#774)

* 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.
This commit is contained in:
Paul Bakaus
2026-09-07 11:40:05 -07:00
committed by GitHub
parent 4db7f6ba4b
commit 93bfe70c58
5 changed files with 335 additions and 35 deletions
+165 -19
View File
@@ -3,7 +3,7 @@
use crate::context::resolve_project_root;
use crate::jsp;
use crate::target_args::TargetOptions;
use crate::target_slug::slug_from_target;
use crate::target_slug::{legacy_slug_from_target, slug_from_target};
use crate::util::{exists, iso_now, js_trim, json_pretty, node_read_error, read_dir_names, safe_read, Env};
use impeccable_common::Io;
use serde_json::{Map, Value};
@@ -393,6 +393,51 @@ fn coerce_slug(value: Option<&str>, cwd: &str) -> Option<String> {
slug_from_target(Some(v), cwd)
}
fn slug_candidates(value: Option<&str>, cwd: &str) -> Vec<String> {
let Some(v) = value.filter(|v| !v.is_empty()) else {
return vec![];
};
if is_ready_slug(v) {
return vec![v.to_string()];
}
let mut slugs = Vec::new();
if let Some(slug) = slug_from_target(Some(v), cwd) {
slugs.push(slug);
}
if let Some(legacy) = legacy_slug_from_target(Some(v), cwd) {
if !slugs.contains(&legacy) {
slugs.push(legacy);
}
}
slugs
}
fn read_newest_safe_snapshot_for_slugs(
slugs: &[String],
target_identity: Option<&str>,
cwd: &str,
env: &Env,
) -> Option<Snapshot> {
let current = slugs.first().and_then(|slug| read_newest_snapshot(slug, cwd, env));
let legacy = target_identity.and_then(|identity| {
slugs.get(1).and_then(|slug| read_newest_snapshot_for_identity(slug, Some(identity), cwd, env))
});
current.into_iter().chain(legacy).max_by(|a, b| a.path.cmp(&b.path))
}
fn read_newest_snapshot_for_identity_slugs(
slugs: &[String],
target_identity: Option<&str>,
cwd: &str,
env: &Env,
) -> Option<Snapshot> {
let current = slugs.first().and_then(|slug| read_newest_snapshot_for_identity(slug, target_identity, cwd, env));
let legacy = target_identity.and_then(|identity| {
slugs.get(1).and_then(|slug| read_newest_snapshot_for_identity(slug, Some(identity), cwd, env))
});
current.into_iter().chain(legacy).max_by(|a, b| a.path.cmp(&b.path))
}
pub fn run(args: &[String], io: &mut Io) -> i32 {
let cwd = io.cwd.to_string_lossy().into_owned();
let env = io.env.clone();
@@ -493,26 +538,30 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
"latest" => {
let target = rest.first().map(String::as_str).unwrap_or("");
let format = rest.get(1).map(String::as_str);
let slug_opt = coerce_slug(rest.first().map(String::as_str), &cwd);
let slugs = slug_candidates(rest.first().map(String::as_str), &cwd);
// JS: format && format !== '--json' (format truthy = non-empty)
let bad_format = format.map(|f| !f.is_empty() && f != "--json").unwrap_or(false);
let Some(slug) = slug_opt.filter(|_| !bad_format) else {
if slugs.is_empty() || bad_format {
io.err("usage: latest <slug-or-target> [--json]\n");
return 1;
};
}
let format_is_json = format == Some("--json");
let target_fingerprint = fingerprint_target(target, &cwd);
let target_path = resolve_local_target_path(target, &cwd);
let target_identity = resolve_target_identity(target, &cwd);
let ready_slug = is_ready_slug(target);
let Some(newest_for_slug) = read_newest_snapshot(&slug, &cwd, &env) else {
let Some(newest_for_slug) =
read_newest_safe_snapshot_for_slugs(&slugs, target_identity.as_deref(), &cwd, &env)
else {
return 2;
};
let mut latest = read_newest_snapshot_for_identity(&slug, target_identity.as_deref(), &cwd, &env);
let mut latest =
read_newest_snapshot_for_identity_slugs(&slugs, target_identity.as_deref(), &cwd, &env);
if latest.is_none() && !ready_slug {
// Legacy snapshots have no identity; preserve their old explicit
// path/URL behavior only when no known identity was selected.
latest = read_newest_snapshot_for_identity(&slug, None, &cwd, &env);
// Identity-less snapshots are safe only under the new/current
// collision-resistant slug. A pre-hash slug suffix alone cannot
// prove which long target owned the historical snapshot.
latest = slugs.first().and_then(|slug| read_newest_snapshot_for_identity(slug, None, &cwd, &env));
}
let latest = latest.unwrap_or(newest_for_slug);
if meta_closed(&latest) {
@@ -520,10 +569,6 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
}
let recorded_target_identity = snapshot_target_identity(&latest);
let matching_identity = recorded_target_identity == target_identity;
if ready_slug && recorded_target_identity.is_none() {
io.err("ambiguous legacy snapshot target; use an explicit ./path or full URL\n");
return 2;
}
if ready_slug && target_path.as_deref().map(exists).unwrap_or(false) && !matching_identity {
io.err("ambiguous snapshot slug; use an explicit ./path or remove the local name collision\n");
return 2;
@@ -564,17 +609,16 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
"close" => {
let slug_arg = rest.first().map(String::as_str).unwrap_or("");
let snapshot_file = rest.get(1).map(String::as_str);
let slug = coerce_slug(rest.first().map(String::as_str), &cwd);
let slugs = slug_candidates(rest.first().map(String::as_str), &cwd);
let snapshot_file_ok = snapshot_file.map(|s| !s.is_empty()).unwrap_or(false);
if slug.is_none() || !snapshot_file_ok || rest.len() > 2 {
if slugs.is_empty() || !snapshot_file_ok || rest.len() > 2 {
io.err("usage: close <resolved-target> <snapshot-file>\n");
return 1;
}
let slug = slug.unwrap();
let snapshot_file = snapshot_file.unwrap();
if jsp::basename(snapshot_file) != snapshot_file
|| !is_snapshot_name(snapshot_file)
|| !snapshot_file.ends_with(&format!("__{}.md", slug))
|| !slugs.iter().any(|slug| snapshot_file.ends_with(&format!("__{}.md", slug)))
{
return 2;
}
@@ -592,6 +636,10 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
// JS #660: a slug + filename does not prove ownership; require the
// resolved target to match a modern snapshot's recorded identity.
let recorded_target_identity = snapshot_target_identity(&snapshot);
let legacy_match = slugs.get(1).is_some_and(|slug| snapshot_file.ends_with(&format!("__{}.md", slug)));
if legacy_match && recorded_target_identity.is_none() {
return 2;
}
if let Some(rid) = &recorded_target_identity {
if Some(rid.clone()) != resolve_target_identity(slug_arg, &cwd) {
return 2;
@@ -610,12 +658,34 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
}
}
"trend" => {
let slug = coerce_slug(rest.first().map(String::as_str), &cwd).unwrap_or_else(|| "null".to_string());
let mut slugs = slug_candidates(rest.first().map(String::as_str), &cwd);
if slugs.is_empty() {
slugs.push("null".to_string());
}
let limit: f64 = match rest.get(1).filter(|s| !s.is_empty()) {
Some(l) => js_number(l),
None => 5.0,
};
let all = list_snapshots(&format!("__{}.md", slug), &cwd, &env);
let target = rest.first().map(String::as_str).unwrap_or("");
let target_identity = resolve_target_identity(target, &cwd);
let mut all: Vec<String> = slugs
.first()
.into_iter()
.flat_map(|slug| list_snapshots(&format!("__{}.md", slug), &cwd, &env))
.collect();
if let Some(legacy) = slugs.get(1) {
all.extend(
list_snapshots(&format!("__{}.md", legacy), &cwd, &env)
.into_iter()
.filter(|path| {
read_snapshot_at(path)
.and_then(|snapshot| snapshot_target_identity(&snapshot))
.as_deref()
== target_identity.as_deref()
}),
);
}
all.sort();
let slice = js_slice_last(&all, limit);
let rows: Vec<Value> = slice
.iter()
@@ -807,4 +877,80 @@ mod tests_660 {
assert!(out.contains("\"body\":"));
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn explicit_long_targets_find_and_close_pre_hash_snapshots() {
let cwd = tmp();
let dir = jsp::join(&[&cwd, ".impeccable", "critique"]);
std::fs::create_dir_all(&dir).unwrap();
let target = "https://example.com/a-very-long-directory-structure-with-many-segments/component-name";
let legacy_slug = legacy_slug_from_target(Some(target), &cwd).unwrap();
assert_ne!(legacy_slug, slug_from_target(Some(target), &cwd).unwrap());
let name = format!("2026-05-12T18-30-00Z__{legacy_slug}.md");
let identity = resolve_target_identity(target, &cwd).unwrap();
let body = format!(
"---\ntarget_identity: {}\nslug: {}\n---\n# Legacy critique\n",
serde_json::to_string(&identity).unwrap(),
legacy_slug
);
std::fs::write(jsp::join(&[&dir, &name]), body).unwrap();
let (code, out, _) = run_capture(&cwd, &["latest", target, "--json"]);
assert_eq!(code, 0);
assert!(out.contains(&name));
let (code, out, _) = run_capture(&cwd, &["trend", target, "5"]);
assert_eq!(code, 0);
assert!(out.contains(&legacy_slug));
let (code, _, _) = run_capture(&cwd, &["close", target, &name]);
assert_eq!(code, 0);
assert!(std::fs::read_to_string(jsp::join(&[&dir, &name])).unwrap().contains("closed: true"));
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn pre_hash_slug_collisions_require_matching_identity() {
let cwd = tmp();
let dir = jsp::join(&[&cwd, ".impeccable", "critique"]);
std::fs::create_dir_all(&dir).unwrap();
let target = "https://example.com/first-prefix-that-is-long-enough/a-very-long-shared-tail/component-name";
let collision = "https://example.com/second-prefix-that-is-long-enough/a-very-long-shared-tail/component-name";
let legacy_slug = legacy_slug_from_target(Some(target), &cwd).unwrap();
assert_eq!(legacy_slug, legacy_slug_from_target(Some(collision), &cwd).unwrap());
assert_ne!(slug_from_target(Some(target), &cwd), slug_from_target(Some(collision), &cwd));
let unidentified_name = format!("2026-05-12T18-30-00Z__{legacy_slug}.md");
let unidentified_body = format!("---\nslug: {legacy_slug}\n---\n# Ambiguous legacy critique\n");
std::fs::write(jsp::join(&[&dir, &unidentified_name]), unidentified_body).unwrap();
assert_eq!(run_capture(&cwd, &["latest", target]).0, 2);
assert_eq!(run_capture(&cwd, &["latest", collision]).0, 2);
assert_eq!(run_capture(&cwd, &["trend", target, "5"]).1.trim(), "[]");
assert_eq!(run_capture(&cwd, &["close", target, &unidentified_name]).0, 2);
let (code, out, _) = run_capture(&cwd, &["latest", &legacy_slug]);
assert_eq!(code, 0);
assert!(out.contains("# Ambiguous legacy critique"));
assert!(run_capture(&cwd, &["trend", &legacy_slug, "5"]).1.contains(&legacy_slug));
assert_eq!(run_capture(&cwd, &["close", &legacy_slug, &unidentified_name]).0, 0);
let identified_name = format!("2026-05-12T18-31-00Z__{legacy_slug}.md");
let identity = resolve_target_identity(target, &cwd).unwrap();
let identified_body = format!(
"---\ntarget_identity: {}\nslug: {}\n---\n# Identified legacy critique\n",
serde_json::to_string(&identity).unwrap(),
legacy_slug
);
std::fs::write(jsp::join(&[&dir, &identified_name]), identified_body).unwrap();
assert_eq!(run_capture(&cwd, &["latest", target]).0, 0);
assert_eq!(run_capture(&cwd, &["latest", collision]).0, 2);
assert!(run_capture(&cwd, &["trend", target, "5"]).1.contains("target_identity"));
assert_eq!(run_capture(&cwd, &["trend", collision, "5"]).1.trim(), "[]");
assert_eq!(run_capture(&cwd, &["close", collision, &identified_name]).0, 2);
assert_eq!(run_capture(&cwd, &["close", target, &identified_name]).0, 0);
let _ = std::fs::remove_dir_all(&cwd);
}
}
+93 -3
View File
@@ -1,7 +1,7 @@
//! JS: lib/surface-briefs.mjs
use crate::jsp;
use crate::target_slug::slug_from_target;
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};
@@ -87,6 +87,16 @@ pub fn surface_brief_path_for_target(target: Option<&str>, project_root: &str) -
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>,
@@ -260,10 +270,17 @@ pub fn resolve_surface_brief(project_root: &str, target: Option<&str>) -> Surfac
return SurfaceResolution { brief: None, candidates: briefs, reason: "invalid-target" };
};
let exact_path = surface_brief_path_for_target(Some(&normalized), project_root);
if let Some(exact) = briefs
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();
@@ -309,3 +326,76 @@ pub fn write_surface_brief(
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(&current, "# 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);
}
}
+73 -9
View File
@@ -2,11 +2,27 @@
use crate::jsp;
use crate::util::js_trim;
use sha2::{Digest, Sha256};
const SLUG_MAX: usize = 50;
const SLUG_HASH_LEN: usize = 8;
/// JS: slugFromTarget(resolved, { cwd })
pub fn slug_from_target(resolved: Option<&str>, cwd: &str) -> Option<String> {
slug_from_target_using(resolved, cwd, kebab)
}
/// Compatibility key used by releases that truncated normalized targets to
/// their last 50 characters without a hash suffix.
pub(crate) fn legacy_slug_from_target(resolved: Option<&str>, cwd: &str) -> Option<String> {
slug_from_target_using(resolved, cwd, legacy_kebab)
}
fn slug_from_target_using(
resolved: Option<&str>,
cwd: &str,
slugger: fn(&str) -> Option<String>,
) -> Option<String> {
let resolved = resolved?;
let trimmed = js_trim(resolved);
if trimmed.is_empty() {
@@ -15,7 +31,7 @@ pub fn slug_from_target(resolved: Option<&str>, cwd: &str) -> Option<String> {
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("http://") || lower.starts_with("https://") {
let (host, pathname) = parse_url_host_path(trimmed)?;
return kebab(&format!("{}{}", host, pathname));
return slugger(&format!("{}{}", host, pathname));
}
let abs = if jsp::is_absolute(trimmed) { trimmed.to_string() } else { jsp::resolve(cwd, &[trimmed]) };
let mut rel = jsp::relative(cwd, cwd, &abs);
@@ -25,7 +41,7 @@ pub fn slug_from_target(resolved: Option<&str>, cwd: &str) -> Option<String> {
if rel.is_empty() || rel == "." {
return None;
}
kebab(&rel)
slugger(&rel)
}
/// Minimal WHATWG URL parse for http(s): returns (hostname lowercased, pathname).
@@ -37,6 +53,30 @@ pub fn parse_url_host_path(s: &str) -> Option<(String, String)> {
/// JS: kebab(value)
pub fn kebab(value: &str) -> Option<String> {
let u = normalized_kebab(value)?;
if u.len() <= SLUG_MAX {
Some(u)
} else {
let digest = Sha256::digest(u.as_bytes());
let hash = format!("{digest:x}");
let tail_len = SLUG_MAX - SLUG_HASH_LEN - 1;
let tail = &u[u.len() - tail_len..];
let tail = tail.strip_prefix('-').unwrap_or(tail);
Some(format!("{tail}-{}", &hash[..SLUG_HASH_LEN]))
}
}
fn legacy_kebab(value: &str) -> Option<String> {
let u = normalized_kebab(value)?;
if u.len() <= SLUG_MAX {
Some(u)
} else {
let tail = &u[u.len() - SLUG_MAX..];
Some(tail.strip_prefix('-').unwrap_or(tail).to_string())
}
}
fn normalized_kebab(value: &str) -> Option<String> {
let lower = value.to_lowercase();
// replace runs of / \ . with '-'
let mut s = String::with_capacity(lower.len());
@@ -81,13 +121,37 @@ pub fn kebab(value: &str) -> Option<String> {
// strip leading/trailing '-' (JS: /^-|-$/g -> one at each end; after collapse there is at most one)
let u = u.strip_prefix('-').unwrap_or(&u).to_string();
let u = u.strip_suffix('-').unwrap_or(&u).to_string();
if u.is_empty() {
return None;
(!u.is_empty()).then_some(u)
}
#[cfg(test)]
mod tests {
use super::{kebab, legacy_kebab, SLUG_MAX};
#[test]
fn truncated_slugs_keep_distinct_full_inputs_distinct() {
let suffix = "a".repeat(SLUG_MAX);
let alpha = kebab(&format!("alpha-prefix-{suffix}")).unwrap();
let beta = kebab(&format!("beta-prefix-{suffix}")).unwrap();
assert_ne!(alpha, beta);
assert!(alpha.len() <= SLUG_MAX);
assert!(beta.len() <= SLUG_MAX);
assert_eq!(alpha, kebab(&format!("alpha-prefix-{suffix}")).unwrap());
}
if u.len() <= SLUG_MAX {
Some(u)
} else {
let tail = &u[u.len() - SLUG_MAX..];
Some(tail.strip_prefix('-').unwrap_or(tail).to_string())
#[test]
fn non_truncated_slugs_are_unchanged() {
assert_eq!(kebab("Button.Primary"), Some("button-primary".to_string()));
assert_eq!(kebab(&"a".repeat(SLUG_MAX)), Some("a".repeat(SLUG_MAX)));
}
#[test]
fn legacy_kebab_preserves_the_previous_truncation_key() {
let value = "a-very-long-directory-structure-with-many-segments-component-name.tsx";
assert_eq!(
legacy_kebab(value),
Some("ry-structure-with-many-segments-component-name-tsx".to_string())
);
}
}
+3 -3
View File
@@ -483,7 +483,7 @@ The build (`scripts/lib/utils.js` `replaceScriptProviderMarker`) rewrites exactl
- non-string / empty after trim -> `null`.
- URL (`/^https?:\/\//i`): `new URL(...)`; invalid -> `null`; slug = `kebab(hostname + pathname)` (port, query, hash dropped; hostname is lowercased by URL).
- Else path: abs = absolute or `path.resolve(cwd, trimmed)`; rel = `path.relative(cwd, abs)`; if rel starts with `..` or is absolute -> rel = basename(abs); if rel is `''` or `'.'` -> `null`; slug = `kebab(rel)`.
`kebab(v)`: lowercase; replace runs of `/`, `\`, `.` (`/[/\\.]+/g`) with `-`; replace `/[^a-z0-9-]+/g` with `-`; collapse `/-+/g` -> `-`; strip leading/trailing `-`; empty -> `null`; if length > 50 keep the LAST 50 chars then strip one leading `-`.
`kebab(v)`: lowercase; replace runs of `/`, `\`, `.` (`/[/\\.]+/g`) with `-`; replace `/[^a-z0-9-]+/g` with `-`; collapse `/-+/g` -> `-`; strip leading/trailing `-`; empty -> `null`. Values up to 50 characters remain unchanged. Longer normalized values keep their last 41 characters (stripping one leading `-`) and append `-` plus the first 8 lowercase hex characters of the SHA-256 digest of the full normalized value.
Examples: `site/pages/index.astro` -> `site-pages-index-astro`; `http://localhost:3000/pricing` -> `localhost-pricing`; `https://Impeccable.Style/docs/audit/` -> `impeccable-style-docs-audit`.
#### `.impeccable/` path resolution (`lib/impeccable-paths.mjs`)
@@ -710,7 +710,7 @@ Tier 2 (`staleness-deep.mjs`, doctor only):
- **Invoked from**: `reference/new-work.md`: `node {{scripts_path}}/surface-brief.mjs read <primary-target>` and `... write <primary-target> <body-file> [related-target ...]`; `context.mjs` SURFACE_CONTEXT_AVAILABLE names `read <path>`. `reference/live.md` says live must not shell out to it.
- **CLI args**: positional `<command> [target] [bodyFile] [related...]`; commands `path`, `list`, `read`, `write`. projectRoot = `resolveProjectRoot(cwd, target ? {targetPath: target} : {})` (so the target itself steers monorepo resolution).
- **Outputs**: `path`: cwd-relative brief path + `\n` (error `surface brief path requires a concrete target` when unslugable). `list`: `JSON.stringify([{slug, path (projectRoot-relative posix), primaryTarget, relatedTargets}], null, 2)` + `\n`. `read`: on resolution prints the brief's full text verbatim (no added newline), exit 0; else if candidates exist prints their summaries JSON to **stderr**, and exits 2 either way. `write`: requires target and bodyFile (else error `usage: surface-brief.mjs write <primary-target> <body-file>`); writes the brief (format above) and prints cwd-relative path + `\n`. Unknown command -> error `usage: surface-brief.mjs <path|list|read|write> [target] [body-file] [related-target ...]`. All thrown errors -> stderr `<message>\n`, exit 1.
- **Outputs**: `path`: cwd-relative brief path + `\n` (error `surface brief path requires a concrete target` when unslugable). `list`: `JSON.stringify([{slug, path (projectRoot-relative posix), primaryTarget, relatedTargets}], null, 2)` + `\n`. `read`: on resolution prints the brief's full text verbatim (no added newline), exit 0; direct target resolution probes both the current hashed long slug and the previous suffix-only long slug, but accepts the collision-prone legacy path only when the brief's target metadata matches the requested target; else if candidates exist prints their summaries JSON to **stderr**, and exits 2 either way. `write`: requires target and bodyFile (else error `usage: surface-brief.mjs write <primary-target> <body-file>`); writes the brief (format above) and prints cwd-relative path + `\n`. Unknown command -> error `usage: surface-brief.mjs <path|list|read|write> [target] [body-file] [related-target ...]`. All thrown errors -> stderr `<message>\n`, exit 1.
- **Tests**: `tests/surface-brief.test.mjs` (library level: slug path `.impeccable/surfaces/src-pages-index-astro.md`, related-target resolution, only-brief/ambiguous, overwrite semantics, route normalization, `route.md` root); `tests/context.test.mjs` brief loading via context.
---
@@ -721,7 +721,7 @@ Tier 2 (`staleness-deep.mjs`, doctor only):
- **CLI args**: `slug <target>`; `write <slug-or-target> <body-file>`; `latest <slug-or-target>`; `trend <slug-or-target> [limit=5]`. `coerceSlug`: value matching `/^[a-z0-9-]+$/` used as-is, else `slugFromTarget(value)` (cwd = process.cwd()).
- **Env vars**: `IMPECCABLE_CRITIQUE_META` (JSON object for frontmatter on `write`; parse failure ignored).
- **Storage**: dir `getCritiqueDir(cwd)` = `<projectRoot>/.impeccable/critique/`. Filename `<stamp>__<slug>.md`, stamp = ISO UTC with `:` and `.` -> `-` and the `-mmmZ` fraction removed: `2026-05-12T18-30-00Z`. File content: `---\n<key>: <value>\n...---\n<body.trim()>\n`, keys = `{...meta, timestamp, slug}` (meta first, then computed override); null/undefined skipped; string values containing `:` or `#` are JSON-quoted. Snapshot filename regex `/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/`; sorted lexicographically (= chronologically). Frontmatter read: `"..."` values JSON-parsed, `/^-?\d+$/` -> Number, else string.
- **Outputs**: `slug`: slug + `\n` exit 0, or stderr `no stable slug for input\n` exit 1. `write`: missing args -> stderr `usage: write <slug-or-target> <body-file>\n` exit 1; else absolute path written + `\n`. `latest`: none -> exit 2 (no output); else prints file body verbatim. `trend`: `JSON.stringify(rows, null, 2)` + `\n`, rows = frontmatter objects of the last N matching files oldest->newest (`[]` if none). Unknown -> stderr `usage: critique-storage.mjs <slug|write|latest|trend> [args]\n` exit 1.
- **Outputs**: `slug`: slug + `\n` exit 0, or stderr `no stable slug for input\n` exit 1. `write`: missing args -> stderr `usage: write <slug-or-target> <body-file>\n` exit 1; else absolute path written + `\n`. `latest`: none -> exit 2 (no output); else prints file body verbatim. For explicit path/URL targets, `latest`, `trend`, and `close` probe both the current hashed long slug and the previous suffix-only long slug, but accept a legacy snapshot only when its recorded target identity matches. Identity-less pre-upgrade snapshots remain available by passing their exact legacy slug, which avoids guessing a path/URL owner for a collision-prone key. `trend`: `JSON.stringify(rows, null, 2)` + `\n`, rows = frontmatter objects of the last N matching files oldest->newest (`[]` if none). Unknown -> stderr `usage: critique-storage.mjs <slug|write|latest|trend> [args]\n` exit 1.
- **Gotchas**: `latest`/`trend` match by suffix `__<slug>.md`; `readLatestSnapshotAcrossTargets` uses suffix `.md`. Main-module guard compares realpaths so symlinked invocation works.
- **Tests**: `tests/critique-storage.test.mjs` (slug rules, stamp format, round-trip, newest selection, meta cannot override timestamp/slug, quoting `:`/`#`, CLI slug/exit codes/symlink/latest exit 2, trend ordering).
+1 -1
View File
@@ -1,5 +1,5 @@
{
"stdout": "ry-structure-with-many-segments-component-name-tsx\n",
"stdout": "ure-with-many-segments-component-name-tsx-47c1e3a4\n",
"stderr": "",
"exit": 0,
"signal": null,