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 +4 -0
- package/README.zh-TW.md +4 -0
- package/dist/cli.cjs +150 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +151 -12
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +50 -2
- package/skills/codex/SKILL.md +40 -0
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
|
|
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
|
|
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
|
-
|
|
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(/[
|
|
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
|
|
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)(
|
|
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
|
|
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) {
|