Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part 11 — Diagnostics micro-DSL: validators from constraints, severity, code actions

Diagnostics are the editor's flag-and-fix surface. A validator inspects an AST node, decides if there is a problem, and reports it as a Diagnostic with severity, range, message, and optional code actions. The Diagnostics micro-DSL declares validators per Concept and, where applicable, auto-derives them from @Property({ constraint }) declarations on the kernel — the article 04 mass-production property at full strength: one declarative fact in the kernel produces a validator for free.

The micro-DSL closes the loop on what can be a problem: anything the kernel constrains (regex, enum, range), anything the kernel requires (cardinality), anything a custom predicate names (cross-aggregate uniqueness, dependency cycles, missing satisfactions). The first two come for free; the third requires explicit declaration. Each diagnostic optionally carries one or more code actions — repair lambdas the user can apply with one keystroke — which mutate the AST through the PatchBus and so participate in undo/redo coherently.

Concern

The author writes readonly id = 'FEAT-156' — an id that does not match the declared ^FEATURE-\d+$ constraint. The editor should immediately surface a red squiggle, the Problems panel should list the violation, and the editor should offer a quick-fix that proposes FEATURE-156. The author writes a Feature with no acceptance children — a violation of the 1..* cardinality. Same expectation: red squiggle, Problems panel entry, optional quick-fix to insert a placeholder AC.

Producing this from scratch requires writing an LSP diagnostics handler, walking the AST, comparing each @Property value against its constraint, building per-violation Diagnostic objects, registering code actions, wiring the textDocument/codeAction handler to surface them. The Diagnostics micro-DSL collapses this into auto-derivation plus optional @Diagnostic decorators.

The Surface

import { Diagnostic, CodeAction } from '@frenchexdev/ide-dsl-diagnostics';
import { Concept, Property, ChildLink, PatchBus } from '@frenchexdev/ide-dsl-kernel';

@Concept({ id: 'cmf.req.Feature' })
export class Feature {
  @Property({ type: 'string', constraint: /^FEATURE-\d+$/ })
  id!: string;

  @ChildLink({ target: 'cmf.req.AcceptanceCriterion', card: '1..*' })
  acceptance!: AcceptanceCriterion[];

  // Auto-derived: one diagnostic per @Property({ constraint }), one per cardinality minimum.
  // No declaration needed for the regex check or the 1..* check.

  // Custom diagnostic with code action.
  @Diagnostic({ severity: 'warning', code: 'FEAT-NOT-SATISFIED' })
  static checkSatisfaction(self: Feature, ctx) {
    const refs = ctx.workspace.findReferences(self.id, '@Satisfies');
    if (refs.length === 0) {
      return {
        message: `Feature ${self.id} is declared but not satisfied by any test.`,
        range: ctx.rangeOf(self, 'id'),
        actions: [
          CodeAction.create('Add @FeatureTest', async (patch: PatchBus) => {
            await patch.insertChild(self.parentDocument.id, 'tests', {
              concept: 'cmf.req.FeatureTest',
              properties: { satisfies: self.id },
            });
          }),
        ],
      };
    }
    return null;
  }
}

Two surfaces. The implicit one — @Property({ constraint }) — produces a validator with default severity error and a default message templated from the constraint. The explicit one — @Diagnostic — declares a method that runs against each Concept instance and returns a diagnostic descriptor (or null). Code actions attached to the diagnostic are lambda over the PatchBus from article 05; the kernel's mutation contract carries the repair through the standard channel.

