glotfile 0.4.5 → 0.5.0

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.
@@ -176,9 +176,6 @@ function validate(raw) {
176
176
  if (lint.ignore !== void 0 && (!Array.isArray(lint.ignore) || !lint.ignore.every((g) => typeof g === "string"))) {
177
177
  fail("config.lint.ignore must be an array of strings");
178
178
  }
179
- if (lint.dictionary !== void 0 && (!Array.isArray(lint.dictionary) || !lint.dictionary.every((w) => typeof w === "string"))) {
180
- fail("config.lint.dictionary must be an array of strings");
181
- }
182
179
  if (lint.spelling !== void 0) {
183
180
  if (!isObject(lint.spelling)) fail("config.lint.spelling must be an object");
184
181
  if (lint.spelling.locales !== void 0 && (!isObject(lint.spelling.locales) || !Object.values(lint.spelling.locales).every((v) => typeof v === "string"))) {
@@ -243,6 +240,20 @@ function validate(raw) {
243
240
  lv.value = lv.value.trim();
244
241
  }
245
242
  }
243
+ if (entry.suppressions !== void 0) {
244
+ if (!Array.isArray(entry.suppressions)) fail(`key "${key}" suppressions must be an array`);
245
+ for (const s of entry.suppressions) {
246
+ if (!isObject(s) || typeof s.locale !== "string" || typeof s.source !== "string") {
247
+ fail(`key "${key}" has an invalid suppression (needs string rule, locale, source)`);
248
+ }
249
+ if (!RULE_IDS.includes(s.rule)) {
250
+ fail(`key "${key}" suppression has unknown rule id "${String(s.rule)}"`);
251
+ }
252
+ if (s.at !== void 0 && typeof s.at !== "string") {
253
+ fail(`key "${key}" suppression "at" must be a string`);
254
+ }
255
+ }
256
+ }
246
257
  if (entry.notes !== void 0) {
247
258
  if (!Array.isArray(entry.notes)) fail(`key "${key}" notes must be an array`);
248
259
  for (const n of entry.notes) {
@@ -520,6 +531,36 @@ var init_normalize = __esm({
520
531
  }
521
532
  });
522
533
 
534
+ // src/server/lint/suppress.ts
535
+ import { createHash } from "crypto";
536
+ function sourceSignature(entry, sourceLocale) {
537
+ const lv = entry.values[sourceLocale];
538
+ if (entry.plural) {
539
+ return Object.entries(lv?.forms ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([cat, body]) => `${cat}:${normalizeSource(body ?? "")}`).join("|");
540
+ }
541
+ return normalizeSource(lv?.value ?? "");
542
+ }
543
+ function sourceHash(entry, sourceLocale) {
544
+ return createHash("sha256").update(sourceSignature(entry, sourceLocale)).digest("hex").slice(0, 12);
545
+ }
546
+ function findSuppression(entry, sourceLocale, ruleId, locale) {
547
+ if (!entry.suppressions?.length) return void 0;
548
+ const current = sourceHash(entry, sourceLocale);
549
+ return entry.suppressions.find((s) => s.rule === ruleId && s.locale === locale && s.source === current);
550
+ }
551
+ function pruneStaleSuppressions(entry, sourceLocale) {
552
+ if (!entry.suppressions?.length) return;
553
+ const current = sourceHash(entry, sourceLocale);
554
+ entry.suppressions = entry.suppressions.filter((s) => s.source === current);
555
+ if (!entry.suppressions.length) delete entry.suppressions;
556
+ }
557
+ var init_suppress = __esm({
558
+ "src/server/lint/suppress.ts"() {
559
+ "use strict";
560
+ init_normalize();
561
+ }
562
+ });
563
+
523
564
  // src/server/state.ts
524
565
  import { readFileSync as readFileSync2, existsSync as existsSync2, rmSync as rmSync3 } from "fs";
525
566
  import { randomUUID } from "crypto";
@@ -541,6 +582,7 @@ function normalizeState(state) {
541
582
  const remapped = {};
542
583
  for (const [loc, lv] of Object.entries(entry.values)) remapped[canonLocale(loc)] = lv;
543
584
  entry.values = remapped;
585
+ for (const s of entry.suppressions ?? []) s.locale = canonLocale(s.locale);
544
586
  }
545
587
  for (const output of state.config.outputs) {
546
588
  if (!output.localeMap) continue;
@@ -641,6 +683,7 @@ function setSourceValue(state, key, value) {
641
683
  lv.state = "needs-review";
642
684
  }
643
685
  }
686
+ pruneStaleSuppressions(entry, state.config.sourceLocale);
644
687
  }
645
688
  }
646
689
  function setTargetValue(state, key, locale, value, clock = systemClock) {
@@ -664,6 +707,7 @@ function setSourcePluralForms(state, key, forms) {
664
707
  lv.state = "needs-review";
665
708
  }
666
709
  }
710
+ pruneStaleSuppressions(entry, state.config.sourceLocale);
667
711
  }
