instrlint 0.1.5 → 0.1.6
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/cli.cjs +316 -39
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +294 -17
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +87 -0
- package/skills/codex/SKILL.md +84 -0
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ Then **restart Claude Code** to activate the command. Then in your editor:
|
|
|
169
169
|
| D | 60–69 | Poor |
|
|
170
170
|
| F | < 60 | Critical |
|
|
171
171
|
|
|
172
|
-
Deductions: critical finding −10 (cap −40), warning −5 (cap −30), info −1 (cap −10).
|
|
172
|
+
Deductions: critical finding −10 (cap −40), warning −5 (cap −30), info −1 (cap −10). Root file length penalty: proportional above 200 lines (201–300: −5, 301–400: −8, 401–500: −10, 501–600: −15, 601+: −20, cap −30). Budget penalty: continuous above 25% of context window (cap −30).
|
|
173
173
|
|
|
174
174
|
## Contributing
|
|
175
175
|
|
package/README.zh-TW.md
CHANGED
|
@@ -175,7 +175,7 @@ npx instrlint install --codex
|
|
|
175
175
|
| D | 60–69 | 較差 |
|
|
176
176
|
| F | < 60 | 嚴重 |
|
|
177
177
|
|
|
178
|
-
扣分規則:嚴重問題 −10(上限 −40)、警告 −5(上限 −30)、建議 −1(上限 −10
|
|
178
|
+
扣分規則:嚴重問題 −10(上限 −40)、警告 −5(上限 −30)、建議 −1(上限 −10)。根指令檔行數懲罰:超過 200 行後按比例扣分(201–300 行 −5、301–400 −8、401–500 −10、501–600 −15、601+ −20,上限 −30)。預算懲罰:超過 context 視窗 25% 後連續計算(上限 −30)。
|
|
179
179
|
|
|
180
180
|
## 貢獻
|
|
181
181
|
|
package/dist/cli.cjs
CHANGED
|
@@ -32,8 +32,9 @@ var import_commander = require("commander");
|
|
|
32
32
|
|
|
33
33
|
// src/commands/run-command.ts
|
|
34
34
|
var import_child_process = require("child_process");
|
|
35
|
+
var import_fs13 = require("fs");
|
|
35
36
|
var import_readline = require("readline");
|
|
36
|
-
var
|
|
37
|
+
var import_path11 = require("path");
|
|
37
38
|
var import_chalk4 = __toESM(require("chalk"), 1);
|
|
38
39
|
|
|
39
40
|
// src/core/scanner.ts
|
|
@@ -1269,13 +1270,13 @@ function isNegated(text, word) {
|
|
|
1269
1270
|
const lower = sentence.toLowerCase();
|
|
1270
1271
|
for (const neg of NEGATION_WORDS) {
|
|
1271
1272
|
const pattern = new RegExp(
|
|
1272
|
-
`\\b${neg}\\b(?:\\s+\\w+){0,1}\\s+\\b${escapedWord}\\b`,
|
|
1273
|
+
`\\b${neg}\\b(?:\\s+\\w+){0,1}\\s+\\b${escapedWord}\\b(?!['\\u2019]s\\b)`,
|
|
1273
1274
|
"i"
|
|
1274
1275
|
);
|
|
1275
1276
|
if (pattern.test(lower)) return true;
|
|
1276
1277
|
}
|
|
1277
1278
|
const notPattern = new RegExp(
|
|
1278
|
-
`\\b(?:do\\s+)?not\\b(?:\\s+\\w+){0,1}\\s+\\b${escapedWord}\\b`,
|
|
1279
|
+
`\\b(?:do\\s+)?not\\b(?:\\s+\\w+){0,1}\\s+\\b${escapedWord}\\b(?!['\\u2019]s\\b)`,
|
|
1279
1280
|
"i"
|
|
1280
1281
|
);
|
|
1281
1282
|
if (notPattern.test(lower)) return true;
|
|
@@ -1305,7 +1306,15 @@ var POLARITY_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
1305
1306
|
"all",
|
|
1306
1307
|
"every",
|
|
1307
1308
|
"each",
|
|
1308
|
-
"any"
|
|
1309
|
+
"any",
|
|
1310
|
+
// Pronouns and copulas — appear in any sentence regardless of topic;
|
|
1311
|
+
// their negation carries no semantic domain information
|
|
1312
|
+
"it",
|
|
1313
|
+
"its",
|
|
1314
|
+
"be",
|
|
1315
|
+
"by",
|
|
1316
|
+
"own",
|
|
1317
|
+
"on"
|
|
1309
1318
|
]);
|
|
1310
1319
|
function collectRuleLines3(instructions) {
|
|
1311
1320
|
const sources = [
|
|
@@ -1632,6 +1641,9 @@ var en_default = {
|
|
|
1632
1641
|
"install.outdatedVersions": "installed: {{installed}} \u2192 current: {{current}}",
|
|
1633
1642
|
"install.updateCmd": "npx instrlint install {{flag}} --force",
|
|
1634
1643
|
"install.updatePrompt": "Update skill now?",
|
|
1644
|
+
"verification.confirmed": "LLM confirmed",
|
|
1645
|
+
"verification.uncertain": "LLM uncertain",
|
|
1646
|
+
"verification.filteredCount": "\u2298 {{count}} false positive{{s}} filtered by LLM",
|
|
1635
1647
|
"fix.manualActions": "MANUAL ACTIONS NEEDED",
|
|
1636
1648
|
"fix.hookCreate": "Add to .claude/settings.json:",
|
|
1637
1649
|
"fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
|
|
@@ -1739,6 +1751,9 @@ var zh_TW_default = {
|
|
|
1739
1751
|
"install.outdatedVersions": "\u5DF2\u5B89\u88DD\uFF1A{{installed}} \u2192 \u6700\u65B0\uFF1A{{current}}",
|
|
1740
1752
|
"install.updateCmd": "npx instrlint install {{flag}} --force",
|
|
1741
1753
|
"install.updatePrompt": "\u662F\u5426\u7ACB\u5373\u66F4\u65B0\uFF1F",
|
|
1754
|
+
"verification.confirmed": "LLM \u5DF2\u78BA\u8A8D",
|
|
1755
|
+
"verification.uncertain": "LLM \u4E0D\u78BA\u5B9A",
|
|
1756
|
+
"verification.filteredCount": "\u2298 \u5DF2\u904E\u6FFE {{count}} \u500B{{s}} false positive\uFF08\u7531 LLM \u5224\u65B7\uFF09",
|
|
1742
1757
|
"fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
|
|
1743
1758
|
"fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
|
|
1744
1759
|
"fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
|
|
@@ -1988,7 +2003,10 @@ function printTopIssues(findings, output) {
|
|
|
1988
2003
|
const icon = f.severity === "critical" ? import_chalk2.default.red("\u2716") : f.severity === "warning" ? import_chalk2.default.yellow("\u26A0") : import_chalk2.default.blue("\u2139");
|
|
1989
2004
|
const msg = t(f.messageKey, f.messageParams);
|
|
1990
2005
|
const truncated = msg.length > 68 ? `${msg.slice(0, 68)}\u2026` : msg;
|
|
1991
|
-
|
|
2006
|
+
const verifyBadge = f.verification?.verdict === "confirmed" ? import_chalk2.default.green(` \u2713 ${t("verification.confirmed")}`) : f.verification?.verdict === "uncertain" ? import_chalk2.default.yellow(` \u2753 ${t("verification.uncertain")}`) : "";
|
|
2007
|
+
output.log(
|
|
2008
|
+
` ${import_chalk2.default.white(`${i + 1}.`)} ${icon} ${truncated}${verifyBadge}`
|
|
2009
|
+
);
|
|
1992
2010
|
}
|
|
1993
2011
|
if (sorted.length > 5) {
|
|
1994
2012
|
output.log(
|
|
@@ -2058,6 +2076,13 @@ function printCombinedTerminal(report, output = console) {
|
|
|
2058
2076
|
import_chalk2.default.gray(` \u2500\u2500`) + ` ${summary} ` + import_chalk2.default.gray("\u2500".repeat(pad))
|
|
2059
2077
|
);
|
|
2060
2078
|
}
|
|
2079
|
+
if (report.rejectedByVerification) {
|
|
2080
|
+
output.log(
|
|
2081
|
+
import_chalk2.default.gray(
|
|
2082
|
+
` ${t("verification.filteredCount", { count: String(report.rejectedByVerification), s: plural(report.rejectedByVerification) })}`
|
|
2083
|
+
)
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2061
2086
|
output.log("");
|
|
2062
2087
|
}
|
|
2063
2088
|
function reportJson(report) {
|
|
@@ -2095,6 +2120,9 @@ function reportMarkdown(report, extraSections = []) {
|
|
|
2095
2120
|
`| ${t("markdown.critical")} | ${criticals} |`,
|
|
2096
2121
|
`| ${t("markdown.warning")} | ${warnings} |`,
|
|
2097
2122
|
`| ${t("markdown.info")} | ${infos} |`,
|
|
2123
|
+
...report.rejectedByVerification ? [
|
|
2124
|
+
`| ${t("verification.filteredCount", { count: String(report.rejectedByVerification), s: plural(report.rejectedByVerification) })} | |`
|
|
2125
|
+
] : [],
|
|
2098
2126
|
""
|
|
2099
2127
|
];
|
|
2100
2128
|
const window = budget.totalBaseline + budget.availableTokens;
|
|
@@ -2181,8 +2209,9 @@ function reportMarkdown(report, extraSections = []) {
|
|
|
2181
2209
|
lines.push(`## ${t(labelKey)}`, "");
|
|
2182
2210
|
for (const f of group) {
|
|
2183
2211
|
const loc = f.line != null ? ` ${t("markdown.lineRef", { line: String(f.line) })}` : "";
|
|
2212
|
+
const verifyBadge = f.verification?.verdict === "confirmed" ? ` \u2713 *${t("verification.confirmed")}*: ${f.verification.reason}` : f.verification?.verdict === "uncertain" ? ` \u2753 *${t("verification.uncertain")}*: ${f.verification.reason}` : "";
|
|
2184
2213
|
lines.push(
|
|
2185
|
-
`- ${mdSeverityIcon(f)} ${t(f.messageKey, f.messageParams)}${loc}`
|
|
2214
|
+
`- ${mdSeverityIcon(f)} ${t(f.messageKey, f.messageParams)}${loc}${verifyBadge}`
|
|
2186
2215
|
);
|
|
2187
2216
|
}
|
|
2188
2217
|
lines.push("");
|
|
@@ -2469,9 +2498,202 @@ $2`
|
|
|
2469
2498
|
return updated;
|
|
2470
2499
|
}
|
|
2471
2500
|
|
|
2472
|
-
// src/
|
|
2473
|
-
var
|
|
2501
|
+
// src/verifiers/candidates.ts
|
|
2502
|
+
var import_crypto = require("crypto");
|
|
2474
2503
|
var import_path9 = require("path");
|
|
2504
|
+
|
|
2505
|
+
// src/verifiers/policy.ts
|
|
2506
|
+
function shouldVerify(finding) {
|
|
2507
|
+
if (finding.category === "stale-ref") return false;
|
|
2508
|
+
if (finding.category === "budget") return false;
|
|
2509
|
+
if (finding.category === "structure") return false;
|
|
2510
|
+
if (finding.category === "dead-rule") return !finding.autoFixable;
|
|
2511
|
+
if (finding.category === "contradiction") return true;
|
|
2512
|
+
if (finding.category === "duplicate") {
|
|
2513
|
+
return finding.severity === "warning" && !finding.autoFixable;
|
|
2514
|
+
}
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/verifiers/candidates.ts
|
|
2519
|
+
function findLineText(parsed, filePath, lineNumber) {
|
|
2520
|
+
const sources = [
|
|
2521
|
+
parsed.rootFile,
|
|
2522
|
+
...parsed.subFiles,
|
|
2523
|
+
...parsed.rules
|
|
2524
|
+
];
|
|
2525
|
+
const file = sources.find((f) => f.path === filePath);
|
|
2526
|
+
if (!file) return "";
|
|
2527
|
+
const line = file.lines.find((l) => l.lineNumber === lineNumber);
|
|
2528
|
+
return line?.text.trim() ?? "";
|
|
2529
|
+
}
|
|
2530
|
+
function ruleRef(parsed, filePath, lineNumber, projectRoot) {
|
|
2531
|
+
return {
|
|
2532
|
+
file: toRelative(filePath, projectRoot),
|
|
2533
|
+
line: lineNumber,
|
|
2534
|
+
text: findLineText(parsed, filePath, lineNumber)
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function buildContext(finding, parsed, projectRoot) {
|
|
2538
|
+
if (finding.category === "contradiction") {
|
|
2539
|
+
const { fileA, lineA } = finding.messageParams ?? {};
|
|
2540
|
+
if (!fileA || !lineA) return null;
|
|
2541
|
+
return {
|
|
2542
|
+
type: "contradiction",
|
|
2543
|
+
ruleA: ruleRef(parsed, fileA, Number(lineA), projectRoot),
|
|
2544
|
+
ruleB: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot)
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
if (finding.category === "duplicate") {
|
|
2548
|
+
const { otherFile, otherLine } = finding.messageParams ?? {};
|
|
2549
|
+
if (!otherFile || !otherLine) return null;
|
|
2550
|
+
return {
|
|
2551
|
+
type: "duplicate",
|
|
2552
|
+
ruleA: ruleRef(parsed, otherFile, Number(otherLine), projectRoot),
|
|
2553
|
+
ruleB: ruleRef(parsed, finding.file, finding.line ?? 0, projectRoot)
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
function hashFinding(finding) {
|
|
2559
|
+
const key = `${finding.category}:${finding.file}:${finding.line ?? 0}:${finding.messageKey}`;
|
|
2560
|
+
return (0, import_crypto.createHash)("sha256").update(key).digest("hex").slice(0, 12);
|
|
2561
|
+
}
|
|
2562
|
+
var QUESTIONS = {
|
|
2563
|
+
contradiction: {
|
|
2564
|
+
en: 'Do rules A and B actually contradict each other in practice? A real contradiction means a developer following both rules would be forced to violate one of them. Respond with JSON only: {"verdict":"confirmed"|"rejected"|"uncertain","reason":"<\u226420 words>"}',
|
|
2565
|
+
"zh-TW": '\u898F\u5247 A \u548C\u898F\u5247 B \u5728\u5BE6\u969B\u958B\u767C\u4E2D\u771F\u7684\u76F8\u4E92\u77DB\u76FE\u55CE\uFF1F\u771F\u6B63\u7684\u77DB\u76FE\u662F\u6307\uFF1A\u540C\u6642\u9075\u5B88\u5169\u689D\u898F\u5247\u5728\u67D0\u4E9B\u60C5\u5883\u4E0B\u662F\u4E0D\u53EF\u80FD\u7684\u3002\u50C5\u7528 JSON \u56DE\u7B54\uFF1A{"verdict":"confirmed"|"rejected"|"uncertain","reason":"<20 \u5B57\u4EE5\u5167>"}'
|
|
2566
|
+
},
|
|
2567
|
+
duplicate: {
|
|
2568
|
+
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
|
+
"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>"}'
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
function questionFor(category, locale) {
|
|
2573
|
+
const lang = locale === "zh-TW" ? "zh-TW" : "en";
|
|
2574
|
+
const question = QUESTIONS[category]?.[lang] ?? QUESTIONS[category]?.["en"];
|
|
2575
|
+
if (!question) {
|
|
2576
|
+
process.stderr.write(
|
|
2577
|
+
`[instrlint] Warning: no verification question for category "${category}", skipping
|
|
2578
|
+
`
|
|
2579
|
+
);
|
|
2580
|
+
return "";
|
|
2581
|
+
}
|
|
2582
|
+
return question;
|
|
2583
|
+
}
|
|
2584
|
+
function toRelative(filePath, projectRoot) {
|
|
2585
|
+
const rel = (0, import_path9.relative)(projectRoot, filePath);
|
|
2586
|
+
return rel.startsWith("..") ? filePath : rel;
|
|
2587
|
+
}
|
|
2588
|
+
function normalizeFilePaths(finding, projectRoot) {
|
|
2589
|
+
const normalized = {
|
|
2590
|
+
...finding,
|
|
2591
|
+
file: toRelative(finding.file, projectRoot)
|
|
2592
|
+
};
|
|
2593
|
+
if (normalized.messageParams) {
|
|
2594
|
+
const params = { ...normalized.messageParams };
|
|
2595
|
+
if (params["fileA"])
|
|
2596
|
+
params["fileA"] = toRelative(params["fileA"], projectRoot);
|
|
2597
|
+
if (params["otherFile"])
|
|
2598
|
+
params["otherFile"] = toRelative(params["otherFile"], projectRoot);
|
|
2599
|
+
normalized.messageParams = params;
|
|
2600
|
+
}
|
|
2601
|
+
return normalized;
|
|
2602
|
+
}
|
|
2603
|
+
function buildCandidates(findings, parsed, projectRoot, locale) {
|
|
2604
|
+
const candidates = [];
|
|
2605
|
+
for (const finding of findings) {
|
|
2606
|
+
if (!shouldVerify(finding)) continue;
|
|
2607
|
+
const context = buildContext(finding, parsed, projectRoot);
|
|
2608
|
+
if (!context) continue;
|
|
2609
|
+
candidates.push({
|
|
2610
|
+
id: hashFinding(finding),
|
|
2611
|
+
category: finding.category,
|
|
2612
|
+
question: questionFor(finding.category, locale),
|
|
2613
|
+
context,
|
|
2614
|
+
originalFinding: normalizeFilePaths(finding, projectRoot)
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
return {
|
|
2618
|
+
version: 1,
|
|
2619
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2620
|
+
projectRoot,
|
|
2621
|
+
candidates
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// src/verifiers/verdicts.ts
|
|
2626
|
+
var import_fs11 = require("fs");
|
|
2627
|
+
function applyVerdicts(findings, verdictsFile) {
|
|
2628
|
+
const verdictMap = new Map(verdictsFile.verdicts.map((v) => [v.id, v]));
|
|
2629
|
+
const withVerdicts = findings.map((f) => {
|
|
2630
|
+
const verdict = verdictMap.get(hashFinding(f));
|
|
2631
|
+
if (!verdict) return f;
|
|
2632
|
+
return {
|
|
2633
|
+
...f,
|
|
2634
|
+
verification: { verdict: verdict.verdict, reason: verdict.reason }
|
|
2635
|
+
};
|
|
2636
|
+
});
|
|
2637
|
+
const kept = withVerdicts.filter(
|
|
2638
|
+
(f) => f.verification?.verdict !== "rejected"
|
|
2639
|
+
);
|
|
2640
|
+
const rejectedCount = withVerdicts.length - kept.length;
|
|
2641
|
+
return { findings: kept, rejectedCount };
|
|
2642
|
+
}
|
|
2643
|
+
function loadVerdictsFile(filePath) {
|
|
2644
|
+
let raw;
|
|
2645
|
+
try {
|
|
2646
|
+
raw = (0, import_fs11.readFileSync)(filePath, "utf8");
|
|
2647
|
+
} catch {
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
`Cannot read verdicts file: ${filePath}
|
|
2650
|
+
Run instrlint --emit-candidates first, then ask the host LLM to write verdicts.`
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
let parsed;
|
|
2654
|
+
try {
|
|
2655
|
+
parsed = JSON.parse(raw);
|
|
2656
|
+
} catch {
|
|
2657
|
+
throw new Error(`verdicts.json is not valid JSON: ${filePath}`);
|
|
2658
|
+
}
|
|
2659
|
+
const obj = parsed;
|
|
2660
|
+
if (typeof parsed !== "object" || parsed === null || obj["version"] !== 1 || !Array.isArray(obj["verdicts"])) {
|
|
2661
|
+
throw new Error(
|
|
2662
|
+
`verdicts.json has unexpected format (expected {version:1, verdicts:[...]}): ${filePath}`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
const VALID_VERDICTS = /* @__PURE__ */ new Set(["confirmed", "rejected", "uncertain"]);
|
|
2666
|
+
const MAX_REASON_LENGTH = 500;
|
|
2667
|
+
for (const v of obj["verdicts"]) {
|
|
2668
|
+
if (typeof v !== "object" || v === null) {
|
|
2669
|
+
throw new Error(`verdicts.json: each verdict must be an object`);
|
|
2670
|
+
}
|
|
2671
|
+
const item = v;
|
|
2672
|
+
if (typeof item["id"] !== "string" || item["id"].length === 0) {
|
|
2673
|
+
throw new Error(`verdicts.json: verdict missing string "id"`);
|
|
2674
|
+
}
|
|
2675
|
+
if (!VALID_VERDICTS.has(item["verdict"])) {
|
|
2676
|
+
throw new Error(
|
|
2677
|
+
`verdicts.json: invalid verdict "${item["verdict"]}" for id "${item["id"]}" (must be confirmed|rejected|uncertain)`
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
if (typeof item["reason"] !== "string") {
|
|
2681
|
+
throw new Error(
|
|
2682
|
+
`verdicts.json: verdict "${item["id"]}" missing string "reason"`
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
if (item["reason"].length > MAX_REASON_LENGTH) {
|
|
2686
|
+
throw new Error(
|
|
2687
|
+
`verdicts.json: verdict "${item["id"]}" reason exceeds ${MAX_REASON_LENGTH} characters`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return parsed;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/commands/install-command.ts
|
|
2695
|
+
var import_fs12 = require("fs");
|
|
2696
|
+
var import_path10 = require("path");
|
|
2475
2697
|
var import_os2 = require("os");
|
|
2476
2698
|
var import_url2 = require("url");
|
|
2477
2699
|
function resolveSkillFile(target) {
|
|
@@ -2479,15 +2701,15 @@ function resolveSkillFile(target) {
|
|
|
2479
2701
|
const subDir = target === "claude-code" ? "claude-code" : "codex";
|
|
2480
2702
|
for (const levels of [2, 3]) {
|
|
2481
2703
|
const parts = Array(levels).fill("..");
|
|
2482
|
-
const candidate = (0,
|
|
2483
|
-
if ((0,
|
|
2704
|
+
const candidate = (0, import_path10.join)(thisFile, ...parts, "skills", subDir, "SKILL.md");
|
|
2705
|
+
if ((0, import_fs12.existsSync)(candidate)) return candidate;
|
|
2484
2706
|
}
|
|
2485
|
-
return (0,
|
|
2707
|
+
return (0, import_path10.join)(thisFile, "..", "..", "skills", subDir, "SKILL.md");
|
|
2486
2708
|
}
|
|
2487
2709
|
function readSkillContent(target) {
|
|
2488
2710
|
const skillPath = resolveSkillFile(target);
|
|
2489
2711
|
try {
|
|
2490
|
-
const raw = (0,
|
|
2712
|
+
const raw = (0, import_fs12.readFileSync)(skillPath, "utf8");
|
|
2491
2713
|
return injectVersion(raw, CURRENT_VERSION);
|
|
2492
2714
|
} catch {
|
|
2493
2715
|
throw new Error(
|
|
@@ -2496,26 +2718,26 @@ function readSkillContent(target) {
|
|
|
2496
2718
|
}
|
|
2497
2719
|
}
|
|
2498
2720
|
function installClaudeCode(content, projectRoot, isProject, force, output) {
|
|
2499
|
-
const targetDir = isProject ? (0,
|
|
2500
|
-
const targetPath = (0,
|
|
2501
|
-
if ((0,
|
|
2721
|
+
const targetDir = isProject ? (0, import_path10.join)(projectRoot, ".claude", "commands") : (0, import_path10.join)((0, import_os2.homedir)(), ".claude", "commands");
|
|
2722
|
+
const targetPath = (0, import_path10.join)(targetDir, "instrlint.md");
|
|
2723
|
+
if ((0, import_fs12.existsSync)(targetPath) && !force) {
|
|
2502
2724
|
output.error(t("install.alreadyExists", { path: targetPath }));
|
|
2503
2725
|
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2504
2726
|
}
|
|
2505
|
-
(0,
|
|
2506
|
-
(0,
|
|
2727
|
+
(0, import_fs12.mkdirSync)(targetDir, { recursive: true });
|
|
2728
|
+
(0, import_fs12.writeFileSync)(targetPath, content, "utf8");
|
|
2507
2729
|
output.log(t("install.installed", { path: targetPath }));
|
|
2508
2730
|
return { exitCode: 0 };
|
|
2509
2731
|
}
|
|
2510
2732
|
function installCodex(content, projectRoot, force, output) {
|
|
2511
|
-
const targetDir = (0,
|
|
2512
|
-
const targetPath = (0,
|
|
2513
|
-
if ((0,
|
|
2733
|
+
const targetDir = (0, import_path10.join)(projectRoot, ".agents", "skills", "instrlint");
|
|
2734
|
+
const targetPath = (0, import_path10.join)(targetDir, "SKILL.md");
|
|
2735
|
+
if ((0, import_fs12.existsSync)(targetPath) && !force) {
|
|
2514
2736
|
output.error(t("install.alreadyExists", { path: targetPath }));
|
|
2515
2737
|
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2516
2738
|
}
|
|
2517
|
-
(0,
|
|
2518
|
-
(0,
|
|
2739
|
+
(0, import_fs12.mkdirSync)(targetDir, { recursive: true });
|
|
2740
|
+
(0, import_fs12.writeFileSync)(targetPath, content, "utf8");
|
|
2519
2741
|
output.log(t("install.installed", { path: targetPath }));
|
|
2520
2742
|
return { exitCode: 0 };
|
|
2521
2743
|
}
|
|
@@ -2574,10 +2796,30 @@ async function runAll(opts, output = console) {
|
|
|
2574
2796
|
output.error(t("error.missingRootFile", { tool: scan.tool }));
|
|
2575
2797
|
return { exitCode: 1, errorMessage: "missing root file" };
|
|
2576
2798
|
}
|
|
2799
|
+
if (opts.fix && opts.emitCandidates) {
|
|
2800
|
+
output.error("--fix and --emit-candidates cannot be used together");
|
|
2801
|
+
return {
|
|
2802
|
+
exitCode: 1,
|
|
2803
|
+
errorMessage: "--fix and --emit-candidates conflict"
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2577
2806
|
if (opts.fix && !opts.force && !isGitClean(projectRoot)) {
|
|
2578
2807
|
output.error(t("error.dirtyWorkingTree"));
|
|
2579
2808
|
return { exitCode: 1, errorMessage: "dirty working tree" };
|
|
2580
2809
|
}
|
|
2810
|
+
if (opts.emitCandidates) {
|
|
2811
|
+
const resolved = (0, import_path11.resolve)(opts.emitCandidates);
|
|
2812
|
+
const rel = (0, import_path11.relative)(projectRoot, resolved);
|
|
2813
|
+
if (rel.startsWith("..")) {
|
|
2814
|
+
output.error(
|
|
2815
|
+
`--emit-candidates path must be within the project directory: ${opts.emitCandidates}`
|
|
2816
|
+
);
|
|
2817
|
+
return {
|
|
2818
|
+
exitCode: 1,
|
|
2819
|
+
errorMessage: "emit-candidates path outside project"
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2581
2823
|
const instructions = loadProject(projectRoot, scan.tool);
|
|
2582
2824
|
const { findings: budgetFindings, summary } = analyzeBudget(instructions);
|
|
2583
2825
|
const { findings: deadRuleFindings } = analyzeDeadRules(
|
|
@@ -2588,15 +2830,40 @@ async function runAll(opts, output = console) {
|
|
|
2588
2830
|
instructions,
|
|
2589
2831
|
projectRoot
|
|
2590
2832
|
);
|
|
2591
|
-
|
|
2833
|
+
let allFindings = [
|
|
2592
2834
|
...budgetFindings,
|
|
2593
2835
|
...deadRuleFindings,
|
|
2594
2836
|
...structureFindings
|
|
2595
2837
|
];
|
|
2838
|
+
if (opts.emitCandidates) {
|
|
2839
|
+
const candidatesFile = buildCandidates(
|
|
2840
|
+
allFindings,
|
|
2841
|
+
instructions,
|
|
2842
|
+
projectRoot,
|
|
2843
|
+
getLocale()
|
|
2844
|
+
);
|
|
2845
|
+
(0, import_fs13.writeFileSync)(opts.emitCandidates, JSON.stringify(candidatesFile, null, 2));
|
|
2846
|
+
if (opts.skipReport) return { exitCode: 0 };
|
|
2847
|
+
}
|
|
2848
|
+
let rejectedByVerification;
|
|
2849
|
+
if (opts.applyVerdicts) {
|
|
2850
|
+
try {
|
|
2851
|
+
const verdictsFile = loadVerdictsFile(opts.applyVerdicts);
|
|
2852
|
+
const result = applyVerdicts(
|
|
2853
|
+
allFindings,
|
|
2854
|
+
verdictsFile
|
|
2855
|
+
);
|
|
2856
|
+
allFindings = result.findings;
|
|
2857
|
+
rejectedByVerification = result.rejectedCount > 0 ? result.rejectedCount : void 0;
|
|
2858
|
+
} catch (err) {
|
|
2859
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
2860
|
+
return { exitCode: 1, errorMessage: "failed to apply verdicts" };
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2596
2863
|
const { score, grade } = calculateScore(allFindings, summary);
|
|
2597
2864
|
const actionPlan = buildActionPlan(allFindings);
|
|
2598
2865
|
const report = {
|
|
2599
|
-
project: (0,
|
|
2866
|
+
project: (0, import_path11.basename)(projectRoot),
|
|
2600
2867
|
tool: instructions.tool,
|
|
2601
2868
|
score,
|
|
2602
2869
|
grade,
|
|
@@ -2604,7 +2871,8 @@ async function runAll(opts, output = console) {
|
|
|
2604
2871
|
tokenMethod: summary.tokenMethod,
|
|
2605
2872
|
findings: allFindings,
|
|
2606
2873
|
budget: summary,
|
|
2607
|
-
actionPlan
|
|
2874
|
+
actionPlan,
|
|
2875
|
+
...rejectedByVerification !== void 0 ? { rejectedByVerification } : {}
|
|
2608
2876
|
};
|
|
2609
2877
|
if (opts.fix) {
|
|
2610
2878
|
const suggestions = buildStructureSuggestions(allFindings);
|
|
@@ -2685,7 +2953,7 @@ async function runAll(opts, output = console) {
|
|
|
2685
2953
|
return { exitCode: 0 };
|
|
2686
2954
|
}
|
|
2687
2955
|
function promptYesNo(question) {
|
|
2688
|
-
return new Promise((
|
|
2956
|
+
return new Promise((resolve2) => {
|
|
2689
2957
|
const rl = (0, import_readline.createInterface)({
|
|
2690
2958
|
input: process.stdin,
|
|
2691
2959
|
output: process.stdout
|
|
@@ -2693,7 +2961,7 @@ function promptYesNo(question) {
|
|
|
2693
2961
|
rl.question(`${question} ${import_chalk4.default.gray("[Y/n]")} `, (answer) => {
|
|
2694
2962
|
rl.close();
|
|
2695
2963
|
const trimmed = answer.trim().toLowerCase();
|
|
2696
|
-
|
|
2964
|
+
resolve2(trimmed === "" || trimmed === "y");
|
|
2697
2965
|
});
|
|
2698
2966
|
});
|
|
2699
2967
|
}
|
|
@@ -2857,8 +3125,8 @@ async function runStructure(opts, output = console) {
|
|
|
2857
3125
|
}
|
|
2858
3126
|
|
|
2859
3127
|
// src/commands/ci-command.ts
|
|
2860
|
-
var
|
|
2861
|
-
var
|
|
3128
|
+
var import_fs14 = require("fs");
|
|
3129
|
+
var import_path12 = require("path");
|
|
2862
3130
|
|
|
2863
3131
|
// src/reporters/sarif.ts
|
|
2864
3132
|
function severityToLevel(severity) {
|
|
@@ -2964,7 +3232,7 @@ async function runCi(opts, output = console) {
|
|
|
2964
3232
|
const { score, grade } = calculateScore(allFindings, summary);
|
|
2965
3233
|
const actionPlan = buildActionPlan(allFindings);
|
|
2966
3234
|
const report = {
|
|
2967
|
-
project: (0,
|
|
3235
|
+
project: (0, import_path12.basename)(projectRoot),
|
|
2968
3236
|
tool: instructions.tool,
|
|
2969
3237
|
score,
|
|
2970
3238
|
grade,
|
|
@@ -2985,7 +3253,7 @@ async function runCi(opts, output = console) {
|
|
|
2985
3253
|
formatted = reportJson(report);
|
|
2986
3254
|
}
|
|
2987
3255
|
if (opts.output != null) {
|
|
2988
|
-
(0,
|
|
3256
|
+
(0, import_fs14.writeFileSync)(opts.output, formatted, "utf8");
|
|
2989
3257
|
const pass = !shouldFail(allFindings, failOn);
|
|
2990
3258
|
const statusKey = pass ? "ci.passed" : "ci.failed";
|
|
2991
3259
|
output.error(
|
|
@@ -2999,8 +3267,8 @@ async function runCi(opts, output = console) {
|
|
|
2999
3267
|
}
|
|
3000
3268
|
|
|
3001
3269
|
// src/commands/init-ci-command.ts
|
|
3002
|
-
var
|
|
3003
|
-
var
|
|
3270
|
+
var import_fs15 = require("fs");
|
|
3271
|
+
var import_path13 = require("path");
|
|
3004
3272
|
function githubWorkflow() {
|
|
3005
3273
|
return `name: instrlint
|
|
3006
3274
|
|
|
@@ -3069,14 +3337,14 @@ instrlint:
|
|
|
3069
3337
|
function runInitCi(opts, output = console) {
|
|
3070
3338
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
3071
3339
|
if (opts.github) {
|
|
3072
|
-
const workflowDir = (0,
|
|
3073
|
-
const workflowPath = (0,
|
|
3074
|
-
if ((0,
|
|
3340
|
+
const workflowDir = (0, import_path13.join)(projectRoot, ".github", "workflows");
|
|
3341
|
+
const workflowPath = (0, import_path13.join)(workflowDir, "instrlint.yml");
|
|
3342
|
+
if ((0, import_fs15.existsSync)(workflowPath) && !opts.force) {
|
|
3075
3343
|
output.error(t("initCi.alreadyExists", { path: workflowPath }));
|
|
3076
3344
|
return { exitCode: 1, errorMessage: "file already exists" };
|
|
3077
3345
|
}
|
|
3078
|
-
(0,
|
|
3079
|
-
(0,
|
|
3346
|
+
(0, import_fs15.mkdirSync)(workflowDir, { recursive: true });
|
|
3347
|
+
(0, import_fs15.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
|
|
3080
3348
|
output.log(t("initCi.created", { path: workflowPath }));
|
|
3081
3349
|
return { exitCode: 0 };
|
|
3082
3350
|
}
|
|
@@ -3096,7 +3364,16 @@ program.enablePositionalOptions().name("instrlint").description(
|
|
|
3096
3364
|
"--format <type>",
|
|
3097
3365
|
"output format (terminal|json|markdown)",
|
|
3098
3366
|
"terminal"
|
|
3099
|
-
).option("--lang <locale>", "output language (en|zh-TW)", "en").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").option("--fix", "auto-fix safe issues (dead rules, stale refs, dupes)").option("--force", "skip git clean check when using --fix").
|
|
3367
|
+
).option("--lang <locale>", "output language (en|zh-TW)", "en").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").option("--fix", "auto-fix safe issues (dead rules, stale refs, dupes)").option("--force", "skip git clean check when using --fix").option(
|
|
3368
|
+
"--emit-candidates <path>",
|
|
3369
|
+
"write low-confidence findings as candidates JSON for host LLM verification"
|
|
3370
|
+
).option(
|
|
3371
|
+
"--apply-verdicts <path>",
|
|
3372
|
+
"apply host LLM verdicts from JSON file to the report"
|
|
3373
|
+
).option(
|
|
3374
|
+
"--skip-report",
|
|
3375
|
+
"suppress terminal output (use with --emit-candidates)"
|
|
3376
|
+
).action(async function() {
|
|
3100
3377
|
const opts = this.opts();
|
|
3101
3378
|
const result = await runAll(opts);
|
|
3102
3379
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|