@hydranium/conformance 1.0.0-next.28 → 1.0.0-next.280

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/model.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
+ import type { Locale } from '@hydranium/protocol';
11
+
10
12
  /**
11
13
  * A fixture value that may be given directly or DEFERRED to check time.
12
14
  *
@@ -79,12 +81,53 @@ export function resolveModel(model: ConformanceModel): ResolvedConformanceModel
79
81
  export interface EditSpec {
80
82
  /** Replacement model text; {@link Deferred} for the same reasons as `ConformanceModel.text`. */
81
83
  readonly to: Deferred<string>;
84
+ /** True of the root after the edit and false of `valid`'s root, so a check can tell the two apart. */
82
85
  readonly expect: (root: unknown) => boolean;
83
86
  }
84
87
 
88
+ /**
89
+ * A reference-picker query for an element that does not exist yet, plus the
90
+ * candidate the adopter expects it to offer.
91
+ *
92
+ * **The URI a create-element flow holds is a FOLDER**, because the file is not
93
+ * written until the dialog is confirmed. A folder URI names no file and so
94
+ * carries no extension, which is the one shape a head cannot route to a grammar
95
+ * by URI alone — it has to resolve the language some other way. That makes this
96
+ * the create dialog's load-bearing precondition and the reason the query is
97
+ * worth a conformance check of its own: a head that gets it wrong answers no
98
+ * candidates or throws, and the dialog never opens.
99
+ */
100
+ export interface ReferenceQuerySpec {
101
+ /** AST type of the element being created — the synthetic source's own type. */
102
+ readonly type: string;
103
+ /** The reference property on the source (or on `syntheticPath`'s leaf) whose candidates the picker fills. */
104
+ readonly property: string;
105
+ /**
106
+ * Steps from the synthetic source down to the node holding `property`, when
107
+ * the reference is not on the source itself. Each step is
108
+ * `[containerProperty, type]` — the kit builds the `SyntheticStep`s, so the
109
+ * fixture names no protocol type.
110
+ */
111
+ readonly path?: ReadonlyArray<readonly [containerProperty: string, type: string]>;
112
+ /**
113
+ * Folder the create flow asks at. {@link Deferred} because a fixture may name
114
+ * a workspace the driver's `connect` only just created. Defaults to the parent
115
+ * of `valid.uri`, which is the folder a sibling of the valid model would go
116
+ * into — the common case, so most fixtures supply only `type` + `property`.
117
+ */
118
+ readonly folderUri?: Deferred<string>;
119
+ /**
120
+ * A candidate label the query MUST offer. Without it an empty result passes,
121
+ * and empty is exactly what the defect this check exists for produces — so
122
+ * the expectation is what makes the check discriminating rather than a
123
+ * smoke test.
124
+ */
125
+ readonly expectCandidate: string;
126
+ }
127
+
85
128
  /**
86
129
  * The per-language fixture. `valid` and `invalid` are defined once and reused
87
- * across heads; the two extras are per-head opt-ins.
130
+ * across heads; every other field is a per-head opt-in.
88
131
  *
89
132
  * Requiring BOTH a valid and an invalid model is the false-green guard — a
90
133
  * vacuous "invalid" model that actually parses clean fails the diagnostics
@@ -98,13 +141,18 @@ export interface EditSpec {
98
141
  * uses `invalid.text` and never calls `edit.expect`, so an LSP-only adopter
99
142
  * has nothing to supply here.
100
143
  * - `completionPosition` — read by the **LSP slice only**.
144
+ * - `referenceQuery` — read by the **data slice only**, and only when the
145
+ * driver supplies `references` (the reference surface is opt-in on the head
146
+ * too, so both halves have to be present for the check to run).
147
+ * - `dependent` and `breakingEdit` — read by the **data slice only**.
148
+ * - `renderedDiagnostic` — read by the **LSP slice only**.
101
149
  *
102
- * Both extras are optional and their checks report *skipped* when absent,
103
- * rather than silently not running. Making either mandatory would defeat the
104
- * shared fixture: the head that ignores the field has to invent a value for it,
105
- * and the two heads' inventions drift apart until one fixture contradicts the
106
- * other. A required field that one head ignores does not produce a shared
107
- * fixture; it produces a misleading one.
150
+ * The opt-ins are optional and their checks report *skipped* when absent,
151
+ * rather than silently not running. Making any of them mandatory would defeat
152
+ * the shared fixture: the head that ignores the field has to invent a value
153
+ * for it, and the two heads' inventions drift apart until one fixture
154
+ * contradicts the other. A required field that one head ignores does not
155
+ * produce a shared fixture; it produces a misleading one.
108
156
  *
109
157
  * The GLSP fixture is separate (per-diagram-type, generic over the adopter
110
158
  * action type) — see `./glsp`.
@@ -114,9 +162,87 @@ export interface LanguageFixture {
114
162
  readonly invalid: ConformanceModel;
115
163
  /**
116
164
  * Optional: an edit plus its observability assertion. The data slice's
117
- * round-trip and subscription-delivery checks run only when it is supplied.
165
+ * checks that need a second, observably different text run only when it is
166
+ * supplied.
118
167
  */
119
168
  readonly edit?: EditSpec;
120
169
  /** Optional: a position at which the LSP completion check requests completion. */
121
170
  readonly completionPosition?: { readonly line: number; readonly character: number };
