@zenithbuild/language-server 0.5.0-beta.2.19 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2026-02-28
9
+
10
+ ### Added
11
+
12
+ - `zenith.strictDomLints` setting: when `true`, ZEN-DOM-* diagnostics are errors; when `false`, hints
13
+ - Diagnostics from compiler JSON warnings (schemaVersion=1 contract)
14
+ - Debounce and cancellation for compile requests
15
+ - On-save validation
16
+ - Code actions for ZEN-DOM-* (querySelector → ref, addEventListener → zenOn)
17
+ - Code actions for window/document → zenWindow/zenDocument
18
+ - Completions for canonical primitives (ref, signal, state, zenOn, zenMount, etc.)
19
+
20
+ ### Changed
21
+
22
+ - Requires @zenithbuild/compiler ^0.6.0 (schemaVersion + warnings contract)
23
+
8
24
  ## [0.2.8] - 2026-01-26
9
25
 
10
26
  ### 📝 Other Changes
@@ -0,0 +1,39 @@
1
+ # @zenithbuild/language-server v0.6.0
2
+
3
+ ## Summary
4
+
5
+ - ZEN-DOM-* diagnostics from compiler JSON; severity flips with `strictDomLints`
6
+ - Debounced diagnostics (150ms) + on-save validation; last edit wins
7
+ - Code actions for ZEN-DOM-QUERY, ZEN-DOM-LISTENER, ZEN-DOM-WRAPPER + window/document convenience
8
+
9
+ ## Breaking Changes
10
+
11
+ None.
12
+
13
+ ## Key Changes
14
+
15
+ - **Diagnostics:** Surface compiler `warnings` as LSP diagnostics; `strictDomLints` setting (warning → error)
16
+ - **Debounce:** 150ms idle before validation; immediate validation on save
17
+ - **Cancellation:** Only latest validation sends diagnostics
18
+ - **Code actions:** Suppress/ref for ZEN-DOM-QUERY; zenOn template for ZEN-DOM-LISTENER; zenWindow/zenDocument for ZEN-DOM-WRAPPER
19
+ - **Convenience:** "Replace with zenWindow()" / "Replace with zenDocument()" on identifier selection (no diagnostic required)
20
+ - **Completions:** zenMount, zenWindow, zenDocument, zenOn, zenResize, collectRefs, signal, ref; soft suggestions for window/document
21
+
22
+ ## Diagnostics / UX Highlights
23
+
24
+ | Code | Default | strictDomLints: true |
25
+ |------|---------|---------------------|
26
+ | ZEN-DOM-QUERY | Warning | Error |
27
+ | ZEN-DOM-LISTENER | Warning | Error |
28
+ | ZEN-DOM-WRAPPER | Warning | Error |
29
+
30
+ ## Upgrade Notes
31
+
32
+ Requires `@zenithbuild/compiler` with JSON `schemaVersion` and `warnings` (v0.6.0+).
33
+
34
+ ## Verification Checklist
35
+
36
+ - [ ] Type `document.querySelector(` → ZEN-DOM-QUERY warning + quick fixes
37
+ - [ ] Toggle `zenith.strictDomLints` → severity flips to error
38
+ - [ ] Select `window` → "Replace with zenWindow()" code action
39
+ - [ ] Rapid typing does not spawn excessive compiler processes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/language-server",
3
- "version": "0.5.0-beta.2.19",
3
+ "version": "0.6.0",
4
4
  "description": "Language Server for Zenith Framework",
5
5
  "main": "./dist/server.js",
6
6
  "types": "./dist/server.d.ts",
@@ -16,7 +16,7 @@
16
16
  "release:major": "bun run scripts/release.ts --bump=major"
17
17
  },
18
18
  "dependencies": {
19
- "@zenithbuild/compiler": "^1.3.0",
19
+ "@zenithbuild/compiler": "^0.6.0",
20
20
  "vscode-languageserver": "^9.0.1",
21
21
  "vscode-languageserver-textdocument": "^1.0.11"
22
22
  },
