instrlint 0.1.10 → 0.2.2
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/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 +10 -2
package/dist/cli.js
CHANGED
|
@@ -172,7 +172,8 @@ var KEYWORD_REGEX = new RegExp(
|
|
|
172
172
|
"gi"
|
|
173
173
|
);
|
|
174
174
|
var PATH_REGEX = /(?<!\w)(?:\.{1,2}\/|(?:src|tests?|dist|lib|docs?|config|scripts?|packages?)\/)[^\s,;`'")\]>]+/g;
|
|
175
|
-
var
|
|
175
|
+
var AT_REF_REGEX = /(?<![a-zA-Z0-9_])@((?:\.\.?\/)?[\w./-]+\.(?:md|txt|json|yaml|yml))\b/g;
|
|
176
|
+
var RULE_IMPERATIVE_WORDS = /\b(must|should|never|always|prefer|avoid|ensure|require|forbid|use|do not|don't)\b|必須|應該|應當|永遠|總是|禁止|不要|不可|不得|避免|請使用|優先使用|請勿/i;
|
|
176
177
|
var RULE_NEGATION_PATTERN = /\b(not|don't|do not)\s+\w+/i;
|
|
177
178
|
var STRONG_IMPERATIVE = /\b(must|shall|always|never)\b/i;
|
|
178
179
|
function parseYamlFrontmatter(content) {
|
|
@@ -228,6 +229,8 @@ function isRule(text) {
|
|
|
228
229
|
if (STRONG_IMPERATIVE.test(body) && /^[A-Z]/.test(body)) return true;
|
|
229
230
|
if (RULE_IMPERATIVE_WORDS.test(body) && /^[A-Z][a-z]/.test(body))
|
|
230
231
|
return true;
|
|
232
|
+
if (RULE_IMPERATIVE_WORDS.test(body) && /^[\u4e00-\u9fff\u3400-\u4dbf]/.test(body))
|
|
233
|
+
return true;
|
|
231
234
|
}
|
|
232
235
|
return false;
|
|
233
236
|
}
|
|
@@ -240,12 +243,15 @@ function extractKeywords(text) {
|
|
|
240
243
|
return [...found];
|
|
241
244
|
}
|
|
242
245
|
function extractPaths(text) {
|
|
243
|
-
const matches = text.matchAll(PATH_REGEX);
|
|
244
246
|
const found = [];
|
|
245
|
-
for (const m of
|
|
247
|
+
for (const m of text.matchAll(PATH_REGEX)) {
|
|
246
248
|
const cleaned = m[0].replace(/[.,;)>\]'"]+$/, "");
|
|
247
249
|
if (cleaned.length > 1) found.push(cleaned);
|
|
248
250
|
}
|
|
251
|
+
for (const m of text.matchAll(AT_REF_REGEX)) {
|
|
252
|
+
const cleaned = (m[1] ?? "").replace(/[.,;)>\]'"]+$/, "");
|
|
253
|
+
if (cleaned.length > 0) found.push(cleaned);
|
|
254
|
+
}
|
|
249
255
|
return [...new Set(found)];
|
|
250
256
|
}
|
|
251
257
|
function parseInstructionFile(filePath) {
|
|
@@ -885,7 +891,7 @@ function analyzeBudget(instructions) {
|
|
|
885
891
|
}
|
|
886
892
|
|
|
887
893
|
// src/detectors/config-overlap.ts
|
|
888
|
-
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
894
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
|
|
889
895
|
import { join as join5 } from "path";
|
|
890
896
|
function readJsonFile(projectRoot, filename) {
|
|
891
897
|
try {
|
|
@@ -926,6 +932,31 @@ function getPrettierField(projectRoot, field) {
|
|
|
926
932
|
}
|
|
927
933
|
return void 0;
|
|
928
934
|
}
|
|
935
|
+
function readCsprojContent(projectRoot) {
|
|
936
|
+
const buildProps = readTextFile(projectRoot, "Directory.Build.props");
|
|
937
|
+
if (buildProps) return buildProps;
|
|
938
|
+
try {
|
|
939
|
+
const files = readdirSync4(projectRoot);
|
|
940
|
+
for (const f of files) {
|
|
941
|
+
if (f.endsWith(".csproj") || f.endsWith(".fsproj")) {
|
|
942
|
+
const content = readTextFile(projectRoot, f);
|
|
943
|
+
if (content) return content;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} catch {
|
|
947
|
+
}
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
function checkCsprojProperty(projectRoot, tag, value) {
|
|
951
|
+
const content = readCsprojContent(projectRoot);
|
|
952
|
+
if (!content) return false;
|
|
953
|
+
return new RegExp(`<${tag}>\\s*${value}\\s*</${tag}>`, "i").test(content);
|
|
954
|
+
}
|
|
955
|
+
function checkEditorConfigPattern(projectRoot, pattern) {
|
|
956
|
+
const content = readTextFile(projectRoot, ".editorconfig");
|
|
957
|
+
if (!content) return false;
|
|
958
|
+
return pattern.test(content);
|
|
959
|
+
}
|
|
929
960
|
function checkEslintRule(projectRoot, ruleName) {
|
|
930
961
|
const parsed = readJsonFile(projectRoot, ".eslintrc.json");
|
|
931
962
|
if (!parsed || typeof parsed !== "object") return false;
|
|
@@ -1091,6 +1122,52 @@ var PATTERNS = [
|
|
|
1091
1122
|
rulePattern: /\b(no|avoid|prefer\s*named)\s*(default\s*export)/i,
|
|
1092
1123
|
configCheck: (root) => checkEslintRule(root, "import/no-default-export") || checkEslintRule(root, "no-restricted-exports"),
|
|
1093
1124
|
configName: "eslint-plugin-import (no-default-export)"
|
|
1125
|
+
},
|
|
1126
|
+
// ─── C# patterns ─────────────────────────────────────────────────────────
|
|
1127
|
+
{
|
|
1128
|
+
id: "csharp-nullable",
|
|
1129
|
+
rulePattern: /\b(nullable\s*reference\s*type|#nullable\s*enable|enable\s*nullable|null\s*reference\s*exception)\b/i,
|
|
1130
|
+
configCheck: (root) => checkCsprojProperty(root, "Nullable", "enable") || checkCsprojProperty(root, "Nullable", "annotations") || checkCsprojProperty(root, "Nullable", "warnings"),
|
|
1131
|
+
configName: ".csproj (<Nullable>enable</Nullable>)"
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
id: "csharp-implicit-usings",
|
|
1135
|
+
rulePattern: /\b(global\s*usings?|implicit\s*usings?)\b/i,
|
|
1136
|
+
configCheck: (root) => checkCsprojProperty(root, "ImplicitUsings", "enable"),
|
|
1137
|
+
configName: ".csproj (<ImplicitUsings>enable</ImplicitUsings>)"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
id: "csharp-test-framework",
|
|
1141
|
+
rulePattern: /\b(xunit|nunit|mstest|microsoft\.testing\.framework)\b/i,
|
|
1142
|
+
configCheck: (root) => {
|
|
1143
|
+
const content = readCsprojContent(root);
|
|
1144
|
+
if (!content) return false;
|
|
1145
|
+
return /PackageReference\s+Include="(xunit|NUnit|MSTest|Microsoft\.Testing)/i.test(
|
|
1146
|
+
content
|
|
1147
|
+
);
|
|
1148
|
+
},
|
|
1149
|
+
configName: ".csproj (PackageReference xUnit / NUnit / MSTest)"
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
id: "csharp-formatter",
|
|
1153
|
+
rulePattern: /\bdotnet\s*format\b/i,
|
|
1154
|
+
configCheck: (root) => checkEditorConfigPattern(root, /^\[.*\.cs\]/m) && checkEditorConfigPattern(root, /^csharp_/m),
|
|
1155
|
+
configName: ".editorconfig ([*.cs] csharp_style_* settings)"
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
id: "csharp-naming",
|
|
1159
|
+
rulePattern: /\bdotnet_naming\b|PascalCase\s+for\s+(all\s+)?(public|class|method|property|type)\b/i,
|
|
1160
|
+
configCheck: (root) => checkEditorConfigPattern(root, /^dotnet_naming_rule\./m),
|
|
1161
|
+
configName: ".editorconfig (dotnet_naming_rule.*)"
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
id: "csharp-unused",
|
|
1165
|
+
rulePattern: /\b(IDE0059|CS0168|CS0219|dead\s*code|unused.*IDE|roslyn.*unused)\b/i,
|
|
1166
|
+
configCheck: (root) => checkEditorConfigPattern(
|
|
1167
|
+
root,
|
|
1168
|
+
/^dotnet_diagnostic\.(IDE0059|CS0168|CS0219)/m
|
|
1169
|
+
),
|
|
1170
|
+
configName: ".editorconfig (dotnet_diagnostic.IDE0059 / CS0168)"
|
|
1094
1171
|
}
|
|
1095
1172
|
];
|
|
1096
1173
|
function collectRuleLines(instructions) {
|
|
@@ -1141,6 +1218,7 @@ function detectConfigOverlaps(instructions, projectRoot) {
|
|
|
1141
1218
|
|
|
1142
1219
|
// src/utils/text.ts
|
|
1143
1220
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1221
|
+
// English grammatical function words
|
|
1144
1222
|
"the",
|
|
1145
1223
|
"a",
|
|
1146
1224
|
"an",
|
|
@@ -1154,10 +1232,30 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
1154
1232
|
"of",
|
|
1155
1233
|
"with",
|
|
1156
1234
|
"that",
|
|
1157
|
-
"this"
|
|
1235
|
+
"this",
|
|
1236
|
+
// Chinese filler bigrams (grammatical / no topic content)
|
|
1237
|
+
"\u7684\u6642",
|
|
1238
|
+
"\u6642\u5019",
|
|
1239
|
+
"\u4E00\u500B",
|
|
1240
|
+
"\u6240\u6709",
|
|
1241
|
+
"\u6BCF\u500B"
|
|
1158
1242
|
]);
|
|
1243
|
+
var CJK_RUN = /[\u4e00-\u9fff\u3400-\u4dbf]+/g;
|
|
1244
|
+
var ASCII_WORD = /[a-z0-9]+/g;
|
|
1159
1245
|
function tokenizeWords(text) {
|
|
1160
|
-
|
|
1246
|
+
const words = [];
|
|
1247
|
+
const lower = text.toLowerCase();
|
|
1248
|
+
for (const m of lower.matchAll(ASCII_WORD)) {
|
|
1249
|
+
if (m[0].length > 1) words.push(m[0]);
|
|
1250
|
+
}
|
|
1251
|
+
for (const m of text.matchAll(CJK_RUN)) {
|
|
1252
|
+
const run = m[0];
|
|
1253
|
+
if (run.length < 2) continue;
|
|
1254
|
+
for (let i = 0; i < run.length - 1; i++) {
|
|
1255
|
+
words.push(run.slice(i, i + 2));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return words;
|
|
1161
1259
|
}
|
|
1162
1260
|
function removeStopWords(words) {
|
|
1163
1261
|
return words.filter((w) => !STOP_WORDS.has(w));
|
|
@@ -1238,8 +1336,10 @@ function analyzeDeadRules(instructions, projectRoot) {
|
|
|
1238
1336
|
|
|
1239
1337
|
// src/detectors/contradiction.ts
|
|
1240
1338
|
var NEGATION_WORDS = ["never", "don't", "avoid", "forbid"];
|
|
1339
|
+
var CJK_NEGATIONS = ["\u7981\u6B62", "\u4E0D\u8981", "\u4E0D\u53EF", "\u4E0D\u5F97", "\u907F\u514D", "\u8ACB\u52FF", "\u52FF"];
|
|
1340
|
+
var CJK_ONLY_RE = /^[\u4e00-\u9fff\u3400-\u4dbf]+$/;
|
|
1241
1341
|
function isNegated(text, word) {
|
|
1242
|
-
const sentences = text.split(/[
|
|
1342
|
+
const sentences = text.split(/[.!?。!?]+\s*/);
|
|
1243
1343
|
const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1244
1344
|
const wordPresent = new RegExp(`\\b${escapedWord}\\b`, "i");
|
|
1245
1345
|
for (const sentence of sentences) {
|
|
@@ -1258,6 +1358,14 @@ function isNegated(text, word) {
|
|
|
1258
1358
|
);
|
|
1259
1359
|
if (notPattern.test(lower)) return true;
|
|
1260
1360
|
}
|
|
1361
|
+
if (CJK_ONLY_RE.test(word)) {
|
|
1362
|
+
for (const sentence of sentences) {
|
|
1363
|
+
if (!sentence.includes(word)) continue;
|
|
1364
|
+
for (const neg of CJK_NEGATIONS) {
|
|
1365
|
+
if (sentence.includes(neg)) return true;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1261
1369
|
return false;
|
|
1262
1370
|
}
|
|
1263
1371
|
var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -1294,7 +1402,26 @@ var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
1294
1402
|
"be",
|
|
1295
1403
|
"by",
|
|
1296
1404
|
"own",
|
|
1297
|
-
"on"
|
|
1405
|
+
"on",
|
|
1406
|
+
// Chinese polarity / imperative bigrams (analogues of English never/always/use/must)
|
|
1407
|
+
"\u6C38\u9060",
|
|
1408
|
+
"\u7E3D\u662F",
|
|
1409
|
+
"\u7981\u6B62",
|
|
1410
|
+
"\u4E0D\u8981",
|
|
1411
|
+
"\u4E0D\u53EF",
|
|
1412
|
+
"\u4E0D\u5F97",
|
|
1413
|
+
"\u907F\u514D",
|
|
1414
|
+
"\u8ACB\u52FF",
|
|
1415
|
+
"\u5FC5\u9808",
|
|
1416
|
+
"\u61C9\u8A72",
|
|
1417
|
+
"\u61C9\u7576",
|
|
1418
|
+
// Chinese generic verb bigrams (HOW to comply, not WHAT topic)
|
|
1419
|
+
"\u4F7F\u7528",
|
|
1420
|
+
"\u63A1\u7528",
|
|
1421
|
+
"\u57F7\u884C",
|
|
1422
|
+
"\u9032\u884C",
|
|
1423
|
+
"\u53EF\u4EE5",
|
|
1424
|
+
"\u9700\u8981"
|
|
1298
1425
|
]);
|
|
1299
1426
|
function collectRuleLines3(instructions) {
|
|
1300
1427
|
const sources = [
|
|
@@ -1399,7 +1526,7 @@ function detectStaleRefs(instructions, projectRoot) {
|
|
|
1399
1526
|
|
|
1400
1527
|
// src/detectors/scope-classifier.ts
|
|
1401
1528
|
var PATH_REF_PATTERN = /\b(?:src|tests?|lib|dist)\//i;
|
|
1402
|
-
var HOOK_PATTERN = /\b(?:never|don't|do\s+not|forbid)\b
|
|
1529
|
+
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;
|
|
1403
1530
|
function classifyScope(instructions) {
|
|
1404
1531
|
const findings = [];
|
|
1405
1532
|
const rootFile = instructions.rootFile;
|
|
@@ -2436,14 +2563,15 @@ function readInstalledVersion(path) {
|
|
|
2436
2563
|
return null;
|
|
2437
2564
|
}
|
|
2438
2565
|
}
|
|
2439
|
-
function checkSkillUpdate(projectRoot) {
|
|
2566
|
+
function checkSkillUpdate(projectRoot, globalRoot) {
|
|
2567
|
+
const resolvedGlobal = globalRoot ?? homedir();
|
|
2440
2568
|
const candidates = [
|
|
2441
2569
|
{
|
|
2442
2570
|
path: join7(projectRoot, ".claude", "commands", "instrlint.md"),
|
|
2443
2571
|
isProject: true
|
|
2444
2572
|
},
|
|
2445
2573
|
{
|
|
2446
|
-
path: join7(
|
|
2574
|
+
path: join7(resolvedGlobal, ".claude", "commands", "instrlint.md"),
|
|
2447
2575
|
isProject: false
|
|
2448
2576
|
}
|
|
2449
2577
|
];
|
|
@@ -2486,7 +2614,7 @@ import { relative as relative3 } from "path";
|
|
|
2486
2614
|
function shouldVerify(finding) {
|
|
2487
2615
|
if (finding.category === "stale-ref") return false;
|
|
2488
2616
|
if (finding.category === "budget") return false;
|
|
2489
|
-
if (finding.category === "structure") return
|
|
2617
|
+
if (finding.category === "structure") return true;
|
|
2490
2618
|
if (finding.category === "dead-rule") return !finding.autoFixable;
|
|
2491
2619
|
if (finding.category === "contradiction") return true;
|
|
2492
2620
|
if (finding.category === "duplicate") {
|
|
@@ -2533,6 +2661,13 @@ function buildContext(finding, parsed, projectRoot) {
|
|
|
2533
2661
|
ruleB: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot)
|
|
2534
2662
|
};
|
|
2535
2663
|
}
|
|
2664
|
+
if (finding.category === "structure") {
|
|
2665
|
+
return {
|
|
2666
|
+
type: "structure",
|
|
2667
|
+
rule: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot),
|
|
2668
|
+
suggestion: finding.suggestion
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2536
2671
|
return null;
|
|
2537
2672
|
}
|
|
2538
2673
|
function hashFinding(finding) {
|
|
@@ -2547,6 +2682,10 @@ var QUESTIONS = {
|
|
|
2547
2682
|
duplicate: {
|
|
2548
2683
|
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>"}',
|
|
2549
2684
|
"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>"}'
|
|
2685
|
+
},
|
|
2686
|
+
structure: {
|
|
2687
|
+
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>"}',
|
|
2688
|
+
"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>"}'
|
|
2550
2689
|
}
|
|
2551
2690
|
};
|
|
2552
2691
|
function questionFor(category, locale) {
|