171
+ /**
172
+ * Optional: the create-dialog reference query. Read by the **data slice
173
+ * only**, and only when the driver exposes the opt-in reference surface.
174
+ */
175
+ readonly referenceQuery?: ReferenceQuerySpec;
176
+ /**
177
+ * Optional: a second document that REFERENCES {@link valid}, so the data
178
+ * slice can provoke a CASCADE — a rebuild of this document caused by
179
+ * editing the one it points at, with its own text never touched.
180
+ *
181
+ * Opt-in because a grammar need not have cross-document references at all,
182
+ * and because only the adopter knows which pair of documents forms one.
183
+ * Supplying it IS the claim that editing `valid` rebuilds this document; the
184
+ * check then holds the head to reporting that on `onDocumentsBuilt`, which
185
+ * is the only channel that can carry it — the document has no subscriber and
186
+ * its file did not change, so neither the update channel nor a filesystem
187
+ * watcher can.
188
+ */
189
+ readonly dependent?: ConformanceModel;
190
+ /**
191
+ * Optional: a replacement text for {@link valid} that changes what
192
+ * {@link dependent} shows, typically by breaking its reference into
193
+ * `valid`, so that the dependent reports a diagnostic. Read by the **data
194
+ * slice only**, and only with `dependent`.
195
+ *
196
+ * {@link edit} does not serve here: an edit that leaves the dependent as it
197
+ * was may send a watcher of the dependent nothing, as the framework's head
198
+ * suppresses an update that changes nothing observable. Supplying this is
199
+ * the claim that writing it makes a watched `dependent` receive an update
200
+ * event, which the check then holds to being a rebuild that names no
201
+ * client.
202
+ */
203
+ readonly breakingEdit?: Deferred<string>;
204
+ /**
205
+ * Optional: a locale plus the sentence the server must publish in it. Read
206
+ * by the **LSP slice only**.
207
+ */
208
+ readonly renderedDiagnostic?: RenderedDiagnosticSpec;
209
+ }
210
+
211
+ /**
212
+ * A locale, and one sentence the server must produce in it for the `invalid`
213
+ * fixture.
214
+ *
215
+ * **Opt-in, and it has to be.** The framework ships no catalogue and selects no
216
+ * locale, so a server that installs no renderer correctly publishes English —
217
+ * mandating this check would fail every adopter without i18n for doing the right
218
+ * thing. Supplying the field is the adopter saying "I render server-side, hold me
219
+ * to it".
220
+ *
221
+ * `expected` is a SUBSTRING, not the whole message. The kit owns no grammar, so
222
+ * it cannot know how many diagnostics `invalid` produces or in what order, and
223
+ * an adopter should be able to pin the translated fragment without restating a
224
+ * sentence they may reword. A substring long enough to be wrong if the render
225
+ * did not happen is the whole requirement.
226
+ *
227
+ * `absentWithLocale` is what makes the check a pair rather than a single
228
+ * assertion: "the message contains X" also passes for a server whose English
229
+ * happens to contain X, and for one that renders regardless of locale. Naming
230
+ * the fragment that must disappear once the locale is declared is what
231
+ * distinguishes those.
232
+ */
233
+ export interface RenderedDiagnosticSpec {
234
+ /** The locale to declare at `initialize` — the tag whose catalogue the server has. */
235
+ readonly locale: Locale;
236
+ /** A fragment of the translated sentence, present in some diagnostic of the `invalid` fixture. */
237
+ readonly expected: string;
238
+ /**
239
+ * A fragment that must be absent once `locale` is declared, and present
240
+ * without it — normally a piece of the server's own English.
241
+ *
242
+ * Optional only because a catalogue may translate a message whose English
243
+ * shares no distinctive fragment with it. Omitting it drops the second half
244
+ * of the pair and leaves a check that a render-nothing server can pass; the
245
+ * kit reports that rather than pretending otherwise.
246
+ */
247
+ readonly absentWithLocale?: string;
122
248
  }
@@ -57,7 +57,7 @@ const vitestRunner: ConformanceRunner = {
57
57
  export function runDataConformance<TTransfer extends TransferElement, TDiagnostic extends TransferDiagnostic = TransferDiagnostic>(
58
58
  options: DataConformanceOptions<TTransfer, TDiagnostic>
59
59
  ): void {
60
- emitConformanceSuite(vitestRunner, options.suiteTitle ?? 'conformance: data-server', buildDataChecks(options));
60
+ emitConformanceSuite(vitestRunner, options.suiteTitle ?? 'data-server conformance', buildDataChecks(options));
61
61
  }
62
62
 
63
63
  /**
@@ -66,7 +66,7 @@ export function runDataConformance<TTransfer extends TransferElement, TDiagnosti
66
66
  * per language) and a ran-vs-skipped summary.
67
67
  */
68
68
  export function runLspConformance(options: LspConformanceOptions): void {
69
- emitConformanceSuite(vitestRunner, options.suiteTitle ?? 'conformance: lsp', buildLspChecks(options));
69
+ emitConformanceSuite(vitestRunner, options.suiteTitle ?? 'lsp conformance', buildLspChecks(options));
70
70
  }
71
71
 
72
72
  /**
@@ -77,5 +77,5 @@ export function runLspConformance(options: LspConformanceOptions): void {
77
77
  export function runGlspConformance<TAction, TDriver extends GlspConformanceDriver<TAction>>(
78
78
  options: GlspConformanceOptions<TAction, TDriver>
79
79
  ): void {
80
- emitConformanceSuite(vitestRunner, options.suiteTitle ?? 'conformance: glsp', buildGlspChecks(options));
80
+ emitConformanceSuite(vitestRunner, options.suiteTitle ?? 'glsp conformance', buildGlspChecks(options));
81
81
  }