contractId and the fold
The fold turns a declared name, the name a publisher's source gives the component, into its contractId. Same input, same output, on every machine and in every locale. No guessing. No transliteration. No silent merges.
| Input | The declared name, as a sequence of Unicode code points |
|---|---|
| Output | An identifier matching ^[a-z0-9]+(-[a-z0-9]+)*$, or a rejection |
| Applied by | Publisher, at publish (SR-130) |
| Locale | The result does not depend on host locale or Unicode version |
| Length | No maximum. The fold never truncates. |
Fold steps
The five steps apply in this order.
Step 1: lowercase ASCII letters #
Lowercase the ASCII letters A to Z only. Apply no other case mapping and no Unicode normalization, before, during, or after.
ICON → icon
Café → café (é is unchanged)
Step 2: replace separator runs #
Replace every run of one or more characters from the separator class with a single hyphen.
| Separator | Character | Code point |
|---|---|---|
| Space | | U+0020 |
| Underscore | _ | U+005F |
| Period | . | U+002E |
| Forward slash | / | U+002F |
These four characters are the entire separator class. The hyphen is not in the class. Case changes are not separators: the fold does not hyphenate at case boundaries.
button_primary → button-primary
forms/button → forms-button
card-_header → card--header
Step 3: collapse hyphen runs #
Collapse every run of two or more hyphens to a single hyphen.
button--primary → button-primary
card--header → card-header
Step 4: trim hyphens #
Remove leading and trailing hyphens.
- → (empty)
Step 5: change nothing else #
No other transformation applies. The fold does not truncate and imposes no maximum length. A 300-character declared name of ASCII letters folds to a 300-character identifier.
Classification of the result
| Result after step 5 | Outcome | Reason string | Spec |
|---|---|---|---|
| Empty | Rejected | empty after fold | SR-131 |
Contains any character outside a to z, 0 to 9, and hyphen | Rejected | carries residual character | SR-132 |
| Otherwise | The result is the identifier | None | SR-130 |
A residual character is never transliterated, stripped, or decomposed. The rejection output names the offending character. Café is rejected with reason carries residual character, naming é; it never becomes cafe.
Worked examples
Each column shows the value after that step. "(same)" means the step made no change.
| Declared name | Step 1 | Step 2 | Step 3 | Step 4 | Outcome |
|---|---|---|---|---|---|
Button_Primary | button_primary | button-primary | (same) | (same) | button-primary |
Button Primary | button primary | button-primary | (same) | (same) | button-primary |
Button.Primary | button.primary | button-primary | (same) | (same) | button-primary |
Button--primary | button--primary | (same) | button-primary | (same) | button-primary |
card-_header | (same) | card--header | card-header | (same) | card-header |
forms/Button | forms/button | forms-button | (same) | (same) | forms-button |
iconBadge | iconbadge | (same) | (same) | (same) | iconbadge |
ICON | icon | (same) | (same) | (same) | icon |
Café | café | (same) | (same) | (same) | Rejected: carries residual character (é) |
___ | (same) | - | (same) | (empty) | Rejected: empty after fold |
/ | (same) | - | (same) | (empty) | Rejected: empty after fold |
| A single space | (same) | - | (same) | (empty) | Rejected: empty after fold |
Collisions
Scope #
Two or more declared names that fold to the same identifier are a collision. A name from a Custom Elements Manifest (CEM) and a name from a React source collide exactly as two names in one file would.
Reporting #
- One collision report per identifier. The report names the identifier and every declared name that folded to it.
- The publisher writes no contract for any of the colliding names.
- A publish collects every rejection and every collision and reports them together. It does not stop at the first finding.
| Declared names | Fold result | Outcome |
|---|---|---|
Button Primary, button_primary, Button--Primary | button-primary (all three) | One collision report for button-primary, naming all three declared names. No contract is written for any of them. |
Combined publish example #
A single publish with the declared names Café, ___, Button Primary, button_primary, and Button--Primary reports three findings together:
Café: rejected,carries residual character, namingé___: rejected,empty after foldbutton-primary: one collision namingButton Primary,button_primary, andButton--Primary
Declared name
Derivation profile #
v0.1 defines no derivation profile. Which source identifier counts as the declared name (for example a React export name, a CEM tagName, or a design-tool name) is left to the publisher's own documentation, stated per source kind.
The specification recommends (SHOULD, not a requirement) that a publisher fold the same source identifier for a component on every publish, so contractId stays stable across publishes.
DSDS compatibility
Every fold result is a subset of the DSDS (Design System Documentation Specification) component id pattern:
contractId pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
DSDS id pattern: ^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)*$
Every valid contractId is therefore a valid DSDS id. The reverse does not hold: a DSDS id can contain a period, and a contractId cannot.
What the schema checks
The component contract schema checks only that contractId matches ^[a-z0-9]+(-[a-z0-9]+)*$. It does not check that the value is the fold of a declared name, and it does not detect collisions. Both are publisher obligations. This repository ships no checker tool for them.