@reticlehq/eslint-plugin 2.13.1 → 2.14.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/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export declare const rules: {
5
5
  readonly "require-signal-on-mutation": import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutationWithoutSignal", [import("./options.js").RawRuleOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
6
6
  name: string;
7
7
  };
8
- readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
8
+ readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag" | "pathTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
9
9
  name: string;
10
10
  };
11
11
  };
@@ -17,7 +17,7 @@ declare const plugin: {
17
17
  readonly "require-signal-on-mutation": import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutationWithoutSignal", [import("./options.js").RawRuleOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
18
18
  name: string;
19
19
  };
20
- readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
20
+ readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag" | "pathTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
21
  name: string;
22
22
  };
23
23
  };
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * Rule: no-internal-tags.
3
3
  *
4
- * Bans design-doc reference codes and internal version strings from comments, so a reader of the source
5
- * never meets a token that only means something in a document they cannot see.
4
+ * Bans design-doc reference codes and internal version strings from comments, TEST DESCRIPTIONS and
5
+ * the FILE AND DIRECTORY NAMES of the module itself, so a reader of the source never meets a token
6
+ * that only means something in a document they cannot see.
7
+ *
8
+ * Those four surfaces are the ones the project rule names, and the rule enforced two of them. A path
9
+ * is the first thing a reader meets, before any comment, so `n5-ring-buffer.ts` is the shape this
10
+ * exists to prevent stated in the loudest available place.
6
11
  *
7
12
  * This rule exists because the prose version of it did not hold. An audit of the codebase found the
8
13
  * machine-checked rules at ~100% compliance and every prose-only rule violated — 59 tracking codes
@@ -18,6 +23,6 @@
18
23
  * `H2`, `Welford M2` when part of a sentence) is matched only when it stands alone as a tag.
19
24
  */
20
25
  import { ESLintUtils } from '@typescript-eslint/utils';
