Unify local application identity tools - #16
Conversation
Move UUID creation into a general generate group so agents can request batches without Plan state. Let initialization derive either identity value while preserving schema-valid explicit keys.
|
Stack note update: this PR was originally described as 1 of 2. The feature stack now has three chunks: #16 for generators and flexible initialization, #18 for product Compile UX, and #19 for retained Compilation status and download. The independent Node advisory repair in #17 should land before this stack. No code in this PR is made obsolete by the later chunks. |
ReviewNo defects. The derivation holds up against adversarial Unicode, and the two properties most likely to be subtly wrong, normalization stability and the byte ceiling, both check out empirically. Verification
Command surface behaves as claimed: The removed command fails cleanly with a pointer to help rather than doing something surprising, and the emitted UUID has the version 7 nibble and the correct variant bits. Property testing the derivationI ran All held. The interesting cases:
The long case lands on exactly 63 bytes rather than near it, so the prefix arithmetic is right at the boundary rather than approximately right. Normalization pairs all converged: Two details that are doing real workNormalizing before digesting, not after. const normalizedName = name.normalize("NFKD");
const readable = asciiWords(normalizedName);
const candidate = readable.length === 0
? `app_${stableDigest(normalizedName)}`
: ...
The explicit transliteration map. const LATIN_TRANSLITERATIONS = new Map([
["Æ", "ae"], ["Ø", "o"], ["Ł", "l"], ["Þ", "th"], ["ß", "ss"], ...
]);NFKD decomposes Worth a comment on that constant explaining why it exists, since it looks like an arbitrary list and it is actually "the characters NFKD will not help with." One note: derivation is deliberately lossySix distinct names collapse to one key: Same for I checked whether that matters and it does not, today. On the service, So two Projects may share a key and the collisions are harmless. Raising it only because the derivation being non-injective is a fact somebody will need if uniqueness is ever added. A unique index on The removal
The one thing worth confirming outside this diff is whether any published skill or documentation still instructs an agent to run |
Two strings that look identical and are notThis PR turns a name a human typed into an identifier a computer can use. "Oscar Party" becomes Here is the fact that makes it strange. Open a Node console: "café" === "café" // falseThose are not a typo. They are two different strings that render identically and, to any reader, mean the same word. WhyThere are two ways to write Precomposed: one code point, U+00E9, LATIN SMALL LETTER E WITH ACUTE. Decomposed: two code points, "café".length // 4
"café".length // 5Same glyphs on screen, different data in memory. And which one you get depends on where the text came from. macOS filesystems historically hand you decomposed forms. Most Linux tooling and most web input produce precomposed. Copy a name out of a PDF and you get whichever the PDF had. So a user on a Mac and a user on Linux can type what they both experience as the same name and hand your program two different strings. Why that would break thisWithout care, the derivation produces two different keys for the same name. One user's project is The fix is one call: const normalizedName = name.normalize("NFKD");Unicode defines normalization forms, and normalizing both strings to the same form makes them comparable:
This code picks NFKD, and the K is the interesting choice. Compatibility decomposition unpacks characters that are not accented letters but are typographic variants of something simpler: I confirmed both of the first two land where you would hope: After normalizing, stripping the combining marks leaves plain ASCII letters behind, which is exactly what an identifier needs. What NFKD will not do for youTry this and it fails: "Þór".normalize("NFKD") // still contains Þ
So NFKD leaves them, the ASCII filter drops them, and Hence the map: const LATIN_TRANSLITERATIONS = new Map([
["Æ", "ae"], ["Ø", "o"], ["Ł", "l"], ["Þ", "th"], ["ß", "ss"], ...
]);These are not arbitrary. The general point: normalization gets you a long way and it is not transliteration. If your input can contain letters outside Latin-1, you either need a table for the ones that matter or a fallback for everything else. The fallback, and where it goesWhat about a name with no ASCII in it at all? No transliteration table is going to help here without becoming a romanization library. So the code gives up on readability and hashes: const candidate = readable.length === 0
? `app_${stableDigest(normalizedName)}`
: ...Unreadable, deterministic, and valid. The user can always pass an explicit key if they want something meaningful, which is the right escape hatch. One detail in that line is worth pointing out because it is easy to get backwards. Bounding the lengthIdentifiers usually have a maximum. Here it is 63 bytes, chosen to fit an iOS application identifier component. Truncation alone is not enough, because two long names sharing a prefix would collapse to the same key. So: const suffix = `_${stableDigest(normalizedName)}`;
const prefix = candidate
.slice(0, MAX_DERIVED_APPLICATION_KEY_BYTES - suffix.length)
.replace(/_+$/, "");
return `${prefix}${suffix}`;Readable prefix, plus a digest of the whole name. You keep enough to recognise it and enough entropy to distinguish it. I checked the boundary with a 200-character name and got exactly 63 bytes, not 62 or 64. The Note it measures with Deliberately throwing information awayOne more thing worth understanding rather than treating as a bug. These all produce the same key: The derivation is not injective. Case is folded, punctuation and emoji are dropped, whitespace runs collapse. Different names, one key. Is that a bug? It depends entirely on whether anything requires keys to be unique. I checked the service side: Which makes it a reasonable design. The derived key is a convenience default, not an identity. If uniqueness were ever added, this derivation would become a source of confusing failures, and the right response then would be to stop deriving rather than to make the derivation cleverer. The habit worth taking: when you write a lossy transformation, know what depends on it being lossless. Usually nothing does, and then lossy is fine. Occasionally something does, and it is much better to find that out now than from a constraint violation later. ChecklistDeriving an identifier from human text, in order:
|
Record why normalization alone cannot preserve these letters before the ASCII identifier filter. This keeps the explicit map from looking arbitrary.
|
Addressed the durable-code follow-up in The later Skills workflow no longer names |
Preserve the reviewed application identity commits while bringing in the lockfile repair required by the Node 24 quality gate.
Stack
CLI stack 1 of 2. The follow-up product Compile and retained-Compilation commands will target this branch.
Summary
Derived keys are deterministic, normalization-stable, bounded for the current iOS identifier component, and use readable prefixes with digest fallbacks where needed.
Verification
No package was published and no compatibility alias was added.