ID |
|
|---|---|
Status |
Backlog |
Bucket |
architecture |
Priority |
6 |
Theme |
lsp |
Updated |
2026-08-06 |
Parent-context-aware schema coordinates for per-directive Behavior policy
Premise partly overtaken by R347 Slice 2 (noted 2026-07-15). R347 (
lsp-structural-consolidation) Slice 2 shipped a standaloneDirectivePolicy(graphitron-lsp/.../parsing/DirectivePolicy.java):bindsLiveMethod(name)now owns the formerMETHOD_VALIDATING_DIRECTIVESset, and the five copy-pasted@recordcarve-outs plus the privately-owned method-validating set route through it. That is a third shape R123’s A/B forks never considered, and it already resolves the set-duplication half of the smell below (step 4). It did not, however, make the per-directive discrimination stop being a name lookup: Diagnostics still callsDirectivePolicy.bindsLiveMethod(directiveName)at validation time. R123’s surviving value is exactly fork A, the parent-context coordinate that carries the policy on the model so no consumer re-derives it by directive name. Rework this item onto the shippedDirectivePolicybaseline before implementing; do not implement the stale step 4 (the set it names is already centralized).
Problem
R119 keyed the LSP’s directive vocabulary on GraphQL-spec schema coordinates: Directive, DirectiveArg, InputType, InputField. The granularity is correct per the GraphQL spec, but it under-specifies one axis the LSP needs.
InputField("ExternalCodeReference", "method") is one coordinate, shared across every directive that carries an ECR slot:
-
@service,@condition,@externalField,@reference(path:[{condition:}]); themethod:value is a method invocation. Validate as a method on the siblingclassName. -
@record,@enum; themethod:value wraps a type, not a method invocation. Skip method validation.
The canonical overlay binds the shared coordinate to one MethodNameBinding(classNameCoord) arm. Diagnostics' validator then re-discriminates by the enclosing directive name; since R347 Slice 2 that discrimination is DirectivePolicy.bindsLiveMethod(directiveName) (graphitron-lsp/.../parsing/DirectivePolicy.java, called from Diagnostics.java:734 at the time of writing), which centralizes the former hand-coded METHOD_VALIDATING_DIRECTIVES set but still keys on the directive name at validation time rather than on the coordinate.
That set is exactly the smell development-principles.adoc calls "two consumers evaluate the same predicate over a model field" ; the Behavior overlay says "this is a method slot", the validator says "but only for these directives". The classifier knows the per-directive policy at parse time; collapsing both onto one arm and re-deriving via Set.contains(directiveName) in Diagnostics is the smell.
The same shape would surface for any future Behavior arm whose semantics differ by enclosing directive (the spec’s "structurally inert on @externalField / @enum / @record" rule for argMapping is the next one queued).
Two design forks
A. Parent-context-aware SchemaCoordinate. Extend the sealed hierarchy so the same input-field can carry distinct coordinates depending on its enclosing directive:
public sealed interface SchemaCoordinate {
record Directive(String name) implements SchemaCoordinate {}
record DirectiveArg(String directive, String arg) implements SchemaCoordinate {}
record InputType(String name) implements SchemaCoordinate {}
record InputField(String type, String field) implements SchemaCoordinate {}
record DirectiveArgInputField(String directive, String arg, String inputType, String field)
implements SchemaCoordinate {} // the new arm
}
The canonical overlay binds the new arm per (directive, arg, type, field) combination instead of (type, field). Diagnostics' METHOD_VALIDATING_DIRECTIVES set retires; the overlay carries the per-directive-context decision once.
B. Per-policy axis on Behavior. Keep the coordinate granularity; widen the arm:
public sealed interface Behavior {
record MethodNameBinding(SchemaCoordinate classNameCoord, ValidateAs validate)
implements Behavior {}
enum ValidateAs { METHOD_INVOCATION, TYPE_WRAPPER }
}
The canonical overlay still has one binding for ExternalCodeReference.method, but it must now describe both meanings simultaneously ; which it can’t, because the same coordinate can mean either depending on context. So this fork only works if (A) is also applied: distinct coordinates carry distinct ValidateAs values.
In other words, B is a follower of A; A alone solves the problem.
Recommended shape
Pick A. The new arm is purely additive: existing consumers continue to handle the four GraphQL-spec coordinates; the new DirectiveArgInputField arm fires for the parent-context-aware overlay entries.
The migration:
-
Add
DirectiveArgInputFieldtoSchemaCoordinate. -
Update
LspVocabulary.coordinateAtandleafCoordinatesto emit the new arm when descending from a directive arg into an input type’s field. Today’sInputField(type, field)becomesDirectiveArgInputField(directive, arg, type, field). Unconditional ; the cursor is always inside some directive’s argument tree, so the parent context always exists. -
Update
CanonicalOverlayto bind the seven method-validating directives toMethodNameBindingand not bind@record/`@enum’s method. -
(Largely done by R347 Slice 2.) The
METHOD_VALIDATING_DIRECTIVESset is already centralized intoDirectivePolicy.bindsLiveMethod; what remains for fork A is to make the coordinate carry the policy soDiagnosticsandMethodCompletions.generatestop callingbindsLiveMethod(directiveName)and read the decision off theDirectivePolicybinding directly. Rework this step against the shipped predicate rather than the retired set. -
The structural startup invariant continues to fire on every overlay coordinate.
Step 2 is the load-bearing change. The 4-arm SchemaCoordinate is replaced wholesale by a 5-arm hierarchy with InputField(type, field) retained for any future "type-keyed coordinate" use case (none today inside the LSP, but it’s the more-general arm).
Alternative shape worth considering: drop InputField(type, field) entirely and have DirectiveArgInputField always carry the parent-context. Smaller hierarchy, but it would prevent future use cases that key purely on input-type position. Defer the choice to the implementer; the spec is the right place to decide.
Out of scope
-
The
argMappingcontent-syntax inertness rule R119 spec body line 286-288 names. That rule is also a per-enclosing-directive policy ("structurally inert on @externalField / @enum / @record"), and would benefit from the same parent-context-aware coordinate split. R119 ships withArgMappingBindingas a marker; this item’s resolution unlocks the parent-context split forargMappingtoo. Filing the content-syntax validator as a separate item is correct; the policy axis lands here.
Surfaced from
R119 phase 3 self-review (architect agent pass on commit 4ae827d). Findings #1 and #2 from that review fold into this single item. The architect’s stronger-shape recommendation: option A (extend SchemaCoordinate).
Fact-base note (2026-08-06)
Fork A’s surviving ask (the parent-context policy carried on the model so no consumer re-derives it by directive name) is store-delivered: applied_ and intent_ rows key by coordinate with the parent in the key, so the policy read becomes a join rather than a name lookup. Close as delivered when the capture relations are consumable from the LSP, or re-scope to that read.
Context and the whole-board picture: roadmap/audits/2026-08-06-fact-base-impact-sweep.md.