instrlint 0.1.10 → 0.2.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
@@ -178,6 +178,10 @@ The `--verify` flag triggers a two-pass protocol where the host agent (Claude Co
178
178
 
179
179
  instrlint never calls an LLM API. It delegates judgment to whatever model is already running the session.
180
180
 
181
+ ### Refactoring walkthrough (`/instrlint` interactive)
182
+
183
+ After the report, the skill can walk you through CLAUDE.md splitting decisions interactively. Each section is classified into one of four buckets: worth extracting (load rate < 30%), extractable but always-loaded (> 80%, no token saving), should delete not move (duplicates source code), or must stay (cross-conversation context). Run `/instrlint` and follow the prompts.
184
+
181
185
  ## Score and grade
182
186
 
183
187
  | Grade | Score | Meaning |
package/README.zh-TW.md CHANGED
@@ -184,6 +184,10 @@ npx instrlint install --codex
184
184
 
185
185
  instrlint 從不呼叫任何 LLM API,而是將判斷委託給當前 session 中正在運行的 model。
186
186
 
187
+ ### CLAUDE.md 拆分引導(`/instrlint` 互動模式)
188
+
189
+ 報告結束後,skill 可以互動引導你決定 CLAUDE.md 的拆分方式。每個段落會被分入四個桶:值得抽出(載入率 < 30%)、可抽出但不省 token(> 80%,一律載入)、該刪不是該搬(與原始碼或 config 重複)、或必須留在 CLAUDE.md(跨 conversation 的上下文)。執行 `/instrlint` 後依提示操作即可。
190
+
187
191
  ## 分數與等級
188
192
 
189
193
  | 等級 | 分數 | 說明 |
package/dist/cli.cjs CHANGED
@@ -199,7 +199,8 @@ var KEYWORD_REGEX = new RegExp(
199
199
  "gi"
200
200
  );
201
201
  var PATH_REGEX = /(?<!\w)(?:\.{1,2}\/|(?:src|tests?|dist|lib|docs?|config|scripts?|packages?)\/)[^\s,;`'")\]>]+/g;
202
- var RULE_IMPERATIVE_WORDS = /\b(must|should|never|always|prefer|avoid|ensure|require|forbid|use|do not|don't)\b/i;
202
+ var AT_REF_REGEX = /(?<![a-zA-Z0-9_])@((?:\.\.?\/)?[\w./-]+\.(?:md|txt|json|yaml|yml))\b/g;
203
+ var RULE_IMPERATIVE_WORDS = /\b(must|should|never|always|prefer|avoid|ensure|require|forbid|use|do not|don't)\b|必須|應該|應當|永遠|總是|禁止|不要|不可|不得|避免|請使用|優先使用|請勿/i;
203
204
  var RULE_NEGATION_PATTERN = /\b(not|don't|do not)\s+\w+/i;
204
205
  var STRONG_IMPERATIVE = /\b(must|shall|always|never)\b/i;
205
206
  function parseYamlFrontmatter(content) {
@@ -255,6 +256,8 @@ function isRule(text) {
255
256
  if (STRONG_IMPERATIVE.test(body) && /^[A-Z]/.test(body)) return true;
256
257
  if (RULE_IMPERATIVE_WORDS.test(body) && /^[A-Z][a-z]/.test(body))
257
258
  return true;
259
+ if (RULE_IMPERATIVE_WORDS.test(body) && /^[\u4e00-\u9fff\u3400-\u4dbf]/.test(body))
260
+ return true;
258
261
  }
259
262
  return false;
260
263
  }
@@ -267,12 +270,15 @@ function extractKeywords(text) {
267
270
  return [...found];
268
271
  }
269
272
  function extractPaths(text) {
270
- const matches = text.matchAll(PATH_REGEX);
271
273
  const found = [];
272
- for (const m of matches) {
274
+ for (const m of text.matchAll(PATH_REGEX)) {
273
275
  const cleaned = m[0].replace(/[.,;)>\]'"]+$/, "");
274
276
  if (cleaned.length > 1) found.push(cleaned);
275
277
  }
278
+ for (const m of text.matchAll(AT_REF_REGEX)) {
279
+ const cleaned = (m[1] ?? "").replace(/[.,;)>\]'"]+$/, "");
280
+ if (cleaned.length > 0) found.push(cleaned);
281
+ }
276
282
  return [...new Set(found)];
277
283
  }
278
284
  function parseInstructionFile(filePath) {
@@ -953,6 +959,31 @@ function getPrettierField(projectRoot, field) {
953
959
  }
954
960
  return void 0;
955
961
  }
962
+ function readCsprojContent(projectRoot) {
963
+ const buildProps = readTextFile(projectRoot, "Directory.Build.props");
964
+ if (buildProps) return buildProps;
965
+ try {
966
+ const files = (0, import_fs6.readdirSync)(projectRoot);
967
+ for (const f of files) {
968
+ if (f.endsWith(".csproj") || f.endsWith(".fsproj")) {
969
+ const content = readTextFile(projectRoot, f);
970
+ if (content) return content;
971
+ }
972
+ }
973
+ } catch {
974
+ }
975
+ return null;
976
+ }
977
+ function checkCsprojProperty(projectRoot, tag, value) {
978
+ const content = readCsprojContent(projectRoot);
979
+ if (!content) return false;
980
+ return new RegExp(`<${tag}>\\s*${value}\\s*</${tag}>`, "i").test(content);
981
+ }
982
+ function checkEditorConfigPattern(projectRoot, pattern) {
983
+ const content = readTextFile(projectRoot, ".editorconfig");
984
+ if (!content) return false;
985
+ return pattern.test(content);
986
+ }
956
987
  function checkEslintRule(projectRoot, ruleName) {
957
988
  const parsed = readJsonFile(projectRoot, ".eslintrc.json");
958
989
  if (!parsed || typeof parsed !== "object") return false;
@@ -1118,6 +1149,52 @@ var PATTERNS = [
1118
1149
  rulePattern: /\b(no|avoid|prefer\s*named)\s*(default\s*export)/i,
1119
1150
  configCheck: (root) => checkEslintRule(root, "import/no-default-export") || checkEslintRule(root, "no-restricted-exports"),
1120
1151
  configName: "eslint-plugin-import (no-default-export)"
1152
+ },
1153
+ // ─── C# patterns ─────────────────────────────────────────────────────────
1154
+ {
1155
+ id: "csharp-nullable",
1156
+ rulePattern: /\b(nullable\s*reference\s*type|#nullable\s*enable|enable\s*nullable|null\s*reference\s*exception)\b/i,
1157
+ configCheck: (root) => checkCsprojProperty(root, "Nullable", "enable") || checkCsprojProperty(root, "Nullable", "annotations") || checkCsprojProperty(root, "Nullable", "warnings"),
1158
+ configName: ".csproj (<Nullable>enable</Nullable>)"
1159
+ },
1160
+ {
1161
+ id: "csharp-implicit-usings",
1162
+ rulePattern: /\b(global\s*usings?|implicit\s*usings?)\b/i,
1163
+ configCheck: (root) => checkCsprojProperty(root, "ImplicitUsings", "enable"),
1164
+ configName: ".csproj (<ImplicitUsings>enable</ImplicitUsings>)"
1165
+ },
1166
+ {
1167
+ id: "csharp-test-framework",
1168
+ rulePattern: /\b(xunit|nunit|mstest|microsoft\.testing\.framework)\b/i,
1169
+ configCheck: (root) => {
1170
+ const content = readCsprojContent(root);
1171
+ if (!content) return false;
1172
+ return /PackageReference\s+Include="(xunit|NUnit|MSTest|Microsoft\.Testing)/i.test(
1173
+ content
1174
+ );
1175
+ },
1176
+ configName: ".csproj (PackageReference xUnit / NUnit / MSTest)"
1177
+ },
1178
+ {
1179
+ id: "csharp-formatter",
1180
+ rulePattern: /\bdotnet\s*format\b/i,
1181
+ configCheck: (root) => checkEditorConfigPattern(root, /^\[.*\.cs\]/m) && checkEditorConfigPattern(root, /^csharp_/m),
1182
+ configName: ".editorconfig ([*.cs] csharp_style_* settings)"
1183
+ },
1184
+ {
1185
+ id: "csharp-naming",
1186
+ rulePattern: /\bdotnet_naming\b|PascalCase\s+for\s+(all\s+)?(public|class|method|property|type)\b/i,
1187
+ configCheck: (root) => checkEditorConfigPattern(root, /^dotnet_naming_rule\./m),
1188
+ configName: ".editorconfig (dotnet_naming_rule.*)"
1189
+ },
1190
+ {
1191
+ id: "csharp-unused",
1192
+ rulePattern: /\b(IDE0059|CS0168|CS0219|dead\s*code|unused.*IDE|roslyn.*unused)\b/i,
1193
+ configCheck: (root) => checkEditorConfigPattern(
1194
+ root,
1195
+ /^dotnet_diagnostic\.(IDE0059|CS0168|CS0219)/m
1196
+ ),
1197
+ configName: ".editorconfig (dotnet_diagnostic.IDE0059 / CS0168)"
1121
1198
  }
1122
1199
  ];
1123
1200
  function collectRuleLines(instructions) {
@@ -1168,6 +1245,7 @@ function detectConfigOverlaps(instructions, projectRoot) {
1168
1245
 
1169
1246
  // src/utils/text.ts
1170
1247
  var STOP_WORDS = /* @__PURE__ */ new Set([
1248
+ // English grammatical function words
1171
1249
  "the",
1172
1250
  "a",
1173
1251
  "an",
@@ -1181,10 +1259,30 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1181
1259
  "of",
1182
1260
  "with",
1183
1261
  "that",
1184
- "this"
1262
+ "this",
1263
+ // Chinese filler bigrams (grammatical / no topic content)
1264
+ "\u7684\u6642",
1265
+ "\u6642\u5019",
1266
+ "\u4E00\u500B",
1267
+ "\u6240\u6709",
1268
+ "\u6BCF\u500B"
1185
1269
  ]);
1270
+ var CJK_RUN = /[\u4e00-\u9fff\u3400-\u4dbf]+/g;
1271
+ var ASCII_WORD = /[a-z0-9]+/g;
1186
1272
  function tokenizeWords(text) {
1187
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1);
1273
+ const words = [];
1274
+ const lower = text.toLowerCase();
1275
+ for (const m of lower.matchAll(ASCII_WORD)) {
1276
+ if (m[0].length > 1) words.push(m[0]);
1277
+ }
1278
+ for (const m of text.matchAll(CJK_RUN)) {
1279
+ const run = m[0];
1280
+ if (run.length < 2) continue;
1281
+ for (let i = 0; i < run.length - 1; i++) {
1282
+ words.push(run.slice(i, i + 2));
1283
+ }
1284
+ }
1285
+ return words;
1188
1286
  }
1189
1287
  function removeStopWords(words) {
1190
1288
  return words.filter((w) => !STOP_WORDS.has(w));
@@ -1265,8 +1363,10 @@ function analyzeDeadRules(instructions, projectRoot) {
1265
1363
 
1266
1364
  // src/detectors/contradiction.ts
1267
1365
  var NEGATION_WORDS = ["never", "don't", "avoid", "forbid"];
1366
+ var CJK_NEGATIONS = ["\u7981\u6B62", "\u4E0D\u8981", "\u4E0D\u53EF", "\u4E0D\u5F97", "\u907F\u514D", "\u8ACB\u52FF", "\u52FF"];
1367
+ var CJK_ONLY_RE = /^[\u4e00-\u9fff\u3400-\u4dbf]+$/;
1268
1368
  function isNegated(text, word) {
1269
- const sentences = text.split(/[.!?]+\s+/);
1369
+ const sentences = text.split(/[.!?。!?]+\s*/);
1270
1370
  const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1271
1371
  const wordPresent = new RegExp(`\\b${escapedWord}\\b`, "i");
1272
1372
  for (const sentence of sentences) {
@@ -1285,6 +1385,14 @@ function isNegated(text, word) {
1285
1385
  );
1286
1386
  if (notPattern.test(lower)) return true;
1287
1387
  }
1388
+ if (CJK_ONLY_RE.test(word)) {
1389
+ for (const sentence of sentences) {
1390
+ if (!sentence.includes(word)) continue;
1391
+ for (const neg of CJK_NEGATIONS) {
1392
+ if (sentence.includes(neg)) return true;
1393
+ }
1394
+ }
1395
+ }
1288
1396
  return false;
1289
1397
  }
1290
1398
  var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
@@ -1321,7 +1429,26 @@ var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
1321
1429
  "be",
1322
1430
  "by",
1323
1431
  "own",
1324
- "on"
1432
+ "on",
1433
+ // Chinese polarity / imperative bigrams (analogues of English never/always/use/must)
1434
+ "\u6C38\u9060",
1435
+ "\u7E3D\u662F",
1436
+ "\u7981\u6B62",
1437
+ "\u4E0D\u8981",
1438
+ "\u4E0D\u53EF",
1439
+ "\u4E0D\u5F97",
1440
+ "\u907F\u514D",
1441
+ "\u8ACB\u52FF",
1442
+ "\u5FC5\u9808",
1443
+ "\u61C9\u8A72",
1444
+ "\u61C9\u7576",
1445
+ // Chinese generic verb bigrams (HOW to comply, not WHAT topic)
1446
+ "\u4F7F\u7528",
1447
+ "\u63A1\u7528",
1448
+ "\u57F7\u884C",
1449
+ "\u9032\u884C",
1450
+ "\u53EF\u4EE5",
1451
+ "\u9700\u8981"
1325
1452
  ]);
1326
1453
  function collectRuleLines3(instructions) {
1327
1454
  const sources = [
@@ -1426,7 +1553,7 @@ function detectStaleRefs(instructions, projectRoot) {
1426
1553
 
1427
1554
  // src/detectors/scope-classifier.ts
1428
1555
  var PATH_REF_PATTERN = /\b(?:src|tests?|lib|dist)\//i;
1429
- var HOOK_PATTERN = /\b(?:never|don't|do\s+not|forbid)\b.*\b(?:commit|push|merge|build|run)\b/i;
1556
+ var HOOK_PATTERN = /\b(?:never|don't|do\s+not|forbid)\b(?:\s+\w+){0,3}\s+\b(?:commit|push|merge|rebase|tag|checkout|amend)\b/i;
1430
1557
  function classifyScope(instructions) {
1431
1558
  const findings = [];
1432
1559
  const rootFile = instructions.rootFile;
@@ -2463,14 +2590,15 @@ function readInstalledVersion(path) {
2463
2590
  return null;
2464
2591
  }
2465
2592
  }
2466
- function checkSkillUpdate(projectRoot) {
2593
+ function checkSkillUpdate(projectRoot, globalRoot) {
2594
+ const resolvedGlobal = globalRoot ?? (0, import_os.homedir)();
2467
2595
  const candidates = [
2468
2596
  {
2469
2597
  path: (0, import_path8.join)(projectRoot, ".claude", "commands", "instrlint.md"),
2470
2598
  isProject: true
2471
2599
  },
2472
2600
  {
2473
- path: (0, import_path8.join)((0, import_os.homedir)(), ".claude", "commands", "instrlint.md"),
2601
+ path: (0, import_path8.join)(resolvedGlobal, ".claude", "commands", "instrlint.md"),
2474
2602
  isProject: false
2475
2603
  }
2476
2604
  ];
@@ -2513,7 +2641,7 @@ var import_path9 = require("path");
2513
2641
  function shouldVerify(finding) {
2514
2642
  if (finding.category === "stale-ref") return false;
2515
2643
  if (finding.category === "budget") return false;
2516
- if (finding.category === "structure") return false;
2644
+ if (finding.category === "structure") return true;
2517
2645
  if (finding.category === "dead-rule") return !finding.autoFixable;
2518
2646
  if (finding.category === "contradiction") return true;
2519
2647
  if (finding.category === "duplicate") {
@@ -2560,6 +2688,13 @@ function buildContext(finding, parsed, projectRoot) {
2560
2688
  ruleB: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot)
2561
2689
  };
2562
2690
  }
2691
+ if (finding.category === "structure") {
2692
+ return {
2693
+ type: "structure",
2694
+ rule: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot),
2695
+ suggestion: finding.suggestion
2696
+ };
2697
+ }
2563
2698
  return null;
2564
2699
  }
2565
2700
  function hashFinding(finding) {
@@ -2574,6 +2709,10 @@ var QUESTIONS = {
2574
2709
  duplicate: {
2575
2710
  en: 'Are rules A and B true semantic duplicates \u2014 do they say the same thing in different words, such that keeping both adds no value? Respond with JSON only: {"verdict":"confirmed"|"rejected"|"uncertain","reason":"<\u226420 words>"}',
2576
2711
  "zh-TW": '\u898F\u5247 A \u548C\u898F\u5247 B \u5728\u8A9E\u610F\u4E0A\u771F\u7684\u662F\u91CD\u8907\u7684\u55CE\u2014\u2014\u7528\u4E0D\u540C\u7684\u63AA\u8FAD\u8AAA\u540C\u4E00\u4EF6\u4E8B\uFF0C\u4FDD\u7559\u5169\u689D\u6BEB\u7121\u984D\u5916\u50F9\u503C\uFF1F\u50C5\u7528 JSON \u56DE\u7B54\uFF1A{"verdict":"confirmed"|"rejected"|"uncertain","reason":"<20 \u5B57\u4EE5\u5167>"}'
2712
+ },
2713
+ structure: {
2714
+ en: 'Is this structural suggestion accurate? Should this rule truly move to a git hook or path-scoped rule file, or is it an architectural principle / general guidance that belongs in CLAUDE.md? Respond with JSON only: {"verdict":"confirmed"|"rejected"|"uncertain","reason":"<\u226420 words>"}',
2715
+ "zh-TW": '\u9019\u689D\u7D50\u69CB\u5EFA\u8B70\u662F\u5426\u6E96\u78BA\uFF1F\u9019\u689D\u898F\u5247\u771F\u7684\u66F4\u9069\u5408\u79FB\u5230 git hook \u6216 path-scoped rule file \u4E2D\uFF0C\u9084\u662F\u5B83\u662F\u67B6\u69CB\u8AAA\u660E / \u4E00\u822C\u539F\u5247\uFF0C\u61C9\u7559\u5728 CLAUDE.md \u4E2D\uFF1F\u50C5\u7528 JSON \u56DE\u7B54\uFF1A{"verdict":"confirmed"|"rejected"|"uncertain","reason":"<20 \u5B57\u4EE5\u5167>"}'
2577
2716
  }
2578
2717
  };
2579
2718
  function questionFor(category, locale) {