#tag-indexA declared projection of tag values onto an #ideas row. It exists because a tag’s value is arbitrary and a row’s fields must be encodable; a projection makes the value narrow and checked, which is what lets it ride a row at all.
#import "@rookery/core:0.1.0": ideas, tag-index
#let INDEX = tag-index((
cycle: (family: "cycle-"), // -> "26-27"
kind: (family: "venue-", one-of: KINDS), // -> "postdoc"
deadline: (key: "date-deadline", stamp: true), // -> "20261101"
stage: (from: stage-of), // derived
))
#context {
for e in ideas(index: INDEX) [- #e.kind, due #e.deadline]
}Each field names exactly one extractor:
| Form | What it projects |
|---|---|
key: "<tag>" | That tag’s value, or none where the idea does not carry it. |
family: "<prefix>" | The first flat tag whose key starts with the prefix, prefix stripped. one-of: restricts and orders the candidates, so an idea carrying two of a family resolves to the earliest you listed rather than to whichever key order happens to yield first. |
from: <function> | Called with the idea’s whole tag dictionary. A derived value — the current stage of a dated log, how far something got — is a computation rather than a tag value, and this is the only form that makes one filterable or sortable. |
Any of the three may carry stamp: true, which renders a datetime as a zero-padded [year][month][day] string. That is not only a display choice: a fixed-width numeric string sorts lexically in date order, so a stamped field is a free sort key.
A projected value must be a scalar — a string, an integer, a float, a boolean or none — and the build fails where one is not. A field may not take the name of a row field above, for the same reason: naming one href and silently replacing every link on the page is the failure this refusal prevents.