Note: I obviously used AI to help write this proposal, but I hope I did enough due diligence to ensure it is not AI slop.
The ask
Let value block fields use backtick-quoted names — the same lexical form already used for enum values — so specs can spell externally-mandated record keys exactly as they appear on the wire.
value HttpHeaders {
`Content-Type`: String?
`Content-Length`: Integer?
`X-Request-Id`: String?
}
This is a lexical relaxation, not a new type constructor. value blocks remain closed structs. No new kinds of values, no runtime semantics, no type-system changes.
Why this matters
I'm using Allium to spec a shared TypeScript library that wraps Zendesk and Jira's REST APIs. Multiple internal applications consume the library — a Jira-Zendesk connector, a customer-facing site, end-to-end test suites. The Allium specs define the library's observable contract: the value types callers import, the operations they call, the guarantees they get. When a consuming AI or human opens the spec, they should see exactly the shapes the library expects developers to construct in their TypeScript.
The README describes Allium as focusing on observable behavior rather than implementation details. The record shapes this proposal targets sit squarely on that line. When a consuming developer writes { "=": "open", ">=": "solved" } to build a query, those keys are the observable contract — not an implementation detail. The spec should be able to name what callers actually see.
But it can't, because many of the keys that external systems mandate aren't legal Allium identifiers. This comes up across the ecosystem: HTTP headers (Content-Type, X-Request-Id), JSON Schema keywords ($ref, $schema), Kubernetes annotations (kubernetes.io/ingress.class), and caller-facing DSLs like the one in my library. Today, modelling any of these forces a rename-and-document workaround that introduces exactly the kind of spec drift Allium exists to prevent.
Evidence: the rename-table smell (real production spec)
In the library's Allium spec, a typed query builder for Zendesk ticket search defines operator maps like this:
value ComparisonOperatorMap {
eq: String?
lt: String?
gt: String?
lte: String?
gte: String?
}
The caller's TypeScript never uses these names — they write { "=": "open", ">=": "solved" }. The spec compensates with a five-row rename invariant:
@invariant OperatorSymbolMapping
-- eq -> "="
-- lt -> "<"
-- gt -> ">"
-- lte -> "<="
-- gte -> ">="
Those rows exist only because Allium can't spell the real keys. The caller writes =, <, >, <=, >= — those are the observable keys, the shapes developers actually construct. The spec calling them eq, lt, gt, lte, gte is a lie about the library's own interface: names that exist nowhere in the caller's code and nowhere on the wire.
This is the observable-behaviour vs implementation-detail distinction that Allium is built around, just showing up in an unexpected place. The current spec gets it exactly backwards: it hides the observable facts (the real key names callers type) behind invented identifiers.
Under this proposal, the spec matches the caller-facing object exactly:
value ComparisonOperatorMap {
`=`: String?
`<`: String?
`>`: String?
`<=`: String?
`>=`: String?
}
The value block now shows what callers actually type. Observable behavior and implementation detail, cleanly separated.
My current workaround
Today I rename keys to identifiers (eq, lt, gt, etc.) and document the mapping in a named @invariant block. This works but:
- Introduces a translation layer every reader must hold in their head or AI in it's context
- Entangles language-limitation workarounds with genuine protocol facts
- Creates names (
eq, lte) that exist nowhere outside the spec — not in the caller's code, not on the wire
For a library where the spec doubles as the contract consuming LLMs or developers read, that last point is the sharpest: the spec shows names that no user of the library will ever type.
Precedent within Allium
Backtick-quoted literals already exist for enum values referencing external standards (e.g. `de-CH-1996`, `no-cache`). The lexer already handles them. This proposal extends that mechanism to one additional position — field names inside value blocks — for the same reason: faithfully representing externally-mandated strings.
Referencing backtick-quoted fields
At reference sites you write the same backticks you wrote at the declaration:
headers.`Content-Type`
query.`=`
map.`>=`
Inside a scope where the field resolves without a leading expression, the unqualified form also takes backticks:
Backtick-quoted fields work everywhere identifier fields work today — derived values, when clauses, invariants, ensures — with no new syntax beyond the backticks themselves.
If someone writes headers.Content-Type without backticks, the checker should test the failed identifier against backtick-quoted fields on the referenced type and emit a targeted error pointing at the declaration site.
Scope
Deliberately narrow:
- In scope:
value-block field declarations and expression-site references to those fields.
- Not asking for: open dictionaries /
Map<K,V>, integer/pattern/computed keys, changes to entity/variant/config/rule names, runtime semantics changes.
Proposed specification
I've drafted a detailed specification covering grammar rules, character class (inheriting ALP-017), case convention handling, scope of the relaxation, error messages, code generation guidance, transition graph non-interaction, and explicit out-of-scope boundaries.
Full specification (7 clauses)
1. Grammar.
In a value block, a field declaration may begin with either an identifier or a backtick-quoted token. In expression contexts (invariants, projections, ensures, derived values, when clauses), a dotted reference <expr>.<field> admits a backtick-quoted <field>, and an unqualified reference inside an entity-level or value-level scope admits a backtick-quoted <field>. Collision rule: within a single value block, it is a checker error to declare a backtick-quoted field whose byte content (after stripping the surrounding backticks) equals an identifier-form field declared in the same block, and vice versa. eq and `eq` cannot coexist; `Content-Type` and Content-Type cannot coexist (the latter being illegal anyway). Two backtick-quoted fields whose contents differ by even one byte (`Content-Type` vs `content-type`) remain distinct under ALP-017's byte-exact rule. Redeclaration rules are otherwise unchanged.
2. Character class.
Inherits ALP-017: printable Unicode in categories L, M, N, P, S, excluding backtick and whitespace. Byte-exact comparison after UTF-8 encoding. Empty string is rejected. Control characters, bidirectional overrides and zero-width characters are rejected.
3. Case convention.
The checker's lowercase-for-fields rule is suspended inside backticks. `Content-Type` does not warn. The rule remains in force for identifier-form field names.
4. Scope of relaxation.
value-block field declarations only. Entity field declarations remain identifier-only. Variant field declarations remain identifier-only. Config parameter names, derived value names, relationship names, rule/trigger/invariant/contract/surface names remain identifier-only. The language reference records the rationale as: "backticks are admitted at data-at-the-boundary positions (enum values, value-block field names) and forbidden at names of internal artefacts."
5. Error messages.
When a reference site uses a bare identifier foo and no such field exists, but a backtick-quoted field `foo` or a backtick-quoted field whose byte-equal form would be foo exists on the referenced type, the checker emits a targeted error naming the backtick-quoted form and pointing at the declaration site. (The checker is not required to guess arbitrary external-string misspellings — only the no-op-unquoting case.)
6. Transition graphs.
Transition graphs live only in entity and variant bodies. Backtick-quoted field names live only in value blocks. The two constructs do not interact: no field can be both backtick-quoted and the subject of a transitions block under the current language version. If a future ALP relaxes clause 4 to admit backtick-quoted entity fields, the transition-graph interaction would require its own analysis.
7. Out of scope.
Open dictionaries / Map<K, V>. Identifier positions outside value blocks. Integer, pattern or computed keys. The ALP-017 provenance annotation. Runtime validation semantics. value blocks remain closed records.
"Why value-block fields but not entity fields?"
I want to be upfront: the same argument applies to entities. Entities regularly model shapes defined by external systems — a ZendeskTicket or JiraTicket mirrors what arrives over HTTP, and those fields are just as externally-mandated as value-block fields. I expect that if this lands for value blocks, entity fields will be the next request. Possibly my own.
That said, I think value blocks are the right starting point. The immediate pain is sharpest here — well at least for us functional programmers as value types exist purely to describe data shapes. Entities carry more internal machinery (lifecycle, transitions, rules, derived values), so the interaction surface is larger and deserves its own analysis.
If this were to expand to entities, I'd expect it limited to entities marked external — the ones explicitly modelling shapes the system doesn't control. The checker could enforce this syntactically: backtick field names allowed after external entity and value, rejected after plain entity. No backtick-quoted derived values, relationship names, or config parameters. And with the same "external standards" justification that enums use, not as a general escape hatch. That keeps the blast radius manageable while solving the real problem: modelling data shapes you don't always control.
Note: I obviously used AI to help write this proposal, but I hope I did enough due diligence to ensure it is not AI slop.
The ask
Let
valueblock fields use backtick-quoted names — the same lexical form already used for enum values — so specs can spell externally-mandated record keys exactly as they appear on the wire.This is a lexical relaxation, not a new type constructor.
valueblocks remain closed structs. No new kinds of values, no runtime semantics, no type-system changes.Why this matters
I'm using Allium to spec a shared TypeScript library that wraps Zendesk and Jira's REST APIs. Multiple internal applications consume the library — a Jira-Zendesk connector, a customer-facing site, end-to-end test suites. The Allium specs define the library's observable contract: the
valuetypes callers import, the operations they call, the guarantees they get. When a consuming AI or human opens the spec, they should see exactly the shapes the library expects developers to construct in their TypeScript.The README describes Allium as focusing on observable behavior rather than implementation details. The record shapes this proposal targets sit squarely on that line. When a consuming developer writes
{ "=": "open", ">=": "solved" }to build a query, those keys are the observable contract — not an implementation detail. The spec should be able to name what callers actually see.But it can't, because many of the keys that external systems mandate aren't legal Allium identifiers. This comes up across the ecosystem: HTTP headers (
Content-Type,X-Request-Id), JSON Schema keywords ($ref,$schema), Kubernetes annotations (kubernetes.io/ingress.class), and caller-facing DSLs like the one in my library. Today, modelling any of these forces a rename-and-document workaround that introduces exactly the kind of spec drift Allium exists to prevent.Evidence: the rename-table smell (real production spec)
In the library's Allium spec, a typed query builder for Zendesk ticket search defines operator maps like this:
The caller's TypeScript never uses these names — they write
{ "=": "open", ">=": "solved" }. The spec compensates with a five-row rename invariant:Those rows exist only because Allium can't spell the real keys. The caller writes
=,<,>,<=,>=— those are the observable keys, the shapes developers actually construct. The spec calling themeq,lt,gt,lte,gteis a lie about the library's own interface: names that exist nowhere in the caller's code and nowhere on the wire.This is the observable-behaviour vs implementation-detail distinction that Allium is built around, just showing up in an unexpected place. The current spec gets it exactly backwards: it hides the observable facts (the real key names callers type) behind invented identifiers.
Under this proposal, the spec matches the caller-facing object exactly:
The value block now shows what callers actually type. Observable behavior and implementation detail, cleanly separated.
My current workaround
Today I rename keys to identifiers (
eq,lt,gt, etc.) and document the mapping in a named@invariantblock. This works but:eq,lte) that exist nowhere outside the spec — not in the caller's code, not on the wireFor a library where the spec doubles as the contract consuming LLMs or developers read, that last point is the sharpest: the spec shows names that no user of the library will ever type.
Precedent within Allium
Backtick-quoted literals already exist for enum values referencing external standards (e.g.
`de-CH-1996`,`no-cache`). The lexer already handles them. This proposal extends that mechanism to one additional position — field names insidevalueblocks — for the same reason: faithfully representing externally-mandated strings.Referencing backtick-quoted fields
At reference sites you write the same backticks you wrote at the declaration:
Inside a scope where the field resolves without a leading expression, the unqualified form also takes backticks:
Backtick-quoted fields work everywhere identifier fields work today — derived values,
whenclauses, invariants,ensures— with no new syntax beyond the backticks themselves.If someone writes
headers.Content-Typewithout backticks, the checker should test the failed identifier against backtick-quoted fields on the referenced type and emit a targeted error pointing at the declaration site.Scope
Deliberately narrow:
value-block field declarations and expression-site references to those fields.Map<K,V>, integer/pattern/computed keys, changes to entity/variant/config/rule names, runtime semantics changes.Proposed specification
I've drafted a detailed specification covering grammar rules, character class (inheriting ALP-017), case convention handling, scope of the relaxation, error messages, code generation guidance, transition graph non-interaction, and explicit out-of-scope boundaries.
Full specification (7 clauses)
1. Grammar.
In a
valueblock, a field declaration may begin with either an identifier or a backtick-quoted token. In expression contexts (invariants, projections,ensures, derived values,whenclauses), a dotted reference<expr>.<field>admits a backtick-quoted<field>, and an unqualified reference inside an entity-level or value-level scope admits a backtick-quoted<field>. Collision rule: within a singlevalueblock, it is a checker error to declare a backtick-quoted field whose byte content (after stripping the surrounding backticks) equals an identifier-form field declared in the same block, and vice versa.eqand`eq`cannot coexist;`Content-Type`andContent-Typecannot coexist (the latter being illegal anyway). Two backtick-quoted fields whose contents differ by even one byte (`Content-Type`vs`content-type`) remain distinct under ALP-017's byte-exact rule. Redeclaration rules are otherwise unchanged.2. Character class.
Inherits ALP-017: printable Unicode in categories L, M, N, P, S, excluding backtick and whitespace. Byte-exact comparison after UTF-8 encoding. Empty string is rejected. Control characters, bidirectional overrides and zero-width characters are rejected.
3. Case convention.
The checker's lowercase-for-fields rule is suspended inside backticks.
`Content-Type`does not warn. The rule remains in force for identifier-form field names.4. Scope of relaxation.
value-block field declarations only. Entity field declarations remain identifier-only. Variant field declarations remain identifier-only. Config parameter names, derived value names, relationship names, rule/trigger/invariant/contract/surface names remain identifier-only. The language reference records the rationale as: "backticks are admitted at data-at-the-boundary positions (enum values, value-block field names) and forbidden at names of internal artefacts."5. Error messages.
When a reference site uses a bare identifier
fooand no such field exists, but a backtick-quoted field`foo`or a backtick-quoted field whose byte-equal form would befooexists on the referenced type, the checker emits a targeted error naming the backtick-quoted form and pointing at the declaration site. (The checker is not required to guess arbitrary external-string misspellings — only the no-op-unquoting case.)6. Transition graphs.
Transition graphs live only in entity and variant bodies. Backtick-quoted field names live only in
valueblocks. The two constructs do not interact: no field can be both backtick-quoted and the subject of atransitionsblock under the current language version. If a future ALP relaxes clause 4 to admit backtick-quoted entity fields, the transition-graph interaction would require its own analysis.7. Out of scope.
Open dictionaries /
Map<K, V>. Identifier positions outsidevalueblocks. Integer, pattern or computed keys. The ALP-017 provenance annotation. Runtime validation semantics.valueblocks remain closed records."Why value-block fields but not entity fields?"
I want to be upfront: the same argument applies to entities. Entities regularly model shapes defined by external systems — a
ZendeskTicketorJiraTicketmirrors what arrives over HTTP, and those fields are just as externally-mandated asvalue-block fields. I expect that if this lands forvalueblocks, entity fields will be the next request. Possibly my own.That said, I think
valueblocks are the right starting point. The immediate pain is sharpest here — well at least for us functional programmers asvaluetypes exist purely to describe data shapes. Entities carry more internal machinery (lifecycle, transitions, rules, derived values), so the interaction surface is larger and deserves its own analysis.If this were to expand to entities, I'd expect it limited to entities marked
external— the ones explicitly modelling shapes the system doesn't control. The checker could enforce this syntactically: backtick field names allowed afterexternal entityandvalue, rejected after plainentity. No backtick-quoted derived values, relationship names, or config parameters. And with the same "external standards" justification that enums use, not as a general escape hatch. That keeps the blast radius manageable while solving the real problem: modelling data shapes you don't always control.