The code field is a stable identifier that other tooling (the Diagnostics micro-DSL's own settings UI, third-party rule disablers, the compliance reporter) can use to reference the rule. Codes follow the convention <DSL-PREFIX>-<NN> and are documented in the suite's diagnostic reference.

Kernel boundary

Reads:

  • The Structure Model — to enumerate @Property({ constraint }) declarations for auto-derivation.
  • The current document AST — for per-document validation passes.
  • The workspace AST — for cross-aggregate validation (checkSatisfaction walks references across files).

Writes:

  • Through PatchBus, only in code-action callbacks. The validator itself is read-only; the repair, when applied, mutates through the standard channel.

The two-mode boundary is important. Validators run on every change (often), and must be cheap and side-effect-free. Code actions run only on user invocation (rare), and may be expensive and side-effecting; they go through PatchBus so the EditLog records them and undo/redo works.

Emitted artefacts

// LSP server registration (generated, in server/handlers/diagnostics.generated.ts)
import { Diagnostic } from 'vscode-languageserver';
import { kernel } from '@frenchexdev/ide-dsl-kernel';
import { diagnosticRegistry } from './_registry.generated';

export async function publishDiagnostics(uri: string): Promise<Diagnostic[]> {
  const doc = kernel.getDocument(uri);
  if (!doc) return [];
  const violations = await diagnosticRegistry.validate(doc);
  return violations.map(toLspDiagnostic);
}

// LSP code action handler (generated)
export async function handleCodeAction(params): Promise<CodeAction[]> {
  // ... routes to diagnosticRegistry.actionsFor(diagnostic) ...
}

Plus a contribution declaration so the LSP host registers the dynamic registration of textDocument/publishDiagnostics and textDocument/codeAction.

Composition with peers

  • Refactoring (article 17) — refactorings are larger structural transforms; code actions are local repairs. Both mutate through PatchBus; the boundary is scope (one node vs. several files).
  • Hover (article 10) — orthogonal at the same position; both attach metadata to a token but in different UI surfaces.
  • CodeLens (article 12) — both can offer actions on a node; CodeLens is always-visible above the node, code actions appear on demand (lightbulb).
  • LSP host (article 20) — routes textDocument/publishDiagnostics and textDocument/codeAction.

MPS aspect referent

MPS Constraints aspect. MPS attaches constraints to Concepts and runs them against AST nodes; violations surface in MPS' equivalent of the Problems panel; MPS intentions (the equivalent of code actions) repair them. We adopt the per-Concept declarative shape for explicit diagnostics; we add the auto-derivation from kernel @Property({ constraint }) (which MPS does not have because its M2 does not carry constraints in the same shape). The integration with PatchBus for repair is the TypeScript-shaped translation of MPS' intention-applies-via-the-typed-mutation-API mechanism.

Boundary justification

Why not in the kernel? The validators consume kernel constraints and produce LSP-shaped output. The output side is VSCode-coupled (Diagnostic, CodeAction, severity enums, range types) — putting it in the kernel would violate criterion 3 from article 02. The validation logic itself could in principle live in the kernel, but separating what is constrained (kernel) from how violations are surfaced (Diagnostics micro-DSL) keeps the kernel small and lets a different host (a CLI lint tool, a CI check) consume the same constraints with its own surfacing.

Why not in Hover? Already addressed in article 10's Boundary justification.

Why not in Refactoring? Code actions are local repairs to a single diagnostic; refactorings are user-initiated structural transforms. The user's mental model is "this thing is wrong, fix it locally" (code action) versus "I want to restructure" (refactoring). The micro-DSLs respect that distinction.

Requirements

FEAT-MICRODSL-11 in assets/features.ts:

  • validatorsDerivedFromKernelPropertyConstraints — the Surface section names the auto-derivation, the Kernel boundary section confirms reading @Property({ constraint }).
  • severityMappingStated — the Surface section shows the explicit severity: 'warning' and the default error for auto-derived; the LSP host uses these directly.
  • codeActionsAsRepairLambdas — the worked example shows CodeAction.create with a (patch: PatchBus) => ... callback.
  • boundaryAgainstHoverJustified — the Boundary justification section repeats the severity / surface / panel argument from article 10.

Article 12 picks up with CodeLens, the at-a-glance annotation surface.

⬇ Download