@hublo/sentinel 1.5.0-alpha.21 → 1.5.0-alpha.22

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.
@@ -15,7 +15,7 @@ import {
15
15
  palette,
16
16
  registerAdapters,
17
17
  resolve
18
- } from "../chunk-AFM4AJHA.js";
18
+ } from "../chunk-BN6SCBFW.js";
19
19
  import {
20
20
  resolveContext
21
21
  } from "../chunk-KLEXJFJ6.js";
@@ -5283,14 +5283,25 @@ function scanGraph(cwd, config) {
5283
5283
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
5284
5284
  import { tmpdir as tmpdir2 } from "node:os";
5285
5285
  import { join as join20 } from "node:path";
5286
- var PUBLISHED_THRESHOLDS = {
5287
- complexity: 20,
5288
- "max-depth": 4,
5289
- "max-lines": 300,
5290
- "max-lines-per-function": 50,
5286
+ var GAUGE_THRESHOLDS = {
5287
+ // McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
5288
+ // within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
5289
+ complexity: 10,
5290
+ // IfSQ SP-2, "few people can understand nesting to more than 3 levels". Our p90 agrees at 3.
5291
+ "max-depth": 3,
5292
+ // No research anywhere for a FILE's length: IfSQ's three citations (500, 100-150, 143) are all
5293
+ // about routines. So our p90 of 157, rounded down.
5294
+ "max-lines": 150,
5295
+ // Our p90. IfSQ SP-1 says 150 and is discarded for being more permissive than our own code.
5296
+ "max-lines-per-function": 109,
5297
+ // No research. Our p90 is 15, the rule's default is 10, so the default stays as the stricter one.
5291
5298
  "max-statements": 10,
5299
+ // No research, and none needed: beyond three parameters the fix is an object or currying, so this
5300
+ // is a design boundary and not a dial. Our p90 of 4 is the looser candidate and loses.
5292
5301
  "max-params": 3,
5293
- "max-nested-callbacks": 10,
5302
+ // Same nesting family as `max-depth`, no citation of its own. Our p90 agrees at 3.
5303
+ "max-nested-callbacks": 3,
5304
+ // 1 is the rule's floor, there is no stricter value to pick.
5294
5305
  "max-classes-per-file": 1
5295
5306
  };
5296
5307
  var CATEGORIES = [
@@ -5312,7 +5323,7 @@ function sizeRulesConfig() {
5312
5323
  // `warn` exits 0, and both report the same 18 diagnostics. So the tool measures, this role
5313
5324
  // judges, and a non-zero exit means the tool failed.
5314
5325
  rules: Object.fromEntries(
5315
- Object.entries(PUBLISHED_THRESHOLDS).map(([rule, threshold]) => [rule, ["warn", threshold]])
5326
+ Object.entries(GAUGE_THRESHOLDS).map(([rule, threshold]) => [rule, ["warn", threshold]])
5316
5327
  )
5317
5328
  };
5318
5329
  }
@@ -5341,7 +5352,8 @@ function ruleOf(code) {
5341
5352
  return /\(([^)]+)\)\s*$/.exec(code)?.[1];
5342
5353
  }
5343
5354
  function valueOf(message) {
5344
- return Number(/\((\d+)\)/.exec(message)?.[1] ?? 0);
5355
+ const match = /\((\d+)\)/.exec(message) ?? /complexity of (\d+)/.exec(message);
5356
+ return Number(match?.[1] ?? 0);
5345
5357
  }
5346
5358
  function breachesOf(raw) {
5347
5359
  let parsed;
@@ -5355,7 +5367,7 @@ function breachesOf(raw) {
5355
5367
  for (const diagnostic of diagnostics) {
5356
5368
  const rule = ruleOf(diagnostic.code ?? "");
5357
5369
  const file = diagnostic.filename;
5358
- if (!rule || !file || !(rule in PUBLISHED_THRESHOLDS)) continue;
5370
+ if (!rule || !file || !(rule in GAUGE_THRESHOLDS)) continue;
5359
5371
  const value = valueOf(diagnostic.message ?? "");
5360
5372
  const line = diagnostic.labels?.[0]?.span?.line ?? 0;
5361
5373
  const key = `${file}\0${rule}`;
@@ -5524,59 +5536,59 @@ var metricsHelp = {
5524
5536
  },
5525
5537
  complexity: {
5526
5538
  label: "Files with a complex function",
5527
- what: "Files holding at least one function whose cyclomatic complexity passes 20.",
5539
+ what: "Files holding at least one function whose cyclomatic complexity passes 10.",
5528
5540
  read: "Count of files, so it is comparable between modules only against their file count, which the unit carries. It is the same measure as the band rows above, seen per file rather than per function. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5529
- caveat: "It does not say which function, nor whether branching is wrong there: a parser legitimately branches. \u26D4 This is a GAUGE and not the line your lint refuses at. It runs oxlint with its own config in a temporary directory, at the threshold the rule publishes, and never reads the module's `.oxlintrc.json`. So it answers the same question the same way in every module, including the 146 of 198 that have not adopted lint, and it WILL disagree with a green build: the `nest` preset refuses `complexity` at 30 while this reports above 20.",
5530
- source: "oxlint complexity, at ESLint's published default of 20. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5541
+ caveat: "It does not say which function, nor whether branching is wrong there: a parser legitimately branches. \u26D4 This is a GAUGE and not the line your lint refuses at. It runs oxlint with its own config in a temporary directory, at a threshold fixed once for every module, and never reads the module's `.oxlintrc.json`. So it answers the same question the same way in every module, including the 146 of 198 that have not adopted lint, and it WILL disagree with a green build: the `nest` preset refuses `complexity` at 30 while this reports above 10.",
5542
+ source: "oxlint complexity, above 10. The threshold comes from McCabe 1976, which puts the limit at 10, and from our own p90 at 11: the one indicator where two independent references agree within a unit. 94.8% of our functions already sit in the 1-10 band. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5531
5543
  },
5532
5544
  "max-depth": {
5533
5545
  label: "Files with deep nesting",
5534
- what: "Files holding a block nested more than four levels deep.",
5546
+ what: "Files holding a block nested more than three levels deep.",
5535
5547
  read: "The rarest of the indicators, 0.6 firings per 100 files across the repository, so one occurrence is worth opening rather than filtering. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5536
5548
  caveat: "Depth is not complexity: four flat `if`s in a row never trip it, and one `if` inside three loops does.",
5537
- source: "oxlint max-depth, at ESLint's published default of 4. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5549
+ source: "oxlint max-depth, above 3. The threshold is IfSQ SP-2, which says few people can understand nesting of conditional statements beyond 3 levels, and our p90 agrees at 3. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5538
5550
  },
5539
5551
  "max-lines": {
5540
- label: "Files over 300 lines",
5541
- what: "Files longer than 300 lines, blank lines and comments included.",
5552
+ label: "Long files",
5553
+ what: "Files longer than 150 lines, blank lines and comments included.",
5542
5554
  read: "A size signal and nothing else. Read it beside the duplication figure: a long file that duplicates nothing is a different problem from a long file that repeats itself. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5543
5555
  caveat: "Length is not a defect. A generated file, a long constant table or a barrel is long by nature, which is why this is reported and never failed on.",
5544
- source: "oxlint max-lines, at ESLint's published default of 300. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5556
+ source: "oxlint max-lines, above 150. The threshold is our p90 of 157 rounded down, because no research anywhere sets a FILE's length: IfSQ's three citations (500, 100-150, 143) are all about routines. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5545
5557
  },
5546
5558
  "max-lines-per-function": {
5547
5559
  label: "Files with a long function",
5548
- what: "Files holding at least one function longer than 50 lines.",
5560
+ what: "Files holding at least one function longer than 109 lines.",
5549
5561
  read: "\u26A0\uFE0F A DESCRIPTOR, not part of the maintainability figure. It fires on 46.9 files per 100 across the repository, so it would fire on nearly every pull request. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5550
- caveat: "What is excluded from the figure is the SCORE, not the reading: the count stays here precisely because it is frequent, and a module far from 46.9 is saying something.",
5551
- source: "oxlint max-lines-per-function, at ESLint's published default of 50. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5562
+ caveat: "Length is a signal and not a defect: a long function can be a flat list of fields. Read it beside the statement count, which is the other half of the same question, and which it does NOT duplicate: measured here, 255 long functions breach no statement count and 699 dense ones are not long.",
5563
+ source: "oxlint max-lines-per-function, above 109. The threshold is our p90. IfSQ SP-1 says 150, citing IBM on routines over 500 lines, and is discarded here for being more permissive than our own code. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5552
5564
  },
5553
5565
  "max-statements": {
5554
5566
  label: "Files with a long function body",
5555
5567
  what: "Files holding at least one function of more than ten statements.",
5556
5568
  read: "\u26A0\uFE0F A DESCRIPTOR, not part of the maintainability figure, for the same reason as the row above: 42.9 firings per 100 files. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5557
5569
  caveat: "It counts statements and not work: ten calls in a row trip it where one dense expression does not.",
5558
- source: "oxlint max-statements, at ESLint's published default of 10. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5570
+ source: "oxlint max-statements, above 10. The threshold is the rule's own default, kept because our p90 of 15 is the looser of the two; it is not redundant with the line count, 699 functions breach it without being long and 255 are long without breaching it. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5559
5571
  },
5560
5572
  "max-params": {
5561
5573
  label: "Files with a wide signature",
5562
5574
  what: "Files holding at least one function taking more than three parameters.",
5563
5575
  read: "The most frequent of the scored indicators, 9.6 per 100 files. A wide signature is usually an object waiting to be named. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5564
5576
  caveat: "A destructured object parameter counts as one, so the figure does not see a function taking fifteen fields in one bag.",
5565
- source: "oxlint max-params, at ESLint's published default of 3. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5577
+ source: "oxlint max-params, above 3. The threshold is the rule\u2019s own default, kept because our p90 of 4 is looser; beyond three parameters the fix is an object or currying, so this is a design boundary rather than a dial. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5566
5578
  },
5567
5579
  "max-nested-callbacks": {
5568
5580
  label: "Files with nested callbacks",
5569
- what: "Files holding callbacks nested more than ten deep.",
5581
+ what: "Files holding callbacks nested more than three deep.",
5570
5582
  read: "Zero everywhere measured so far. Kept because a non-zero value is then unambiguous. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5571
5583
  caveat: "It counts callbacks and not promise chains or awaits, so modern asynchronous code can be deeply sequential without ever tripping it.",
5572
- source: "oxlint max-nested-callbacks, at ESLint's published default of 10. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5584
+ source: "oxlint max-nested-callbacks, above 3. The threshold is IfSQ SP-2 again, same nesting family, and our p90 agrees at 3. It sat at 10 while the deepest chain in the repository is 4, so it reported 0 files for as long as it shipped. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5573
5585
  },
5574
5586
  "max-classes-per-file": {
5575
5587
  label: "Files with several classes",
5576
5588
  what: "Files declaring more than one class.",
5577
5589
  read: "1.8 per 100 files. On a nest service it usually means a DTO file, which is a convention rather than a defect. A file is counted once per indicator, however many times the rule fires inside it. Measured on `libs/nest/auth`, counting diagnostics instead would have inflated the total by 1.8x, and by 2.8x at lower thresholds, because `max-nested-callbacks` reports once per level.",
5578
5590
  caveat: "It says nothing about the size of the classes, and a file with one 800-line class never appears here.",
5579
- source: "oxlint max-classes-per-file, at ESLint's published default of 1. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5591
+ source: "oxlint max-classes-per-file, above 1. The threshold is the rule's floor, there is no stricter value to pick. The rule for every one of the eight is `min(research, our p90, the rule's default)`, and a vendor default alone is not a reference: at 50, max-lines-per-function flagged 21.3% of our files and carried 77% of the union of all eight. The exclusions are the same list the duplication product pass is given, passed as `--ignore-pattern`: the `ignorePatterns` key in oxlint's config is accepted and silently ignored, which had this gauge measuring the test suite until the file counts were compared, 137 against 111 on `libs/nest/auth`."
5580
5592
  },
5581
5593
  usedOnlyByTests: {
5582
5594
  label: "Used only by tests",
@@ -5584,6 +5596,13 @@ var metricsHelp = {
5584
5596
  read: "A third state between alive and dead, and the only one of the three that names a design question rather than a defect: a production module shipping something only the suites consume. Zero is the ordinary value.",
5585
5597
  caveat: "It says nothing about whether that is wrong. A test factory deliberately published by a library belongs here and is doing its job. What it refuses to do is call those files dead, which is what a walk that ignores tests does: measured on `libs/nest/auth`, 11 live files are imported only from a test, `auth.module.ts` among them, and 47 projects depend on that module. \u26A0\uFE0F It sees FILES, so an export used only by specs inside a file that production also imports is invisible here.",
5586
5598
  source: "The workspace index in `workspace/consumers.ts`: one scan of the tracked files, specifiers resolved against the tsconfig aliases, an importer classified as a test by the same conventions the duplication pass uses. Measured on this repository: 278 such files in all, 39 of them in `libs/front/components`."
5599
+ },
5600
+ measuredFiles: {
5601
+ label: "Files the gauge read",
5602
+ what: "How many product files the size indicators were measured over.",
5603
+ read: "The denominator of every size row above. None of them compares between modules without it: three breaches over 50 files and three over 3 000 are not the same module.",
5604
+ caveat: "It is the gauge\u2019s population and not the module\u2019s: `src` only, tests, mocks and generated files excluded, which is the same list every other pass is given. So it is smaller than what `ls` would count, deliberately, and it will differ from the duplication pass\u2019s own file count, which drops files shorter than the detection window.",
5605
+ source: "oxlint's `number_of_files` for the run this role drives, with the role's product exclusions passed as `--ignore-pattern`. Verified against the graph pass on `libs/nest/auth`: both read the same 99 files."
5587
5606
  }
5588
5607
  };
5589
5608
  function helpFor(key) {
@@ -6000,7 +6019,7 @@ import { existsSync as existsSync15, mkdirSync, readFileSync as readFileSync16,
6000
6019
  import { dirname as dirname2, join as join21, relative as relative4, resolve as resolve3, sep } from "node:path";
6001
6020
  var CACHE_DIR = join21("node_modules", ".cache", "sentinel");
6002
6021
  var CACHE_FILE = "consumer-index.json";
6003
- var SHAPE = 3;
6022
+ var SHAPE = 4;
6004
6023
  var EXTENSIONS2 = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
6005
6024
  function workspaceRootFrom(cwd) {
6006
6025
  let dir = resolve3(cwd);
@@ -6070,6 +6089,24 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
6070
6089
  }
6071
6090
  return void 0;
6072
6091
  }
6092
+ function resolveRelative(importer, specifier, workspaceRoot) {
6093
+ const base = join21(dirname2(importer), specifier).split(sep).join("/");
6094
+ if (base.startsWith("..")) return void 0;
6095
+ const isFile = (candidate) => {
6096
+ try {
6097
+ return statSync4(join21(workspaceRoot, candidate)).isFile();
6098
+ } catch {
6099
+ return false;
6100
+ }
6101
+ };
6102
+ if (isFile(base)) return base;
6103
+ for (const extension of EXTENSIONS2) if (isFile(base + extension)) return base + extension;
6104
+ for (const extension of EXTENSIONS2) {
6105
+ const index = `${base}/index${extension}`;
6106
+ if (isFile(index)) return index;
6107
+ }
6108
+ return void 0;
6109
+ }
6073
6110
  function isTestFile(path2) {
6074
6111
  return /\.(test|spec|steps|e2e-spec|integration-spec|stories)\.[cm]?[jt]sx?$|(^|\/)(tests|__tests__|test|testing|stubs|__mocks__|mocks)\//.test(
6075
6112
  path2
@@ -6091,8 +6128,18 @@ function buildConsumerIndex(workspaceRoot, moduleRoots) {
6091
6128
  if (table.size === 0) return { imported, testOnly, builtFrom: "no alias table" };
6092
6129
  const result = spawnSync5(
6093
6130
  "git",
6094
- ["grep", "-I", "-n", "-o", "-E", "@[A-Za-z0-9_-]+/[A-Za-z0-9_./-]+", "--", "*.ts", "*.tsx"],
6095
- { cwd: workspaceRoot, encoding: "utf8", maxBuffer: 512 * 1024 * 1024 }
6131
+ [
6132
+ "grep",
6133
+ "-I",
6134
+ "-n",
6135
+ "-o",
6136
+ "-E",
6137
+ "@[A-Za-z0-9_-]+/[A-Za-z0-9_./-]+|from '(\\.\\./)+[^']+'",
6138
+ "--",
6139
+ "*.ts",
6140
+ "*.tsx"
6141
+ ],
6142
+ { cwd: workspaceRoot, encoding: "utf8", maxBuffer: 1024 * 1024 * 1024 }
6096
6143
  );
6097
6144
  if (result.error || result.status !== 0 && result.status !== 1) {
6098
6145
  return { imported, testOnly, builtFrom: "git grep failed" };
@@ -6106,8 +6153,16 @@ function buildConsumerIndex(workspaceRoot, moduleRoots) {
6106
6153
  if (first < 0 || second < 0) continue;
6107
6154
  const importer = line.slice(0, first).split(sep).join("/");
6108
6155
  const specifier = line.slice(second + 1);
6109
- if (!seen.has(specifier)) seen.set(specifier, resolveSpecifier2(specifier, table, workspaceRoot));
6110
- const target = seen.get(specifier);
6156
+ let target;
6157
+ const relative5 = /^from '((?:\.\.\/)+[^']+)'$/.exec(specifier)?.[1];
6158
+ if (relative5) {
6159
+ target = resolveRelative(importer, relative5, workspaceRoot);
6160
+ } else {
6161
+ if (!seen.has(specifier)) {
6162
+ seen.set(specifier, resolveSpecifier2(specifier, table, workspaceRoot));
6163
+ }
6164
+ target = seen.get(specifier);
6165
+ }
6111
6166
  if (!target) continue;
6112
6167
  if (moduleOf(importer, moduleRoots) === moduleOf(target, moduleRoots)) continue;
6113
6168
  imported.add(target);
@@ -6478,7 +6533,7 @@ function measuredWith(config) {
6478
6533
  // never appear as `(module)`: a gauge whose scale moves per module compares nothing.
6479
6534
  row(
6480
6535
  "size indicators",
6481
- `${Object.entries(PUBLISHED_THRESHOLDS).map(([rule, threshold]) => `${rule} ${threshold}`).join(", ")} (published defaults, oxlint run outside the module)`
6536
+ `${Object.entries(GAUGE_THRESHOLDS).map(([rule, threshold]) => `${rule} ${threshold}`).join(", ")} (min(research, our p90, the rule's default), oxlint run outside the module)`
6482
6537
  ),
6483
6538
  // ⚠️ The graph pass's one knob, and it has to be here: it changes both the community count and
6484
6539
  // `Q`, so the two figures are meaningless without it. The seed is NOT printed beside it,
@@ -6773,10 +6828,13 @@ function assembleReport(ctx, config, product, tests, dead, complexity, graph, co
6773
6828
  } else if (!unreadable) {
6774
6829
  notes.push("fewer than two files, so no graph figure is reported");
6775
6830
  }
6831
+ if (size && size.files > 0) {
6832
+ rows.push({ metric: "measuredFiles", value: size.files, unit: "files the gauge read" });
6833
+ }
6776
6834
  if (size) {
6777
6835
  const counts = /* @__PURE__ */ new Map();
6778
6836
  for (const breach of size.breaches) counts.set(breach.rule, (counts.get(breach.rule) ?? 0) + 1);
6779
- for (const [rule, threshold] of Object.entries(PUBLISHED_THRESHOLDS)) {
6837
+ for (const [rule, threshold] of Object.entries(GAUGE_THRESHOLDS)) {
6780
6838
  const count = counts.get(rule) ?? 0;
6781
6839
  if (count === 0) continue;
6782
6840
  rows.push({
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-AFM4AJHA.js";
9
+ } from "./chunk-BN6SCBFW.js";
10
10
  import {
11
11
  BaseAdapter
12
12
  } from "./chunk-KLEXJFJ6.js";
@@ -548,6 +548,13 @@ The related work carries two results that challenge the Complexity category: Her
548
548
  that most code complexity metrics correlate highly with LOC, and Moser et al. that process
549
549
  metrics outperform code metrics as fault predictors.
550
550
 
551
+ ### IfSQ
552
+
553
+ The one standard found that attaches a citation to a numeric limit, and the only external source
554
+ used to fix this role’s own thresholds. It is a human-inspection standard rather than a tool, so its
555
+ table sits with the decision it serves, in **11.4**: three of its fifteen indicators carry a number,
556
+ and none of them is about a file’s length.
557
+
551
558
  ---
552
559
 
553
560
  ## 6. The tool, measured rather than read
@@ -866,6 +873,8 @@ each one fires per 100 files, which is measured rather than judged:
866
873
  | `max-params` | 3 | 9.6 |
867
874
  | `max-nested-callbacks` | 10 | 0 |
868
875
 
876
+ ⛔ **Superseded by 10.7, and closed by 11.6.** The two figures below, and the break they rest on, were measured on four modules before two defects in the gauge were fixed: diagnostics counted as breaches, and a population that included the test suite. Over 177 modules the same two rules measure 10.4 and 6.9 per 100 files against the next at 4.6, a ratio of 1.5 rather than 4.5. Section 11 then changed the thresholds themselves, and at those the most frequent rule reaches 11.0% of files against 10.7% for the next: there is no break left to split on, so `DESCRIPTORS`, `scored` and `described` are removed and every indicator counts.
877
+
869
878
  **Which are out, and why.** There is a break in the data, a factor of 4.5, not a line we drew:
870
879
 
871
880
  | kept as a descriptor | per 100 files |
@@ -968,17 +977,18 @@ touch no production file at all and the median touches **4**.
968
977
 
969
978
  ### 9.7 What is still a decision
970
979
 
971
- | open | what it needs |
972
- | ---------------------------------- | ------------------------------------------------------------------------------------------------ |
973
- | the `/10` in the `MI` | the distribution over the 198 modules. It changes the spread, never the order |
974
- | the criticality order of the seven | one argument per indicator. Rarity rules two out, it does not rank the rest |
975
- | the overhead line of the SQL study | the harness rebuilt. #90 corrects the attribution and withdraws the 71%, which was never a ratio |
976
- | the workspace index for libraries | built and measured in 10.4 and 10.5. Nothing open here any more |
977
- | a feedback channel on findings | unchanged from section 8, and it is what would let any of this be proven wrong |
980
+ | open | what it needs |
981
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
982
+ | the `/10` in the `MI` | the distribution over the 198 modules. It changes the spread, never the order |
983
+ | the criticality order of the **eight** | one argument per indicator. **Rarity no longer rules any out:** see 11.6, where the split is removed. Ranking the eight is still open |
984
+ | the overhead line of the SQL study | the harness rebuilt. #90 corrects the attribution and withdraws the 71%, which was never a ratio |
985
+ | the workspace index for libraries | built and measured in 10.4 and 10.5. Nothing open here any more |
986
+ | a feedback channel on findings | unchanged from section 8, and it is what would let any of this be proven wrong |
978
987
 
979
988
  The one measurement that unblocks the first two is the same: **the eight indicators over all 198
980
- modules, once.** Until then the figures in this section come from four modules and are quoted as
981
- such.
989
+ modules, once.** That measurement now exists, in 10.7 over 177 modules and in 11 over a five-module
990
+ bench, and it is what fixed the thresholds. The figures in THIS section still come from four modules
991
+ and are quoted as such.
982
992
 
983
993
  ---
984
994
 
@@ -1173,7 +1183,245 @@ the binary failing. Measured: same 18 diagnostics either way, exit 1 against exi
1173
1183
 
1174
1184
  ---
1175
1185
 
1176
- ## 11. Sources
1186
+ ### 10.7 The whole repository, measured, and what it does to 9.3
1187
+
1188
+ Section 9.3 chose which indicators enter the maintainability figure by how often each fires per 100
1189
+ files, and excluded two of them on a **break of a factor 4.5** in that data. Those figures came from
1190
+ four modules, measured before two defects found while building the gauge were fixed. Here is the
1191
+ same question over **177 modules and 29 770 files**, with the gauge reading `src` only and counting
1192
+ a file once per indicator.
1193
+
1194
+ | indicator | per 100 files | modules above zero |
1195
+ | ------------------------ | ------------: | -----------------: |
1196
+ | `max-lines-per-function` | **10.4** | 75 |
1197
+ | `max-statements` | **6.9** | 82 |
1198
+ | `max-params` | 4.6 | 69 |
1199
+ | `max-classes-per-file` | 3.5 | 53 |
1200
+ | `max-lines` | 2.1 | 59 |
1201
+ | `complexity` | 0.8 | 33 |
1202
+ | `max-depth` | 0.0 | 12 |
1203
+ | `max-nested-callbacks` | 0.0 | 0 |
1204
+
1205
+ ⛔ **The 4.5 break is gone, and it was an artefact of the measurement rather than a fact about the
1206
+ code.** Section 9.3 reported 46.9 and 42.9 for the two descriptors; the **same three modules**
1207
+ measured now give **14.4 and 7.7**. Two defects fixed tonight explain the direction and the order of
1208
+ magnitude:
1209
+
1210
+ - diagnostics were counted where a breach is a (file, rule) pair. `max-nested-callbacks` fires once
1211
+ per nesting level; measured, counting diagnostics inflates by **1.8x at the published thresholds
1212
+ and 2.8x at lower ones**.
1213
+ - the population included the test suite, because `ignorePatterns` in oxlint's config is accepted
1214
+ and silently ignored. Measured on `libs/front/components`: `max-lines-per-function` falls from
1215
+ **33.6 to 23.9** per 100 files once the specs leave.
1216
+
1217
+ What the clean data shows instead is a **smooth gradient**, consecutive ratios of 1.5, 1.5, 1.3,
1218
+ 1.7, then 2.6 and finally 17.1 between `complexity` and `max-depth`:
1219
+
1220
+ max-lines-per-function 10.4 -> max-statements 6.9 x1.5
1221
+ max-statements 6.9 -> max-params 4.6 x1.5
1222
+ max-params 4.6 -> max-classes 3.5 x1.3
1223
+ max-classes 3.5 -> max-lines 2.1 x1.7
1224
+ max-lines 2.1 -> complexity 0.8 x2.6
1225
+ complexity 0.8 -> max-depth 0.0 x17.1
1226
+
1227
+ ⚠️ **So the descriptor split now rests on nothing measured.** The two rules excluded from the figure
1228
+ are still the two most frequent, which is a reason to treat them with care, but 1.5x is not the 4.5x
1229
+ the decision was taken on, and inventing a cut here would be exactly the kind of threshold this
1230
+ document refuses elsewhere. The relative ORDER did survive: `complexity` below `max-lines` below
1231
+ `max-params` as before, with only `max-classes-per-file` moving up, from below `complexity` to above
1232
+ it.
1233
+
1234
+ **This is left open rather than re-decided.** What the data supports today is one clean statement:
1235
+ `max-nested-callbacks` fires **nowhere** in the repository, and `max-depth` in 12 modules for 14
1236
+ files in all, so those two carry almost no information here whatever the figure does with them.
1237
+
1238
+ ---
1239
+
1240
+ ## 11. How each threshold was chosen
1241
+
1242
+ Sections 9.3 and 10.7 left one question open: the gauge ran at the thresholds ESLint ships, which is
1243
+ not a choice, and 10.7 showed the one decision taken on those figures rested on a measurement
1244
+ defect. This section closes it. Every number in `GAUGE_THRESHOLDS` comes from one rule applied eight
1245
+ times, and the rule is stated before any figure so it cannot be fitted to a result afterwards.
1246
+
1247
+ ### 11.1 The rule: `min(research, our p90, the rule's default)`
1248
+
1249
+ Three candidates per indicator, the strictest wins, and there is no fourth. The minimum is what
1250
+ makes it safe in **both** directions of failure, and both directions were observed here:
1251
+
1252
+ - ⛔ **A threshold above what the code reaches detects nothing.** `max-nested-callbacks` shipped at
1253
+ 10 while the deepest chain in the repository is 4. It reported 0 files in 177 modules, for as long
1254
+ as it existed. 10.7 recorded that as "carries almost no information"; the cause is simpler, the
1255
+ scale was wrong.
1256
+ - ⛔ **Research can be more permissive than our own code, and then our code wins.** IfSQ's SP-1 puts
1257
+ a routine at 150 lines. Our p90 is 109. Adopting 150 because a published standard says so would
1258
+ relax a control to match a document, which is the opposite of measuring.
1259
+
1260
+ A vendor default is a candidate, never a reference: it is shipped to fit every codebase, so it was
1261
+ calibrated on none. What it cost here is in 11.6.
1262
+
1263
+ ### 11.2 Three traps in deriving a threshold from our own distribution
1264
+
1265
+ ⚠️ **The p90 over the whole population collapses to 0 for four of the eight.** Ninety per cent of
1266
+ files never nest a block, never nest a callback, declare at most one class and take at most one
1267
+ parameter, so the percentile sits at the floor and a threshold of 0 flags everything. So "our p90"
1268
+ means **the p90 over the files the rule touches**, which is the only convention that does not
1269
+ collapse:
1270
+
1271
+ | indicator | p90 over all 8 083 files | p90 over the files it touches |
1272
+ | ---------------------- | -----------------------: | ----------------------------: |
1273
+ | `max-depth` | 0 | 3 |
1274
+ | `max-nested-callbacks` | 0 | 3 |
1275
+ | `max-classes-per-file` | 0 | 5 |
1276
+ | `max-params` | 3 | 4 |
1277
+
1278
+ ⚠️ **Rounding goes downward only.** `max-lines` derives a p90 of 157 and publishes 150. Rounding up
1279
+ would move a threshold away from the evidence that produced it, in the one direction the rule above
1280
+ exists to forbid.
1281
+
1282
+ ⛔ **The harness read seven of the eight message shapes.** oxlint parenthesises the measured value,
1283
+ `too many statements (20)`, except for `complexity`, which writes `has a complexity of 15`. A reader
1284
+ of the parenthesised form alone returns 0 for every complexity breach and says nothing about it: the
1285
+ first run of this bench reported **0 files above complexity 10, 15 and 20 alike**, which reads as
1286
+ "nothing to see" rather than as a broken instrument. The same defect sat in the role's own
1287
+ `valueOf`, where it was latent because no report row consumed the value yet. Both are fixed, and the
1288
+ regression test now uses the messages oxlint really emits rather than a paraphrase.
1289
+
1290
+ ### 11.3 The bench
1291
+
1292
+ **8 083 product files over five modules**: `console` and `host-admin` (react), `front-components`
1293
+ (react library), `recruitment` and `network` (nest). The exclusion list is the role's own, so no
1294
+ spec, fixture, mock, story, generated or declaration file is in any figure below. Distributions come
1295
+ from one oxlint run per rule set with every maximum at 1, so each occurrence reports its real value
1296
+ instead of only its breach.
1297
+
1298
+ ### 11.4 What IfSQ gives, and what it does not
1299
+
1300
+ IfSQ (ifsq.org) is the one standard found that states numeric limits with citations attached. It is
1301
+ a standard for **human inspection**: the level pages say what to look for, and `assessment.html`
1302
+ says who inspects, from self-assessment to third-party certification. There is no defect-density
1303
+ rule and no grade boundary anywhere on the site, checked page by page.
1304
+
1305
+ **Three of its fifteen indicators carry a number:**
1306
+
1307
+ | code | indicator | threshold | citations given |
1308
+ | -------- | ------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1309
+ | **SP-1** | Routine Too Long | > 150 lines, comments and blank lines excluded | IBM, "the most error-prone routines were those larger than 500 lines"; "when routines averaged 100 to 150 lines each, code was more stable"; "routines with 143 lines or less are 2.4 times less expensive to fix" |
1310
+ | **SP-2** | Nesting Too Deep | > 3 levels of conditional nesting | "few people can understand more than 3 levels of nested if statements". ⚠️ One finding page says "more than four" instead |
1311
+ | **SP-3** | Routine Too Complex | > 10 binary terms in conditionals | McCabe 1976 |
1312
+
1313
+ ⛔ **IfSQ sources nothing about a FILE's length.** All three citations above are about routines. So
1314
+ `max-lines` has no research candidate at all, and its threshold is our p90 and nothing else.
1315
+
1316
+ The remaining twelve state a defect to look for with no measurement: WIP-1 Vague "To Do", WIP-2
1317
+ Disabled Code, WIP-3 Empty Statement Block, SPM-1 Magic Numbers, SPM-2 Magic Strings, SPM-3
1318
+ Copy/Paste Programming, DP-1 Parameter Not Checked, DP-2 Status Ignored After Call, DP-3 Unexpected
1319
+ State Not Trapped, and the three Causes for Concern, which are an inspector's judgement. Of those,
1320
+ SPM-3 is already covered by the duplication pass and WIP-2 partly by the dead-code pass; WIP-1,
1321
+ WIP-3, SPM-1, SPM-2, DP-1 and DP-2 are candidates for indicators this role does not have yet.
1322
+
1323
+ ### 11.5 The eight thresholds, with their origin
1324
+
1325
+ | indicator | was | **is** | research | our p90 | what decided it |
1326
+ | ------------------------ | --: | ------: | -------- | ------: | --------------------------------------------------------------------------------------------------------- |
1327
+ | `complexity` | 20 | **10** | 10 | 11 | the only indicator where two independent references agree within a unit |
1328
+ | `max-lines-per-function` | 50 | **109** | 150 | 109 | research is more permissive than our code, so our code wins |
1329
+ | `max-lines` | 300 | **150** | none | 157 | our p90, rounded down. No research sets a file's length |
1330
+ | `max-depth` | 4 | **3** | 3 | 3 | research and our code agree |
1331
+ | `max-nested-callbacks` | 10 | **3** | 3 (SP-2) | 3 | same nesting family as SP-2, no citation of its own. Was above the repository's maximum of 4 |
1332
+ | `max-statements` | 10 | **10** | none | 15 | the default is the stricter candidate and stays |
1333
+ | `max-params` | 3 | **3** | none | 4 | the default stays. Beyond three parameters the fix is an object or currying, so this is a design boundary |
1334
+ | `max-classes-per-file` | 1 | **1** | none | 5 | 1 is the rule's floor, there is no stricter value |
1335
+
1336
+ Five move, and `complexity` is worth one line on its own: 94.8% of our 11 050 measured functions
1337
+ already sit in the 1-10 band the SEI calls low risk, so 10 is a ratchet rather than a demand.
1338
+
1339
+ | SEI band | functions | share |
1340
+ | ------------------- | --------: | ---------: |
1341
+ | 1-10, low risk | 10 475 | **94.8 %** |
1342
+ | 11-15 | 319 | 2.9 % |
1343
+ | 16-20 | 120 | 1.1 % |
1344
+ | 21-50, high risk | 128 | 1.2 % |
1345
+ | over 50, untestable | 8 | 0.1 % |
1346
+
1347
+ ### 11.6 What the eight say together, and the end of the descriptor split
1348
+
1349
+ A threshold is only half a decision: what matters is what the **set** reports.
1350
+
1351
+ | | at the vendor defaults | at the thresholds above |
1352
+ | ---------------------- | ------------------------------: | ----------------------: |
1353
+ | union of the eight | 2 236 files, 27.7 % | **1 708 files, 21.1 %** |
1354
+ | largest single rule | `max-lines-per-function`, 1 724 | `max-statements`, 891 |
1355
+ | its share of the union | **77 %** | **52 %** |
1356
+
1357
+ ⛔ **At the defaults one rule was the report.** `max-lines-per-function` at 50 flagged 21.3% of all
1358
+ product files and carried 77% of the union, so the other seven indicators were inaudible: the
1359
+ report said "your functions are longer than 50 lines" and nothing else could be heard beside it.
1360
+
1361
+ And this closes what 10.7 left open. The descriptor split held `max-statements` and
1362
+ `max-lines-per-function` out of the figure on a **break of a factor 4.5** between 42.9/46.9 and 9.6
1363
+ per 100 files. 10.7 already showed those numbers were an artefact of two defects. At the thresholds
1364
+ above the two most frequent rules are 11.0% and 10.7%, and the next is 7.8%:
1365
+
1366
+ | indicator | files | per 100 files | ratio to the next |
1367
+ | ------------------------ | ----: | ------------: | ----------------: |
1368
+ | `max-statements` | 891 | 11.0 | 1.03 |
1369
+ | `max-lines` | 864 | 10.7 | 1.37 |
1370
+ | `max-lines-per-function` | 630 | 7.8 | 1.26 |
1371
+ | `complexity` | 499 | 6.2 | 1.34 |
1372
+ | `max-params` | 373 | 4.6 | 2.32 |
1373
+ | `max-classes-per-file` | 161 | 2.0 | 17.89 |
1374
+ | `max-nested-callbacks` | 9 | 0.1 | 1.13 |
1375
+ | `max-depth` | 8 | 0.1 | - |
1376
+
1377
+ **The top five are a smooth gradient, 1.03 to 1.37, so there is no population to exclude.** The one
1378
+ large break, 17.9, sits at the BOTTOM, between `max-classes-per-file` and the two nesting rules: it
1379
+ says those two are rare here, not that the frequent ones are noise. `DESCRIPTORS`, `scored` and
1380
+ `described` are removed: every indicator counts, and the asymmetry that needed explaining in two
1381
+ places no longer exists. Note also that the two nesting rules moved from reporting **nothing at all**
1382
+ to reporting 9 and 8 files: lowering them from 10 and 4 to 3 is what gave them any information.
1383
+
1384
+ ### 11.7 Why `max-statements` is not a second reading of `max-lines-per-function`
1385
+
1386
+ The obvious objection to keeping both is that a long function has many statements. Measured by
1387
+ joining the two diagnostics on the same function span, over 25 658 functions of which 10 441 carry
1388
+ both values:
1389
+
1390
+ | | |
1391
+ | --------------------------------------- | -------------------: |
1392
+ | Pearson, statements against lines | 0.726 |
1393
+ | Spearman | 0.600 |
1394
+ | lines per statement, p25 / median / p90 | 2.5 / **4.4** / 14.0 |
1395
+
1396
+ Correlated, so not independent. But the density varies by a factor of **5.6** across functions, and
1397
+ that is what forbids giving them one value. At the chosen thresholds:
1398
+
1399
+ | | functions | what it is |
1400
+ | ----------------------------- | --------: | ------------------------ |
1401
+ | over both | 399 | long and dense |
1402
+ | `max-statements` only | **699** | dense logic in few lines |
1403
+ | `max-lines-per-function` only | **255** | long with little logic |
1404
+ | overlap | **29 %** | |
1405
+
1406
+ Both shapes are real, and in this repository they are two different kinds of file:
1407
+
1408
+ | file | lines | statements | shape |
1409
+ | --------------------------------------- | ----: | ---------: | ---------------------- |
1410
+ | `add-to-network/JobsEvaluationList.tsx` | 259 | 2 | one large JSX return |
1411
+ | `entities/job-offer.entity.ts` | 48 | 46 | one statement per line |
1412
+
1413
+ So `max-lines-per-function` measures volume including markup, and `max-statements` measures
1414
+ imperative density, and neither sees what the other sees. Giving `max-statements` the same 109 would
1415
+ kill it: at the median density that is a function of roughly 480 lines, so the line count would
1416
+ always fire first and the statement count would never say anything again.
1417
+
1418
+ ---
1419
+
1420
+ ## 12. Sources
1421
+
1422
+ - IfSQ. _Level 1, Level 2 and Level 3 Standards for Code Quality._ <https://www.ifsq.org/level-1.html>,
1423
+ <https://www.ifsq.org/level-2.html>, <https://www.ifsq.org/structured-programming.html>,
1424
+ <https://www.ifsq.org/research.html>, <https://www.ifsq.org/assessment.html>
1177
1425
 
1178
1426
  - Ben Khalfallah, H. _Code Health Meter: A Quantitative and Graph-Theoretic Foundation for
1179
1427
  Automated Code Quality and Architecture Assessment._ ACM TOSEM 35(4), article 114, March 2026.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.5.0-alpha.21",
3
+ "version": "1.5.0-alpha.22",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",