copy: a shortened id keeps its prefix, the way git shortens a hash
Build Debug APK / build (push) Failing after 3m9s
Build Debug APK / build (push) Failing after 3m9s
truncate_id/short_id kept the first `keep` characters and the last four (3fa9c1d2…a1b2), cutting ids from the middle like readable text. Every id here is random (UUIDv4/v5, node keys, BLAKE3 hashes), so the end is no more distinguishing than the start, and the prefix is the conventional handle: 3fa9c1d2…. A scheme label such as `blake3:` is kept whole and not counted, so a blob hash still shows eight characters of the hash itself. Every hash chip on desktop and Android (via the short_id/truncate_id exports) and the browser extension's truncateId change with it; ADR 0026 records the amendment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X3HRVnoLened7RocqUkvFs
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8c43229925
commit
c42454854d
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Truncation that cuts from the middle.
|
||||
* Truncation: readable text cuts from the middle, ids keep their prefix.
|
||||
*
|
||||
* An end cut drops whatever tells two long strings apart when they share a
|
||||
* beginning, so every truncation in this project keeps both ends instead. The
|
||||
* rule and its reasoning live in the Rust lib (`copy::elide_middle`,
|
||||
* beginning, so a truncation of readable text keeps both ends. An id is random,
|
||||
* so it is shortened to its prefix, the way git shortens a hash. The rules and
|
||||
* their reasoning live in the Rust lib (`copy::elide_middle`,
|
||||
* `copy::truncate_id`); the extension has no way to call that, so this is the
|
||||
* same rule written again, and has to move when that one does.
|
||||
*/
|
||||
|
||||
const ELLIPSIS = "…";
|
||||
|
||||
/** How many trailing characters `truncateId` keeps after the ellipsis. */
|
||||
const ID_TAIL = 4;
|
||||
|
||||
/** How many characters an ellipsis has to save before `truncateId` cuts at all. */
|
||||
const ELLIPSIS_MIN_SAVINGS = 4;
|
||||
|
||||
@@ -49,16 +47,20 @@ export function elideMiddle(text, maxChars) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A hex id or uuid cut to its first `keep` characters, an ellipsis, and its
|
||||
* last few. Passes through when the cut would save almost nothing.
|
||||
* A hex id or uuid cut to its first `keep` characters and an ellipsis. A
|
||||
* `scheme:` label such as `blake3:` is kept whole and not counted. Passes
|
||||
* through when the cut would save almost nothing.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {number} keep
|
||||
* @returns {string}
|
||||
*/
|
||||
export function truncateId(id, keep) {
|
||||
if (Array.from(id).length <= keep + ID_TAIL + 1 + ELLIPSIS_MIN_SAVINGS) return id;
|
||||
return elideBetween(id, keep, ID_TAIL);
|
||||
const colon = id.indexOf(":");
|
||||
const scheme = colon > 0 && /^[A-Za-z0-9]+$/.test(id.slice(0, colon)) ? id.slice(0, colon + 1) : "";
|
||||
const body = Array.from(id.slice(scheme.length));
|
||||
if (body.length <= keep + 1 + ELLIPSIS_MIN_SAVINGS) return id;
|
||||
return `${scheme}${body.slice(0, keep).join("")}${ELLIPSIS}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,12 +34,15 @@ per modality, and the framework-independent pieces shared in Rust:
|
||||
| Android | `ui/IdRef.kt` (`IdRef`) | tap to toggle full/short |
|
||||
| TUI | *(none yet)* | undecided — see Consequences |
|
||||
|
||||
Truncation itself is `synchronicity::copy::short_id`: the first 8 characters,
|
||||
`…`, and the last 4, passing an id through untouched when the cut would save
|
||||
four characters or fewer. The cut is in the middle, like every truncation in
|
||||
the project (see `copy::elide_middle`): two ids minted together can share a
|
||||
prefix, and a prefixed one (`blake3:…`) spends most of an 8-character head on
|
||||
the part every id has in common. It lives in
|
||||
Truncation itself is `synchronicity::copy::short_id`: the first 8 characters
|
||||
and `…`, passing an id through untouched when the cut would save four
|
||||
characters or fewer. A `scheme:` label (`blake3:`) is kept whole and not
|
||||
counted. *Amended 2026-09-28:* this used to keep the last 4 characters as well,
|
||||
cutting in the middle like readable text. Ids now keep only their prefix, the
|
||||
way git shortens a hash: every id here is random (UUIDv4/v5, node keys, BLAKE3
|
||||
hashes), so its end is no more distinguishing than its start, and the prefix
|
||||
is the conventional handle. Readable text still cuts from the middle (see
|
||||
`copy::elide_middle`, and CLAUDE.md). It lives in
|
||||
`lib` rather than in either frontend so the FFI can precompute both forms
|
||||
(`ffi.rs`'s `JobItem`) and Android renders the same string desktop does
|
||||
without a second implementation of the rule.
|
||||
|
||||
+30
-20
@@ -50,9 +50,6 @@ pub mod human_display {
|
||||
/// What stands in for the characters a truncation dropped.
|
||||
pub const ELLIPSIS: char = '…';
|
||||
|
||||
/// How many trailing characters [`truncate_id`] keeps after the ellipsis.
|
||||
const ID_TAIL: usize = 4;
|
||||
|
||||
/// How many characters an ellipsis has to save before [`truncate_id`] bothers
|
||||
/// cutting at all.
|
||||
const ELLIPSIS_MIN_SAVINGS: usize = 4;
|
||||
@@ -60,11 +57,11 @@ const ELLIPSIS_MIN_SAVINGS: usize = 4;
|
||||
/// `text` with its middle replaced by an ellipsis so that it is at most
|
||||
/// `max_chars` characters long, ellipsis included.
|
||||
///
|
||||
/// Every truncation in this project cuts from the middle rather than the end:
|
||||
/// what tells two long strings apart is as often at the end as at the start
|
||||
/// (`Quarterly report - draft` / `Quarterly report - final`, an episode
|
||||
/// number, a file extension, the last digits of a hash), and an end cut throws
|
||||
/// exactly that away. Layout-driven truncation follows the same rule through
|
||||
/// Every truncation of readable text in this project cuts from the middle
|
||||
/// rather than the end: what tells two long strings apart is as often at the
|
||||
/// end as at the start (`Quarterly report - draft` / `Quarterly report -
|
||||
/// final`, an episode number, a file extension), and an end cut throws exactly
|
||||
/// that away. Ids are the exception — see [`truncate_id`]. Layout-driven truncation follows the same rule through
|
||||
/// each toolkit's own middle ellipsis; this is for a cut made by character
|
||||
/// count. The head gets the odd character, and whitespace next to the cut is
|
||||
/// dropped rather than shown.
|
||||
@@ -87,16 +84,28 @@ fn elide_between(text: &str, head: usize, tail: usize) -> String {
|
||||
format!("{}{ELLIPSIS}{}", text[..head_end].trim_end(), text[tail_start..].trim_start())
|
||||
}
|
||||
|
||||
/// Truncate a hex id or UUID to its first `keep` characters, an ellipsis, and
|
||||
/// its last few, for compact list rows — the full value should stay available
|
||||
/// via a tap/hover/long-press/click elsewhere. Passes through unchanged when
|
||||
/// the cut would save four characters or fewer.
|
||||
/// Shorten a hex id or UUID to its first `keep` characters and an ellipsis,
|
||||
/// the way git shortens a commit hash. An id is random, so its end is no more
|
||||
/// distinguishing than its start, and the prefix is the conventional handle —
|
||||
/// the part people read aloud, compare and type. A `scheme:` label such as
|
||||
/// `blake3:` is kept whole and not counted, since every id of that kind shares
|
||||
/// it. The full value should stay available via a tap/hover/long-press/click
|
||||
/// elsewhere. Passes through unchanged when the cut would save four characters
|
||||
/// or fewer.
|
||||
pub fn truncate_id(id: &str, keep: usize) -> String {
|
||||
if id.chars().count() <= keep + ID_TAIL + 1 + ELLIPSIS_MIN_SAVINGS {
|
||||
id.to_string()
|
||||
} else {
|
||||
elide_between(id, keep, ID_TAIL)
|
||||
let (scheme, body) = match id.split_once(':') {
|
||||
Some((scheme, _))
|
||||
if !scheme.is_empty() && scheme.chars().all(|c| c.is_ascii_alphanumeric()) =>
|
||||
{
|
||||
id.split_at(scheme.len() + 1)
|
||||
}
|
||||
_ => ("", id),
|
||||
};
|
||||
if body.chars().count() <= keep + 1 + ELLIPSIS_MIN_SAVINGS {
|
||||
return id.to_string();
|
||||
}
|
||||
let head: String = body.chars().take(keep).collect();
|
||||
format!("{scheme}{head}{ELLIPSIS}")
|
||||
}
|
||||
|
||||
/// [`truncate_id`] at the everyday 8-character cut.
|
||||
@@ -607,11 +616,12 @@ mod tests {
|
||||
type Resolve = fn(Option<&str>) -> (String, bool);
|
||||
|
||||
#[rstest]
|
||||
#[case("blake3:abcdef0123456789", "blake3:a…6789")]
|
||||
#[case("1234567890123456789012", "12345678…9012")]
|
||||
#[case("1234567890123456789012", "12345678…")]
|
||||
#[case("blake3:abcdef0123456789", "blake3:abcdef01…")]
|
||||
#[case("short", "short")]
|
||||
#[case("seventeen-chars-1", "seventeen-chars-1")]
|
||||
fn short_id_truncates_long_ids_and_passes_through_short_ones(
|
||||
#[case("1234567890123", "1234567890123")]
|
||||
#[case("12345678901234", "12345678…")]
|
||||
fn short_id_keeps_the_prefix_and_passes_through_short_ones(
|
||||
#[case] input: &str,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user