add docs and AI skills
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user