@grexx/grexxlinter 0.2.2764 → 0.2.2933

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
@@ -15052,6 +15052,35 @@ import { createRequire } from "node:module";
15052
15052
  import { dirname, join, relative, resolve, sep } from "node:path";
15053
15053
  import { fileURLToPath } from "node:url";
15054
15054
 
15055
+ // src/deps.ts
15056
+ var DEPS_MANIFEST_PATH = "studio.deps.yaml";
15057
+ function defaultDepMount(name) {
15058
+ return `vendor/${name}`;
15059
+ }
15060
+ var TRAILING_SLASHES = /\/{1,256}$/;
15061
+ function depMountsOf(manifest) {
15062
+ const deps = manifest?.dependencies;
15063
+ if (!Array.isArray(deps)) return [];
15064
+ const out = [];
15065
+ for (const entry of deps) {
15066
+ const { name, mount } = entry ?? {};
15067
+ if (typeof name !== "string" || name === "") continue;
15068
+ const raw = typeof mount === "string" && mount !== "" ? mount : defaultDepMount(name);
15069
+ const normalized = raw.replace(TRAILING_SLASHES, "");
15070
+ if (normalized === "") continue;
15071
+ out.push({ name, mount: normalized });
15072
+ }
15073
+ return out;
15074
+ }
15075
+ function depOfPath(path, mounts) {
15076
+ let best;
15077
+ for (const m of mounts) {
15078
+ if (!path.startsWith(`${m.mount}/`)) continue;
15079
+ if (best === void 0 || m.mount.length > best.mount.length) best = m;
15080
+ }
15081
+ return best?.name;
15082
+ }
15083
+
15055
15084
  // src/coverage.ts
15056
15085
  function condKey(c) {
15057
15086
  if (!c) return null;
@@ -15342,6 +15371,14 @@ function layoutRootOf(paths) {
15342
15371
  if (paths.length !== 1) return void 0;
15343
15372
  return statSync(paths[0]).isDirectory() ? paths[0] : void 0;
15344
15373
  }
15374
+ function readDepMounts(root) {
15375
+ if (root === void 0) return [];
15376
+ try {
15377
+ return depMountsOf((0, import_yaml.parse)(readFileSync(join(root, DEPS_MANIFEST_PATH), "utf8")));
15378
+ } catch {
15379
+ return [];
15380
+ }
15381
+ }
15345
15382
  function repoPathOf(root, file) {
15346
15383
  return root === void 0 ? void 0 : relative(root, file).split(sep).join("/");
15347
15384
  }
@@ -15659,6 +15696,23 @@ function pushUncovered(push, coverage, casetype) {
15659
15696
  const n = coverage.uncoveredCount(casetype);
15660
15697
  if (n > 0) push("unknown-casetype", "", casetype, `${n} rule clause(s) for "${casetype}" are behaviour-only and not checked by the schema`);
15661
15698
  }
15699
+ function isInactive(node) {
15700
+ return !!node && typeof node === "object" && node.inactive === true;
15701
+ }
15702
+ function underInactive(root, pointer) {
15703
+ if (isInactive(root)) return true;
15704
+ let cur = root;
15705
+ for (const raw of pointer.split("/").slice(1)) {
15706
+ if (cur == null || typeof cur !== "object") return false;
15707
+ cur = cur[raw.replaceAll("~1", "/").replaceAll("~0", "~")];
15708
+ if (isInactive(cur)) return true;
15709
+ }
15710
+ return false;
15711
+ }
15712
+ function dropInactive(messages, root) {
15713
+ if (isInactive(root)) return [];
15714
+ return messages.filter((m) => !underInactive(root, m.path));
15715
+ }
15662
15716
  function dedupeMessages(messages) {
15663
15717
  const seen = /* @__PURE__ */ new Set();
15664
15718
  return messages.filter((m) => {
@@ -15690,7 +15744,7 @@ function lintDocument(ctx, input) {
15690
15744
  });
15691
15745
  }
15692
15746
  if (settings.reportUncovered) pushUncovered(push, ctx.coverage, casetype);
15693
- const deduped = dedupeMessages(messages);
15747
+ const deduped = dropInactive(dedupeMessages(messages), input.value);
15694
15748
  const ok = !deduped.some((m) => meetsThreshold(m.severity, settings.failOn));
15695
15749
  return { file: input.file, casetype, messages: deduped, skipped: false, ok };
15696
15750
  }
@@ -15733,13 +15787,18 @@ function parseErrorResult(file, message, settings) {
15733
15787
  messages: [{ severity: "error", rule: "parse-error", casetype: null, path: "", field: null, message, file }]
15734
15788
  };
15735
15789
  }
