@grexx/grexxlinter 0.2.343 → 0.2.350

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/cli.js CHANGED
@@ -14456,7 +14456,7 @@ var require__ = __commonJS({
14456
14456
  });
14457
14457
 
14458
14458
  // src/cli.ts
14459
- var import_yaml2 = __toESM(require_dist(), 1);
14459
+ var import_yaml3 = __toESM(require_dist(), 1);
14460
14460
  import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
14461
14461
  import { availableParallelism } from "node:os";
14462
14462
  import { extname, join as join2, relative } from "node:path";
@@ -14464,6 +14464,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
14464
14464
  import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
14465
14465
 
14466
14466
  // src/node.ts
14467
+ var import_yaml = __toESM(require_dist(), 1);
14467
14468
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
14468
14469
  import { createRequire } from "node:module";
14469
14470
  import { dirname, join, resolve } from "node:path";
@@ -14518,9 +14519,11 @@ function createSchemaRegistry(schemas) {
14518
14519
  validateSchema: false
14519
14520
  });
14520
14521
  const casetypes = [];
14522
+ const byId = /* @__PURE__ */ new Map();
14521
14523
  for (const schema of schemas) {
14522
14524
  const id = typeof schema.$id === "string" ? schema.$id : void 0;
14523
14525
  if (!id) continue;
14526
+ byId.set(id, schema);
14524
14527
  if (!ajv.getSchema(id)) ajv.addSchema(schema, id);
14525
14528
  const m = /^https:\/\/grexx\.net\/studio-next\/schemas\/([^/]+)\.schema\.json$/.exec(id);
14526
14529
  if (m) casetypes.push(m[1]);
@@ -14538,12 +14541,81 @@ function createSchemaRegistry(schemas) {
14538
14541
  cache.set(casetype, fn);
14539
14542
  return fn;
14540
14543
  }
14544
+ function resolveRef(ref, base) {
14545
+ const hash = ref.indexOf("#");
14546
+ const path = hash === -1 ? ref : ref.slice(0, hash);
14547
+ const frag = hash === -1 ? "" : ref.slice(hash + 1);
14548
+ let schema;
14549
+ if (path === "") {
14550
+ schema = base;
14551
+ } else {
14552
+ const file = path.replace(/^\.\//, "");
14553
+ schema = byId.get(file.startsWith("http") ? file : `${SCHEMA_ID_BASE}${file}`);
14554
+ }
14555
+ if (!schema) return null;
14556
+ if (!frag) return schema;
14557
+ let cur = schema;
14558
+ for (const raw of frag.split("/").filter(Boolean)) {
14559
+ const key2 = raw.replace(/~1/g, "/").replace(/~0/g, "~");
14560
+ if (cur == null || typeof cur !== "object") return null;
14561
+ cur = cur[key2];
14562
+ }
14563
+ return cur ?? null;
14564
+ }
14541
14565
  return {
14542
14566
  casetypes,
14543
14567
  has: (casetype) => validatorFor(casetype) != null,
14544
- validatorFor
14568
+ validatorFor,
14569
+ resolveRef
14570
+ };
14571
+ }
14572
+
14573
+ // src/types.ts
14574
+ var DEFAULT_RULE_SEVERITY = {
14575
+ "required-field": "error",
14576
+ "forbidden-field": "error",
14577
+ "additional-field": "warning",
14578
+ "invalid-value": "error",
14579
+ "value-case": "warning",
14580
+ "invalid-type": "error",
14581
+ "invalid-format": "error",
14582
+ "invalid-reference": "error",
14583
+ "unknown-casetype": "warning",
14584
+ "parse-error": "error",
14585
+ schema: "error"
14586
+ };
14587
+ var DEFAULT_LINTER_SETTINGS = {
14588
+ rules: {},
14589
+ failOn: "error",
14590
+ ignoreCasetypes: [],
14591
+ includeCasetypes: [],
14592
+ reportUncovered: false,
14593
+ include: ["**/*.yaml", "**/*.yml"],
14594
+ exclude: ["node_modules", "dist", ".git"]
14595
+ };
14596
+ var LINT_RULE_DESCRIPTORS = [
14597
+ { id: "required-field", label: "Required field", description: "A field the casetype state requires (MUST) is missing.", defaultSeverity: "error" },
14598
+ { id: "forbidden-field", label: "Forbidden field", description: "A field present that the casetype state forbids (MUST_NOT).", defaultSeverity: "error" },
14599
+ { id: "additional-field", label: "Additional field", description: "A field not defined by the schema. Allowed in principle, but flagged for review.", defaultSeverity: "warning" },
14600
+ { id: "invalid-value", label: "Invalid value", description: "Value not allowed by an enum / const.", defaultSeverity: "error" },
14601
+ { id: "value-case", label: "Value case mismatch", description: "Value matches an allowed option except for case / camelCase / separators (e.g. FORM vs form).", defaultSeverity: "warning" },
14602
+ { id: "invalid-type", label: "Invalid type", description: "Value has the wrong JSON type.", defaultSeverity: "error" },
14603
+ { id: "invalid-format", label: "Invalid format", description: "Value fails a pattern / format constraint.", defaultSeverity: "error" },
14604
+ { id: "invalid-reference", label: "Invalid reference", description: "A case-reference field points at a file whose `_type` is not in the field's allowedCasetypes.", defaultSeverity: "error" },
14605
+ { id: "unknown-casetype", label: "Unknown casetype", description: "No schema found for the document `_type`.", defaultSeverity: "warning" },
14606
+ { id: "parse-error", label: "Parse error", description: "The YAML document could not be parsed.", defaultSeverity: "error" },
14607
+ { id: "schema", label: "Other schema error", description: "Any other constraint reported by the validator.", defaultSeverity: "error" }
14608
+ ];
14609
+ function normalizeSettings(partial) {
14610
+ return {
14611
+ ...DEFAULT_LINTER_SETTINGS,
14612
+ ...partial,
14613
+ rules: { ...DEFAULT_LINTER_SETTINGS.rules, ...partial?.rules ?? {} }
14545
14614
  };
14546
14615
  }
14616
+ function severityFor(rule, settings) {
14617
+ return settings.rules[rule] ?? DEFAULT_RULE_SEVERITY[rule];
14618
+ }
14547
14619
 
14548
14620
  // src/node.ts
14549
14621
  var require2 = createRequire(import.meta.url);
@@ -14576,6 +14648,95 @@ function loadCoverage(dir) {
14576
14648
  return null;
14577
14649
  }
14578
14650
  }
