@hublo/sentinel 1.5.0-alpha.21 → 1.5.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin/sentinel.js +4 -4
- package/dist/{chunk-KLEXJFJ6.js → chunk-IFK6KGUW.js} +1 -1
- package/dist/{chunk-YQRDKWSH.js → chunk-JEYGPPSL.js} +11 -1
- package/dist/chunk-JEYGPPSL.js.map +1 -0
- package/dist/{chunk-AMCGMLPS.js → chunk-SPES5MQT.js} +2 -2
- package/dist/{chunk-AFM4AJHA.js → chunk-TL2P2X45.js} +284 -46
- package/dist/index.js +3 -3
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-AWWMBSH5.js → validate-7VBODB4M.js} +2 -2
- package/docs/static-analysis-adoption.md +195 -0
- package/docs/static-analysis-study.md +378 -11
- package/docs/using-sentinel.md +3 -2
- package/package.json +2 -1
- package/plugins/sonar-cognitive.js +71 -0
- package/dist/chunk-YQRDKWSH.js.map +0 -1
- /package/dist/{chunk-AMCGMLPS.js.map → chunk-SPES5MQT.js.map} +0 -0
|
@@ -53,15 +53,16 @@ import {
|
|
|
53
53
|
selfCommand,
|
|
54
54
|
splitToolArgs,
|
|
55
55
|
walk
|
|
56
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-IFK6KGUW.js";
|
|
57
57
|
import {
|
|
58
58
|
DEFAULT_MAX_DIAGNOSTICS,
|
|
59
59
|
WORKSPACE_ROOT_MARKER,
|
|
60
60
|
findWorkspaceRoot,
|
|
61
61
|
moduleName,
|
|
62
|
+
ownPackageDir,
|
|
62
63
|
readOwnPackage,
|
|
63
64
|
readProjectPackageJson
|
|
64
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-JEYGPPSL.js";
|
|
65
66
|
import {
|
|
66
67
|
REPORT_DIR,
|
|
67
68
|
SCRIPTS,
|
|
@@ -5104,7 +5105,7 @@ var SIZE = { width: 1600, height: 1200, margin: 30 };
|
|
|
5104
5105
|
function hueOf(community) {
|
|
5105
5106
|
return `hsl(${community * 137 % 360} 65% 52%)`;
|
|
5106
5107
|
}
|
|
5107
|
-
function
|
|
5108
|
+
function layOut(graph, communities) {
|
|
5108
5109
|
const drawn = graph.copy();
|
|
5109
5110
|
circular.assign(drawn);
|
|
5110
5111
|
forceAtlas2.assign(drawn, { iterations: ITERATIONS, settings: forceAtlas2.inferSettings(drawn) });
|
|
@@ -5116,11 +5117,144 @@ function writeGraphPicture(graph, communities, path2) {
|
|
|
5116
5117
|
drawn.setNodeAttribute(node, "color", hueOf(communities[node] ?? 0));
|
|
5117
5118
|
drawn.setNodeAttribute(node, "label", named.has(node) ? node.split("/").pop() ?? node : BLANK);
|
|
5118
5119
|
});
|
|
5120
|
+
return drawn;
|
|
5121
|
+
}
|
|
5122
|
+
function writeGraphPicture(drawn, path2) {
|
|
5119
5123
|
return new Promise((resolve5) => {
|
|
5120
5124
|
renderSvg(drawn, path2, SIZE, () => resolve5());
|
|
5121
5125
|
});
|
|
5122
5126
|
}
|
|
5123
5127
|
|
|
5128
|
+
// src/roles/static-analysis/graph/explorer.ts
|
|
5129
|
+
function place(graph) {
|
|
5130
|
+
const nodes = graph.nodes();
|
|
5131
|
+
let minX = Infinity;
|
|
5132
|
+
let maxX = -Infinity;
|
|
5133
|
+
let minY = Infinity;
|
|
5134
|
+
let maxY = -Infinity;
|
|
5135
|
+
for (const node of nodes) {
|
|
5136
|
+
const x = graph.getNodeAttribute(node, "x");
|
|
5137
|
+
const y = graph.getNodeAttribute(node, "y");
|
|
5138
|
+
if (x < minX) minX = x;
|
|
5139
|
+
if (x > maxX) maxX = x;
|
|
5140
|
+
if (y < minY) minY = y;
|
|
5141
|
+
if (y > maxY) maxY = y;
|
|
5142
|
+
}
|
|
5143
|
+
const usableWidth = SIZE.width - 2 * SIZE.margin;
|
|
5144
|
+
const usableHeight = SIZE.height - 2 * SIZE.margin;
|
|
5145
|
+
const spanX = maxX - minX || 1;
|
|
5146
|
+
const spanY = maxY - minY || 1;
|
|
5147
|
+
const scale = Math.min(usableWidth / spanX, usableHeight / spanY);
|
|
5148
|
+
const offsetX = SIZE.margin + (usableWidth - spanX * scale) / 2;
|
|
5149
|
+
const offsetY = SIZE.margin + (usableHeight - spanY * scale) / 2;
|
|
5150
|
+
const placed = /* @__PURE__ */ new Map();
|
|
5151
|
+
for (const node of nodes) {
|
|
5152
|
+
const x = graph.getNodeAttribute(node, "x");
|
|
5153
|
+
const y = graph.getNodeAttribute(node, "y");
|
|
5154
|
+
placed.set(node, {
|
|
5155
|
+
x: offsetX + (x - minX) * scale,
|
|
5156
|
+
y: offsetY + (y - minY) * scale,
|
|
5157
|
+
// `size` is already the sqrt-damped degree `picture.ts` computed, in the same units as the
|
|
5158
|
+
// standalone image, so the two drawings show a file at the same weight.
|
|
5159
|
+
r: graph.getNodeAttribute(node, "size")
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
return placed;
|
|
5163
|
+
}
|
|
5164
|
+
function round(value) {
|
|
5165
|
+
return (Math.round(value * 100) / 100).toString();
|
|
5166
|
+
}
|
|
5167
|
+
function hueOf2(community) {
|
|
5168
|
+
return `hsl(${community * 137 % 360} 65% 52%)`;
|
|
5169
|
+
}
|
|
5170
|
+
function jsonInScript(value) {
|
|
5171
|
+
return value.replace(/</g, "\\u003c");
|
|
5172
|
+
}
|
|
5173
|
+
function buildExplorer(drawn, communities) {
|
|
5174
|
+
const placed = place(drawn);
|
|
5175
|
+
const nodes = [...placed.keys()];
|
|
5176
|
+
const index = new Map(nodes.map((node, position) => [node, position]));
|
|
5177
|
+
const edges = [];
|
|
5178
|
+
const segments = [];
|
|
5179
|
+
drawn.forEachEdge((_edge, _attributes, source, target) => {
|
|
5180
|
+
const from = placed.get(source);
|
|
5181
|
+
const to = placed.get(target);
|
|
5182
|
+
const sourceIndex = index.get(source);
|
|
5183
|
+
const targetIndex = index.get(target);
|
|
5184
|
+
if (!from || !to || sourceIndex === void 0 || targetIndex === void 0) return;
|
|
5185
|
+
segments.push(`M${round(from.x)} ${round(from.y)}L${round(to.x)} ${round(to.y)}`);
|
|
5186
|
+
edges.push(sourceIndex, targetIndex);
|
|
5187
|
+
});
|
|
5188
|
+
const used = [...new Set(nodes.map((node) => communities[node] ?? 0))].sort((a, b) => a - b);
|
|
5189
|
+
const palette2 = used.map((community) => `.g svg .k${community}{fill:${hueOf2(community)}}`).join("");
|
|
5190
|
+
const circles = nodes.map((node) => {
|
|
5191
|
+
const spot = placed.get(node);
|
|
5192
|
+
if (!spot) return "";
|
|
5193
|
+
const community = communities[node] ?? 0;
|
|
5194
|
+
return `<circle cx="${round(spot.x)}" cy="${round(spot.y)}" r="${round(spot.r)}" class="k${community}"/>`;
|
|
5195
|
+
}).join("");
|
|
5196
|
+
const payload = JSON.stringify({ f: nodes, e: edges });
|
|
5197
|
+
const markup = `<section class="g">
|
|
5198
|
+
<p class="read">One dot per file, sized by how many imports touch it, coloured by the community it landed in. The shape is the answer to how many lobes there are and whether they are joined. <b>Point at a dot for its path, click it to keep only that file and its neighbours, click the background to come back.</b></p>
|
|
5199
|
+
<div class="gbar"><output id="gname"> </output><button type="button" id="greset" hidden>show everything</button></div>
|
|
5200
|
+
<svg viewBox="0 0 ${SIZE.width} ${SIZE.height}" role="img" aria-label="The module's import graph, one dot per file">
|
|
5201
|
+
<path class="ge" d="${segments.join("")}"/>
|
|
5202
|
+
<path class="gh" d=""/>
|
|
5203
|
+
<g class="gn">${circles}</g>
|
|
5204
|
+
</svg>
|
|
5205
|
+
<style>${palette2}</style>
|
|
5206
|
+
<script type="application/json" id="gdata">${jsonInScript(payload)}</script>
|
|
5207
|
+
<script>${SCRIPT}</script>
|
|
5208
|
+
</section>`;
|
|
5209
|
+
return { markup, bytes: Buffer.byteLength(markup, "utf8") };
|
|
5210
|
+
}
|
|
5211
|
+
var SCRIPT = `(function(){
|
|
5212
|
+
var root=document.currentScript.closest('.g');
|
|
5213
|
+
var data=JSON.parse(root.querySelector('#gdata').textContent);
|
|
5214
|
+
var dots=root.querySelectorAll('.gn circle');
|
|
5215
|
+
var name=root.querySelector('#gname');
|
|
5216
|
+
var reset=root.querySelector('#greset');
|
|
5217
|
+
var highlight=root.querySelector('.gh');
|
|
5218
|
+
var out=[],inn=[];
|
|
5219
|
+
for(var i=0;i<data.f.length;i++){out.push([]);inn.push([]);}
|
|
5220
|
+
for(var k=0;k<data.e.length;k+=2){out[data.e[k]].push(data.e[k+1]);inn[data.e[k+1]].push(data.e[k]);}
|
|
5221
|
+
function show(i){
|
|
5222
|
+
name.textContent=data.f[i]+' ('+out[i].length+' imported, '+inn[i].length+' importing)';
|
|
5223
|
+
}
|
|
5224
|
+
function clear(){
|
|
5225
|
+
root.classList.remove('gsel');
|
|
5226
|
+
highlight.setAttribute('d','');
|
|
5227
|
+
for(var i=0;i<dots.length;i++)dots[i].classList.remove('gon','gpick');
|
|
5228
|
+
name.innerHTML=' ';
|
|
5229
|
+
reset.hidden=true;
|
|
5230
|
+
}
|
|
5231
|
+
function pick(i){
|
|
5232
|
+
root.classList.add('gsel');
|
|
5233
|
+
var near={};near[i]=1;
|
|
5234
|
+
var d='';
|
|
5235
|
+
function seg(a,b){
|
|
5236
|
+
d+='M'+dots[a].getAttribute('cx')+' '+dots[a].getAttribute('cy')+'L'+dots[b].getAttribute('cx')+' '+dots[b].getAttribute('cy');
|
|
5237
|
+
}
|
|
5238
|
+
for(var a=0;a<out[i].length;a++){near[out[i][a]]=1;seg(i,out[i][a]);}
|
|
5239
|
+
for(var b=0;b<inn[i].length;b++){near[inn[i][b]]=1;seg(inn[i][b],i);}
|
|
5240
|
+
highlight.setAttribute('d',d);
|
|
5241
|
+
for(var j=0;j<dots.length;j++){
|
|
5242
|
+
dots[j].classList.toggle('gon',!!near[j]);
|
|
5243
|
+
dots[j].classList.toggle('gpick',j===i);
|
|
5244
|
+
}
|
|
5245
|
+
show(i);
|
|
5246
|
+
reset.hidden=false;
|
|
5247
|
+
}
|
|
5248
|
+
for(var n=0;n<dots.length;n++){
|
|
5249
|
+
(function(i){
|
|
5250
|
+
dots[i].addEventListener('mouseenter',function(){show(i);});
|
|
5251
|
+
dots[i].addEventListener('click',function(event){event.stopPropagation();pick(i);});
|
|
5252
|
+
})(n);
|
|
5253
|
+
}
|
|
5254
|
+
root.querySelector('svg').addEventListener('click',clear);
|
|
5255
|
+
reset.addEventListener('click',clear);
|
|
5256
|
+
})();`;
|
|
5257
|
+
|
|
5124
5258
|
// src/roles/static-analysis/graph/scan.ts
|
|
5125
5259
|
import Graph from "graphology";
|
|
5126
5260
|
import louvain from "graphology-communities-louvain";
|
|
@@ -5283,16 +5417,35 @@ function scanGraph(cwd, config) {
|
|
|
5283
5417
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5284
5418
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
5285
5419
|
import { join as join20 } from "node:path";
|
|
5286
|
-
var
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5420
|
+
var GAUGE_THRESHOLDS = {
|
|
5421
|
+
// McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
|
|
5422
|
+
// within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
|
|
5423
|
+
complexity: 10,
|
|
5424
|
+
// IfSQ SP-2, "few people can understand nesting to more than 3 levels". Our p90 agrees at 3.
|
|
5425
|
+
"max-depth": 3,
|
|
5426
|
+
// No research anywhere for a FILE's length: IfSQ's three citations (500, 100-150, 143) are all
|
|
5427
|
+
// about routines. So our p90 of 157, rounded down.
|
|
5428
|
+
"max-lines": 150,
|
|
5429
|
+
// Our p90. IfSQ SP-1 says 150 and is discarded for being more permissive than our own code.
|
|
5430
|
+
"max-lines-per-function": 109,
|
|
5431
|
+
// No research. Our p90 is 15, the rule's default is 10, so the default stays as the stricter one.
|
|
5291
5432
|
"max-statements": 10,
|
|
5433
|
+
// No research, and none needed: beyond three parameters the fix is an object or currying, so this
|
|
5434
|
+
// is a design boundary and not a dial. Our p90 of 4 is the looser candidate and loses.
|
|
5292
5435
|
"max-params": 3,
|
|
5293
|
-
|
|
5294
|
-
"max-
|
|
5436
|
+
// Same nesting family as `max-depth`, no citation of its own. Our p90 agrees at 3.
|
|
5437
|
+
"max-nested-callbacks": 3,
|
|
5438
|
+
// 1 is the rule's floor, there is no stricter value to pick.
|
|
5439
|
+
"max-classes-per-file": 1,
|
|
5440
|
+
// Cognitive complexity, Campbell 2018, measured by Sonar's own rule, loaded
|
|
5441
|
+
// through `plugins/sonar-cognitive.js`.
|
|
5442
|
+
// Sonar publishes 15 for S3776, and that is a vendor default rather than a research value: one of
|
|
5443
|
+
// their own engineers has said publicly that no formula supports it. Our p90 is 7, so 7 wins, and
|
|
5444
|
+
// it reports 342 of 8 083 files, 4.2%, which sits inside the band the eight above occupy.
|
|
5445
|
+
"cognitive-complexity": 7
|
|
5295
5446
|
};
|
|
5447
|
+
var COGNITIVE = "cognitive-complexity";
|
|
5448
|
+
var cognitivePlugin = () => join20(ownPackageDir(), "plugins", "sonar-cognitive.js");
|
|
5296
5449
|
var CATEGORIES = [
|
|
5297
5450
|
"correctness",
|
|
5298
5451
|
"suspicious",
|
|
@@ -5306,13 +5459,20 @@ function sizeRulesConfig() {
|
|
|
5306
5459
|
return {
|
|
5307
5460
|
plugins: [],
|
|
5308
5461
|
categories: Object.fromEntries(CATEGORIES.map((category) => [category, "off"])),
|
|
5462
|
+
// Sonar's cognitive complexity, in the SAME run as the eight above rather than a second
|
|
5463
|
+
// invocation: the parse is the expensive part and it is already paid here. Measured over the
|
|
5464
|
+
// whole bench, 8 083 files, the rule adds 2.4 s to a pass that was going to read them anyway.
|
|
5465
|
+
jsPlugins: [cognitivePlugin()],
|
|
5309
5466
|
// ⛔ `warn` and not `error`, for the reason `tool-config.ts` gives about jscpd's `threshold`:
|
|
5310
5467
|
// at `error` oxlint exits 1 as soon as one file breaches, which is indistinguishable from the
|
|
5311
5468
|
// binary failing to run. Measured on `libs/nest/auth`, same config otherwise: `error` exits 1,
|
|
5312
5469
|
// `warn` exits 0, and both report the same 18 diagnostics. So the tool measures, this role
|
|
5313
5470
|
// judges, and a non-zero exit means the tool failed.
|
|
5314
5471
|
rules: Object.fromEntries(
|
|
5315
|
-
Object.entries(
|
|
5472
|
+
Object.entries(GAUGE_THRESHOLDS).map(([rule, threshold]) => [
|
|
5473
|
+
rule === COGNITIVE ? `sonar/${rule}` : rule,
|
|
5474
|
+
["warn", threshold]
|
|
5475
|
+
])
|
|
5316
5476
|
)
|
|
5317
5477
|
};
|
|
5318
5478
|
}
|
|
@@ -5341,7 +5501,10 @@ function ruleOf(code) {
|
|
|
5341
5501
|
return /\(([^)]+)\)\s*$/.exec(code)?.[1];
|
|
5342
5502
|
}
|
|
5343
5503
|
function valueOf(message) {
|
|
5344
|
-
|
|
5504
|
+
const match = /\((\d+)\)/.exec(message) ?? /complexity of (\d+)/.exec(message) ?? // Sonar's own wording, a third shape again: "reduce its Cognitive Complexity from 7 to the 0
|
|
5505
|
+
// allowed". It parenthesises nothing, and the `0` after it is the threshold, not the value.
|
|
5506
|
+
/Complexity from (\d+) to/.exec(message);
|
|
5507
|
+
return Number(match?.[1] ?? 0);
|
|
5345
5508
|
}
|
|
5346
5509
|
function breachesOf(raw) {
|
|
5347
5510
|
let parsed;
|
|
@@ -5355,7 +5518,7 @@ function breachesOf(raw) {
|
|
|
5355
5518
|
for (const diagnostic of diagnostics) {
|
|
5356
5519
|
const rule = ruleOf(diagnostic.code ?? "");
|
|
5357
5520
|
const file = diagnostic.filename;
|
|
5358
|
-
if (!rule || !file || !(rule in
|
|
5521
|
+
if (!rule || !file || !(rule in GAUGE_THRESHOLDS)) continue;
|
|
5359
5522
|
const value = valueOf(diagnostic.message ?? "");
|
|
5360
5523
|
const line = diagnostic.labels?.[0]?.span?.line ?? 0;
|
|
5361
5524
|
const key = `${file}\0${rule}`;
|
|
@@ -5524,59 +5687,66 @@ var metricsHelp = {
|
|
|
5524
5687
|
},
|
|
5525
5688
|
complexity: {
|
|
5526
5689
|
label: "Files with a complex function",
|
|
5527
|
-
what: "Files holding at least one function whose cyclomatic complexity passes
|
|
5690
|
+
what: "Files holding at least one function whose cyclomatic complexity passes 10.",
|
|
5528
5691
|
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
|
|
5530
|
-
source: "oxlint complexity, at
|
|
5692
|
+
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.",
|
|
5693
|
+
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
5694
|
},
|
|
5532
5695
|
"max-depth": {
|
|
5533
5696
|
label: "Files with deep nesting",
|
|
5534
|
-
what: "Files holding a block nested more than
|
|
5697
|
+
what: "Files holding a block nested more than three levels deep.",
|
|
5535
5698
|
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
5699
|
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
|
|
5700
|
+
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
5701
|
},
|
|
5539
5702
|
"max-lines": {
|
|
5540
|
-
label: "
|
|
5541
|
-
what: "Files longer than
|
|
5703
|
+
label: "Long files",
|
|
5704
|
+
what: "Files longer than 150 lines, blank lines and comments included.",
|
|
5542
5705
|
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
5706
|
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,
|
|
5707
|
+
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
5708
|
},
|
|
5546
5709
|
"max-lines-per-function": {
|
|
5547
5710
|
label: "Files with a long function",
|
|
5548
|
-
what: "Files holding at least one function longer than
|
|
5711
|
+
what: "Files holding at least one function longer than 109 lines.",
|
|
5549
5712
|
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: "
|
|
5551
|
-
source: "oxlint max-lines-per-function,
|
|
5713
|
+
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.",
|
|
5714
|
+
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
5715
|
},
|
|
5553
5716
|
"max-statements": {
|
|
5554
5717
|
label: "Files with a long function body",
|
|
5555
5718
|
what: "Files holding at least one function of more than ten statements.",
|
|
5556
5719
|
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
5720
|
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,
|
|
5721
|
+
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
5722
|
},
|
|
5560
5723
|
"max-params": {
|
|
5561
5724
|
label: "Files with a wide signature",
|
|
5562
5725
|
what: "Files holding at least one function taking more than three parameters.",
|
|
5563
5726
|
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
5727
|
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,
|
|
5728
|
+
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
5729
|
},
|
|
5567
5730
|
"max-nested-callbacks": {
|
|
5568
5731
|
label: "Files with nested callbacks",
|
|
5569
|
-
what: "Files holding callbacks nested more than
|
|
5732
|
+
what: "Files holding callbacks nested more than three deep.",
|
|
5570
5733
|
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
5734
|
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
|
|
5735
|
+
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
5736
|
},
|
|
5574
5737
|
"max-classes-per-file": {
|
|
5575
5738
|
label: "Files with several classes",
|
|
5576
5739
|
what: "Files declaring more than one class.",
|
|
5577
5740
|
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
5741
|
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,
|
|
5742
|
+
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`."
|
|
5743
|
+
},
|
|
5744
|
+
"cognitive-complexity": {
|
|
5745
|
+
label: "Files hard to follow",
|
|
5746
|
+
what: "Files holding at least one function whose cognitive complexity passes 7.",
|
|
5747
|
+
read: "The other complexity figure, and it answers a different question from McCabe's: not how many paths a test suite must cover, but how much a reader has to hold in their head. The two disagree on purpose. A flat `switch` with twelve cases is cyclomatically 13 and cognitively 1, because nothing is nested and nothing surprises; three `if`s inside each other are cyclomatically 4 and cognitively 6, because each level is charged for the levels above it. When a file is high here and low there, the code is tangled rather than merely branchy, and that is the one this role would act on first.",
|
|
5748
|
+
caveat: "\u26A0\uFE0F In JavaScript and TypeScript, sequences of `||` and `??` do NOT increment, deliberately: in this language they are overwhelmingly default-value shorthand rather than control flow, and SonarSource made that exclusion universal in SonarQube 10.8. So a long chain of fallbacks scores zero here while `&&` chains are charged. \u26D4 And it is a GAUGE, not the line your lint refuses at: no preset in this repository enforces cognitive complexity at all today.",
|
|
5749
|
+
source: "Sonar's own `cognitive-complexity` rule from `eslint-plugin-sonarjs`, loaded into oxlint through `plugins/sonar-cognitive.js` and run in the same invocation as the size rules. It is USED and never copied: that package ships under the Sonar Source-Available License and sentinel is MIT. The algorithm is Campbell 2018 and its author works at SonarSource, so their implementation is the specification's own house rather than one reading of it; verified against Campbell's published example, which scores 7 in both. The threshold is 7, from `min(research, our p90, the rule's default)`: Sonar publishes 15 for S3776 and that is a vendor default rather than a research value, since one of their own engineers has said publicly that no formula supports it, while our p90 over the files the rule touches is 7. It reports 342 of 8 083 files on the bench, 4.2%."
|
|
5580
5750
|
},
|
|
5581
5751
|
usedOnlyByTests: {
|
|
5582
5752
|
label: "Used only by tests",
|
|
@@ -5584,6 +5754,13 @@ var metricsHelp = {
|
|
|
5584
5754
|
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
5755
|
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
5756
|
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`."
|
|
5757
|
+
},
|
|
5758
|
+
measuredFiles: {
|
|
5759
|
+
label: "Files the gauge read",
|
|
5760
|
+
what: "How many product files the size indicators were measured over.",
|
|
5761
|
+
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.",
|
|
5762
|
+
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.",
|
|
5763
|
+
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
5764
|
}
|
|
5588
5765
|
};
|
|
5589
5766
|
function helpFor(key) {
|
|
@@ -5883,8 +6060,35 @@ ${rest > 0 ? ` <p class="sub">The ${HTML_GROUP_LIMIT} longest are listed; ${r
|
|
|
5883
6060
|
white-space: nowrap;
|
|
5884
6061
|
}
|
|
5885
6062
|
/* The graph. The svg carries a viewBox, so dropping its fixed width lets it fill the column. */
|
|
5886
|
-
.
|
|
5887
|
-
.
|
|
6063
|
+
.g svg { width: 100%; height: auto; display: block; }
|
|
6064
|
+
.g p.read { margin: 0 0 0.8rem; padding: 0 0.6rem; color: var(--ink-soft); font-size: 0.88rem; max-width: 52rem; }
|
|
6065
|
+
/* Every edge in one path, faint: it carries the shape, not a readable dependency. */
|
|
6066
|
+
.g .ge { stroke: var(--rule); stroke-width: 0.6; fill: none; }
|
|
6067
|
+
/* The selected file's own edges, drawn on top only once something is picked. */
|
|
6068
|
+
.g .gh { stroke: var(--brand); stroke-width: 1.4; fill: none; }
|
|
6069
|
+
.g .gn circle { cursor: pointer; }
|
|
6070
|
+
/* Dim the module once a file is picked, so its neighbourhood is what is left to read. The
|
|
6071
|
+
opacity sits on the GROUP and the exceptions raise themselves back: one rule instead of one
|
|
6072
|
+
class per dot, which matters at 4 380 of them. */
|
|
6073
|
+
.g.gsel .gn circle { opacity: 0.12; }
|
|
6074
|
+
.g.gsel .gn circle.gon { opacity: 1; }
|
|
6075
|
+
.g.gsel .gn circle.gpick { opacity: 1; stroke: var(--ink); stroke-width: 2; }
|
|
6076
|
+
.g.gsel .ge { opacity: 0.25; }
|
|
6077
|
+
/* The caption. A min-height and not a conditional row: the drawing must not jump by a line
|
|
6078
|
+
when a name appears, which is exactly what pointing at a dot would cause. */
|
|
6079
|
+
.gbar {
|
|
6080
|
+
display: flex; gap: 0.6rem; align-items: center; justify-content: space-between;
|
|
6081
|
+
min-height: 1.9rem; padding: 0 0.6rem; margin-bottom: 0.3rem;
|
|
6082
|
+
}
|
|
6083
|
+
.gbar output {
|
|
6084
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.82rem;
|
|
6085
|
+
color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
6086
|
+
}
|
|
6087
|
+
.gbar button {
|
|
6088
|
+
font: inherit; font-size: 0.8rem; cursor: pointer; white-space: nowrap;
|
|
6089
|
+
padding: 0.2rem 0.6rem; border: 1px solid var(--rule); border-radius: 0.3rem;
|
|
6090
|
+
background: var(--surface); color: var(--ink-soft);
|
|
6091
|
+
}
|
|
5888
6092
|
|
|
5889
6093
|
/* The collapsed reserves. Closed by default: the figures are what the page opens on. */
|
|
5890
6094
|
details { border-bottom: 1px solid var(--rule); }
|
|
@@ -5965,10 +6169,7 @@ ${report.notes.map((note) => ` <tr><td class="read">${escape(note)}</td></t
|
|
|
5965
6169
|
` : ""}
|
|
5966
6170
|
${report.picture ? `
|
|
5967
6171
|
<h2>Shape</h2>
|
|
5968
|
-
<section class="picture">
|
|
5969
|
-
<p class="read">One dot per file, sized by how many imports touch it, coloured by the community it landed in. It shows the SHAPE: how many lobes, whether they are joined, where everything converges. The names are in the tables below.</p>
|
|
5970
6172
|
${report.picture}
|
|
5971
|
-
</section>
|
|
5972
6173
|
` : ""}
|
|
5973
6174
|
<h2>What these numbers do not say</h2>
|
|
5974
6175
|
<section>
|
|
@@ -6000,7 +6201,7 @@ import { existsSync as existsSync15, mkdirSync, readFileSync as readFileSync16,
|
|
|
6000
6201
|
import { dirname as dirname2, join as join21, relative as relative4, resolve as resolve3, sep } from "node:path";
|
|
6001
6202
|
var CACHE_DIR = join21("node_modules", ".cache", "sentinel");
|
|
6002
6203
|
var CACHE_FILE = "consumer-index.json";
|
|
6003
|
-
var SHAPE =
|
|
6204
|
+
var SHAPE = 4;
|
|
6004
6205
|
var EXTENSIONS2 = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
6005
6206
|
function workspaceRootFrom(cwd) {
|
|
6006
6207
|
let dir = resolve3(cwd);
|
|
@@ -6070,6 +6271,24 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
|
|
|
6070
6271
|
}
|
|
6071
6272
|
return void 0;
|
|
6072
6273
|
}
|
|
6274
|
+
function resolveRelative(importer, specifier, workspaceRoot) {
|
|
6275
|
+
const base = join21(dirname2(importer), specifier).split(sep).join("/");
|
|
6276
|
+
if (base.startsWith("..")) return void 0;
|
|
6277
|
+
const isFile = (candidate) => {
|
|
6278
|
+
try {
|
|
6279
|
+
return statSync4(join21(workspaceRoot, candidate)).isFile();
|
|
6280
|
+
} catch {
|
|
6281
|
+
return false;
|
|
6282
|
+
}
|
|
6283
|
+
};
|
|
6284
|
+
if (isFile(base)) return base;
|
|
6285
|
+
for (const extension of EXTENSIONS2) if (isFile(base + extension)) return base + extension;
|
|
6286
|
+
for (const extension of EXTENSIONS2) {
|
|
6287
|
+
const index = `${base}/index${extension}`;
|
|
6288
|
+
if (isFile(index)) return index;
|
|
6289
|
+
}
|
|
6290
|
+
return void 0;
|
|
6291
|
+
}
|
|
6073
6292
|
function isTestFile(path2) {
|
|
6074
6293
|
return /\.(test|spec|steps|e2e-spec|integration-spec|stories)\.[cm]?[jt]sx?$|(^|\/)(tests|__tests__|test|testing|stubs|__mocks__|mocks)\//.test(
|
|
6075
6294
|
path2
|
|
@@ -6091,8 +6310,18 @@ function buildConsumerIndex(workspaceRoot, moduleRoots) {
|
|
|
6091
6310
|
if (table.size === 0) return { imported, testOnly, builtFrom: "no alias table" };
|
|
6092
6311
|
const result = spawnSync5(
|
|
6093
6312
|
"git",
|
|
6094
|
-
[
|
|
6095
|
-
|
|
6313
|
+
[
|
|
6314
|
+
"grep",
|
|
6315
|
+
"-I",
|
|
6316
|
+
"-n",
|
|
6317
|
+
"-o",
|
|
6318
|
+
"-E",
|
|
6319
|
+
"@[A-Za-z0-9_-]+/[A-Za-z0-9_./-]+|from '(\\.\\./)+[^']+'",
|
|
6320
|
+
"--",
|
|
6321
|
+
"*.ts",
|
|
6322
|
+
"*.tsx"
|
|
6323
|
+
],
|
|
6324
|
+
{ cwd: workspaceRoot, encoding: "utf8", maxBuffer: 1024 * 1024 * 1024 }
|
|
6096
6325
|
);
|
|
6097
6326
|
if (result.error || result.status !== 0 && result.status !== 1) {
|
|
6098
6327
|
return { imported, testOnly, builtFrom: "git grep failed" };
|
|
@@ -6106,8 +6335,16 @@ function buildConsumerIndex(workspaceRoot, moduleRoots) {
|
|
|
6106
6335
|
if (first < 0 || second < 0) continue;
|
|
6107
6336
|
const importer = line.slice(0, first).split(sep).join("/");
|
|
6108
6337
|
const specifier = line.slice(second + 1);
|
|
6109
|
-
|
|
6110
|
-
const
|
|
6338
|
+
let target;
|
|
6339
|
+
const relative5 = /^from '((?:\.\.\/)+[^']+)'$/.exec(specifier)?.[1];
|
|
6340
|
+
if (relative5) {
|
|
6341
|
+
target = resolveRelative(importer, relative5, workspaceRoot);
|
|
6342
|
+
} else {
|
|
6343
|
+
if (!seen.has(specifier)) {
|
|
6344
|
+
seen.set(specifier, resolveSpecifier2(specifier, table, workspaceRoot));
|
|
6345
|
+
}
|
|
6346
|
+
target = seen.get(specifier);
|
|
6347
|
+
}
|
|
6111
6348
|
if (!target) continue;
|
|
6112
6349
|
if (moduleOf(importer, moduleRoots) === moduleOf(target, moduleRoots)) continue;
|
|
6113
6350
|
imported.add(target);
|
|
@@ -6478,7 +6715,7 @@ function measuredWith(config) {
|
|
|
6478
6715
|
// never appear as `(module)`: a gauge whose scale moves per module compares nothing.
|
|
6479
6716
|
row(
|
|
6480
6717
|
"size indicators",
|
|
6481
|
-
`${Object.entries(
|
|
6718
|
+
`${Object.entries(GAUGE_THRESHOLDS).map(([rule, threshold]) => `${rule} ${threshold}`).join(", ")} (min(research, our p90, the rule's default), oxlint run outside the module)`
|
|
6482
6719
|
),
|
|
6483
6720
|
// ⚠️ The graph pass's one knob, and it has to be here: it changes both the community count and
|
|
6484
6721
|
// `Q`, so the two figures are meaningless without it. The seed is NOT printed beside it,
|
|
@@ -6773,10 +7010,13 @@ function assembleReport(ctx, config, product, tests, dead, complexity, graph, co
|
|
|
6773
7010
|
} else if (!unreadable) {
|
|
6774
7011
|
notes.push("fewer than two files, so no graph figure is reported");
|
|
6775
7012
|
}
|
|
7013
|
+
if (size && size.files > 0) {
|
|
7014
|
+
rows.push({ metric: "measuredFiles", value: size.files, unit: "files the gauge read" });
|
|
7015
|
+
}
|
|
6776
7016
|
if (size) {
|
|
6777
7017
|
const counts = /* @__PURE__ */ new Map();
|
|
6778
7018
|
for (const breach of size.breaches) counts.set(breach.rule, (counts.get(breach.rule) ?? 0) + 1);
|
|
6779
|
-
for (const [rule, threshold] of Object.entries(
|
|
7019
|
+
for (const [rule, threshold] of Object.entries(GAUGE_THRESHOLDS)) {
|
|
6780
7020
|
const count = counts.get(rule) ?? 0;
|
|
6781
7021
|
if (count === 0) continue;
|
|
6782
7022
|
rows.push({
|
|
@@ -6824,11 +7064,12 @@ async function emit(report, shape, cwd, graph, coverage) {
|
|
|
6824
7064
|
mkdirSync2(join22(cwd, REPORT_DIR), { recursive: true });
|
|
6825
7065
|
const written = [];
|
|
6826
7066
|
if (shape === "html" && graph.files > 1 && !hasUnreadableCode(coverage)) {
|
|
7067
|
+
const drawn = layOut(graph.graph, graph.communityOf);
|
|
6827
7068
|
const picture = `${TARGET}-graph.svg`;
|
|
6828
7069
|
const path2 = join22(cwd, REPORT_DIR, picture);
|
|
6829
|
-
await writeGraphPicture(
|
|
7070
|
+
await writeGraphPicture(drawn, path2);
|
|
6830
7071
|
written.push(`${REPORT_DIR}/${picture}`);
|
|
6831
|
-
report.picture =
|
|
7072
|
+
report.picture = buildExplorer(drawn, graph.communityOf).markup;
|
|
6832
7073
|
}
|
|
6833
7074
|
const name = shape === "json" ? "static-analysis.json" : "static-analysis.html";
|
|
6834
7075
|
const body = shape === "json" ? renderJson(report) : renderHtml(report);
|
|
@@ -6838,9 +7079,6 @@ async function emit(report, shape, cwd, graph, coverage) {
|
|
|
6838
7079
|
written: ${written.join(", ")}
|
|
6839
7080
|
`);
|
|
6840
7081
|
}
|
|
6841
|
-
function inlineable(svg) {
|
|
6842
|
-
return svg.replace(/^<\?xml[^>]*\?>\s*/, "").replace(/^(<svg)\s+width="[^"]*"\s+height="[^"]*"/, "$1");
|
|
6843
|
-
}
|
|
6844
7082
|
function metricsOf(report) {
|
|
6845
7083
|
return {
|
|
6846
7084
|
passes: report.passes,
|
package/dist/index.js
CHANGED
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
registerAdapters,
|
|
7
7
|
resolve,
|
|
8
8
|
setDefaultRunner
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-TL2P2X45.js";
|
|
10
10
|
import {
|
|
11
11
|
BaseAdapter
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-IFK6KGUW.js";
|
|
13
|
+
import "./chunk-JEYGPPSL.js";
|
|
14
14
|
import "./chunk-AYEOGJQR.js";
|
|
15
15
|
import "./chunk-O7REVMOC.js";
|
|
16
16
|
import "./chunk-QXFCZON7.js";
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
} from "../../../chunk-NUOQXAYR.js";
|
|
4
4
|
import {
|
|
5
5
|
installWorkspaceSetup
|
|
6
|
-
} from "../../../chunk-
|
|
6
|
+
} from "../../../chunk-SPES5MQT.js";
|
|
7
7
|
import "../../../chunk-ELZHIN6E.js";
|
|
8
8
|
import {
|
|
9
9
|
clearDeepMocks,
|
|
10
10
|
resetDeepMocks
|
|
11
11
|
} from "../../../chunk-2XLX6PFR.js";
|
|
12
|
-
import "../../../chunk-
|
|
12
|
+
import "../../../chunk-JEYGPPSL.js";
|
|
13
13
|
|
|
14
14
|
// src/roles/test/setup/jest-parity.ts
|
|
15
15
|
import { expect, vi } from "vitest";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
pinWorkspaceTimezone
|
|
3
|
-
} from "../../../chunk-
|
|
3
|
+
} from "../../../chunk-SPES5MQT.js";
|
|
4
4
|
import "../../../chunk-ELZHIN6E.js";
|
|
5
|
-
import "../../../chunk-
|
|
5
|
+
import "../../../chunk-JEYGPPSL.js";
|
|
6
6
|
|
|
7
7
|
// src/roles/test/setup/workspace-entry.ts
|
|
8
8
|
await pinWorkspaceTimezone();
|