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 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). Budget penalty: −5 if baseline > 25% of context, −15 if > 50%.
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)。預算懲罰:baseline 超過 context 25% 5,超過 50% 15。
178
+ 扣分規則:嚴重問題 −10(上限 −40)、警告 −5(上限 −30)、建議 −1(上限 −10)。根指令檔行數懲罰:超過 200 行後按比例扣分(201–300 −5、301–400 −8、401–50010、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 import_path10 = require("path");
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
- output.log(` ${import_chalk2.default.white(`${i + 1}.`)} ${icon} ${truncated}`);
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/commands/install-command.ts
2473
- var import_fs11 = require("fs");
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, import_path9.join)(thisFile, ...parts, "skills", subDir, "SKILL.md");
2483
- if ((0, import_fs11.existsSync)(candidate)) return candidate;
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, import_path9.join)(thisFile, "..", "..", "skills", subDir, "SKILL.md");
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, import_fs11.readFileSync)(skillPath, "utf8");
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, import_path9.join)(projectRoot, ".claude", "commands") : (0, import_path9.join)((0, import_os2.homedir)(), ".claude", "commands");
2500
- const targetPath = (0, import_path9.join)(targetDir, "instrlint.md");
2501
- if ((0, import_fs11.existsSync)(targetPath) && !force) {
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, import_fs11.mkdirSync)(targetDir, { recursive: true });
2506
- (0, import_fs11.writeFileSync)(targetPath, content, "utf8");
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, import_path9.join)(projectRoot, ".agents", "skills", "instrlint");
2512
- const targetPath = (0, import_path9.join)(targetDir, "SKILL.md");
2513
- if ((0, import_fs11.existsSync)(targetPath) && !force) {
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, import_fs11.mkdirSync)(targetDir, { recursive: true });
2518
- (0, import_fs11.writeFileSync)(targetPath, content, "utf8");
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
- const allFindings = [
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, import_path10.basename)(projectRoot),
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((resolve) => {
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
- resolve(trimmed === "" || trimmed === "y");
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 import_fs12 = require("fs");
2861
- var import_path11 = require("path");
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, import_path11.basename)(projectRoot),
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, import_fs12.writeFileSync)(opts.output, formatted, "utf8");
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 import_fs13 = require("fs");
3003
- var import_path12 = require("path");
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, import_path12.join)(projectRoot, ".github", "workflows");
3073
- const workflowPath = (0, import_path12.join)(workflowDir, "instrlint.yml");
3074
- if ((0, import_fs13.existsSync)(workflowPath) && !opts.force) {
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, import_fs13.mkdirSync)(workflowDir, { recursive: true });
3079
- (0, import_fs13.writeFileSync)(workflowPath, githubWorkflow(), "utf8");
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").action(async function() {
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);