@yawlabs/ctxlint 0.25.2 → 0.25.3
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/.pre-commit-hooks.yaml +1 -1
- package/CONTEXT_LINT_SPEC.md +50 -12
- package/README.md +2 -2
- package/context-lint-rules.json +20 -0
- package/dist/index.js +1561 -423
- package/package.json +1 -1
package/.pre-commit-hooks.yaml
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
|
|
5
5
|
# of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
|
|
6
6
|
# this in sync with package.json on each bump.
|
|
7
|
-
entry: npx @yawlabs/ctxlint@0.25.
|
|
7
|
+
entry: npx @yawlabs/ctxlint@0.25.3 --strict
|
|
8
8
|
language: node
|
|
9
9
|
always_run: true
|
|
10
10
|
pass_filenames: false
|
package/CONTEXT_LINT_SPEC.md
CHANGED
|
@@ -16,7 +16,7 @@ This specification defines a standard set of lint rules for validating AI agent
|
|
|
16
16
|
The specification includes:
|
|
17
17
|
|
|
18
18
|
- A complete reference of context file formats across 16 AI coding clients (21+ file patterns)
|
|
19
|
-
-
|
|
19
|
+
- 43 lint rules organized into 12 categories with defined severities
|
|
20
20
|
- A machine-readable rule and format catalog ([`context-lint-rules.json`](./context-lint-rules.json))
|
|
21
21
|
- Auto-fix definitions for rules that support automated correction
|
|
22
22
|
- Frontmatter schema requirements per client
|
|
@@ -292,6 +292,7 @@ Context files reference build and test commands (e.g., `npm run build`, `make te
|
|
|
292
292
|
|
|
293
293
|
- Package manager scripts: `npm run`, `pnpm`, `yarn`, `bun`
|
|
294
294
|
- Build tools: `make`, `cargo`, `go build`, `go test`
|
|
295
|
+
- JVM build tools: `./gradlew`, `gradle`, `./mvnw`, `mvn` (and the Windows `gradlew.bat` / `mvnw.cmd` wrappers). Match the tool name only when it is followed by whitespace or the end of the command: `gradle.properties` and `gradle/libs.versions.toml` are file names written in the same backticks. A wrapper reached by a relative path (`../../gradlew`) runs from a directory the line does not name and is not extracted.
|
|
295
296
|
- Test runners: `vitest`, `jest`, `pytest`, `mocha`
|
|
296
297
|
- Other tools: `npx`, `python`, `tsc`, `eslint`, `prettier`, `deno`
|
|
297
298
|
|
|
@@ -311,7 +312,7 @@ Context files consume an agent's context window. Counting tokens helps teams und
|
|
|
311
312
|
|
|
312
313
|
## 3. Lint Rules
|
|
313
314
|
|
|
314
|
-
|
|
315
|
+
43 rules organized into 12 categories.
|
|
315
316
|
|
|
316
317
|
Severity levels:
|
|
317
318
|
|
|
@@ -340,16 +341,18 @@ Validates that file paths referenced in context files exist in the project.
|
|
|
340
341
|
|
|
341
342
|
Validates that commands referenced in context files are actually available in the project.
|
|
342
343
|
|
|
343
|
-
| Rule ID
|
|
344
|
-
|
|
|
345
|
-
| `commands/script-not-found`
|
|
346
|
-
| `commands/make-target-not-found`
|
|
347
|
-
| `commands/no-makefile`
|
|
348
|
-
| `commands/npx-not-in-deps`
|
|
349
|
-
| `commands/tool-not-found`
|
|
350
|
-
| `commands/package-json-missing`
|
|
351
|
-
| `commands/exit-status-masked`
|
|
352
|
-
| `commands/unknown-subcommand`
|
|
344
|
+
| Rule ID | Severity | Trigger | Message |
|
|
345
|
+
| ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
346
|
+
| `commands/script-not-found` | error | `npm run`, `pnpm`, `yarn`, or `bun` script name is not in `package.json#scripts` | `"{cmd}" — script "{name}" not found in package.json` |
|
|
347
|
+
| `commands/make-target-not-found` | error | `make` target is not in Makefile | `"{cmd}" — target "{name}" not found in Makefile` |
|
|
348
|
+
| `commands/no-makefile` | error | `make` command used but no Makefile exists | `"{cmd}" — no Makefile found in project` |
|
|
349
|
+
| `commands/npx-not-in-deps` | warning | `npx` package is not in dependencies or `node_modules/.bin` | `"{cmd}" — "{pkg}" not found in dependencies` |
|
|
350
|
+
| `commands/tool-not-found` | warning | Common tool (`vitest`, `jest`, `eslint`, etc.) is not in dependencies or `node_modules/.bin` | `"{cmd}" — "{tool}" not found in dependencies or node_modules/.bin` |
|
|
351
|
+
| `commands/package-json-missing` | info | `package.json` is missing or unparseable AND the file references at least one command that would otherwise have been validated | `package.json missing or unparseable — command checks skipped` |
|
|
352
|
+
| `commands/exit-status-masked` | warning | A verifier heads a pipeline whose last stage is a filter, and the pipeline is followed by a success claim that reads the filter's status | `"{cmd}" — exit status comes from "{filter}", not "{verifier}"; the success claim cannot fail` |
|
|
353
|
+
| `commands/unknown-subcommand` | error | A documented invocation of a `package.json#bin` binary uses a subcommand the CLI does not dispatch | `"{cmd}" — "{sub}" is not a subcommand of {bin} (known: {known})` |
|
|
354
|
+
| `commands/gradle-project-not-found` | error | A Gradle task path's project part matches no project declared by a statically readable settings file | `"{cmd}" — project "{path}" not found in {settingsFile}` |
|
|
355
|
+
| `commands/maven-module-not-found` | error | A Maven `-pl` / `-rf` selector matches no module of a statically readable reactor | `"{cmd}" — {option} "{selector}" matches no module in the Maven reactor` |
|
|
353
356
|
|
|
354
357
|
**Notes:**
|
|
355
358
|
|
|
@@ -392,6 +395,41 @@ Sibling rule: [`session/unverified-gate-claimed-clean`](./AGENT_SESSION_LINT_SPE
|
|
|
392
395
|
|
|
393
396
|
**Never execute the binary** with `--help` to discover subcommands. The motivating bug is a CLI that _hangs_ on unrecognized input; shelling out to it is how a linter inherits that hang.
|
|
394
397
|
|
|
398
|
+
#### `commands/gradle-project-not-found`
|
|
399
|
+
|
|
400
|
+
JVM repositories rarely have a `package.json`, so every rule gated on one is silent there, and their context files are dense with `./gradlew :server:test`. Gradle reads the **last** colon segment of a task path as the task and every earlier segment as a project path, resolved one level at a time against the children of the project matched so far. A project segment that matches nothing fails the build before any task runs (`Cannot locate tasks that match ':sever:test' as project 'sever' not found in root project 'es'.`).
|
|
401
|
+
|
|
402
|
+
Only the project part is checked. Task names are contributed by plugins at configuration time and have no static ground truth; the project set does, in `settings.gradle(.kts)`, when that file can be read as a **closed** set.
|
|
403
|
+
|
|
404
|
+
**Detection algorithm:**
|
|
405
|
+
|
|
406
|
+
1. Qualifying commands start with `./gradlew`, `gradlew`, `gradlew.bat` or `gradle`. Split into words honoring quotes, drop a trailing `# comment`, stop at the first `&&`, `||`, `|`, `;` or redirection (a `<placeholder>` is not a redirection). Skip a command whose last word is a line continuation (`\`, `^`, a backtick).
|
|
407
|
+
2. Skip the whole command when it retargets the build: `-p`/`--project-dir`, `-c`/`--settings-file`, `-b`/`--build-file`, `--include-build`, `-I`/`--init-script`, in the separate, `=` and attached (`-psamples`) forms; or when an earlier line of the same fenced block, or of the same run of `$`-prompt lines, runs `cd`/`pushd`.
|
|
408
|
+
3. Walk the words. Skip the value of a value-taking build option (`-x`, `-D`, `-P`, `-g`, `--console`, `--warning-mode`, `--max-workers`, ...); a short option with an attached value (`-xjavadoc`) is one word. Any other long option, except Gradle's built-in boolean options (`--offline`, `--parallel`, `--continue`, ...), is a task option (`--tests Foo`, `--dependency g:a`): skip the following word unless it is itself an option, since its value may contain a colon.
|
|
409
|
+
4. A remaining word of the form `:a:b:task` or `a:b:task` (name characters only, so `:<module>:test` is skipped) is a task path; its project path is every segment but the last. Skip it when a project segment is an unmarked placeholder (`module-name`, `module`, `my-*`, `your-*`, ...). Relative paths are checked only when the context file sits in the build root, where they resolve against the root project.
|
|
410
|
+
5. The build root is the nearest directory at or above the context file, bounded by the project root, that holds a settings file. For `./gradlew` that directory must also hold the wrapper: a nested build without one (`build-logic/`, an included build) cannot run `./gradlew` from its own directory, so the rule emits nothing. If it holds both `settings.gradle` and `settings.gradle.kts`, the rule emits nothing.
|
|
411
|
+
6. Read the settings file. Literal `include` (with implicit parents: `include 'a:b'` also registers `:a`), `includeFlat`, literal `includeBuild` names (directory basename, plus a literal `name =` / `setName` override) and literal `project(':a').name = 'b'` renames build the set; `buildSrc/` adds a build name. Conditionals are flattened, which yields a superset. Every literal `includeBuild` target is followed, because Gradle flattens composite builds: a build included by an included build is addressable from the root. The set is **open**, and the rule emits nothing, on any non-literal `include`/`includeBuild`/rename argument, a compound or subscript rename (`it.name += '-lib'`, `project(':a')['name'] = 'b'`), a bare `name =` inside any block not known to be harmless (`maven {}`, `repositories {}`, ...), `apply from:`, a settings plugin not on a verified allowlist (Develocity, Gradle Enterprise, Common Custom User Data, foojay), a `buildscript` block, `settingsEvaluated`/`beforeSettings`, Groovy dynamic dispatch (`settings.'include'(...)`, `invokeMethod`, `metaClass`), a Groovy slashy string, code loading (`evaluate`, `GroovyShell`), a non-UTF-8 settings file, or an included build whose own settings are open.
|
|
412
|
+
7. Resolve each project segment the way Gradle's `NameMatcher` does: exact, then case-insensitive, prefix, camelCase and kebab-case abbreviation. The first segment's candidates also include included-build names and `buildSrc`; a path entering another build is not checked further. Report only when a segment matches **nothing**. An ambiguous abbreviation also fails the build, but adding one project can flip it, so it is not reported.
|
|
413
|
+
|
|
414
|
+
**Accepted gap:** init scripts outside the repository (`~/.gradle/init.d`) can include projects in `beforeSettings`. No static reader can see them, the same way `commands/unknown-subcommand` accepts binaries resolved from `PATH`.
|
|
415
|
+
|
|
416
|
+
**Never execute Gradle.** Configuring a build to ask it runs arbitrary build logic, needs a JDK and the network, and takes minutes.
|
|
417
|
+
|
|
418
|
+
#### `commands/maven-module-not-found`
|
|
419
|
+
|
|
420
|
+
`./mvnw -pl flink-core-api -Dtest=MemorySizeTest test` selects a reactor module. Maven matches a selector containing `:` as an id (`:artifactId` or `groupId:artifactId`) and anything else as a path that must be a reactor project's directory or POM file. Maven 3.9 resolves the path against the directory of the POM it starts from; Maven 4 collects the reactor from the root even when started in a subdirectory. A selector matching nothing fails the build (`Could not find the selected project in the reactor: x` in 3.9, `The requested required projects x do not exist.` in 4.0).
|
|
421
|
+
|
|
422
|
+
**Detection algorithm:**
|
|
423
|
+
|
|
424
|
+
1. Qualifying commands start with `./mvnw`, `mvnw`, `mvnw.cmd`, `mvn` or `mvn.cmd`. Tokenize as for Gradle; skip a command with a line-continuation word.
|
|
425
|
+
2. Skip the whole command on `-f`/`--file` (re-roots the reactor, including the attached `-fpom.xml` form), `-N`/`--non-recursive` (shrinks it to one POM), `-af`/`--at-file`, when `.mvn/maven.config` sets `-f` (in any form), `--file`, `-pl` or `-N`, or after a `cd` earlier in the same fence or prompt run.
|
|
426
|
+
3. Collect selectors from `-pl`/`--projects` (comma list; strip a leading `!`, `-` or `+`) and `-rf`/`--resume-from` (one selector). Skip the value of every other value-taking option (`-D`, `-P`, `-s`, `-T`, `-b`, ...). Skip `?`-prefixed selectors (optional in Maven 4) and placeholders, tested against the whole selector and against its artifactId part (`<module>`, `{module}`, `%MODULE%`, `[module]`, `path/to/module`, `:module`, `com.example:my-module`, `...`).
|
|
427
|
+
4. Candidate base directories are the project root plus every ancestor of the context file, up to the root, that holds a `pom.xml`. The doc does not say where the command runs, so a selector is reported only when, from **no** base, it matches a project of **any** base's reactor. A path selector that leaves the project root from every base names a module the lint cannot see, and is skipped.
|
|
428
|
+
5. Read each base's reactor: the root POM plus, recursively, every `<module>` and `<subprojects>` entry of the project and of **every** profile (a superset). Paths are compared in the file system's own casing. The reactor is unknowable, and the rule emits nothing, when a module path uses a `${property}`, a listed module's POM cannot be read, a POM is not UTF-8 or does not parse as a balanced element tree, or an aggregator (`packaging` `pom`) on a model newer than 4.0.0 -- including a version implied only by the POM namespace -- declares no top-level module list (Maven 4 automatic subproject discovery).
|
|
429
|
+
6. Match path selectors against project directories and POM files; match id selectors against `artifactId`/`groupId` as written, where each `${property}` inside an id may stand for any text (`flink-dist-scala_${scala.binary.version}`).
|
|
430
|
+
|
|
431
|
+
**Never execute Maven.**
|
|
432
|
+
|
|
395
433
|
### 3.3 staleness — freshness detection
|
|
396
434
|
|
|
397
435
|
Detects context files that haven't been updated while their referenced code has changed. Requires git.
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Useful if you want `ctxlint` available in every project without per-project setu
|
|
|
73
73
|
| Check | What it finds |
|
|
74
74
|
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
75
|
| **Broken paths** | File references in context that don't exist in your project |
|
|
76
|
-
| **Wrong commands** | Build/test commands that don't match your package.json scripts or Makefile targets
|
|
76
|
+
| **Wrong commands** | Build/test commands that don't match your package.json scripts or Makefile targets, and Gradle project paths or Maven `-pl` modules your build doesn't declare |
|
|
77
77
|
| **Stale context** | Context files not updated after recent code changes |
|
|
78
78
|
| **Token waste** | How much context window your files consume per session |
|
|
79
79
|
| **Redundancy** | Content the agent can already infer (e.g. "We use React" when react is in package.json) |
|
|
@@ -530,7 +530,7 @@ ctxlint is the reference implementation of four open specifications for linting
|
|
|
530
530
|
|
|
531
531
|
| Spec | What it covers |
|
|
532
532
|
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
533
|
-
| **[AI Context File Linting Spec](./CONTEXT_LINT_SPEC.md)** |
|
|
533
|
+
| **[AI Context File Linting Spec](./CONTEXT_LINT_SPEC.md)** | 43 rules for validating context files (CLAUDE.md, .cursorrules, AGENTS.md, etc.) across 16 clients. Covers file formats, frontmatter schemas, path/command validation, staleness, token budgets, redundancy, and contradictions. |
|
|
534
534
|
| **[MCP Config Linting Spec](./MCP_CONFIG_LINT_SPEC.md)** | 29 rules for validating MCP server configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, etc.) across 8 clients. Covers schema validation, hardcoded secrets, env var syntax, deprecated transports, and cross-file consistency. |
|
|
535
535
|
| **[Agent Session Linting Spec](./AGENT_SESSION_LINT_SPEC.md)** | 13 rules for auditing agent session data (history, memory) across 8 agents. Covers cross-project secret consistency, config drift, stale memory, and loop detection. |
|
|
536
536
|
| **[Agent Skill Linting Spec](./AGENT_SKILL_LINT_SPEC.md)** | 5 rules for auditing Claude Code skill (`SKILL.md`) and agent (`.md`) definitions under `~/.claude`. Covers frontmatter presence, broken refs, trigger-phrase collisions, orphaned skills, and dead tool restrictions. (v1, experimental) |
|
package/context-lint-rules.json
CHANGED
|
@@ -190,6 +190,26 @@
|
|
|
190
190
|
"fixable": false,
|
|
191
191
|
"stability": "experimental"
|
|
192
192
|
},
|
|
193
|
+
{
|
|
194
|
+
"id": "commands/gradle-project-not-found",
|
|
195
|
+
"category": "commands",
|
|
196
|
+
"severity": "error",
|
|
197
|
+
"description": "A documented Gradle task path names a project that the build's settings file does not declare.",
|
|
198
|
+
"trigger": "A ./gradlew or gradle invocation has a task path (:a:b:task) whose project part matches no project, included build or buildSrc under Gradle's name matching (exact, case-insensitive, prefix, camelCase and kebab-case abbreviation), where the project set was read from a settings file that is statically enumerable.",
|
|
199
|
+
"message": "\"{cmd}\" — project \"{path}\" not found in {settingsFile}",
|
|
200
|
+
"fixable": false,
|
|
201
|
+
"stability": "experimental"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"id": "commands/maven-module-not-found",
|
|
205
|
+
"category": "commands",
|
|
206
|
+
"severity": "error",
|
|
207
|
+
"description": "A documented Maven -pl or -rf selector matches no module of the reactor.",
|
|
208
|
+
"trigger": "A ./mvnw or mvn invocation selects a module with -pl/--projects or -rf/--resume-from, and the selector matches no reactor project by relative directory, POM file, :artifactId or groupId:artifactId, where the reactor was read from the root POM's modules and subprojects (all profiles included) and is statically enumerable.",
|
|
209
|
+
"message": "\"{cmd}\" — {option} \"{selector}\" matches no module in the Maven reactor",
|
|
210
|
+
"fixable": false,
|
|
211
|
+
"stability": "experimental"
|
|
212
|
+
},
|
|
193
213
|
{
|
|
194
214
|
"id": "staleness/stale",
|
|
195
215
|
"category": "staleness",
|