@@ -5,6 +5,10 @@ import {
5
5
 
6
6
  export const EVENT_BINDING_DIAGNOSTIC_CODE = 'zenith.event.binding.syntax';
7
7
 
8
+ export const ZEN_DOM_QUERY = 'ZEN-DOM-QUERY';
9
+ export const ZEN_DOM_LISTENER = 'ZEN-DOM-LISTENER';
10
+ export const ZEN_DOM_WRAPPER = 'ZEN-DOM-WRAPPER';
11
+
8
12
  export interface EventBindingCodeActionData {
9
13
  replacement: string;
10
14
  title: string;
@@ -22,6 +26,9 @@ export interface ZenithCodeAction {
22
26
 
23
27
  interface ZenithTextDocumentLike {
24
28
  uri: string;
29
+ getText(): string;
30
+ positionAt(offset: number): { line: number; character: number };
31
+ offsetAt(position: { line: number; character: number }): number;
25
32
  }
26
33
 
27
34
  export function buildEventBindingCodeActions(
@@ -58,3 +65,155 @@ export function buildEventBindingCodeActions(
58
65
 
59
66
  return actions;
60
67
  }
68
+
69
+ export function buildDomLintCodeActions(
70
+ document: ZenithTextDocumentLike,
71
+ diagnostics: ZenithDiagnostic[]
72
+ ): ZenithCodeAction[] {
73
+ const actions: ZenithCodeAction[] = [];
74
+ const text = document.getText();
75
+
76
+ for (const diagnostic of diagnostics) {
77
+ const code = diagnostic.code;
78
+ if (code !== ZEN_DOM_QUERY && code !== ZEN_DOM_LISTENER && code !== ZEN_DOM_WRAPPER) {
79
+ continue;
80
+ }
81
+
82
+ const startOffset = document.offsetAt(diagnostic.range.start);
83
+ const endOffset = document.offsetAt(diagnostic.range.end);
84
+ const lineStart = text.lastIndexOf('\n', startOffset) + 1;
85
+ const lineEnd = text.indexOf('\n', endOffset);
86
+ const lineEndOffset = lineEnd === -1 ? text.length : lineEnd;
87
+ const lineContent = text.substring(lineStart, lineEndOffset);
88
+
89
+ if (code === ZEN_DOM_QUERY) {
90
+ const insertPos = { line: diagnostic.range.start.line, character: 0 };
91
+ actions.push({
92
+ title: 'Suppress with // zen-allow:dom-query <reason>',
93
+ kind: 'quickfix',
94
+ diagnostics: [diagnostic],
95
+ edit: {
96
+ changes: {
97
+ [document.uri]: [{
98
+ range: { start: insertPos, end: insertPos },
99
+ newText: '// zen-allow:dom-query <reason>\n'
100
+ }]
101
+ }
102
+ }
103
+ });
104
+ actions.push({
105
+ title: 'Convert to ref() (partial / TODO)',
106
+ kind: 'quickfix',
107
+ diagnostics: [diagnostic],
108
+ edit: {
109
+ changes: {
110
+ [document.uri]: [{
111
+ range: { start: insertPos, end: insertPos },
112
+ newText: '// TODO: use ref<T>() + zenMount instead\nconst elRef = ref<HTMLElement>();\n'
113
+ }]
114
+ }
115
+ }
116
+ });
117
+ } else if (code === ZEN_DOM_LISTENER) {
118
+ const insertPos = { line: diagnostic.range.start.line, character: 0 };
119
+ const lineRange = {
120
+ start: document.positionAt(lineStart),
121
+ end: document.positionAt(lineEndOffset)
122
+ };
123
+ const commentedLine = lineContent.replace(/^(\s*)/, '$1// ');
124
+ actions.push({
125
+ title: 'Replace with zenOn template',
126
+ kind: 'quickfix',
127
+ diagnostics: [diagnostic],
128
+ edit: {
129
+ changes: {
130
+ [document.uri]: [
131
+ {
132
+ range: { start: insertPos, end: insertPos },
133
+ newText: '// zenOn(target, eventName, handler) - register disposer via ctx.cleanup\n// const off = zenOn(doc, \'keydown\', handler); ctx.cleanup(off);\n'
134
+ },
135
+ {
136
+ range: lineRange,
137
+ newText: commentedLine
138
+ }
139
+ ]
140
+ }
141
+ }
142
+ });
143
+ } else if (code === ZEN_DOM_WRAPPER) {
144
+ let newText = lineContent;
145
+ if (lineContent.includes('window') && !lineContent.includes('zenWindow')) {
146
+ newText = newText.replace(/\bwindow\b/g, 'zenWindow()');
147
+ }
148
+ if (lineContent.includes('document') && !lineContent.includes('zenDocument')) {
149
+ newText = newText.replace(/\bdocument\b/g, 'zenDocument()');
150
+ }
151
+ if (lineContent.includes('globalThis.window')) {
152
+ newText = newText.replace(/globalThis\.window/g, 'zenWindow()');
153
+ }
154
+ if (lineContent.includes('globalThis.document')) {
155
+ newText = newText.replace(/globalThis\.document/g, 'zenDocument()');
156
+ }
157
+ if (newText !== lineContent) {
158
+ actions.push({
159
+ title: 'Replace with zenWindow() / zenDocument()',
160
+ kind: 'quickfix',
161
+ diagnostics: [diagnostic],
162
+ edit: {
163
+ changes: {
164
+ [document.uri]: [{
165
+ range: {
166
+ start: document.positionAt(lineStart),
167
+ end: document.positionAt(lineEndOffset)
168
+ },
169
+ newText
170
+ }]
171
+ }
172
+ }
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ return actions;
179
+ }
180
+
181
+ /**
182
+ * Convenience code actions: Replace window/document with zenWindow()/zenDocument()
183
+ * even when there is no ZEN-DOM-WRAPPER diagnostic.
184
+ */
185
+ export function buildWindowDocumentCodeActions(
186
+ document: ZenithTextDocumentLike,
187
+ range: ZenithRange
188
+ ): ZenithCodeAction[] {
189
+ const text = document.getText();
190
+ const startOffset = document.offsetAt(range.start);
191
+ const endOffset = document.offsetAt(range.end);
192
+ const selected = text.substring(startOffset, endOffset);
193
+
194
+ if (selected === 'window') {
195
+ return [{
196
+ title: 'Replace with zenWindow()',
197
+ kind: 'refactor',
198
+ diagnostics: [],
199
+ edit: {
200
+ changes: {
201
+ [document.uri]: [{ range, newText: 'zenWindow()' }]
202
+ }
203
+ }
204
+ }];
205
+ }
206
+ if (selected === 'document') {
207
+ return [{
208
+ title: 'Replace with zenDocument()',
209
+ kind: 'refactor',
210
+ diagnostics: [],
211
+ edit: {
212
+ changes: {
213
+ [document.uri]: [{ range, newText: 'zenDocument()' }]
214
+ }
215
+ }
216
+ }];
217
+ }
218
+ return [];
219
+ }
@@ -155,7 +155,33 @@ export async function collectDiagnostics(
155
155
  try {
156
156
  process.env.ZENITH_CACHE = '1';
157
157
  const { compile } = await import('@zenithbuild/compiler');
158
- await compile(text, filePath);
158
+ const result = await compile(text, filePath);
159
+
160
+ // 2) Surface ZEN-DOM-* warnings from compiler JSON as LSP diagnostics.
161
+ interface CompilerWarning {
162
+ code?: string;
163
+ message?: string;
164
+ range?: { start?: { line?: number; column?: number }; end?: { line?: number; column?: number } };
165
+ }
166
+ const warnings: CompilerWarning[] = (result as { warnings?: CompilerWarning[] }).warnings ?? [];
167
+ const domLintSeverity = settings.strictDomLints ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning;
168
+ for (const w of warnings) {
169
+ const range = w.range;
170
+ const startLine = (range?.start?.line ?? 1) - 1;
171
+ const startChar = (range?.start?.column ?? 1) - 1;
172
+ const endLine = (range?.end?.line ?? range?.start?.line ?? 1) - 1;
173
+ const endChar = (range?.end?.column ?? range?.start?.column ?? 1);
174
+ diagnostics.push({
175
+ severity: domLintSeverity,
176
+ range: {
177
+ start: { line: startLine, character: startChar },
178
+ end: { line: endLine, character: endChar }
179
+ },
180
+ message: w.message ?? 'DOM lint',
181
+ source: 'zenith-compiler',
182
+ code: w.code
183
+ });
184
+ }
159
185
  } catch (error: any) {
160
186
  const message = String(error?.message || 'Unknown compiler error');
161
187
  const isContractViolation = message.includes(COMPONENT_SCRIPT_CONTRACT_MESSAGE);
package/src/server.ts CHANGED
@@ -66,7 +66,7 @@ import {
66
66
  } from './router';
67
67
 
68
68
  import { collectDiagnostics } from './diagnostics';
69
- import { buildEventBindingCodeActions } from './code-actions';
69
+ import { buildEventBindingCodeActions, buildDomLintCodeActions, buildWindowDocumentCodeActions } from './code-actions';
70
70
  import { DEFAULT_SETTINGS, normalizeSettings, ZenithServerSettings } from './settings';
71
71
 
72
72
  // Create connection and document manager
@@ -78,9 +78,10 @@ let projectGraphs: Map<string, ProjectGraph> = new Map();
78
78
  let workspaceFolders: string[] = [];
79
79
  let globalSettings: ZenithServerSettings = DEFAULT_SETTINGS;
80
80
 
81
- // Lifecycle hooks with documentation
81
+ // Lifecycle hooks and platform primitives with documentation
82
82
  const LIFECYCLE_HOOKS = [
83
83
  { name: 'state', doc: 'Declare a reactive state variable', snippet: 'state ${1:name} = ${2:value}', kind: CompletionItemKind.Keyword },
84
+ { name: 'zenMount', doc: 'Mount callback with ctx.cleanup for disposers', snippet: 'zenMount((ctx) => {\n\t$0\n})', kind: CompletionItemKind.Function },
84
85
  { name: 'zenOnMount', doc: 'Called when component is mounted to the DOM', snippet: 'zenOnMount(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
85
86
  { name: 'zenOnDestroy', doc: 'Called when component is removed from the DOM', snippet: 'zenOnDestroy(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
86
87
  { name: 'zenOnUpdate', doc: 'Called after any state update causes a re-render', snippet: 'zenOnUpdate(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
@@ -88,6 +89,16 @@ const LIFECYCLE_HOOKS = [
88
89
  { name: 'useFetch', doc: 'Fetch data with caching and SSG support', snippet: 'useFetch("${1:url}")', kind: CompletionItemKind.Function }
89
90
  ];
90
91
 
92
+ const PLATFORM_PRIMITIVES = [
93
+ { name: 'zenWindow', doc: 'SSR-safe window access (returns null when not in browser)', snippet: 'zenWindow()', kind: CompletionItemKind.Function },
94
+ { name: 'zenDocument', doc: 'SSR-safe document access (returns null when not in browser)', snippet: 'zenDocument()', kind: CompletionItemKind.Function },
95
+ { name: 'zenOn', doc: 'Event subscription with disposer; register via ctx.cleanup', snippet: 'zenOn(${1:target}, \'${2:event}\', ${3:handler})', kind: CompletionItemKind.Function },
96
+ { name: 'zenResize', doc: 'Window resize handler; returns disposer for ctx.cleanup', snippet: 'zenResize(({ w, h }) => {\n\t$0\n})', kind: CompletionItemKind.Function },
97
+ { name: 'collectRefs', doc: 'Collect multiple refs into a deterministic node list', snippet: 'collectRefs(${1:refA}, ${2:refB})', kind: CompletionItemKind.Function },
98
+ { name: 'signal', doc: 'Create a signal for explicit get/set', snippet: 'signal(${1:0})', kind: CompletionItemKind.Function },
99
+ { name: 'ref', doc: 'Create a ref for DOM node or value', snippet: 'ref<${1:HTMLElement}>()', kind: CompletionItemKind.Function }
100
+ ];
101
+
91
102
  // Common HTML elements
92
103
  const HTML_ELEMENTS = [
93
104
  { tag: 'div', doc: 'Generic container element' },
@@ -360,6 +371,44 @@ connection.onCompletion((params: TextDocumentPositionParams): CompletionItem[] =
360
371
  }
361
372
  }
362
373
 
374
+ // Platform primitives (zenWindow, zenDocument, zenOn, zenResize, collectRefs, signal, ref)
375
+ for (const prim of PLATFORM_PRIMITIVES) {
376
+ if (!ctx.currentWord || prim.name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
377
+ completions.push({
378
+ label: prim.name,
379
+ kind: prim.kind,
380
+ detail: 'Zenith Platform',
381
+ documentation: { kind: MarkupKind.Markdown, value: prim.doc },
382
+ insertText: prim.snippet,
383
+ insertTextFormat: InsertTextFormat.Snippet,
384
+ sortText: `0_${prim.name}`
385
+ });
386
+ }
387
+ }
388
+
389
+ // Soft suggestions: window -> zenWindow, document -> zenDocument (do not block normal JS)
390
+ const lc = ctx.currentWord.toLowerCase();
391
+ if (lc === 'window' || lc.startsWith('wind')) {
392
+ completions.push({
393
+ label: 'zenWindow',
394
+ kind: CompletionItemKind.Function,
395
+ detail: 'Zenith (SSR-safe)',
396
+ documentation: { kind: MarkupKind.Markdown, value: 'Use zenWindow() instead of window for SSR-safe access.' },
397
+ insertText: 'zenWindow()',
398
+ sortText: '0_zenWindow'
399
+ });
400
+ }
401
+ if (lc === 'document' || lc.startsWith('doc')) {
402
+ completions.push({
403
+ label: 'zenDocument',
404
+ kind: CompletionItemKind.Function,
405
+ detail: 'Zenith (SSR-safe)',
406
+ documentation: { kind: MarkupKind.Markdown, value: 'Use zenDocument() instead of document for SSR-safe access.' },
407
+ insertText: 'zenDocument()',
408
+ sortText: '0_zenDocument'
409
+ });
410
+ }
411
+
363
412
  // Router hooks when router is imported
364
413
  if (routerEnabled) {
365
414
  for (const hook of Object.values(ROUTER_HOOKS)) {
@@ -674,8 +723,10 @@ connection.onCodeAction((params: CodeActionParams): CodeAction[] => {
674
723
  if (!document) {
675
724
  return [];
676
725
  }
677
-
678
- return buildEventBindingCodeActions(document, params.context.diagnostics);
726
+ const eventActions = buildEventBindingCodeActions(document, params.context.diagnostics);
727
+ const domLintActions = buildDomLintCodeActions(document, params.context.diagnostics);
728
+ const windowDocActions = buildWindowDocumentCodeActions(document, params.range);
729
+ return [...eventActions, ...domLintActions, ...windowDocActions];
679
730
  });
680
731
 
681
732
  connection.onHover((params: TextDocumentPositionParams): Hover | null => {
@@ -822,9 +873,26 @@ connection.onHover((params: TextDocumentPositionParams): Hover | null => {
822
873
  return null;
823
874
  });
824
875
 
825
- // Validate documents and provide diagnostics
876
+ // Debounce + cancellation for diagnostics (prevents editor lag from rapid typing)
877
+ const DEBOUNCE_MS = 150;
878
+ const validationTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
879
+ const validationIds = new Map<string, number>();
880
+
826
881
  documents.onDidChangeContent(change => {
827
- validateDocument(change.document);
882
+ const uri = change.document.uri;
883
+ const existing = validationTimeouts.get(uri);
884
+ if (existing) clearTimeout(existing);
885
+ validationTimeouts.set(
886
+ uri,
887
+ setTimeout(() => {
888
+ validationTimeouts.delete(uri);
889
+ validateDocument(change.document);
890
+ }, DEBOUNCE_MS)
891
+ );
892
+ });
893
+
894
+ documents.onDidSave(event => {
895
+ validateDocument(event.document);
828
896
  });
829
897
 
830
898
  documents.onDidOpen(event => {
@@ -832,11 +900,17 @@ documents.onDidOpen(event => {
832
900
  });
833
901
 
834
902
  async function validateDocument(document: TextDocument) {
835
- const graph = getProjectGraph(document.uri);
836
- const filePath = document.uri.replace('file://', '');
903
+ const uri = document.uri;
904
+ const id = (validationIds.get(uri) ?? 0) + 1;
905
+ validationIds.set(uri, id);
906
+
907
+ const graph = getProjectGraph(uri);
908
+ const filePath = uri.replace('file://', '');
837
909
  const projectRoot = detectProjectRoot(path.dirname(filePath), workspaceFolders);
838
910
  const diagnostics = await collectDiagnostics(document, graph, globalSettings, projectRoot);
839
- connection.sendDiagnostics({ uri: document.uri, diagnostics });
911
+
912
+ if (validationIds.get(uri) !== id) return;
913
+ connection.sendDiagnostics({ uri, diagnostics });
840
914
  }
841
915
 
842
916
  connection.onDidChangeConfiguration((change) => {
package/src/settings.ts CHANGED
@@ -2,14 +2,17 @@ export type ComponentScriptsMode = 'forbid' | 'allow';
2
2
 
3
3
  export interface ZenithServerSettings {
4
4
  componentScripts: ComponentScriptsMode;
5
+ strictDomLints: boolean;
5
6
  }
6
7
 
7
8
  export const DEFAULT_SETTINGS: ZenithServerSettings = Object.freeze({
8
- componentScripts: 'forbid'
9
+ componentScripts: 'forbid',
10
+ strictDomLints: false
9
11
  });
10
12
 
11
13
  export function normalizeSettings(input: unknown): ZenithServerSettings {
12
- const maybe = (input || {}) as { componentScripts?: unknown };
14
+ const maybe = (input || {}) as { componentScripts?: unknown; strictDomLints?: unknown };
13
15
  const mode = maybe.componentScripts === 'allow' ? 'allow' : 'forbid';
14
- return { componentScripts: mode };
16
+ const strictDomLints = maybe.strictDomLints === true;
17
+ return { componentScripts: mode, strictDomLints };
15
18
  }
@@ -1,9 +1,9 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
 
4
- import { collectContractDiagnostics, CONTRACT_MESSAGES } from '../src/diagnostics';
4
+ import { collectDiagnostics, collectContractDiagnostics, CONTRACT_MESSAGES } from '../src/diagnostics';
5
5
  import { buildEventBindingCodeActions } from '../src/code-actions';
6
- import { DEFAULT_SETTINGS } from '../src/settings';
6
+ import { DEFAULT_SETTINGS, normalizeSettings } from '../src/settings';
7
7
 
8
8
  const PROJECT_ROOT = '/tmp/zenith-site';
9
9
 
@@ -99,3 +99,22 @@ test('css import contract allows local precompiled css with suffixes', () => {
99
99
  assert.ok(!messages.includes(CONTRACT_MESSAGES.cssBareImport));
100
100
  assert.ok(!messages.includes(CONTRACT_MESSAGES.cssEscape));
101
101
  });
102
+
103
+ test('ZEN-DOM-QUERY diagnostic appears for querySelector and severity maps with strictDomLints', async () => {
104
+ const document = doc(
105
+ 'file:///tmp/zenith-site/src/pages/index.zen',
106
+ '<script lang="ts">\nconst el = document.querySelector(".foo");\n</script>\n<div class="foo">hi</div>'
107
+ );
108
+
109
+ const settingsDefault = normalizeSettings({ strictDomLints: false });
110
+ const diagnosticsDefault = await collectDiagnostics(document, null, settingsDefault, PROJECT_ROOT);
111
+ const queryDefault = diagnosticsDefault.filter((d) => d.code === 'ZEN-DOM-QUERY');
112
+ assert.ok(queryDefault.length >= 1, `expected ZEN-DOM-QUERY diagnostic, got: ${JSON.stringify(diagnosticsDefault.map((d) => d.code))}`);
113
+ assert.equal(queryDefault[0]?.severity, 2, 'ZEN-DOM-QUERY should be Warning (2) when strictDomLints=false');
114
+
115
+ const settingsStrict = normalizeSettings({ strictDomLints: true });
116
+ const diagnosticsStrict = await collectDiagnostics(document, null, settingsStrict, PROJECT_ROOT);
117
+ const queryStrict = diagnosticsStrict.filter((d) => d.code === 'ZEN-DOM-QUERY');
118
+ assert.ok(queryStrict.length >= 1, `expected ZEN-DOM-QUERY diagnostic in strict mode, got: ${JSON.stringify(diagnosticsStrict.map((d) => d.code))}`);
119
+ assert.equal(queryStrict[0]?.severity, 1, 'ZEN-DOM-QUERY should be Error (1) when strictDomLints=true');
120
+ });