14651
+ function loadCaseRefs(dir) {
14652
+ const file = join(dir, ".caserefs.json");
14653
+ if (!existsSync(file) || !statSync(file).isFile()) return {};
14654
+ try {
14655
+ return JSON.parse(readFileSync(file, "utf8"));
14656
+ } catch {
14657
+ return {};
14658
+ }
14659
+ }
14660
+ var docCache = /* @__PURE__ */ new Map();
14661
+ function targetDoc(absPath) {
14662
+ if (docCache.has(absPath)) return docCache.get(absPath);
14663
+ let doc = null;
14664
+ if (existsSync(absPath) && statSync(absPath).isFile()) {
14665
+ try {
14666
+ doc = (0, import_yaml.parse)(readFileSync(absPath, "utf8"));
14667
+ } catch {
14668
+ doc = null;
14669
+ }
14670
+ }
14671
+ docCache.set(absPath, doc);
14672
+ return doc;
14673
+ }
14674
+ function targetType(absPath, fragment) {
14675
+ const doc = targetDoc(absPath);
14676
+ if (doc == null || typeof doc !== "object") return "not-found";
14677
+ if (!fragment) {
14678
+ const t2 = doc._type;
14679
+ return typeof t2 === "string" ? t2 : null;
14680
+ }
14681
+ let found = null;
14682
+ const walk = (n) => {
14683
+ if (found || !n || typeof n !== "object") return;
14684
+ if (Array.isArray(n)) {
14685
+ n.forEach(walk);
14686
+ return;
14687
+ }
14688
+ const o = n;
14689
+ if (o._uid === fragment) {
14690
+ found = o;
14691
+ return;
14692
+ }
14693
+ for (const v of Object.values(o)) walk(v);
14694
+ };
14695
+ walk(doc);
14696
+ if (!found) return "not-found";
14697
+ const t = found._type;
14698
+ return typeof t === "string" ? t : null;
14699
+ }
14700
+ var looksLikePath = (s) => s.includes("/") || s.endsWith(".yaml") || s.endsWith(".yml");
14701
+ function referenceFindings(value, file, caseRefs, settings) {
14702
+ const sev = severityFor("invalid-reference", settings);
14703
+ if (sev === "off" || Object.keys(caseRefs).length === 0) return [];
14704
+ const baseDir = dirname(file);
14705
+ const out = [];
14706
+ const checkRef = (ref, allowed, casetype, field, path) => {
14707
+ if (!looksLikePath(ref)) return;
14708
+ const frag = /^(.*\.ya?ml)#(.+)$/.exec(ref);
14709
+ const pathPart = frag ? frag[1] : ref;
14710
+ const fragment = frag ? frag[2] : "";
14711
+ const t = targetType(resolve(baseDir, pathPart), fragment);
14712
+ if (t === "not-found") {
14713
+ out.push({ severity: sev, rule: "invalid-reference", casetype, path, field, message: `reference target not found: "${ref}"`, file });
14714
+ } else if (t !== null && !allowed.includes(t)) {
14715
+ out.push({ severity: sev, rule: "invalid-reference", casetype, path, field, message: `field "${field}" references a "${t}" but must reference one of: ${allowed.join(", ")}`, file });
14716
+ }
14717
+ };
14718
+ const walk = (node, path) => {
14719
+ if (Array.isArray(node)) {
14720
+ node.forEach((n, i) => walk(n, `${path}/${i}`));
14721
+ return;
14722
+ }
14723
+ if (!node || typeof node !== "object") return;
14724
+ const o = node;
14725
+ const refs = typeof o._type === "string" ? caseRefs[o._type] : void 0;
14726
+ if (refs) {
14727
+ for (const [field, allowed] of Object.entries(refs)) {
14728
+ const v = o[field];
14729
+ if (typeof v === "string") checkRef(v, allowed, o._type, field, `${path}/${field}`);
14730
+ else if (Array.isArray(v)) v.forEach((item, i) => {
14731
+ if (typeof item === "string") checkRef(item, allowed, o._type, field, `${path}/${field}/${i}`);
14732
+ });
14733
+ }
14734
+ }
14735
+ for (const [k, v] of Object.entries(o)) walk(v, `${path}/${k}`);
14736
+ };
14737
+ walk(value, "");
14738
+ return out;
14739
+ }
14579
14740
  function createNodeContext(opts = {}) {
14580
14741
  const schemasDir = resolveSchemasDir(opts.schemasDir);
14581
14742
  const registry = createSchemaRegistry(loadSchemasFromDir(schemasDir));
@@ -14584,40 +14745,7 @@ function createNodeContext(opts = {}) {
14584
14745
  }
14585
14746
 
14586
14747
  // src/yaml.ts
14587
- var import_yaml = __toESM(require_dist(), 1);
14588
-
14589
- // src/types.ts
14590
- var DEFAULT_RULE_SEVERITY = {
14591
- "required-field": "error",
14592
- "forbidden-field": "error",
14593
- "unknown-field": "warning",
14594
- "invalid-value": "error",
14595
- "value-case": "warning",
14596
- "invalid-type": "error",
14597
- "invalid-format": "error",
14598
- "unknown-casetype": "warning",
14599
- "parse-error": "error",
14600
- schema: "error"
14601
- };
14602
- var DEFAULT_LINTER_SETTINGS = {
14603
- rules: {},
14604
- failOn: "error",
14605
- ignoreCasetypes: [],
14606
- includeCasetypes: [],
14607
- reportUncovered: false,
14608
- include: ["**/*.yaml", "**/*.yml"],
14609
- exclude: ["node_modules", "dist", ".git"]
14610
- };
14611
- function normalizeSettings(partial) {
14612
- return {
14613
- ...DEFAULT_LINTER_SETTINGS,
14614
- ...partial,
14615
- rules: { ...DEFAULT_LINTER_SETTINGS.rules, ...partial?.rules ?? {} }
14616
- };
14617
- }
14618
- function severityFor(rule, settings) {
14619
- return settings.rules[rule] ?? DEFAULT_RULE_SEVERITY[rule];
14620
- }
14748
+ var import_yaml2 = __toESM(require_dist(), 1);
14621
14749
 
14622
14750
  // src/lint.ts
14623
14751
  function ruleForKeyword(keyword) {
@@ -14628,7 +14756,7 @@ function ruleForKeyword(keyword) {
14628
14756
  return "forbidden-field";
14629
14757
  case "additionalProperties":
14630
14758
  case "unevaluatedProperties":
14631
- return "unknown-field";
14759
+ return "additional-field";
14632
14760
  case "const":
14633
14761
  case "enum":
14634
14762
  return "invalid-value";
@@ -14656,7 +14784,7 @@ function humanize(err) {
14656
14784
  return `field "${lastSegment(err.instancePath) ?? ""}" is not allowed for this casetype state`;
14657
14785
  case "additionalProperties":
14658
14786
  case "unevaluatedProperties":
14659
- return `unknown field "${String(p.additionalProperty)}"`;
14787
+ return `additional field "${String(p.additionalProperty)}" not defined by the schema`;
14660
14788
  case "const":
14661
14789
  return `must equal ${JSON.stringify(p.allowedValue)}`;
14662
14790
  case "enum":
@@ -14683,6 +14811,92 @@ function locate(err) {
14683
14811
  }
14684
14812
  return { field: lastSegment(err.instancePath), path: err.instancePath };
14685
14813
  }
14814
+ function collectConsts(cond, out = /* @__PURE__ */ new Map()) {
14815
+ if (!cond || typeof cond !== "object") return out;
14816
+ const c = cond;
14817
+ const props = c.properties;
14818
+ if (props) {
14819
+ for (const [k, sub] of Object.entries(props)) {
14820
+ if (sub && typeof sub === "object" && "const" in sub) {
14821
+ (out.get(k) ?? out.set(k, /* @__PURE__ */ new Set()).get(k)).add(sub.const);
14822
+ }
14823
+ }
14824
+ }
14825
+ for (const key2 of ["allOf", "anyOf", "oneOf"]) {
14826
+ if (Array.isArray(c[key2])) c[key2].forEach((s) => collectConsts(s, out));
14827
+ }
14828
+ return out;
14829
+ }
14830
+ function parentPointer(pointer) {
14831
+ const i = pointer.lastIndexOf("/");
14832
+ return i <= 0 ? "" : pointer.slice(0, i);
14833
+ }
14834
+ function fmtStateValue(v) {
14835
+ if (v === void 0 || v === null || v === "") return "null";
14836
+ if (typeof v === "string") return v;
14837
+ if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{\u2026}";
14838
+ return String(v);
14839
+ }
14840
+ function deref(schema, resolve2, docRoot) {
14841
+ let cur = schema;
14842
+ for (let i = 0; cur && typeof cur === "object" && typeof cur.$ref === "string" && i < 16; i++) {
14843
+ const ref = cur.$ref;
14844
+ const next = resolve2(ref, docRoot.v ?? void 0);
14845
+ if (!next) break;
14846
+ if (!ref.startsWith("#")) docRoot.v = next;
14847
+ cur = next;
14848
+ }
14849
+ return cur ?? null;
14850
+ }
14851
+ function propSchema(schema, key2) {
14852
+ const direct = schema.properties;
14853
+ if (direct && key2 in direct) return direct[key2];
14854
+ for (const branch of schema.allOf ?? []) {
14855
+ const t = branch.then ?? branch;
14856
+ const props = t.properties;
14857
+ if (props && key2 in props) return props[key2];
14858
+ }
14859
+ return null;
14860
+ }
14861
+ function schemaAtPointer(rootSchema, pointer, resolve2) {
14862
+ const docRoot = { v: rootSchema };
14863
+ let schema = deref(rootSchema, resolve2, docRoot);
14864
+ for (const raw of pointer.split("/").filter(Boolean)) {
14865
+ if (!schema) return null;
14866
+ if (/^\d+$/.test(raw)) {
14867
+ let items = schema.items;
14868
+ if (Array.isArray(items)) items = items[Number(raw)] ?? items[0];
14869
+ schema = deref(items, resolve2, docRoot);
14870
+ } else {
14871
+ const key2 = raw.replace(/~1/g, "/").replace(/~0/g, "~");
14872
+ schema = deref(propSchema(schema, key2), resolve2, docRoot);
14873
+ }
14874
+ }
14875
+ return schema;
14876
+ }
14877
+ function describeState(rootSchema, err, root, resolve2) {
14878
+ const m = /^#\/allOf\/(\d+)\/(then|else)\b/.exec(err.schemaPath);
14879
+ if (!m || !rootSchema || typeof rootSchema !== "object") return null;
14880
+ const branch = m[2];
14881
+ const objPath = err.keyword === "false schema" ? parentPointer(err.instancePath) : err.instancePath;
14882
+ const govSchema = schemaAtPointer(rootSchema, objPath, resolve2);
14883
+ const allOf = govSchema?.allOf;
14884
+ const clause = Array.isArray(allOf) ? allOf[Number(m[1])] : void 0;
14885
+ if (!clause?.if) return null;
14886
+ const consts = collectConsts(clause.if);
14887
+ if (consts.size === 0) return null;
14888
+ const obj = valueAtPointer(root, objPath);
14889
+ if (!obj || typeof obj !== "object") return null;
14890
+ const rec = obj;
14891
+ if (branch === "then") {
14892
+ for (const [field, values] of consts) if (!values.has(rec[field])) return null;
14893
+ const parts2 = [...consts].map(([f, vs]) => `${f}=${fmtStateValue([...vs][0])}`);
14894
+ return `(${parts2.join(", ")})`;
14895
+ }
14896
+ for (const field of consts.keys()) if (!(field in rec)) return null;
14897
+ const parts = [...consts.keys()].map((f) => `${f}=${fmtStateValue(rec[f])}`);
14898
+ return `(${parts.join(", ")})`;
14899
+ }
14686
14900
  function valueAtPointer(root, pointer) {
14687
14901
  if (!pointer) return root;
14688
14902
  let cur = root;
@@ -14734,6 +14948,7 @@ function lintDocument(ctx, input) {
14734
14948
  push("unknown-casetype", "", casetype, casetype ? `no schema for casetype "${casetype}"` : "could not determine casetype (no `_type`)");
14735
14949
  } else if (!validate(input.value)) {
14736
14950
  const errors = validate.errors ?? [];
14951
+ const schema = validate.schema;
14737
14952
  const forbidden = /* @__PURE__ */ new Set();
14738
14953
  for (const e of errors) if (e.keyword === "false schema") forbidden.add(e.instancePath);
14739
14954
  for (const e of errors) {
@@ -14752,7 +14967,13 @@ function lintDocument(ctx, input) {
14752
14967
  continue;
14753
14968
  }
14754
14969
  }
14755
- push(ruleForKeyword(e.keyword), path, field, humanize(e));
14970
+ const rule = ruleForKeyword(e.keyword);
14971
+ let message = humanize(e);
14972
+ if (rule === "forbidden-field" || rule === "required-field") {
14973
+ const state = describeState(schema, e, input.value, ctx.registry.resolveRef);
14974
+ if (state) message += ` ${state}`;
14975
+ }
14976
+ push(rule, path, field, message);
14756
14977
  }
14757
14978
  }
14758
14979
  if (settings.reportUncovered && ctx.coverage && casetype) {
@@ -14793,7 +15014,7 @@ function isLintableYaml(text) {
14793
15014
  // src/cli.ts
14794
15015
  var PARALLEL_THRESHOLD = 200;
14795
15016
  var MAX_WORKERS = 8;
14796
- var VERSION = true ? "0.2.343" : "0.0.0-dev";
15017
+ var VERSION = true ? "0.2.350" : "0.0.0-dev";
14797
15018
  function isNewer(latest, current) {
14798
15019
  const a = latest.split(".").map(Number);
14799
15020
  const b = current.split(".").map(Number);
@@ -14812,19 +15033,30 @@ function checkForUpdate(disabled) {
14812
15033
  const timer = setTimeout(() => ctrl.abort(), 1500);
14813
15034
  return fetch("https://registry.npmjs.org/@grexx%2Fgrexxlinter/latest", { signal: ctrl.signal }).then((r) => r.ok ? r.json() : null).then((j) => j?.version && isNewer(j.version, VERSION) ? j.version : null).catch(() => null).finally(() => clearTimeout(timer));
14814
15035
  }
15036
+ var VALID_RULES = new Set(LINT_RULE_DESCRIPTORS.map((d) => d.id));
14815
15037
  function parseArgs(argv) {
14816
- const args = { paths: [], format: "text", quiet: false, noUpdateCheck: false };
15038
+ const args = { paths: [], off: [], format: "text", quiet: false, noUpdateCheck: false };
14817
15039
  for (let i = 0; i < argv.length; i++) {
14818
15040
  const a = argv[i];
14819
15041
  if (a === "--settings") args.settingsFile = argv[++i];
14820
- else if (a === "--schemas-dir") args.schemasDir = argv[++i];
15042
+ else if (a === "--off") {
15043
+ const list = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
15044
+ for (const r of list) {
15045
+ if (!VALID_RULES.has(r)) throw new Error(`--off: unknown rule "${r}" (valid: ${[...VALID_RULES].join(", ")})`);
15046
+ args.off.push(r);
15047
+ }
15048
+ } else if (a === "--schemas-dir") args.schemasDir = argv[++i];
14821
15049
  else if (a === "--fail-on") args.failOn = argv[++i];
14822
15050
  else if (a === "--format") args.format = argv[++i];
15051
+ else if (a === "--report") args.report = argv[++i];
14823
15052
  else if (a === "--quiet") args.quiet = true;
14824
15053
  else if (a === "--no-update-check") args.noUpdateCheck = true;
14825
15054
  else if (a.startsWith("--")) throw new Error(`unknown option: ${a}`);
14826
15055
  else args.paths.push(a);
14827
15056
  }
15057
+ if (args.report && args.report !== "rules" && args.report !== "fields") {
15058
+ throw new Error(`--report must be "rules" or "fields" (got "${args.report}")`);
15059
+ }
14828
15060
  if (args.paths.length === 0) args.paths.push(".");
14829
15061
  return args;
14830
15062
  }
@@ -14879,9 +15111,42 @@ function printText(results, settings, quiet) {
14879
15111
  ${s.files} file(s)${skipped ? `, ${skipped} skipped` : ""} \u2014 ${paint(`${s.errors} error(s)`, COLORS.error)}, ${paint(`${s.warnings} warning(s)`, COLORS.warning)}, ${s.infos} info(s) [fail-on: ${settings.failOn}]`
14880
15112
  );
14881
15113
  }
15114
+ function printReport(results, kind) {
15115
+ const msgs = results.filter((r) => !r.skipped).flatMap((r) => r.messages);
15116
+ if (kind === "rules") {
15117
+ const by2 = /* @__PURE__ */ new Map();
15118
+ for (const m of msgs) {
15119
+ const e = by2.get(m.rule) ?? { count: 0, severity: m.severity };
15120
+ e.count++;
15121
+ by2.set(m.rule, e);
15122
+ }
15123
+ const rows2 = [...by2.entries()].map(([rule, e]) => ({ count: e.count, rule, severity: e.severity })).sort((a, b) => b.count - a.count);
15124
+ const w = Math.max(5, ...rows2.map((r) => String(r.count).length));
15125
+ console.log(`${"count".padStart(w)} rule severity`);
15126
+ for (const r of rows2) {
15127
+ console.log(`${paint(String(r.count).padStart(w), COLORS[r.severity])} ${r.rule.padEnd(16)} ${r.severity}`);
15128
+ }
15129
+ return;
15130
+ }
15131
+ const by = /* @__PURE__ */ new Map();
15132
+ for (const m of msgs) {
15133
+ const key2 = `${m.casetype ?? "?"}\0${m.rule}\0${m.severity}\0${m.message}`;
15134
+ const e = by.get(key2) ?? { count: 0, casetype: m.casetype ?? "?", severity: m.severity, message: m.message };
15135
+ e.count++;
15136
+ by.set(key2, e);
15137
+ }
15138
+ const rows = [...by.values()].sort((a, b) => b.count - a.count || a.casetype.localeCompare(b.casetype));
15139
+ const cw = Math.max(5, ...rows.map((r) => String(r.count).length));
15140
+ const tw = Math.max(8, ...rows.map((r) => r.casetype.length));
15141
+ console.log(`${"count".padStart(cw)} ${"casetype".padEnd(tw)} severity message`);
15142
+ for (const r of rows) {
15143
+ console.log(`${paint(String(r.count).padStart(cw), COLORS[r.severity])} ${r.casetype.padEnd(tw)} ${r.severity.padEnd(8)} ${r.message}`);
15144
+ }
15145
+ }
14882
15146
  var WORKER_FILE = fileURLToPath2(import.meta.url);
14883
15147
  function lintFiles(files, ctx, settings) {
14884
15148
  const out = [];
15149
+ const caseRefs = loadCaseRefs(ctx.schemasDir);
14885
15150
  const parseErr = (file, message) => ({
14886
15151
  file,
14887
15152
  casetype: null,
@@ -14894,7 +15159,7 @@ function lintFiles(files, ctx, settings) {
14894
15159
  if (!isLintableYaml(text)) continue;
14895
15160
  let docs;
14896
15161
  try {
14897
- docs = (0, import_yaml2.parseAllDocuments)(text);
15162
+ docs = (0, import_yaml3.parseAllDocuments)(text);
14898
15163
  } catch (e) {
14899
15164
  out.push(parseErr(file, e.message));
14900
15165
  continue;
@@ -14905,7 +15170,13 @@ function lintFiles(files, ctx, settings) {
14905
15170
  continue;
14906
15171
  }
14907
15172
  const input = { value: doc.toJS(), file };
14908
- out.push(lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input));
15173
+ const result = lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input);
15174
+ const refs = referenceFindings(input.value, file, caseRefs, settings);
15175
+ if (refs.length) {
15176
+ result.messages.push(...refs);
15177
+ if (result.ok && refs.some((m) => meetsThreshold(m.severity, settings.failOn))) result.ok = false;
15178
+ }
15179
+ out.push(result);
14909
15180
  }
14910
15181
  }
14911
15182
  return out;
@@ -14946,6 +15217,9 @@ async function main() {
14946
15217
  let partial = {};
14947
15218
  if (args.settingsFile) partial = JSON.parse(readFileSync2(args.settingsFile, "utf8"));
14948
15219
  if (args.failOn) partial = { ...partial, failOn: args.failOn };
15220
+ if (args.off.length > 0) {
15221
+ partial = { ...partial, rules: { ...partial.rules, ...Object.fromEntries(args.off.map((r) => [r, "off"])) } };
15222
+ }
14949
15223
  const settings = normalizeSettings(partial);
14950
15224
  const files = discover(args.paths, settings.exclude);
14951
15225
  const canParallel = WORKER_FILE.endsWith(".js") && files.length > PARALLEL_THRESHOLD;
@@ -14956,7 +15230,9 @@ async function main() {
14956
15230
  const ctx = createNodeContext({ schemasDir: args.schemasDir });
14957
15231
  results = lintFiles(files, ctx, settings);
14958
15232
  }
14959
- if (args.format === "json") {
15233
+ if (args.report) {
15234
+ printReport(results, args.report);
15235
+ } else if (args.format === "json") {
14960
15236
  const active = results.filter((r) => !r.skipped);
14961
15237
  console.log(JSON.stringify({ summary: summarize(active), results }, null, 2));
14962
15238
  } else {