eslint-plugin-kerfjs 4.4.1 → 5.0.0-beta.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/README.md CHANGED
@@ -83,7 +83,7 @@ npm install
83
83
  npm test
84
84
  ```
85
85
 
86
- The AST rules' test suites use ESLint's `RuleTester` with `@typescript-eslint/parser`. The `ai-assistant-configs` tests are filesystem-driven — they build temp project roots with fixture `node_modules/kerfjs/ai/` bundles and drive the rule's classifier directly, since `RuleTester` doesn't simulate the filesystem.
86
+ The AST rules' test suites use ESLint's `RuleTester` with `@typescript-eslint/parser`. The `ai-assistant-configs` tests are filesystem-driven — they build temp project roots with fixture `node_modules/kerfjs/ai/` bundles, drive the classifier directly, and use the real ESLint API to prove plain lint is read-only while explicit `--fix` performs the external write.
87
87
 
88
88
  ## License
89
89
 
@@ -18,7 +18,7 @@ The rule resolves `kerfjs/ai/manifest.json` from the consumer's installed `kerfj
18
18
  A triggered file is then classified into one of three reported states (a fourth, "up-to-date," is silent):
19
19
 
20
20
  - **Missing** — the consumer's `dest` doesn't exist.
21
- - **Stale** — exists, parses cleanly, but its `kerf-skill-version` line is older than the bundle's.
21
+ - **Stale** — exists, parses cleanly, its `kerf-skill-version` line is older than the bundle's, and its canonical-section hash matches that version in the manifest's history.
22
22
  - **Forked** — the consumer's file no longer matches the canonical layout: marker is missing, marker appears more than once, `kerf-skill-version` line is missing, or the content above the marker has been edited.
23
23
 
24
24
  ## ❌ Reported
@@ -46,7 +46,7 @@ For **missing** files, `--fix` copies the bundled canonical (`node_modules/kerfj
46
46
 
47
47
  For **stale** files, `--fix` replaces only the content **above and including the `KERF-APP-CANONICAL-END` marker**. Everything below the marker — the consumer's append zone — is preserved byte-for-byte. This is the "versioned-section preservation" strategy from the design doc.
48
48
 
49
- The `fix()` callback writes to a file OTHER than the linted source — unusual for an ESLint rule. ESLint only invokes `fix()` under `--fix`, so the side effect is opt-in by definition; plain `eslint` will report the warning without touching disk.
49
+ The `fix()` callback writes to a file OTHER than the linted source — unusual for an ESLint rule. ESLint evaluates fix callbacks during ordinary lint too, so the callback gates this external write on an explicit CLI `--fix` flag. Plain `eslint` reports the warning without touching disk; `eslint --fix` opts into installation or update.
50
50
 
51
51
  ## ❗ Forked — no auto-fix
52
52
 
@@ -55,7 +55,8 @@ If the consumer has edited the file in a way that breaks the canonical/append-zo
55
55
  - **No marker present.** The file pre-dates the marker convention, the consumer deleted the marker, or the file is a hand-written variant. Auto-fixing would either clobber legitimate customizations or leave the file in an ambiguous state.
56
56
  - **Multiple markers.** A well-formed file has exactly one. Auto-fix would have to guess which boundary is real.
57
57
  - **No `kerf-skill-version` line.** The staleness signal is missing; we can't tell what's there.
58
- - **Content above the marker has been edited.** The contract is "above the marker is kerf's; below is yours." Above-the-marker edits are a deliberate fork.
58
+ - **Content above the marker has been edited.** The contract is "above the marker is kerf's; below is yours." The rule checks stale files against the historical canonical hash for their own version, so an edited old section is preserved as a deliberate fork rather than overwritten.
59
+ - **Canonical history unavailable.** An old version absent from the shipped manifest history cannot be proven untouched, so the rule conservatively refuses to overwrite it.
59
60
 
60
61
  Resolution: either restore the canonical layout (move customizations below a freshly-inserted marker, delete extras, re-add the version line) or disable the rule for this project — `'kerfjs/ai-assistant-configs': 'off'` in `eslint.config.js`.
61
62
 
package/index.js CHANGED
@@ -8,7 +8,7 @@ import requireDataKeyInEach from './lib/rules/require-data-key-in-each.js';
8
8
  import requireDelegateDisposer from './lib/rules/require-delegate-disposer.js';
9
9
 
10
10
  const plugin = {
11
- meta: { name: 'eslint-plugin-kerfjs', version: '0.13.0' },
11
+ meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.3' },
12
12
  rules: {
13
13
  'no-inline-jsx-event-handlers': noInlineJsxEventHandlers,
14
14
  'no-raw-with-dynamic-arg': noRawWithDynamicArg,
@@ -8,9 +8,10 @@
8
8
  * `KERF-APP-CANONICAL-END` marker; the consumer's append zone below the
9
9
  * marker is preserved byte-for-byte (the "option 2" strategy from KF-217).
10
10
  *
11
- * Unusual for an ESLint rule: the `fix()` callback writes to a file
12
- * OTHER than the linted source. ESLint only invokes `fix()` when
13
- * `--fix` is enabled, so the side effect is opt-in by definition.
11
+ * Unusual for an ESLint rule: the `fix()` callback writes to a file OTHER than
12
+ * the linted source. ESLint evaluates fix callbacks during ordinary lint too,
13
+ * so the callback must explicitly gate that side effect on the CLI's `--fix`
14
+ * flag.
14
15
  */
15
16
  import { createHash } from 'node:crypto';
16
17
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -118,12 +119,23 @@ export function classifyFile(file, bundleDir, cwd) {
118
119
  }
119
120
 
120
121
  const cmp = compareSemver(consumerVersion, file.version);
122
+ const consumerCanonicalHash = sha256(consumerCanonical);
121
123
  if (cmp === 0) {
122
124
  // Same version. Above-marker section should match the bundled sha256.
123
- if (sha256(consumerCanonical) === file.sha256) return { state: 'ok' };
125
+ if (consumerCanonicalHash === file.sha256) return { state: 'ok' };
124
126
  return { state: 'forked', reason: 'content above marker has been edited' };
125
127
  }
126
128
  if (cmp < 0) {
129
+ const historicalHash = file.history?.[consumerVersion];
130
+ if (!historicalHash) {
131
+ return {
132
+ state: 'forked',
133
+ reason: `canonical hash for version ${consumerVersion} is unavailable`,
134
+ };
135
+ }
136
+ if (consumerCanonicalHash !== historicalHash) {
137
+ return { state: 'forked', reason: 'content above marker has been edited' };
138
+ }
127
139
  return {
128
140
  state: 'stale',
129
141
  consumerVersion,
@@ -210,9 +222,9 @@ const meta = {
210
222
  forked:
211
223
  '{{tool}} drop-in at `{{dest}}` is forked: {{reason}}. Restore the canonical layout (one `KERF-APP-CANONICAL-END` marker, no edits above it) or disable this rule with `\'kerfjs/ai-assistant-configs\': \'off\'`.',
212
224
  },
