Environment
Summary
The language reference defines a surface's related: clause as "associated surfaces reachable from this one; the parenthesised expression evaluates to the entity instance that the target surface's context clause binds to". Nothing restricts the target to the same module, and qualified names are the documented way to reference another module's declarations. But a related: entry naming an imported module's surface warns allium.reference.unknownName ("which imported module 'x' does not define"), while the module visibly declares surface X { ... }. Since check exits 1 on warnings, a cross-module navigation link cannot be expressed without a suppression.
Reproduction
merge.allium:
-- allium: 3
-- merge.allium
entity User {
name: String
}
surface MergeWizard {
facing admin: User
context source: User
exposes:
source.name
}
queue.allium:
-- allium: 3
-- queue.allium
use "./merge.allium" as merge
entity Refusal {
sso_user: merge/User?
label: String
}
surface RefusalQueue {
facing admin: merge/User
context refusal: Refusal
exposes:
refusal.label
related:
merge/MergeWizard(refusal.sso_user) when refusal.sso_user != null
}
allium check . (exit 1), warnings only:
warning allium.reference.unknownName ./queue.allium:20 Reference 'merge/MergeWizard' names 'MergeWizard', which imported module 'merge' does not define.
Expected: no warning (merge defines surface MergeWizard, whose context type User matches the expression's type).
Contrast: the same link written unqualified (MergeWizard(refusal.sso_user)) is recognised as a related-surface reference and drawn as allium.surface.relatedUndefined ("references unknown related surface") — so the checker knows the clause's semantics locally; it is only the cross-module offering that is missing.
Root cause (located)
crates/allium-parser/src/analysis.rs, collect_referenced_trigger_names (v3.6.1 line 4740) builds the set a module offers to importers from collect_trigger_outputs, collect_declared_names, deferred roots, config, and when:-referenced triggers. collect_declared_names (line 4672) matches Entity | ExternalEntity | Value | Enum | Actor | Contract blocks and variants; BlockKind::Surface is not in the list, so no surface name is ever offered, and the related: entry's qualified reference resolves against nothing.
Suggested fix
Offer surface names to importers — either by adding BlockKind::Surface to collect_declared_names (they are declared, module-level, PascalCase names exactly like contracts, which are already offered), or, if surfaces should stay out of the general offering, by resolving related: entries specifically against the imported module's surfaces and, ideally, checking the expression's type against that surface's context type (rule the reference already states for the local case). The first is a one-line change; a test pinning related: alias/Surface(x) as clean and related: alias/NoSuchSurface(x) as unknownName would fix the contract either way.
Real-world cost
A superadmin worklist surface in a ~190-spec production corpus offers each open row the account-merge wizard another module owns, pre-selected on the row's pair; that hand-off is the surface's load-bearing guarantee and related: is the construct for it. Under 3.6.1 it carries a line-scoped allium-ignore allium.reference.unknownName explaining that the checker does not export surfaces, which is the checker documented in the spec.
Environment
allium-x86_64-unknown-linux-gnu.tar.gz, sha256e00c99ae234b10207719257a70e7c2dcad73060864468cd293fb7ad40524e970, Linux x86_64.configanddeferredroots in 3.6.1 (check: qualified references to an imported module'sconfigwarnunknownName, contradicting the documented "Config parameter references" semantics;deferrednames are likewise unresolvable #89 / Offer config blocks and deferred declarations to importers #91); surfaces were never in it.Summary
The language reference defines a surface's
related:clause as "associated surfaces reachable from this one; the parenthesised expression evaluates to the entity instance that the target surface'scontextclause binds to". Nothing restricts the target to the same module, and qualified names are the documented way to reference another module's declarations. But arelated:entry naming an imported module's surface warnsallium.reference.unknownName("which imported module 'x' does not define"), while the module visibly declaressurface X { ... }. Sincecheckexits 1 on warnings, a cross-module navigation link cannot be expressed without a suppression.Reproduction
merge.allium:queue.allium:allium check .(exit 1), warnings only:Expected: no warning (
mergedefinessurface MergeWizard, whosecontexttypeUsermatches the expression's type).Contrast: the same link written unqualified (
MergeWizard(refusal.sso_user)) is recognised as a related-surface reference and drawn asallium.surface.relatedUndefined("references unknown related surface") — so the checker knows the clause's semantics locally; it is only the cross-module offering that is missing.Root cause (located)
crates/allium-parser/src/analysis.rs,collect_referenced_trigger_names(v3.6.1 line 4740) builds the set a module offers to importers fromcollect_trigger_outputs,collect_declared_names, deferred roots,config, andwhen:-referenced triggers.collect_declared_names(line 4672) matchesEntity | ExternalEntity | Value | Enum | Actor | Contractblocks and variants;BlockKind::Surfaceis not in the list, so no surface name is ever offered, and therelated:entry's qualified reference resolves against nothing.Suggested fix
Offer surface names to importers — either by adding
BlockKind::Surfacetocollect_declared_names(they are declared, module-level, PascalCase names exactly like contracts, which are already offered), or, if surfaces should stay out of the general offering, by resolvingrelated:entries specifically against the imported module's surfaces and, ideally, checking the expression's type against that surface'scontexttype (rule the reference already states for the local case). The first is a one-line change; a test pinningrelated: alias/Surface(x)as clean andrelated: alias/NoSuchSurface(x)asunknownNamewould fix the contract either way.Real-world cost
A superadmin worklist surface in a ~190-spec production corpus offers each open row the account-merge wizard another module owns, pre-selected on the row's pair; that hand-off is the surface's load-bearing guarantee and
related:is the construct for it. Under 3.6.1 it carries a line-scopedallium-ignore allium.reference.unknownNameexplaining that the checker does not export surfaces, which is the checker documented in the spec.