15790
+ function tagDep(result, repoPath, mounts) {
15791
+ const dep = repoPath === void 0 || mounts === void 0 ? void 0 : depOfPath(repoPath, mounts);
15792
+ if (dep !== void 0) for (const m of result.messages) m.dep = dep;
15793
+ return result;
15794
+ }
15736
15795
  function lintOneDoc(doc, file, ctx, settings, caseRefs) {
15737
- if (doc.errors.length > 0) return parseErrorResult(file, doc.errors[0].message, settings);
15796
+ const repoPath = repoPathOf(ctx.root, file);
15797
+ if (doc.errors.length > 0) return tagDep(parseErrorResult(file, doc.errors[0].message, settings), repoPath, ctx.depMounts);
15738
15798
  const resolved = toJSOrError(doc);
15739
- if (resolved.error !== void 0) return parseErrorResult(file, resolved.error, settings);
15799
+ if (resolved.error !== void 0) return tagDep(parseErrorResult(file, resolved.error, settings), repoPath, ctx.depMounts);
15740
15800
  const input = { value: resolved.value, file };
15741
15801
  const result = lintDocument({ registry: ctx.registry, coverage: ctx.coverage, settings }, input);
15742
- const repoPath = repoPathOf(ctx.root, file);
15743
15802
  const extra = [
15744
15803
  ...referenceFindings(input.value, file, caseRefs, settings),
15745
15804
  ...repoPath === void 0 ? [] : layoutFindings(input.value, repoPath, settings).map((m) => ({ ...m, file }))
@@ -15748,7 +15807,7 @@ function lintOneDoc(doc, file, ctx, settings, caseRefs) {
15748
15807
  result.messages.push(...extra);
15749
15808
  if (result.ok && extra.some((m) => meetsThreshold(m.severity, settings.failOn))) result.ok = false;
15750
15809
  }
15751
- return result;
15810
+ return tagDep(result, repoPath, ctx.depMounts);
15752
15811
  }
15753
15812
  function lintOneFile(file, ctx, settings, caseRefs) {
15754
15813
  const text = readFileSync2(file, "utf8");
@@ -15769,7 +15828,7 @@ function lintFiles(files, ctx, settings) {
15769
15828
  // src/cli.ts
15770
15829
  var PARALLEL_THRESHOLD = 200;
15771
15830
  var MAX_WORKERS = 8;
15772
- var VERSION = "0.2.2764";
15831
+ var VERSION = "0.2.2933";
15773
15832
  function isNewer(latest, current) {
15774
15833
  const a = latest.split(".").map(Number);
15775
15834
  const b = current.split(".").map(Number);
@@ -15823,12 +15882,15 @@ var BOOL_FLAGS = /* @__PURE__ */ new Map([
15823
15882
  ["--errors-only", (args) => {
15824
15883
  args.errorsOnly = true;
15825
15884
  }],
15885
+ ["--own-only", (args) => {
15886
+ args.ownOnly = true;
15887
+ }],
15826
15888
  ["--no-update-check", (args) => {
15827
15889
  args.noUpdateCheck = true;
15828
15890
  }]
15829
15891
  ]);
15830
15892
  function parseArgs(argv) {
15831
- const args = { paths: [], off: [], format: "text", quiet: false, errorsOnly: false, noUpdateCheck: false };
15893
+ const args = { paths: [], off: [], format: "text", quiet: false, errorsOnly: false, ownOnly: false, noUpdateCheck: false };
15832
15894
  for (let i = 0; i < argv.length; i++) {
15833
15895
  const a = argv[i];
15834
15896
  const withValue = VALUE_FLAGS.get(a);
@@ -15885,7 +15947,9 @@ function printResult(r, quiet, errorsOnly) {
15885
15947
  if (quiet && msgs.length === 0) return;
15886
15948
  const rel = r.file ? relative2(process.cwd(), r.file) : "<input>";
15887
15949
  const casetypeTag = r.casetype ? ` (${r.casetype})` : "";
15888
- const head = `${rel}${casetypeTag}`;
15950
+ const dep = msgs.find((m) => m.dep)?.dep;
15951
+ const depTag = dep ? ` [product: ${dep}]` : "";
15952
+ const head = `${rel}${casetypeTag}${depTag}`;
15889
15953
  if (msgs.length === 0) {
15890
15954
  if (!quiet) console.log(`${paint("ok", COLORS.info)} ${head}`);
15891
15955
  return;
@@ -15896,8 +15960,18 @@ function printResult(r, quiet, errorsOnly) {
15896
15960
  console.log(` ${paint(m.severity.padEnd(7), COLORS[m.severity])} ${m.rule.padEnd(16)} ${loc} ${m.message}`);
15897
15961
  }
15898
15962
  }
15963
+ function depFindingCounts(results) {
15964
+ const by = /* @__PURE__ */ new Map();
15965
+ for (const r of results) {
15966
+ for (const m of r.messages) {
15967
+ if (m.dep) by.set(m.dep, (by.get(m.dep) ?? 0) + 1);
15968
+ }
15969
+ }
15970
+ return by;
15971
+ }
15899
15972
  function printSummary(results, settings) {
15900
- const s = summarize(results.filter((r) => !r.skipped));
15973
+ const active = results.filter((r) => !r.skipped);
15974
+ const s = summarize(active);
15901
15975
  const skipped = results.filter((r) => r.skipped).length;
15902
15976
  const skippedNote = skipped ? `, ${skipped} skipped` : "";
15903
15977
  const errorsText = paint(`${s.errors} error(s)`, COLORS.error);
@@ -15906,6 +15980,11 @@ function printSummary(results, settings) {
15906
15980
  `
15907
15981
  ${s.files} file(s)${skippedNote} \u2014 ${errorsText}, ${warningsText}, ${s.infos} info(s) [fail-on: ${settings.failOn}]`
15908
15982
  );
15983
+ const byDep = depFindingCounts(active);
15984
+ if (byDep.size > 0) {
15985
+ const total = [...byDep.values()].reduce((a, b) => a + b, 0);
15986
+ console.log(`${total} of those are in ${byDep.size} installed marketplace product(s) \u2014 run with --own-only to drop them`);
15987
+ }
15909
15988
  }
15910
15989
  function printText(results, settings, quiet, errorsOnly) {
15911
15990
  for (const r of results) printResult(r, quiet, errorsOnly);
@@ -15944,8 +16023,15 @@ function printReport(results, kind, errorsOnly) {
15944
16023
  console.log(`${paint(String(r.count).padStart(cw), COLORS[r.severity])} ${r.casetype.padEnd(tw)} ${r.severity.padEnd(8)} ${r.message}`);
15945
16024
  }
15946
16025
  }
16026
+ function withoutDepFindings(results, settings) {
16027
+ return results.map((r) => {
16028
+ const messages = r.messages.filter((m) => !m.dep);
16029
+ if (messages.length === r.messages.length) return r;
16030
+ return { ...r, messages, ok: !messages.some((m) => meetsThreshold(m.severity, settings.failOn)) };
16031
+ });
16032
+ }
15947
16033
  var WORKER_FILE = fileURLToPath2(import.meta.url);
15948
- async function runParallel(files, settings, schemasDir, root) {
16034
+ async function runParallel(files, settings, schemasDir, root, depMounts) {
15949
16035
  const n = Math.min(MAX_WORKERS, availableParallelism(), files.length);
15950
16036
  const shards = Array.from({ length: n }, () => []);
15951
16037
  files.forEach((f, i) => shards[i % n].push(f));
@@ -15954,7 +16040,7 @@ async function runParallel(files, settings, schemasDir, root) {
15954
16040
  const parts = await Promise.all(
15955
16041
  shards.map(
15956
16042
  (shard) => new Promise((resolve2, reject) => {
15957
- const w = new Worker(WORKER_FILE, { workerData: { shard, settings, schemasDir, root } });
16043
+ const w = new Worker(WORKER_FILE, { workerData: { shard, settings, schemasDir, root, depMounts } });
15958
16044
  w.once("message", (m) => {
15959
16045
  void w.terminate();
15960
16046
  resolve2(m);
@@ -15967,13 +16053,13 @@ async function runParallel(files, settings, schemasDir, root) {
15967
16053
  } catch (e) {
15968
16054
  if (process.env.GREXXLINT_DEBUG) console.error("[grexxlint] worker pool failed, inline fallback:", e);
15969
16055
  const ctx = createNodeContext({ schemasDir });
15970
- return lintFiles(files, { ...ctx, root }, settings);
16056
+ return lintFiles(files, { ...ctx, root, depMounts }, settings);
15971
16057
  }
15972
16058
  }
15973
16059
  function runWorker() {
15974
- const { shard, settings, schemasDir, root } = workerData;
16060
+ const { shard, settings, schemasDir, root, depMounts } = workerData;
15975
16061
  const ctx = createNodeContext({ schemasDir });
15976
- parentPort.postMessage(lintFiles(shard, { ...ctx, root }, settings));
16062
+ parentPort.postMessage(lintFiles(shard, { ...ctx, root, depMounts }, settings));
15977
16063
  }
15978
16064
  async function main() {
15979
16065
  const args = parseArgs(process.argv.slice(2));
@@ -15987,14 +16073,16 @@ async function main() {
15987
16073
  const settings = normalizeSettings(partial);
15988
16074
  const files = discover(args.paths, settings.exclude);
15989
16075
  const root = layoutRootOf(args.paths);
16076
+ const depMounts = readDepMounts(root);
15990
16077
  const canParallel = WORKER_FILE.endsWith(".js") && files.length > PARALLEL_THRESHOLD;
15991
16078
  let results;
15992
16079
  if (canParallel) {
15993
- results = await runParallel(files, settings, args.schemasDir, root);
16080
+ results = await runParallel(files, settings, args.schemasDir, root, depMounts);
15994
16081
  } else {
15995
16082
  const ctx = createNodeContext({ schemasDir: args.schemasDir });
15996
- results = lintFiles(files, { ...ctx, root }, settings);
16083
+ results = lintFiles(files, { ...ctx, root, depMounts }, settings);
15997
16084
  }
16085
+ if (args.ownOnly) results = withoutDepFindings(results, settings);
15998
16086
  if (args.report) {
15999
16087
  printReport(results, args.report, args.errorsOnly);
16000
16088
  } else if (args.format === "json") {
package/lsp.js CHANGED
@@ -25608,6 +25608,23 @@ function pushUncovered(push, coverage, casetype) {
25608
25608
  const n = coverage.uncoveredCount(casetype);
25609
25609
  if (n > 0) push("unknown-casetype", "", casetype, `${n} rule clause(s) for "${casetype}" are behaviour-only and not checked by the schema`);
25610
25610
  }
25611
+ function isInactive(node) {
25612
+ return !!node && typeof node === "object" && node.inactive === true;
25613
+ }
25614
+ function underInactive(root, pointer) {
25615
+ if (isInactive(root)) return true;
25616
+ let cur = root;
25617
+ for (const raw of pointer.split("/").slice(1)) {
25618
+ if (cur == null || typeof cur !== "object") return false;
25619
+ cur = cur[raw.replaceAll("~1", "/").replaceAll("~0", "~")];
25620
+ if (isInactive(cur)) return true;
25621
+ }
25622
+ return false;
25623
+ }
25624
+ function dropInactive(messages, root) {
25625
+ if (isInactive(root)) return [];
25626
+ return messages.filter((m) => !underInactive(root, m.path));
25627
+ }
25611
25628
  function dedupeMessages(messages) {
25612
25629
  const seen = /* @__PURE__ */ new Set();
25613
25630
  return messages.filter((m) => {
@@ -25639,7 +25656,7 @@ function lintDocument(ctx2, input) {
25639
25656
  });
25640
25657
  }
25641
25658
  if (settings2.reportUncovered) pushUncovered(push, ctx2.coverage, casetype);
25642
- const deduped = dedupeMessages(messages);
25659
+ const deduped = dropInactive(dedupeMessages(messages), input.value);
25643
25660
  const ok = !deduped.some((m) => meetsThreshold(m.severity, settings2.failOn));
25644
25661
  return { file: input.file, casetype, messages: deduped, skipped: false, ok };
25645
25662
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grexx/grexxlinter",
3
- "version": "0.2.2764",
3
+ "version": "0.2.2933",
4
4
  "description": "Lint Grexx Studio casetype YAML against the studio JSON Schemas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -240,6 +240,21 @@
240
240
  }
241
241
  }
242
242
  },
243
+ {
244
+ "$comment": "A mapping always names a destination. Which field carries it depends on toAttributeType (usersession -> toKey, everything else -> toAttribute), and the generated branches already require toKey for usersession and activityAttribute for taskAttribute -- but nothing requires the ordinary toAttribute, so a mapping with no destination at all linted clean. Zero findings across packages/schemas/def-yamls-142 and apps/backend/test/fixtures/compile-6685 (1837 active mappings).",
245
+ "anyOf": [
246
+ {
247
+ "required": [
248
+ "toAttribute"
249
+ ]
250
+ },
251
+ {
252
+ "required": [
253
+ "toKey"
254
+ ]
255
+ }
256
+ ]
257
+ },
243
258
  {
244
259
  "if": {
245
260
  "properties": {
@@ -121,6 +121,17 @@
121
121
  },
122
122
  "cookieConsentEnabled": {
123
123
  "baseline": "MUST_NOT",
124
+ "requiredWhen": [
125
+ {
126
+ "op": "cmp",
127
+ "field": "type",
128
+ "fieldName": "Type metadata",
129
+ "operator": "==",
130
+ "value": "cookieconsent",
131
+ "valueIsField": false,
132
+ "isEmpty": false
133
+ }
134
+ ],
124
135
  "enabledWhen": [
125
136
  {
126
137
  "op": "cmp",
@@ -185,6 +196,17 @@
185
196
  },
186
197
  "cookieConsentType": {
187
198
  "baseline": "MUST_NOT",
199
+ "requiredWhen": [
200
+ {
201
+ "op": "cmp",
202
+ "field": "type",
203
+ "fieldName": "Type metadata",
204
+ "operator": "==",
205
+ "value": "cookieconsent",
206
+ "valueIsField": false,
207
+ "isEmpty": false
208
+ }
209
+ ],
188
210
  "enabledWhen": [
189
211
  {
190
212
  "op": "cmp",
@@ -120,7 +120,23 @@
120
120
  ],
121
121
  "title": "trigger",
122
122
  "type": "object",
123
+ "$comment": "Hand-maintained overlay (deep-merged by scripts/extend.mjs into extended/trigger.schema.json). A trigger always names an activity to run, whatever its `toTriggerType` -- see the allOf below. The manager exports this per toTriggerType value, but the `page` group's `when` is \"[To page] is not «empty» AND [To type] is \\\"page\\\"\", and condToSchema cannot express the not-empty conjunct (`!==` is outside EQ_OPS and `''` is in BAD_VALUES), so that one group is deferred and never reaches the schema. Stating it unconditionally covers `page`, and additionally covers a trigger with no `toTriggerType` at all -- which the per-value groups all miss, since each `if` requires the field to be present. Conditionals generated by packages/schemas/scripts/extend.mjs from rules/trigger.json.",
123
124
  "allOf": [
125
+ {
126
+ "$comment": "Zero findings across packages/schemas/def-yamls-142 and apps/backend/test/fixtures/compile-6685 (659 active triggers).",
127
+ "anyOf": [
128
+ {
129
+ "required": [
130
+ "activityTo"
131
+ ]
132
+ },
133
+ {
134
+ "required": [
135
+ "activityTag"
136
+ ]
137
+ }
138
+ ]
139
+ },
124
140
  {
125
141
  "if": {
126
142
  "properties": {
@@ -453,6 +469,5 @@
453
469
  ]
454
470
  }
455
471
  }
456
- ],
457
- "$comment": "Conditionals generated by packages/schemas/scripts/extend.mjs from rules/trigger.json."
472
+ ]
458
473
  }