213
- // Mark as fixable so ESLint runs the `fix()` callback under `--fix`.
214
- // The callback writes to a separate file and returns null, so ESLint
215
- // applies no edit to the linted source itself — see file header.
225
+ // Mark as fixable so the CLI exposes this rule under `--fix`. The callback
226
+ // writes to a separate file and returns null, so ESLint applies no edit to
227
+ // the linted source itself — see file header.
216
228
  fixable: 'code',
217
229
  };
218
230
 
@@ -225,10 +237,7 @@ function create(context) {
225
237
  const claudeEnabled = options.claude !== false;
226
238
  const cursorEnabled = options.cursor !== false;
227
239
  // ESLint v9: `context.cwd` is a string. ESLint v8: `context.getCwd()`.
228
- const cwd =
229
- (typeof context.cwd === 'string' && context.cwd)
230
- || (typeof context.getCwd === 'function' && context.getCwd())
231
- || process.cwd();
240
+ const cwd = contextCwd(context);
232
241
  const checked = runCheck(cwd);
233
242
  CACHED_RESULT = checked;
234
243
  if (!checked) return;
@@ -248,7 +257,7 @@ function create(context) {
248
257
  messageId: 'missing',
249
258
  data,
250
259
  fix() {
251
- applyFix(file, checked.bundleDir, cwd, '');
260
+ if (isFixCliRun()) applyFix(file, checked.bundleDir, cwd, '');
252
261
  return null;
253
262
  },
254
263
  });
@@ -262,7 +271,9 @@ function create(context) {
262
271
  bundledVersion: result.bundledVersion,
263
272
  },
264
273
  fix() {
265
- applyFix(file, checked.bundleDir, cwd, result.appendZone);
274
+ if (isFixCliRun()) {
275
+ applyFix(file, checked.bundleDir, cwd, result.appendZone);
276
+ }
266
277
  return null;
267
278
  },
268
279
  });
@@ -278,4 +289,16 @@ function create(context) {
278
289
  };
279
290
  }
280
291
 
292
+ function contextCwd(context) {
293
+ return (
294
+ (typeof context.cwd === 'string' && context.cwd)
295
+ || (typeof context.getCwd === 'function' && context.getCwd())
296
+ || process.cwd()
297
+ );
298
+ }
299
+
300
+ function isFixCliRun() {
301
+ return process.argv.includes('--fix');
302
+ }
303
+
281
304
  export default { meta, create };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-kerfjs",
3
- "version": "4.4.1",
3
+ "version": "5.0.0-beta.3",
4
4
  "description": "ESLint rules that enforce kerf's hard rules — catches AI-shaped bugs at edit time.",
5
5
  "type": "module",
6
6
  "license": "MIT",