fix(topdata): let pinned TLK ids evict stale state owners
ci / ci (pull_request) Successful in 3m17s

The custom palette taxonomy (#46) pins display strings to fixed TLK
ids in tlk/custom.tlk.yml. registerInlineAtID demanded those ids be
free, but .tlk_state.json is a gitignored, per-machine cache: on any
machine built before #46, a ref could have dynamically grabbed a
now-pinned id (e.g. feat:yuanti/alternate_form.feat holding 2689),
failing the build with "TLK id N is already reserved by ...".

Make the pin authoritative over the cache: a stale cached owner on a
pinned id is evicted and reallocated a fresh id when next made active.
Only two pins fighting over one id in custom.tlk.yml is now an error.
This self-heals on the next build without deleting the state file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 20:10:46 +02:00
co-authored by Claude Opus 4.8
parent 7b481ca0c0
commit 32cfb9a013
2 changed files with 65 additions and 2 deletions
+16 -2
View File
@@ -109,6 +109,7 @@ type tlkCompiler struct {
active map[string]tlkEntryData
activeKeys map[string]struct{}
reservedByID map[int]string
pinnedByID map[int]string
nextID int
}
@@ -158,6 +159,7 @@ func newTLKCompiler(sourceDir string, legacy *legacyTLKData) (*tlkCompiler, erro
active: map[string]tlkEntryData{},
activeKeys: map[string]struct{}{},
reservedByID: reserved,
pinnedByID: map[int]string{},
nextID: nextID,
}
if legacy != nil {
@@ -380,12 +382,24 @@ func (c *tlkCompiler) registerInlineAtID(key string, id int, entry tlkEntryData)
if id < 0 {
return fmt.Errorf("TLK key %q has negative id %d", key, id)
}
// The pin is authoritative over the per-machine .tlk_state.json cache: a
// stale mapping that dynamically grabbed this id on an older build must
// yield so the pinned key can take it. Only a genuine clash between two
// pins in custom.tlk.yml is an author error.
if owner, ok := c.pinnedByID[id]; ok && owner != key {
return fmt.Errorf("TLK id %d is pinned by both %q and %q", id, owner, key)
}
if mapping, ok := c.state.Entries[key]; ok && mapping.ID != id {
return fmt.Errorf("TLK key %q changed id from %d to %d", key, mapping.ID, id)
// This key held a different cached id; release it so the pin wins.
if c.reservedByID[mapping.ID] == key {
delete(c.reservedByID, mapping.ID)
}
}
if owner, ok := c.reservedByID[id]; ok && owner != key {
return fmt.Errorf("TLK id %d is already reserved by %q", id, owner)
// Evict the stale owner; it gets a fresh id when next made active.
delete(c.state.Entries, owner)
}
c.pinnedByID[id] = key
c.state.Entries[key] = tlkStateMapping{ID: id}
c.reservedByID[id] = key
return c.markActive(key, entry)