Markdown for LLMs
Stable identity
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# Stable identity
Every compiled node carries two independent fingerprints: a StableId and a
content hash (**S**). The StableId answers which node this is — the `id`
field that anchors, labels, and cross-package references point at. The
content hash answers what state the node is in — a digest over the node's
canonical bytes, including its display name, parameters, and labels. The
two move independently: a rename that keeps the StableId still changes the
content hash, because the name is part of the state.
Commands on this page use the public release of `law`, except one
diagnostic that **requires a source checkout** (I); the inputs are small
scratch files under `/tmp/idlane/` whose relevant lines are shown. Each
claim carries a status in words with a letter mark; the full legend is on
the [topic index](/corpus/#how-this-topic-marks-confidence).
## What each edit does to identity
Four everyday edits, and what each one does to the node and to the
packages that use it — all **available in the public release** (S):
| Edit | StableId | Content hash | Consequence for consumers |
|---|---|---|---|
| Edit label text | Kept | Moves | Unaffected: every reference still resolves |
| Move the declaration within its file or to another file of the package | Kept | Kept | Unaffected |
| Rename with a supported `@id` pin | Kept | Moves | The old name keeps resolving; the new name does not resolve |
| Rename without a pin, or with a pin the kind ignores | New node | New | References to the old name break; consumers must move to the new name |
The first two rows are safe at any time. The last row is a breaking
change for every consumer. The third row is the one that surprises
authors, so it has its own example.
## Renaming with a pin: the old name stays the public name
The shared enum `ParcelKind` from the lab fixtures is renamed to
`ParcelClass`, with its old identity pinned:
```law
@id("urn:law:lab:parcels:iface#ParcelKind")
pub enum ParcelClass {
```
The compiled id stays `urn:law:lab:parcels:iface#ParcelKind`; only the
display name and the content hash change. Consumers then see the
following, which is often unexpected:
- A consumer that still writes `labparcels.iface::ParcelKind` compiles
and gets the same reference as before the rename.
- A consumer that writes the new name `labparcels.iface::ParcelClass` is
refused with `LDC-E1105`: the type is not exported by the package.
Two mechanisms explain it. A reference is built from its spelling: both
`ParcelKind` and `pkg::ParcelKind` compile to `namespace#ParcelKind`. A
dependency's export list is built from its compiled node ids, not from
display names. While the pin holds the old id, the old name is exported
and the new one is not. A pin therefore keeps old spellings working and
never forwards the new spelling. Moving consumers to the new name is a
second, breaking migration to a new identity. The commands and the
verbatim refusal are in the
[migrations worked example](/corpus/vocabulary/migrations/#worked-example-a-pinned-enum-rename).
## Explicit identity
Authors pin identity with the `@id("…")` annotation, but the compiler
honors the pin only on some declaration kinds (**S**). Where it is
honored, the compiled `id` is the written value verbatim, whatever the
declaration is named:
```law
@id("urn:law:examples:pin#grown-up")
const AdultAge: Integer = 18;
```
```bash
$ law engine check /tmp/idlane/constpin.law 2>/dev/null
check OK: /tmp/idlane/constpin.law
```
The lowered node carries `id` `urn:law:examples:pin#grown-up` with `name`
`AdultAge` (**S**). The kinds that honor the pin are const, rule, enum,
priority, role, fiction, classification, definition, interpretation,
interpretation group, revision, entrenchment, and calendar (**S**).
Every other kind assembles `namespace#name` and ignores the annotation,
with a warning when the written value differs (**S**). A pin on a
relation, for example, is not executed. The warning below **requires a
source checkout** (I): the public build checks the same file clean and
still ignores the pin, without a warning.
```bash
$ ./law engine check /tmp/idlane/relpin.law 2>&1 | grep "IS NOT EXECUTED"
/tmp/idlane/relpin.law:8:1: warning LDC-E1372: explicit `@id("urn:law:examples:pin#grown-up")` on a `relation adult` declaration IS NOT EXECUTED: lowering will assemble the node id as `urn:law:examples:pin#adult` (§14 requires «используется без изменения», but this declaration does not use the `node_id` path — DECISION-0352 §4, errata E-0230). `@source` references and §202 anchors will take the package-derived id; remove the annotation or await the StableId support decision
```
The lowered id stays `urn:law:examples:pin#adult` (**S**). The kinds that
ignore the pin include relation, entity, record, function, constraint,
procedure, and the source-model declarations (**S**). Source-model blocks
(`source`, `edition`, `publication`, `fragment`) take an `id "…";` item
(**S**).
## Generated identity
Nodes without an explicit identity get one derived from their meaning:
the parent identity plus a deterministic semantic suffix (**S**). The
compiler must never use line numbers, declaration order, or file metadata
as identity input — moving a declaration within a file, or to another
file of the same package, changes neither the id nor the content hash
(**S**).
Generated shapes authors will meet (**S**):
- Split rule heads take the rule identity plus a short hash suffix.
- Branches of a normalized disjunction take `<id>/alt/N` suffixes.
- Norm templates take `<namespace>#<name>` unless an explicit `@id`
overrides them.
References are spelling-derived: a bare `AdultAge` and a qualified
`pkg::AdultAge` both compile to the reference `namespace#AdultAge`
(**S**). A reference resolves when a node carries that id — which is why
a pin keeps old spellings working and never forwards the new spelling.
## Value identity
Identity rules extend to values, not just declarations (**S**):
- An entity value is the pair of its nominal type and its StableId.
There are no implicit conversions between entity types.
- An enum member is its own identity: dotted, bare, and qualified
spellings of the same member are one reference.
- Constants are dereferenced by a single pre-pass before evaluation,
so every use sees the same value.
## Cross-package identity
Within one world, each package is linked by name and each node keeps the
identity its own package gave it (**S**). Two packages linking the same
dependency by the same version share one copy; a StableId collision at
link time is an error, not a silent merge.
A dependency's export list is derived from its compiled node ids, not
from display names (**S**) — the reason a pinned rename keeps the old
name public, as shown [above](#renaming-with-a-pin-the-old-name-stays-the-public-name).
Practical consequences:
- Pin the identity of a shared declaration before renaming it — but only
for kinds that honor the pin. For every other kind a rename is a new
node, and no annotation changes that.
- Renaming without an honored pin creates a new node; renaming with one
keeps the node while the content hash still moves (**S**).
## Editor rename limits
Editor rename is certified only for package-private declarations with
an honored explicit `@id`, plus local binders and parameters. The
compiler index must carry the rename certificate; otherwise the editor
refuses with a reason (**S**). The certificate is visible in the index
output:
```bash
$ law engine index /tmp/idlane/certprobe.law --json 2>/dev/null | python3 -c "import json,sys; ds=json.load(sys.stdin)['index']['declarations']; [print(x['name'],x['kind'],x['visibility'],json.dumps(x.get('rename'))) for x in ds]"
Threshold const internal {"complete": true, "preservesStableId": true}
```
Public names are never renamed by the editor: that would change the
dependency contract. Shared-vocabulary renames stay manual and verified —
follow the migration procedure. Local extension builds hand rename to
the language server, while the published Marketplace build predates that
handoff.
## Where to go next
- The companion page on [modeling vocabulary](/corpus/vocabulary/modeling/) introduces the declarations
that carry identity.
- The companion page on [migrations](/corpus/vocabulary/migrations/) covers identity across releases.
- For construct-level detail, see [the Vocabulary construct reference](/constructs/vocabulary/).
- For a hands-on introduction, see [What we talk about](/tutorials/vocabulary/).