add docs and AI skills

This commit is contained in:
2026-08-17 18:23:10 +02:00
parent c3d5d3541b
commit 09dcfede46
13 changed files with 506 additions and 0 deletions
@@ -0,0 +1,56 @@
---
name: verify-eftl-linter
description: Verify the EFTL linter and VS Code language support with repository checks, tokenizer/parser cases, diagnostic range review, TextMate grammar checks, snippets, examples, and LSP behavior. Use after EFTL implementation changes, while diagnosing regressions, when adding tests, or before declaring a linter feature complete.
---
# Verify EFTL Linter
Validate the core language implementation first, then the LSP and editor surfaces affected by the change. Read [references/verification-matrix.md](references/verification-matrix.md) when selecting cases.
## Run the baseline check
From the repository root, run:
```powershell
node .agents/skills/verify-eftl-linter/scripts/check-repository.mjs
```
The script parses contributed JSON, checks referenced files, compiles TypeScript, runs an existing test script when present, and smoke-tests the compiled tokenizer/parser. Pass `--lint` only when the repository has a working ESLint configuration.
Treat this script as a floor, not as feature proof.
## Build a focused test set
For each changed rule, include:
- the smallest valid form;
- every optional form or bodyless/block alternative;
- a representative nested form;
- missing, duplicated, invalid, and out-of-order attributes or children;
- mismatched, missing, and unexpected closing delimiters;
- incomplete input at end of file for live-edit recovery;
- lowercase and mixed-case spellings when the runtime is case-insensitive;
- LF, CRLF, multiline, and non-ASCII range cases;
- a regression case from `examples/` or the reported bug.
Assert token types and spans separately from parser errors. Assert diagnostic code, severity, message, and exact LSP range; do not rely only on snapshot text.
## Verify by layer
1. Run tokenizer/parser tests without VS Code.
2. Run server-level tests for config, diagnostic caps, declaration/reference scope, and range conversion.
3. Parse all JSON contribution files and inspect TextMate captures for the changed construct.
4. Open a representative `.eftl` file in an Extension Development Host when highlighting, folding, snippets, activation, or navigation changed.
5. Confirm valid examples produce no diagnostics and each invalid example produces the intended minimal set.
When no automated harness exists for a required layer, add one if it is stable and proportionate; otherwise document the exact manual check and result.
## Interpret failures
- A compilation failure blocks all further verification.
- A failure caused by a known baseline issue must be reproduced on the pre-change baseline before being labeled unrelated.
- Diagnostic cascades usually indicate parser recovery problems; fix recovery instead of weakening assertions.
- TextMate highlighting success does not prove tokenizer/parser support, and parser success does not prove editor integration.
- Never declare `npm run lint` successful in the current baseline: ESLint 9 has no flat configuration until the repository adds one.
Report commands, pass/fail counts, skipped layers, and residual risks.
@@ -0,0 +1,4 @@
interface:
display_name: "Verify EFTL Linter"
short_description: "Test EFTL diagnostics and editor support"
default_prompt: "Use $verify-eftl-linter to validate this EFTL linter change end to end."
@@ -0,0 +1,54 @@
# EFTL verification matrix
## Core matrix
| Area | Positive cases | Negative and recovery cases |
| --- | --- | --- |
| Roots | one and multiple EFTL fragments with surrounding user text | close without open, missing close, nested roots if forbidden by confirmed grammar |
| Blocks | correctly nested VAR, IF, WHILE, functions, expressions, comments | crossing closes, incomplete close, unexpected close, unfinished or unknown `[` construct; assert termination |
| Attributes | documented required and optional attributes in varied order | missing required, duplicate, unknown, unquoted, invalid enum/boolean/number |
| IF | condition + then, repeated else-if, optional final else | missing condition/then, else before then, duplicate else, branch after else |
| WHILE | condition + do, confirmed threshold boundaries | missing child, reversed order, invalid threshold, incomplete body |
| FOR | iterable variable and scoped loop variable | missing attributes, unknown iterable when statically knowable, reference after scope |
| Expressions | assignment, evaluation, output, supported literals/operators | unterminated block, malformed operator, missing semicolon where required |
| Functions | bodyless and block variants, precedence rules | mutually exclusive inputs, wrong body shape, invalid literal regex |
| TAG payload | minimal and full positional forms | missing mandatory position, invalid literal enum, preserved empty positions |
| Variables | declaration, reference, shadowing policy, iterable index | invalid type, duplicate declaration policy, undefined reference, invalid index type |
For every tag keyword, test the exact boundary after its name so a longer unknown name such as `[VARIABLE]` is not classified as `[VAR ...]`.
Only assert rules confirmed by the PDFs or runtime grammar. Keep ambiguous rules as pending cases rather than opinionated errors.
## Range matrix
Exercise each diagnostic at:
- first character and end of file;
- after ASCII and non-ASCII text;
- on a later line with LF and CRLF;
- inside a multiline token or comment;
- after an incremental edit that changes preceding line lengths.
LSP positions are zero-based UTF-16 line/character pairs. Prefer storing source offsets internally and converting with the current `TextDocument` at publication time.
## Editor matrix
When syntax changes, inspect:
- language activation and file association;
- TextMate scope on open name, close name, attributes, strings, and delimiters;
- bracket matching and auto-closing;
- folding start/end markers;
- snippet insertion and tab stops;
- go-to-definition selection range;
- consistency of examples and README syntax.
## Release evidence
Record:
1. exact commands executed;
2. automated pass/fail totals;
3. Extension Development Host scenarios checked;
4. known baseline failures reproduced independently;
5. unverified runtime semantics or ambiguous documentation.
@@ -0,0 +1,117 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const args = process.argv.slice(2);
const includeLint = args.includes('--lint');
const positional = args.filter(arg => !arg.startsWith('--'));
const root = resolve(positional[0] ?? process.cwd());
const require = createRequire(import.meta.url);
function fail(message) {
throw new Error(message);
}
function readJson(relativePath) {
const absolutePath = resolve(root, relativePath);
if (!existsSync(absolutePath)) {
fail(`Missing ${relativePath}`);
}
try {
return JSON.parse(readFileSync(absolutePath, 'utf8'));
} catch (error) {
fail(`Invalid JSON in ${relativePath}: ${error.message}`);
}
}
function runNpm(script) {
const windows = process.platform === 'win32';
const command = windows ? (process.env.ComSpec ?? 'cmd.exe') : 'npm';
const commandArgs = windows ? ['/d', '/s', '/c', `npm run ${script}`] : ['run', script];
const result = spawnSync(command, commandArgs, {
cwd: root,
stdio: 'inherit'
});
if (result.error) {
fail(`Could not run npm script ${script}: ${result.error.message}`);
}
if (result.status !== 0) {
fail(`npm run ${script} exited with ${result.status}`);
}
}
function assertContributionPaths(packageJson) {
const paths = [packageJson.main];
for (const language of packageJson.contributes?.languages ?? []) {
paths.push(language.configuration);
}
for (const grammar of packageJson.contributes?.grammars ?? []) {
paths.push(grammar.path);
}
for (const snippet of packageJson.contributes?.snippets ?? []) {
paths.push(snippet.path);
}
for (const relativePath of paths.filter(Boolean)) {
if (!existsSync(resolve(root, relativePath))) {
fail(`package.json references missing path ${relativePath}`);
}
}
}
function smokeTestCore() {
const tokenizerPath = resolve(root, 'out/tokenizer.js');
const parserPath = resolve(root, 'out/parser.js');
if (!existsSync(tokenizerPath) || !existsSync(parserPath)) {
fail('Compiled tokenizer/parser output is missing');
}
const { EftlTokenizer } = require(tokenizerPath);
const { EftlParser } = require(parserPath);
const sample = readFileSync(resolve(root, 'examples/sample.eftl'), 'utf8');
const sampleResult = new EftlTokenizer(sample).tokenize();
const sampleParse = new EftlParser(sampleResult.tokens).parse();
if (sampleResult.errors.length || sampleParse.errors.length) {
fail(`sample.eftl is not accepted: ${sampleResult.errors.length} tokenizer and ${sampleParse.errors.length} parser errors`);
}
const mismatched = new EftlTokenizer('[EFTL][IF][/EFTL]').tokenize();
if (new EftlParser(mismatched.tokens).parse().errors.length === 0) {
fail('Parser smoke case did not reject mismatched nesting');
}
const comment = new EftlTokenizer('[EFTL][!-- unfinished').tokenize();
if (comment.errors.length === 0) {
fail('Tokenizer smoke case did not reject an unterminated comment');
}
}
try {
const packageJson = readJson('package.json');
readJson('language-configuration.json');
readJson('syntaxes/eftl.tmLanguage.json');
readJson('snippets/eftl.json');
runNpm('compile');
assertContributionPaths(packageJson);
smokeTestCore();
if (packageJson.scripts?.test) {
runNpm('test');
} else {
console.warn('WARN: package.json has no test script; only smoke cases were run.');
}
if (includeLint) {
runNpm('lint');
} else if (packageJson.scripts?.lint) {
console.warn('WARN: lint was skipped; pass --lint after configuring ESLint.');
}
console.log('EFTL repository baseline checks passed.');
} catch (error) {
console.error(`FAIL: ${error.message}`);
process.exitCode = 1;
}