21
- export declare const noInternalTags: ESLintUtils.RuleModule<"internalTag", [], unknown, ESLintUtils.RuleListener> & {
26
+ export declare const noInternalTags: ESLintUtils.RuleModule<"internalTag" | "pathTag", [], unknown, ESLintUtils.RuleListener> & {
22
27
  name: string;
23
28
  };
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * Rule: no-internal-tags.
3
3
  *
4
- * Bans design-doc reference codes and internal version strings from comments, so a reader of the source
5
- * never meets a token that only means something in a document they cannot see.
4
+ * Bans design-doc reference codes and internal version strings from comments, TEST DESCRIPTIONS and
5
+ * the FILE AND DIRECTORY NAMES of the module itself, so a reader of the source never meets a token
6
+ * that only means something in a document they cannot see.
7
+ *
8
+ * Those four surfaces are the ones the project rule names, and the rule enforced two of them. A path
9
+ * is the first thing a reader meets, before any comment, so `n5-ring-buffer.ts` is the shape this
10
+ * exists to prevent stated in the loudest available place.
6
11
  *
7
12
  * This rule exists because the prose version of it did not hold. An audit of the codebase found the
8
13
  * machine-checked rules at ~100% compliance and every prose-only rule violated — 59 tracking codes
@@ -17,10 +22,12 @@
17
22
  * Ordinary technical prose that happens to contain a letter and digits inside a larger word (`UTF-8`,
18
23
  * `H2`, `Welford M2` when part of a sentence) is matched only when it stands alone as a tag.
19
24
  */
25
+ import { relative, sep } from 'node:path';
20
26
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
21
27
  import { DOCS_URL_ROOT } from './constants.js';
22
28
  const createRule = ESLintUtils.RuleCreator((name) => `${DOCS_URL_ROOT}#${name}`);
23
29
  const INTERNAL_TAG_MESSAGE = "Internal reference '{{tag}}' does not belong in source. It means nothing to a reader without the design doc — describe the behaviour instead.";
30
+ const PATH_TAG_MESSAGE = "Internal reference '{{tag}}' does not belong in a file or directory name. It means nothing to a reader without the design doc — name the module after what it does.";
24
31
  /** `(W11)`, `B37`, `W10.3`, `N5:` — a short capital-prefixed code used as a label. */
25
32
  const TRACKING_CODE = /(?<![A-Za-z0-9])[A-Z]{1,2}\d{1,2}(?:\.\d{1,2})?(?![A-Za-z0-9])/g;
26
33
  /**
@@ -41,6 +48,16 @@ const EXTERNAL_SPECS = /\b(RFC|WCAG|ECMA|ISO|IEEE|W3C|WHATWG|HTML|CSS|ARIA|HTTP|
41
48
  * banned string legal.
42
49
  */
43
50
  const VERSION_STRING = /(?<![A-Za-z0-9])v\d+\.\d+\.\d+(?![A-Za-z0-9])/g;
51
+ /**
52
+ * The same two shapes over a PATH, case-insensitively.
53
+ *
54
+ * File names here are kebab-case, so a code that would read `N5` in a sentence reads `n5` in a path
55
+ * and the upper-case patterns above cannot see it. The boundaries do the work that case was doing:
56
+ * `e2e`, `utf8`, `http2` and `base64` all fail them, because each needs a non-alphanumeric on both
57
+ * sides of a letters-then-digits token.
58
+ */
59
+ const PATH_TRACKING_CODE = /(?<![A-Za-z0-9])[A-Za-z]{1,2}\d{1,2}(?:\.\d{1,2})?(?![A-Za-z0-9])/g;
60
+ const PATH_VERSION_STRING = /(?<![A-Za-z0-9])v\d+\.\d+\.\d+(?![A-Za-z0-9])/gi;
44
61
  /** Third-party names whose versions are legitimate prose. Reticle is deliberately NOT here. */
45
62
  const THIRD_PARTY = /\b(React|Vite|Node|TypeScript|Playwright|Chrome|Chromium|Firefox|Safari|Zod|Vitest|ESLint|pnpm|MCP)\b[^v]{0,12}$/i;
46
63
  /** Tokens that look like codes but are established technical terms. */
@@ -83,7 +100,7 @@ export const noInternalTags = createRule({
83
100
  type: 'problem',
84
101
  docs: { description: 'Ban design-doc codes and internal version strings from comments.' },
85
102
  schema: [],
86
- messages: { internalTag: INTERNAL_TAG_MESSAGE },
103
+ messages: { internalTag: INTERNAL_TAG_MESSAGE, pathTag: PATH_TAG_MESSAGE },
87
104
  },
88
105
  defaultOptions: [],
89
106
  create(context) {
@@ -109,13 +126,58 @@ export const noInternalTags = createRule({
109
126
  }
110
127
  return found;
111
128
  };
129
+ /**
130
+ * Banned tokens in this module's own path, relative to the project root.
131
+ *
132
+ * Relative, never absolute: an absolute path runs through directories nobody chose as part of
133
+ * this codebase (a CI runner's workspace, a developer's home) and flagging those would report a
134
+ * violation the author cannot fix.
135
+ */
136
+ const pathTags = () => {
137
+ const found = new Set();
138
+ const filename = context.filename;
139
+ // `<input>` and `<text>` are what a linter reports for source with no file, e.g. a RuleTester
140
+ // case or a piped snippet. There is no path to judge.
141
+ if (filename.startsWith('<'))
142
+ return found;
143
+ const rel = relative(context.cwd, filename);
144
+ // Outside the project root entirely: `relative` climbs out with `..`, and the segments above
145
+ // the root are not this codebase's to answer for.
146
+ if (rel.startsWith('..'))
147
+ return found;
148
+ const path = rel.split(sep).join('/');
149
+ // Version FIRST, and its spans claimed, because the tracking-code shape is a prefix of it:
150
+ // `replay-v2.2.0.ts` matches `v2.2.0` as a version and `v2.2` as a code, and reporting both
151
+ // would name the same offence twice under two different explanations.
152
+ const claimed = [];
153
+ for (const pattern of [PATH_VERSION_STRING, PATH_TRACKING_CODE]) {
154
+ pattern.lastIndex = 0;
155
+ for (const match of path.matchAll(pattern)) {
156
+ const start = match.index ?? 0;
157
+ const end = start + match[0].length;
158
+ if (claimed.some((c) => start < c.end && end > c.start))
159
+ continue;
160
+ const tag = match[0].trim();
161
+ claimed.push({ start, end });
162
+ if (ALLOWED.has(tag.replace(/[^A-Za-z0-9]/g, '').toUpperCase()))
163
+ continue;
164
+ found.add(tag);
165
+ }
166
+ }
167
+ return found;
168
+ };
112
169
  return {
113
- Program() {
170
+ Program(node) {
114
171
  for (const comment of context.sourceCode.getAllComments()) {
115
172
  for (const tag of tagsIn(comment.value)) {
116
173
  context.report({ node: comment, messageId: 'internalTag', data: { tag } });
117
174
  }
118
175
  }
176
+ // Reported on the Program node: the offence is the module's name, which has no node of its
177
+ // own, and the first line is where a reader will look for it.
178
+ for (const tag of pathTags()) {
179
+ context.report({ node, messageId: 'pathTag', data: { tag } });
180
+ }
119
181
  },
120
182
  /**
121
183
  * TEST DESCRIPTIONS are string literals, not comments, so a comment-only rule could not see them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/eslint-plugin",
3
- "version": "2.13.1",
3
+ "version": "2.14.0",
4
4
  "description": "Reticle ESLint plugin — keeps the signal layer self-enforcing (mutation-without-signal).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",