instrlint 0.1.9 → 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 +158 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +160 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -2
- package/skills/claude-code/SKILL.md +10 -2
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) {
|
|
@@ -453,7 +459,11 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
453
459
|
".git",
|
|
454
460
|
".claude",
|
|
455
461
|
".turbo",
|
|
456
|
-
"coverage"
|
|
462
|
+
"coverage",
|
|
463
|
+
"tests",
|
|
464
|
+
"test",
|
|
465
|
+
"__tests__",
|
|
466
|
+
"spec"
|
|
457
467
|
]);
|
|
458
468
|
function findSubClaudeFiles(dir, projectRoot, depth = 0) {
|
|
459
469
|
if (depth > 10) return [];
|
|
@@ -949,6 +959,31 @@ function getPrettierField(projectRoot, field) {
|
|
|
949
959
|
}
|
|
950
960
|
return void 0;
|
|
951
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
|
+
}
|
|
952
987
|
function checkEslintRule(projectRoot, ruleName) {
|
|
953
988
|
const parsed = readJsonFile(projectRoot, ".eslintrc.json");
|
|
954
989
|
if (!parsed || typeof parsed !== "object") return false;
|
|
@@ -1114,6 +1149,52 @@ var PATTERNS = [
|
|
|
1114
1149
|
rulePattern: /\b(no|avoid|prefer\s*named)\s*(default\s*export)/i,
|
|
1115
1150
|
configCheck: (root) => checkEslintRule(root, "import/no-default-export") || checkEslintRule(root, "no-restricted-exports"),
|
|
1116
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)"
|
|
1117
1198
|
}
|
|
1118
1199
|
];
|
|
1119
1200
|
function collectRuleLines(instructions) {
|
|
@@ -1164,6 +1245,7 @@ function detectConfigOverlaps(instructions, projectRoot) {
|
|
|
1164
1245
|
|
|
1165
1246
|
// src/utils/text.ts
|
|
1166
1247
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1248
|
+
// English grammatical function words
|
|
1167
1249
|
"the",
|
|
1168
1250
|
"a",
|
|
1169
1251
|
"an",
|
|
@@ -1177,10 +1259,30 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
1177
1259
|
"of",
|
|
1178
1260
|
"with",
|
|
1179
1261
|
"that",
|
|
1180
|
-
"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"
|
|
1181
1269
|
]);
|
|
1270
|
+
var CJK_RUN = /[\u4e00-\u9fff\u3400-\u4dbf]+/g;
|
|
1271
|
+
var ASCII_WORD = /[a-z0-9]+/g;
|
|
1182
1272
|
function tokenizeWords(text) {
|
|
1183
|
-
|
|
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;
|
|
1184
1286
|
}
|
|
1185
1287
|
function removeStopWords(words) {
|
|
1186
1288
|
return words.filter((w) => !STOP_WORDS.has(w));
|
|
@@ -1261,8 +1363,10 @@ function analyzeDeadRules(instructions, projectRoot) {
|
|
|
1261
1363
|
|
|
1262
1364
|
// src/detectors/contradiction.ts
|
|
1263
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]+$/;
|
|
1264
1368
|
function isNegated(text, word) {
|
|
1265
|
-
const sentences = text.split(/[
|
|
1369
|
+
const sentences = text.split(/[.!?。!?]+\s*/);
|
|
1266
1370
|
const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1267
1371
|
const wordPresent = new RegExp(`\\b${escapedWord}\\b`, "i");
|
|
1268
1372
|
for (const sentence of sentences) {
|
|
@@ -1281,6 +1385,14 @@ function isNegated(text, word) {
|
|
|
1281
1385
|
);
|
|
1282
1386
|
if (notPattern.test(lower)) return true;
|
|
1283
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
|
+
}
|
|
1284
1396
|
return false;
|
|
1285
1397
|
}
|
|
1286
1398
|
var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -1296,6 +1408,9 @@ var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
1296
1408
|
"prefer",
|
|
1297
1409
|
"follow",
|
|
1298
1410
|
"keep",
|
|
1411
|
+
"calls",
|
|
1412
|
+
"run",
|
|
1413
|
+
"runs",
|
|
1299
1414
|
// Common modals and auxiliaries
|
|
1300
1415
|
"must",
|
|
1301
1416
|
"should",
|
|
@@ -1314,7 +1429,26 @@ var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
1314
1429
|
"be",
|
|
1315
1430
|
"by",
|
|
1316
1431
|
"own",
|
|
1317
|
-
"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"
|
|
1318
1452
|
]);
|
|
1319
1453
|
function collectRuleLines3(instructions) {
|
|
1320
1454
|
const sources = [
|
|
@@ -1419,7 +1553,7 @@ function detectStaleRefs(instructions, projectRoot) {
|
|
|
1419
1553
|
|
|
1420
1554
|
// src/detectors/scope-classifier.ts
|
|
1421
1555
|
var PATH_REF_PATTERN = /\b(?:src|tests?|lib|dist)\//i;
|
|
1422
|
-
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;
|
|
1423
1557
|
function classifyScope(instructions) {
|
|
1424
1558
|
const findings = [];
|
|
1425
1559
|
const rootFile = instructions.rootFile;
|
|
@@ -2456,14 +2590,15 @@ function readInstalledVersion(path) {
|
|
|
2456
2590
|
return null;
|
|
2457
2591
|
}
|
|
2458
2592
|
}
|
|
2459
|
-
function checkSkillUpdate(projectRoot) {
|
|
2593
|
+
function checkSkillUpdate(projectRoot, globalRoot) {
|
|
2594
|
+
const resolvedGlobal = globalRoot ?? (0, import_os.homedir)();
|
|
2460
2595
|
const candidates = [
|
|
2461
2596
|
{
|
|
2462
2597
|
path: (0, import_path8.join)(projectRoot, ".claude", "commands", "instrlint.md"),
|
|
2463
2598
|
isProject: true
|
|
2464
2599
|
},
|
|
2465
2600
|
{
|
|
2466
|
-
path: (0, import_path8.join)(
|
|
2601
|
+
path: (0, import_path8.join)(resolvedGlobal, ".claude", "commands", "instrlint.md"),
|
|
2467
2602
|
isProject: false
|
|
2468
2603
|
}
|
|
2469
2604
|
];
|
|
@@ -2506,7 +2641,7 @@ var import_path9 = require("path");
|
|
|
2506
2641
|
function shouldVerify(finding) {
|
|
2507
2642
|
if (finding.category === "stale-ref") return false;
|
|
2508
2643
|
if (finding.category === "budget") return false;
|
|
2509
|
-
if (finding.category === "structure") return
|
|
2644
|
+
if (finding.category === "structure") return true;
|
|
2510
2645
|
if (finding.category === "dead-rule") return !finding.autoFixable;
|
|
2511
2646
|
if (finding.category === "contradiction") return true;
|
|
2512
2647
|
if (finding.category === "duplicate") {
|
|
@@ -2553,6 +2688,13 @@ function buildContext(finding, parsed, projectRoot) {
|
|
|
2553
2688
|
ruleB: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot)
|
|
2554
2689
|
};
|
|
2555
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
|
+
}
|
|
2556
2698
|
return null;
|
|
2557
2699
|
}
|
|
2558
2700
|
function hashFinding(finding) {
|
|
@@ -2567,6 +2709,10 @@ var QUESTIONS = {
|
|
|
2567
2709
|
duplicate: {
|
|
2568
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>"}',
|
|
2569
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>"}'
|
|
2570
2716
|
}
|
|
2571
2717
|
};
|
|
2572
2718
|
function questionFor(category, locale) {
|