668
712
  }
669
713
  function setPluralForms(state, key, locale, forms, clock = systemClock) {
@@ -737,6 +781,19 @@ function deleteNote(state, key, id) {
737
781
  if (!entry.notes) return;
738
782
  entry.notes = entry.notes.filter((n) => n.id !== id);
739
783
  }
784
+ function addSuppression(state, key, rule, locale, clock = systemClock) {
785
+ const entry = requireKey(state, key);
786
+ if (!RULE_IDS.includes(rule)) throw new GlotfileError(`Unknown lint rule: ${rule}`);
787
+ const list = (entry.suppressions ?? []).filter((s) => !(s.rule === rule && s.locale === locale));
788
+ list.push({ rule, locale, source: sourceHash(entry, state.config.sourceLocale), at: clock() });
789
+ entry.suppressions = list;
790
+ }
791
+ function removeSuppression(state, key, rule, locale) {
792
+ const entry = requireKey(state, key);
793
+ if (!entry.suppressions) return;
794
+ entry.suppressions = entry.suppressions.filter((s) => !(s.rule === rule && s.locale === locale));
795
+ if (!entry.suppressions.length) delete entry.suppressions;
796
+ }
740
797
  function upsertGlossaryEntry(state, entry) {
741
798
  const i = state.glossary.findIndex((e) => e.term === entry.term);
742
799
  if (i === -1) state.glossary.push(entry);
@@ -782,6 +839,8 @@ var init_state = __esm({
782
839
  init_plurals();
783
840
  init_storage();
784
841
  init_normalize();
842
+ init_suppress();
843
+ init_registry();
785
844
  systemClock = () => (/* @__PURE__ */ new Date()).toISOString();
786
845
  }
787
846
  });
@@ -3705,7 +3764,10 @@ function sortFindings(findings) {
3705
3764
  }
3706
3765
  function countSeverities(findings) {
3707
3766
  let error = 0, warn = 0;
3708
- for (const f of findings) f.severity === "error" ? error++ : warn++;
3767
+ for (const f of findings) {
3768
+ if (f.suppressed) continue;
3769
+ f.severity === "error" ? error++ : warn++;
3770
+ }
3709
3771
  return { error, warn };
3710
3772
  }
3711
3773
  async function loadSpellers(locales, config, load, warn) {
@@ -3731,7 +3793,7 @@ async function runLint(state, options = {}) {
3731
3793
  const active = rules.filter(isActive);
3732
3794
  const spellingOn = active.some((r) => r.id === "spelling");
3733
3795
  const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
3734
- const allowWords = spellingOn ? buildAllowWords(state.glossary, config.dictionary) : /* @__PURE__ */ new Set();
3796
+ const allowWords = spellingOn ? buildAllowWords(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
3735
3797
  const ctx = {
3736
3798
  config,
3737
3799
  sourceLocale: state.config.sourceLocale,
@@ -3743,16 +3805,23 @@ async function runLint(state, options = {}) {
3743
3805
  const ignoreRes = (config.ignore ?? []).map(globToRegExp);
3744
3806
  const localeFilter = options.locales ? new Set(options.locales) : null;
3745
3807
  const findings = [];
3808
+ let suppressed = 0;
3746
3809
  for (const rule of active) {
3747
3810
  const severity = resolveSeverity(rule.id, config);
3748
3811
  for (const raw of rule.run(state, ctx)) {
3749
3812
  if (ignoreRes.some((re) => re.test(raw.key))) continue;
3750
3813
  if (localeFilter && raw.locale !== "" && !localeFilter.has(raw.locale)) continue;
3814
+ const entry = state.keys[raw.key];
3815
+ if (raw.locale !== "" && entry && findSuppression(entry, state.config.sourceLocale, rule.id, raw.locale)) {
3816
+ suppressed++;
3817
+ if (options.includeSuppressed) findings.push({ ...raw, severity, suppressed: true });
3818
+ continue;
3819
+ }
3751
3820
  findings.push({ ...raw, severity });
3752
3821
  }
3753
3822
  }
3754
3823
  const sorted = sortFindings(findings);
3755
- const counts = countSeverities(sorted);
3824
+ const counts = { ...countSeverities(sorted), suppressed };
3756
3825
  return { findings: sorted, counts, ok: counts.error === 0 };
3757
3826
  }
3758
3827
  var init_run2 = __esm({
@@ -3762,6 +3831,7 @@ var init_run2 = __esm({
3762
3831
  init_registry();
3763
3832
  init_rules();
3764
3833
  init_spelling();
3834
+ init_suppress();
3765
3835
  }
3766
3836
  });
3767
3837
 
@@ -3791,6 +3861,33 @@ var init_outputs = __esm({
3791
3861
  }
3792
3862
  });
3793
3863
 
3864
+ // src/server/lint/accept.ts
3865
+ var accept_exports = {};
3866
+ __export(accept_exports, {
3867
+ acceptFindings: () => acceptFindings
3868
+ });
3869
+ function acceptFindings(state, findings, opts = {}, clock = systemClock) {
3870
+ const byRule = {};
3871
+ let accepted = 0;
3872
+ for (const f of findings) {
3873
+ if (f.locale === "" || f.suppressed) continue;
3874
+ if (f.severity === "error" && !opts.includeErrors) continue;
3875
+ if (opts.rules && !opts.rules.includes(f.ruleId)) continue;
3876
+ if (opts.locales && !opts.locales.includes(f.locale)) continue;
3877
+ if (!state.keys[f.key]) continue;
3878
+ addSuppression(state, f.key, f.ruleId, f.locale, clock);
3879
+ byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
3880
+ accepted++;
3881
+ }
3882
+ return { accepted, byRule };
3883
+ }
3884
+ var init_accept = __esm({
3885
+ "src/server/lint/accept.ts"() {
3886
+ "use strict";
3887
+ init_state();
3888
+ }
3889
+ });
3890
+
3794
3891
  // src/server/import/detect.ts
3795
3892
  import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
3796
3893
  import { join as join4 } from "path";
@@ -4465,7 +4562,8 @@ function contains(haystack, needle, caseSensitive) {
4465
4562
  return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
4466
4563
  }
4467
4564
  function runChecks(state, opts = {}) {
4468
- const on = (id) => !opts.only || opts.only.includes(id);
4565
+ const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
4566
+ const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
4469
4567
  const issues = [];
4470
4568
  let spellPending = false;
4471
4569
  const { sourceLocale } = state.config;
@@ -4593,16 +4691,28 @@ function runChecks(state, opts = {}) {
4593
4691
  }
4594
4692
  }
4595
4693
  }
4596
- return { issues, spellPending };
4694
+ const visible = issues.filter((i) => {
4695
+ const entry = state.keys[i.key];
4696
+ return !entry || !findSuppression(entry, sourceLocale, CHECK_RULE[i.check], i.locale);
4697
+ });
4698
+ return { issues: visible, spellPending };
4597
4699
  }
4598
- var CHECK_IDS;
4700
+ var CHECK_IDS, CHECK_RULE;
4599
4701
  var init_checks = __esm({
4600
4702
  "src/server/checks.ts"() {
4601
4703
  "use strict";
4602
4704
  init_placeholders();
4603
4705
  init_run();
4604
4706
  init_spell();
4707
+ init_suppress();
4605
4708
  CHECK_IDS = ["untranslated", "placeholder", "spelling", "length", "glossary"];
4709
+ CHECK_RULE = {
4710
+ untranslated: "empty-translation",
4711
+ placeholder: "placeholder-mismatch",
4712
+ spelling: "spelling",
4713
+ length: "max-length",
4714
+ glossary: "glossary-violation"
4715
+ };
4606
4716
  }
4607
4717
  });
4608
4718
 
@@ -4620,19 +4730,23 @@ function readJson2(path) {
4620
4730
  }
4621
4731
  function loadUiPrefs(path) {
4622
4732
  const raw = readJson2(path);
4623
- return { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
4733
+ const prefs = { theme: isThemeMode(raw.theme) ? raw.theme : DEFAULTS.theme };
4734
+ if (isPanelWidth(raw.keyColumnWidth)) prefs.keyColumnWidth = Math.round(raw.keyColumnWidth);
4735
+ if (isPanelWidth(raw.detailPanelWidth)) prefs.detailPanelWidth = Math.round(raw.detailPanelWidth);
4736
+ return prefs;
4624
4737
  }
4625
4738
  function saveUiPrefs(path, prefs) {
4626
4739
  const merged = { ...readJson2(path), ...prefs };
4627
4740
  writeFileAtomic(path, JSON.stringify(merged, null, 2) + "\n");
4628
4741
  }
4629
- var THEMES, isThemeMode, defaultUiPrefsPath, DEFAULTS;
4742
+ var THEMES, isThemeMode, isPanelWidth, defaultUiPrefsPath, DEFAULTS;
4630
4743
  var init_ui_prefs = __esm({
4631
4744
  "src/server/ui-prefs.ts"() {
4632
4745
  "use strict";
4633
4746
  init_atomic_write();
4634
4747
  THEMES = ["system", "light", "dark"];
4635
4748
  isThemeMode = (v) => THEMES.includes(v);
4749
+ isPanelWidth = (v) => typeof v === "number" && Number.isFinite(v) && v >= 120 && v <= 1200;
4636
4750
  defaultUiPrefsPath = () => join8(homedir(), ".glotfile", "ui.json");
4637
4751
  DEFAULTS = { theme: "system" };
4638
4752
  }
@@ -4687,9 +4801,20 @@ function createApi(deps) {
4687
4801
  app.get("/state", (c) => c.json(load()));
4688
4802
  app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
4689
4803
  app.put("/ui-prefs", async (c) => {
4690
- const { theme } = await c.req.json();
4691
- if (!isThemeMode(theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
4692
- saveUiPrefs(uiPrefsPath, { theme });
4804
+ const body = await c.req.json();
4805
+ const patch = {};
4806
+ if ("theme" in body) {
4807
+ if (!isThemeMode(body.theme)) return c.json({ error: "theme must be system, light, or dark" }, 400);
4808
+ patch.theme = body.theme;
4809
+ }
4810
+ for (const field of ["keyColumnWidth", "detailPanelWidth"]) {
4811
+ if (field in body) {
4812
+ if (!isPanelWidth(body[field])) return c.json({ error: `${field} must be a number between 120 and 1200` }, 400);
4813
+ patch[field] = Math.round(body[field]);
4814
+ }
4815
+ }
4816
+ if (Object.keys(patch).length === 0) return c.json({ error: "no recognized preferences in body" }, 400);
4817
+ saveUiPrefs(uiPrefsPath, patch);
4693
4818
  return c.json({ ok: true });
4694
4819
  });
4695
4820
  app.get("/local-settings", (c) => c.json(loadLocalSettings(projectRoot)));
@@ -5163,12 +5288,47 @@ function createApi(deps) {
5163
5288
  };
5164
5289
  app.get("/lint", async (c) => {
5165
5290
  const state = load();
5291
+ const includeSuppressed = c.req.query("includeSuppressed") === "1";
5166
5292
  const lint = await runLint(state, { loadSpeller: cachedLoader, warn: () => {
5167
- } });
5293
+ }, includeSuppressed });
5168
5294
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, projectRoot)]);
5169
- const counts = countSeverities(findings);
5295
+ const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
5170
5296
  return c.json({ findings, counts, ok: counts.error === 0 });
5171
5297
  });
5298
+ app.post("/keys/:key/suppressions", async (c) => {
5299
+ const key = c.req.param("key");
5300
+ const { rule, locale } = await c.req.json();
5301
+ if (typeof rule !== "string" || !rule) return c.json({ error: "rule is required" }, 400);
5302
+ if (typeof locale !== "string" || !locale) return c.json({ error: "locale is required" }, 400);
5303
+ const s = load();
5304
+ addSuppression(s, key, rule, locale);
5305
+ persist(s);
5306
+ logChange({ kind: "suppression", summary: `Suppressed ${rule} for ${key} [${locale}]`, key, locale, after: rule });
5307
+ return c.json({ ok: true });
5308
+ });
5309
+ app.delete("/keys/:key/suppressions", (c) => {
5310
+ const key = c.req.param("key");
5311
+ const rule = c.req.query("rule") ?? "";
5312
+ const locale = c.req.query("locale") ?? "";
5313
+ if (!rule || !locale) return c.json({ error: "rule and locale are required" }, 400);
5314
+ const s = load();
5315
+ removeSuppression(s, key, rule, locale);
5316
+ persist(s);
5317
+ logChange({ kind: "suppression", summary: `Unsuppressed ${rule} for ${key} [${locale}]`, key, locale, before: rule });
5318
+ return c.json({ ok: true });
5319
+ });
5320
+ app.post("/lint/accept", async (c) => {
5321
+ const body = await c.req.json().catch(() => ({}));
5322
+ const s = load();
5323
+ const lint = await runLint(s, { loadSpeller: cachedLoader, warn: () => {
5324
+ } });
5325
+ const result = acceptFindings(s, lint.findings, { rules: body.rules, locales: body.locales });
5326
+ if (result.accepted > 0) {
5327
+ persist(s);
5328
+ logChange({ kind: "suppression", summary: `Suppressed ${result.accepted} finding(s)`, after: result.byRule });
5329
+ }
5330
+ return c.json({ ok: true, ...result });
5331
+ });
5172
5332
  app.get("/checks", (c) => {
5173
5333
  const param = c.req.query("checks");
5174
5334
  const only = param ? param.split(",").map((s) => s.trim()).filter((s) => CHECK_IDS.includes(s)) : void 0;
@@ -5523,6 +5683,7 @@ var init_api = __esm({
5523
5683
  "src/server/api.ts"() {
5524
5684
  "use strict";
5525
5685
  init_state();
5686
+ init_accept();
5526
5687
  init_scan();
5527
5688
  init_scanner();
5528
5689
  init_context();
@@ -5732,10 +5893,11 @@ function formatText(report) {
5732
5893
  lastKey = f.key;
5733
5894
  }
5734
5895
  const loc = f.locale ? ` ${f.locale}` : "";
5735
- lines.push(` ${f.severity} ${f.ruleId}${loc} ${f.message}`);
5896
+ lines.push(` ${f.severity} ${f.ruleId}${loc} ${f.message}${f.suppressed ? " (suppressed)" : ""}`);
5736
5897
  }
5737
5898
  lines.push("");
5738
- lines.push(`\u2716 ${report.counts.error} error(s), ${report.counts.warn} warning(s)`);
5899
+ const suppressed = report.counts.suppressed ? `, ${report.counts.suppressed} suppressed` : "";
5900
+ lines.push(`\u2716 ${report.counts.error} error(s), ${report.counts.warn} warning(s)${suppressed}`);
5739
5901
  return lines.join("\n") + "\n";
5740
5902
  }
5741
5903
  function formatJson(report) {
@@ -5823,7 +5985,9 @@ function parseArgs(argv) {
5823
5985
  } else if (flag === "--max-warnings" && next) {
5824
5986
  args.maxWarnings = Number(next);
5825
5987
  i++;
5826
- } else if (flag === "--all") args.all = true;
5988
+ } else if (flag === "--include-suppressed") args.includeSuppressed = true;
5989
+ else if (flag === "--accept") args.accept = true;
5990
+ else if (flag === "--all") args.all = true;
5827
5991
  else if (flag === "--limit" && next) {
5828
5992
  args.limit = Number(next);
5829
5993
  i++;
@@ -5995,8 +6159,24 @@ function printReport(report, format, rawText) {
5995
6159
  }
5996
6160
  async function runLintCmd(args) {
5997
6161
  const state = loadState(args.statePath);
6162
+ if (args.accept) {
6163
+ const { acceptFindings: acceptFindings2 } = await Promise.resolve().then(() => (init_accept(), accept_exports));
6164
+ const report2 = await runLint(state, { locales: args.locales });
6165
+ const result = acceptFindings2(state, report2.findings, { rules: args.ruleIds, locales: args.locales });
6166
+ if (result.accepted > 0) saveState(args.statePath, state);
6167
+ console.log(`Suppressed ${result.accepted} warning(s).`);
6168
+ for (const [rule, n] of Object.entries(result.byRule)) console.log(` ${rule}: ${n}`);
6169
+ if (result.accepted > 0) {
6170
+ console.log("Each suppression expires automatically when its key's source text changes.");
6171
+ }
6172
+ return;
6173
+ }
5998
6174
  const rawText = existsSync12(args.statePath) ? readFileSync15(args.statePath, "utf8") : "";
5999
- const report = await runLint(state, { locales: args.locales, ruleIds: args.ruleIds });
6175
+ const report = await runLint(state, {
6176
+ locales: args.locales,
6177
+ ruleIds: args.ruleIds,
6178
+ includeSuppressed: args.includeSuppressed
6179
+ });
6000
6180
  printReport(report, args.format, rawText);
6001
6181
  const tooManyWarnings = args.maxWarnings != null && report.counts.warn > args.maxWarnings;
6002
6182
  if (!report.ok || tooManyWarnings) process.exitCode = 1;
@@ -6008,7 +6188,7 @@ async function runCheck(args) {
6008
6188
  } catch (e) {
6009
6189
  const report2 = {
6010
6190
  findings: [{ ruleId: "load-error", key: "", locale: "", severity: "error", message: e.message }],
6011
- counts: { error: 1, warn: 0 },
6191
+ counts: { error: 1, warn: 0, suppressed: 0 },
6012
6192
  ok: false
6013
6193
  };
6014
6194
  printReport(report2, args.format, "");
@@ -6019,7 +6199,7 @@ async function runCheck(args) {
6019
6199
  const root = dirname5(resolve11(args.statePath));
6020
6200
  const lint = await runLint(state, {});
6021
6201
  const findings = sortFindings([...lint.findings, ...checkOutputs(state, root)]);
6022
- const counts = countSeverities(findings);
6202
+ const counts = { ...countSeverities(findings), suppressed: lint.counts.suppressed };
6023
6203
  const report = { findings, counts, ok: counts.error === 0 };
6024
6204
  printReport(report, args.format, rawText);
6025
6205
  if (!report.ok) process.exitCode = 1;
@@ -6206,12 +6386,14 @@ var COMMAND_HELP = {
6206
6386
  },
6207
6387
  lint: {
6208
6388
  summary: "Check the catalog for problems (placeholders, length, glossary, \u2026).",
6209
- usage: "glotfile lint [--format <text|json|sarif>] [--locale <list>] [--rule <list>] [--max-warnings <n>]",
6389
+ usage: "glotfile lint [--format <text|json|sarif>] [--locale <list>] [--rule <list>] [--max-warnings <n>] [--include-suppressed] [--accept]",
6210
6390
  options: [
6211
6391
  ["--format <fmt>", "Output format: text (default), json, or sarif"],
6212
6392
  ["--locale <list>", "Restrict to these comma-separated locales"],
6213
6393
  ["--rule <list>", "Only run these comma-separated rule ids"],
6214
- ["--max-warnings <n>", "Exit non-zero if warnings exceed n"]
6394
+ ["--max-warnings <n>", "Exit non-zero if warnings exceed n"],
6395
+ ["--include-suppressed", "Also show findings hidden by suppressions"],
6396
+ ["--accept", "Suppress all current warnings (narrow with --rule/--locale); each expires when its source changes"]
6215
6397
  ]
6216
6398
  },
6217
6399
  check: {