@hublo/sentinel 1.5.0-alpha.13 → 1.5.0-alpha.14
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/dist/bin/sentinel.js +2 -2
- package/dist/{chunk-JXI6UJ5I.js → chunk-5EXXX2US.js} +418 -192
- package/dist/{chunk-KH2DCVC6.js → chunk-HO2C5D57.js} +29 -6
- package/dist/chunk-HO2C5D57.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/roles/static-analysis/config.js +1 -1
- package/package.json +12 -2
- package/dist/chunk-KH2DCVC6.js.map +0 -1
package/dist/bin/sentinel.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
palette,
|
|
16
16
|
registerAdapters,
|
|
17
17
|
resolve
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-5EXXX2US.js";
|
|
19
19
|
import {
|
|
20
20
|
resolveContext
|
|
21
21
|
} from "../chunk-KLEXJFJ6.js";
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
readOwnVersion,
|
|
27
27
|
readProjectPackageJson
|
|
28
28
|
} from "../chunk-YQRDKWSH.js";
|
|
29
|
-
import "../chunk-
|
|
29
|
+
import "../chunk-HO2C5D57.js";
|
|
30
30
|
import "../chunk-O7REVMOC.js";
|
|
31
31
|
import "../chunk-QXFCZON7.js";
|
|
32
32
|
|
|
@@ -63,11 +63,15 @@ import {
|
|
|
63
63
|
} from "./chunk-YQRDKWSH.js";
|
|
64
64
|
import {
|
|
65
65
|
REPORT_DIR,
|
|
66
|
+
SCRIPTS,
|
|
67
|
+
SHAPES,
|
|
68
|
+
TARGET,
|
|
66
69
|
defaults,
|
|
67
70
|
defineStaticAnalysis,
|
|
71
|
+
extensionsOf,
|
|
68
72
|
isPlainObject,
|
|
69
73
|
overridableSettings
|
|
70
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-HO2C5D57.js";
|
|
71
75
|
import {
|
|
72
76
|
parseJsonc
|
|
73
77
|
} from "./chunk-QXFCZON7.js";
|
|
@@ -4754,9 +4758,18 @@ function registerLint() {
|
|
|
4754
4758
|
|
|
4755
4759
|
// src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
|
|
4756
4760
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
4757
|
-
import { existsSync as existsSync14, mkdirSync, readFileSync as
|
|
4761
|
+
import { existsSync as existsSync14, mkdirSync, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4758
4762
|
import { join as join19 } from "node:path";
|
|
4759
4763
|
|
|
4764
|
+
// src/roles/static-analysis/complexity/scan.ts
|
|
4765
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
4766
|
+
import { globSync } from "tinyglobby";
|
|
4767
|
+
|
|
4768
|
+
// src/roles/static-analysis/adapters/jscpd/tool-config.ts
|
|
4769
|
+
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4770
|
+
import { tmpdir } from "node:os";
|
|
4771
|
+
import { join as join18 } from "node:path";
|
|
4772
|
+
|
|
4760
4773
|
// src/roles/static-analysis/configs/resolve.ts
|
|
4761
4774
|
import { existsSync as existsSync13 } from "node:fs";
|
|
4762
4775
|
import { join as join17 } from "node:path";
|
|
@@ -4786,6 +4799,255 @@ function adopt(exported) {
|
|
|
4786
4799
|
return defineStaticAnalysis(exported);
|
|
4787
4800
|
}
|
|
4788
4801
|
|
|
4802
|
+
// src/roles/static-analysis/adapters/jscpd/tool-config.ts
|
|
4803
|
+
var OWN_ARTEFACTS = [`**/${STATIC_CONFIG_FILE}`, `**/${REPORT_DIR}/**`];
|
|
4804
|
+
function testGlobs(config) {
|
|
4805
|
+
const relative4 = config.duplication.tests;
|
|
4806
|
+
return {
|
|
4807
|
+
ignore: relative4.map((glob) => `**/${glob}`),
|
|
4808
|
+
// A one-item brace group is avoided rather than trusted: nothing guarantees `{x}` expands.
|
|
4809
|
+
pattern: relative4.length === 1 ? `**/${relative4[0]}` : `**/{${relative4.join(",")}}`
|
|
4810
|
+
};
|
|
4811
|
+
}
|
|
4812
|
+
function exclusions(config, scope = "tests") {
|
|
4813
|
+
const tests = scope === "product" ? testGlobs(config).ignore : [];
|
|
4814
|
+
return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
|
|
4815
|
+
}
|
|
4816
|
+
function fileSettings(config, scope) {
|
|
4817
|
+
return {
|
|
4818
|
+
// The detection window. Stated rather than defaulted, because it decides what is invisible.
|
|
4819
|
+
minTokens: config.duplication.minTokens,
|
|
4820
|
+
minLines: config.duplication.minLines,
|
|
4821
|
+
// `format` is jscpd's name for what we call `languages`. Naming `typescript` alone would
|
|
4822
|
+
// leave out `.tsx`, which is a separate language to it: 300 files of 523 on front/console.
|
|
4823
|
+
format: config.languages,
|
|
4824
|
+
ignore: exclusions(config, scope),
|
|
4825
|
+
// A scan that analyses nothing otherwise exits 0 with only a warning, which reads as a pass.
|
|
4826
|
+
failOnEmpty: true,
|
|
4827
|
+
// The dead-code engine keeps its own section, and its own `minLines`: jscpd's documentation is
|
|
4828
|
+
// explicit that the top-level one is about clones and is never carried into a dead-code run.
|
|
4829
|
+
deadCode: {
|
|
4830
|
+
entry: config.entry,
|
|
4831
|
+
minConfidence: config.deadCode.minConfidence,
|
|
4832
|
+
categories: config.deadCode.categories
|
|
4833
|
+
},
|
|
4834
|
+
// The escape hatch, last so it wins, and empty unless a module asked for it.
|
|
4835
|
+
...config.tools.jscpd
|
|
4836
|
+
};
|
|
4837
|
+
}
|
|
4838
|
+
function duplicationRun(config, paths, scope) {
|
|
4839
|
+
const dir = mkdtempSync(join18(tmpdir(), "sentinel-static-"));
|
|
4840
|
+
const configPath = join18(dir, "jscpd.json");
|
|
4841
|
+
writeFileSync3(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
|
|
4842
|
+
`, "utf8");
|
|
4843
|
+
const args = [
|
|
4844
|
+
"--config",
|
|
4845
|
+
configPath,
|
|
4846
|
+
// The test pass SELECTS its files; the product pass excludes them through `ignore` above. A
|
|
4847
|
+
// flag and not a config key, because `--pattern` is the only documented way in and it takes
|
|
4848
|
+
// exactly one glob, braces expanded. Verified: 478 product files + 87 test files = the 565 of
|
|
4849
|
+
// a blended scan on `apps/front/console`.
|
|
4850
|
+
...scope === "tests" ? ["--pattern", testGlobs(config).pattern] : [],
|
|
4851
|
+
// Always json, and only json, whatever shape the caller asked for. The three shapes are
|
|
4852
|
+
// rendered by this role from the figures, not by the tool: one report lives under
|
|
4853
|
+
// `static-analysis`, assembled from several passes, so a per-pass console table from the tool
|
|
4854
|
+
// would be a fourth voice saying the same numbers differently. `--output` below is jscpd's
|
|
4855
|
+
// output DIRECTORY, not our `--output` shape.
|
|
4856
|
+
"--reporters",
|
|
4857
|
+
"json",
|
|
4858
|
+
"--output",
|
|
4859
|
+
dir,
|
|
4860
|
+
"--no-tips",
|
|
4861
|
+
...paths
|
|
4862
|
+
];
|
|
4863
|
+
return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
|
|
4864
|
+
}
|
|
4865
|
+
function deadCodeRun(config, paths) {
|
|
4866
|
+
const dir = mkdtempSync(join18(tmpdir(), "sentinel-static-"));
|
|
4867
|
+
const configPath = join18(dir, "jscpd.json");
|
|
4868
|
+
writeFileSync3(
|
|
4869
|
+
configPath,
|
|
4870
|
+
`${JSON.stringify(fileSettings(config, "dead-code"), null, 2)}
|
|
4871
|
+
`,
|
|
4872
|
+
"utf8"
|
|
4873
|
+
);
|
|
4874
|
+
return {
|
|
4875
|
+
args: [
|
|
4876
|
+
"--config",
|
|
4877
|
+
configPath,
|
|
4878
|
+
"--dead-code",
|
|
4879
|
+
"--reporters",
|
|
4880
|
+
"json",
|
|
4881
|
+
"--output",
|
|
4882
|
+
dir,
|
|
4883
|
+
"--no-tips",
|
|
4884
|
+
...paths
|
|
4885
|
+
],
|
|
4886
|
+
rawReportDir: dir,
|
|
4887
|
+
dispose: () => rmSync3(dir, { force: true, recursive: true })
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4890
|
+
var RAW_REPORT = {
|
|
4891
|
+
duplication: "jscpd-report.json",
|
|
4892
|
+
"dead-code": "basta-report.json"
|
|
4893
|
+
};
|
|
4894
|
+
|
|
4895
|
+
// src/roles/static-analysis/complexity/cyclomatic.ts
|
|
4896
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4897
|
+
var FUNCTION_NODES = /* @__PURE__ */ new Set([
|
|
4898
|
+
"FunctionDeclaration",
|
|
4899
|
+
"FunctionExpression",
|
|
4900
|
+
"ArrowFunctionExpression"
|
|
4901
|
+
]);
|
|
4902
|
+
var DECISION_NODES = /* @__PURE__ */ new Set([
|
|
4903
|
+
"IfStatement",
|
|
4904
|
+
"ForStatement",
|
|
4905
|
+
"ForInStatement",
|
|
4906
|
+
"ForOfStatement",
|
|
4907
|
+
"WhileStatement",
|
|
4908
|
+
"DoWhileStatement",
|
|
4909
|
+
"CatchClause",
|
|
4910
|
+
"ConditionalExpression"
|
|
4911
|
+
]);
|
|
4912
|
+
var DECISION_OPERATORS = /* @__PURE__ */ new Set(["&&", "||", "??"]);
|
|
4913
|
+
var DECISION_ASSIGNMENTS = /* @__PURE__ */ new Set(["&&=", "||=", "??="]);
|
|
4914
|
+
function walk2(node, visit, stopAt) {
|
|
4915
|
+
if (!node || typeof node !== "object") return;
|
|
4916
|
+
if (Array.isArray(node)) {
|
|
4917
|
+
for (const child of node) walk2(child, visit, stopAt);
|
|
4918
|
+
return;
|
|
4919
|
+
}
|
|
4920
|
+
const current = node;
|
|
4921
|
+
if (typeof current.type === "string") {
|
|
4922
|
+
if (stopAt(current)) return;
|
|
4923
|
+
visit(current);
|
|
4924
|
+
}
|
|
4925
|
+
for (const [field, value] of Object.entries(current)) {
|
|
4926
|
+
if (field === "type" || field === "start" || field === "end") continue;
|
|
4927
|
+
walk2(value, visit, stopAt);
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
function complexityOf(fn) {
|
|
4931
|
+
let count = 1;
|
|
4932
|
+
for (const parameter of Array.isArray(fn.params) ? fn.params : []) {
|
|
4933
|
+
walk2(
|
|
4934
|
+
parameter,
|
|
4935
|
+
(node) => {
|
|
4936
|
+
if (node.type === "AssignmentPattern") count += 1;
|
|
4937
|
+
},
|
|
4938
|
+
(node) => FUNCTION_NODES.has(node.type ?? "")
|
|
4939
|
+
);
|
|
4940
|
+
}
|
|
4941
|
+
const body = fn.body ?? fn;
|
|
4942
|
+
let root = true;
|
|
4943
|
+
walk2(
|
|
4944
|
+
body,
|
|
4945
|
+
(node) => {
|
|
4946
|
+
if (DECISION_NODES.has(node.type ?? "")) count += 1;
|
|
4947
|
+
if (node.type === "SwitchCase" && node.test !== null && node.test !== void 0) count += 1;
|
|
4948
|
+
if (node.type === "LogicalExpression" && DECISION_OPERATORS.has(String(node.operator ?? ""))) {
|
|
4949
|
+
count += 1;
|
|
4950
|
+
}
|
|
4951
|
+
if (node.type === "AssignmentExpression" && DECISION_ASSIGNMENTS.has(String(node.operator ?? ""))) {
|
|
4952
|
+
count += 1;
|
|
4953
|
+
}
|
|
4954
|
+
if ((node.type === "MemberExpression" || node.type === "CallExpression") && node.optional === true) {
|
|
4955
|
+
count += 1;
|
|
4956
|
+
}
|
|
4957
|
+
},
|
|
4958
|
+
(node) => {
|
|
4959
|
+
if (root) {
|
|
4960
|
+
root = false;
|
|
4961
|
+
return false;
|
|
4962
|
+
}
|
|
4963
|
+
return FUNCTION_NODES.has(node.type ?? "");
|
|
4964
|
+
}
|
|
4965
|
+
);
|
|
4966
|
+
return count;
|
|
4967
|
+
}
|
|
4968
|
+
function nameOf(fn, parent) {
|
|
4969
|
+
const own = fn.id?.name;
|
|
4970
|
+
if (own) return own;
|
|
4971
|
+
const key = parent?.id?.name ?? parent?.key?.name;
|
|
4972
|
+
if (key) return key;
|
|
4973
|
+
return "(anonymous)";
|
|
4974
|
+
}
|
|
4975
|
+
function lineAt(source, offset) {
|
|
4976
|
+
if (offset === void 0) return 1;
|
|
4977
|
+
let line = 1;
|
|
4978
|
+
for (let index = 0; index < offset && index < source.length; index += 1) {
|
|
4979
|
+
if (source[index] === "\n") line += 1;
|
|
4980
|
+
}
|
|
4981
|
+
return line;
|
|
4982
|
+
}
|
|
4983
|
+
function complexityOfFile(path, source) {
|
|
4984
|
+
const parsed = parseSync2(path, source);
|
|
4985
|
+
if (parsed.errors.length > 0) return [];
|
|
4986
|
+
const found = [];
|
|
4987
|
+
const parents = /* @__PURE__ */ new Map();
|
|
4988
|
+
walk2(
|
|
4989
|
+
parsed.program,
|
|
4990
|
+
(node) => {
|
|
4991
|
+
for (const value of Object.values(node)) {
|
|
4992
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4993
|
+
parents.set(value, node);
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
if (!FUNCTION_NODES.has(node.type ?? "")) return;
|
|
4997
|
+
found.push({
|
|
4998
|
+
name: nameOf(node, parents.get(node)),
|
|
4999
|
+
line: lineAt(source, node.start),
|
|
5000
|
+
complexity: complexityOf(node)
|
|
5001
|
+
});
|
|
5002
|
+
},
|
|
5003
|
+
() => false
|
|
5004
|
+
);
|
|
5005
|
+
return found;
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
// src/roles/static-analysis/complexity/scan.ts
|
|
5009
|
+
var RISK_BANDS = [
|
|
5010
|
+
{ metric: "complexityModerate", from: 11, to: 20, label: "moderate risk" },
|
|
5011
|
+
{ metric: "complexityHigh", from: 21, to: 50, label: "high risk" },
|
|
5012
|
+
{ metric: "complexityUntestable", from: 51, to: Infinity, label: "untestable" }
|
|
5013
|
+
];
|
|
5014
|
+
var MCCABE_REFERENCE = RISK_BANDS[0].from;
|
|
5015
|
+
function productFiles(cwd, config) {
|
|
5016
|
+
const extensions = extensionsOf(config.languages).map((extension) => extension.slice(1)).join(",");
|
|
5017
|
+
return globSync([`**/*.{${extensions}}`], {
|
|
5018
|
+
cwd,
|
|
5019
|
+
ignore: exclusions(config, "product"),
|
|
5020
|
+
dot: false
|
|
5021
|
+
});
|
|
5022
|
+
}
|
|
5023
|
+
function scanComplexity(cwd, config) {
|
|
5024
|
+
const files = productFiles(cwd, config);
|
|
5025
|
+
const all2 = [];
|
|
5026
|
+
for (const path of files) {
|
|
5027
|
+
let source;
|
|
5028
|
+
try {
|
|
5029
|
+
source = readFileSync14(`${cwd}/${path}`, "utf8");
|
|
5030
|
+
} catch {
|
|
5031
|
+
continue;
|
|
5032
|
+
}
|
|
5033
|
+
for (const fn of complexityOfFile(path, source)) all2.push({ ...fn, path });
|
|
5034
|
+
}
|
|
5035
|
+
all2.sort((left, right) => right.complexity - left.complexity);
|
|
5036
|
+
const bands = {};
|
|
5037
|
+
for (const band of RISK_BANDS) {
|
|
5038
|
+
bands[band.metric] = all2.filter(
|
|
5039
|
+
(fn) => fn.complexity >= band.from && fn.complexity <= band.to
|
|
5040
|
+
).length;
|
|
5041
|
+
}
|
|
5042
|
+
return {
|
|
5043
|
+
files: files.length,
|
|
5044
|
+
functions: all2.length,
|
|
5045
|
+
max: all2[0]?.complexity ?? 0,
|
|
5046
|
+
complex: all2.filter((fn) => fn.complexity >= MCCABE_REFERENCE),
|
|
5047
|
+
bands
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
|
|
4789
5051
|
// src/roles/static-analysis/help/metrics.ts
|
|
4790
5052
|
var metricsHelp = {
|
|
4791
5053
|
duplicatedLinesPercent: {
|
|
@@ -4795,6 +5057,34 @@ var metricsHelp = {
|
|
|
4795
5057
|
caveat: "A clone is not automatically a defect. Two screens that genuinely do the same thing are supposed to look alike, and extracting them can couple two features that have no reason to change together. The usual reading is the Rule of Three: a second occurrence is tolerable, a third is the signal to refactor. This share cannot apply that rule for you, because it counts lines, not how many times one fragment occurs. The denominator is also narrower than the module: a file shorter than the detection window cannot contain a clone, so the tool leaves it out of both counts, which on one front app is 31 files of 596, or 5%.",
|
|
4796
5058
|
source: "jscpd, over the token stream rather than the text: for JavaScript and TypeScript the tokens come from the oxc parser, which emits neither whitespace nor comments, so reindenting a block is not a clone and `--mode` measured identical on all three values. Rule of Three: Fowler, Refactoring, crediting Don Roberts."
|
|
4797
5059
|
},
|
|
5060
|
+
complexityMax: {
|
|
5061
|
+
label: "Complexity, worst function",
|
|
5062
|
+
what: "McCabe's number for the single hardest function of the module's product code.",
|
|
5063
|
+
read: "One number for the module, and the only one of the pair that names a place to go: the function itself is in the list below. Read it against the two published references, 10 and 30, rather than against another module, because a module of 200 functions and one of 2 000 are not comparable on a maximum.",
|
|
5064
|
+
caveat: "It says nothing about how MANY functions are hard, which is what the count beside it is for, and nothing about whether the hardest one is wrong: a parser or a state machine legitimately branches. It is also not what the linter refuses. The `nest` lint preset already fails a build at 30 per function, and the five other presets carry no complexity rule at all, so this figure is the only visibility a React or a tooling module has today.",
|
|
5065
|
+
source: "Computed over the oxc tree by this role, because no tool we have gives it per function: jscpd sums it per FILE, verified on fixtures where ten trivial functions score the same as one function of 10, and escomplex fails to parse 32.6% of one nest service. The counting rule is the common one, ESLint's set of decision points, held against values computed by hand in `tests/static-analysis-cyclomatic.test.ts`."
|
|
5066
|
+
},
|
|
5067
|
+
complexityModerate: {
|
|
5068
|
+
label: "Complexity 11-20, moderate risk",
|
|
5069
|
+
what: "How many functions sit in McCabe's second band, out of how many were measured.",
|
|
5070
|
+
read: "The ordinary band to find something in. A function here is readable and worth a second look when it changes often, not a defect.",
|
|
5071
|
+
caveat: 'A band is a statement about testing effort, not about correctness: McCabe proposed them as the number of independent paths a test suite would have to cover. A parser or a state machine legitimately sits here. \u26D4 Nothing in this role fails on a band, and the line this repository actually refuses is elsewhere: `complexity: ["error", 30]` per function in the `nest` lint preset, which sits inside the NEXT band, and the five other presets carry no complexity rule at all.',
|
|
5072
|
+
source: "McCabe, \"A Complexity Measure\", IEEE TSE SE-2(4), 1976, for the count and for the bands, as published by SonarSource and summarised by Sourcegraph. The counting rule is this role's own, over the oxc tree, and it was held against oxlint's `complexity` rule function by function on this package: 137 functions, 137 identical values, no disagreement either way."
|
|
5073
|
+
},
|
|
5074
|
+
complexityHigh: {
|
|
5075
|
+
label: "Complexity 21-50, high risk",
|
|
5076
|
+
what: "How many functions sit in McCabe's third band.",
|
|
5077
|
+
read: "The band worth acting on, and the one the `nest` lint preset cuts through at 30. A function here needs either a split or a very good reason, and the list below names each one with its line.",
|
|
5078
|
+
caveat: "Same reserve as the band below: it measures paths, not defects. What makes this band different is that it brackets the only complexity line this repository enforces, so a function at 25 is high risk by the published scale and perfectly acceptable to the build, while one at 35 fails a nest build and passes everywhere else.",
|
|
5079
|
+
source: "McCabe, \"A Complexity Measure\", IEEE TSE SE-2(4), 1976, for the count and for the bands, as published by SonarSource and summarised by Sourcegraph. The counting rule is this role's own, over the oxc tree, and it was held against oxlint's `complexity` rule function by function on this package: 137 functions, 137 identical values, no disagreement either way."
|
|
5080
|
+
},
|
|
5081
|
+
complexityUntestable: {
|
|
5082
|
+
label: "Complexity over 50, untestable",
|
|
5083
|
+
what: "How many functions sit in McCabe's top band, the one he called untestable.",
|
|
5084
|
+
read: "Read it as a count of places no test suite can cover exhaustively, and expect it to be zero. Any value here is the first thing to look at in the module.",
|
|
5085
|
+
caveat: "The word is McCabe's and it is about path coverage, not about whether the function works: a 60-path function can be correct and well tested in practice. What it does say is that nobody can enumerate its behaviour, which is what makes a change to it expensive.",
|
|
5086
|
+
source: "McCabe, \"A Complexity Measure\", IEEE TSE SE-2(4), 1976, for the count and for the bands, as published by SonarSource and summarised by Sourcegraph. The counting rule is this role's own, over the oxc tree, and it was held against oxlint's `complexity` rule function by function on this package: 137 functions, 137 identical values, no disagreement either way."
|
|
5087
|
+
},
|
|
4798
5088
|
entryPoints: {
|
|
4799
5089
|
label: "Entry points",
|
|
4800
5090
|
what: "How many files the reachability walk started from, against how many it scanned.",
|
|
@@ -4813,8 +5103,8 @@ var metricsHelp = {
|
|
|
4813
5103
|
label: "Unreachable files",
|
|
4814
5104
|
what: "Files no entry point reaches, each with a confidence the tool assigns.",
|
|
4815
5105
|
read: "The most actionable of the four, and the one to start from: a whole file nothing reaches is a decision, not a cleanup. Open the ones with the highest confidence first.",
|
|
4816
|
-
caveat: "Three classes hide in this count and only
|
|
4817
|
-
source: "jscpd dead-code findings of category `unused-file
|
|
5106
|
+
caveat: "Three classes hide in this count and only one of them is a deletion, so read the reason the tool gives before acting: a file nothing reaches at all, a file only a spec or a story reaches, which is alive and breaks if removed, and an entry point of a framework this role has not declared. The tool labels the second itself as `used-only-by-tests`, and that is not a corner: measured over 1 231 findings on this repository, 659 of them carry it, 53.5%. A finding with no reason given is the one to open first.",
|
|
5107
|
+
source: "jscpd dead-code findings of category `unused-file`, with the engine's own `reasons` and `message` carried through. The 53.5% was counted over every dead-code report produced on this repository; the third class was separated by hand on `apps/front/console`, where three of five findings were its TanStack bootstrap."
|
|
4818
5108
|
},
|
|
4819
5109
|
unusedExports: {
|
|
4820
5110
|
label: "Unused exports",
|
|
@@ -4864,7 +5154,6 @@ function helpFor(key) {
|
|
|
4864
5154
|
}
|
|
4865
5155
|
|
|
4866
5156
|
// src/roles/static-analysis/report.ts
|
|
4867
|
-
var SCRIPTS = { json: "static-analysis:json", html: "static-analysis:html" };
|
|
4868
5157
|
function secondSide(finding) {
|
|
4869
5158
|
if (!finding.other) return "";
|
|
4870
5159
|
const file = (at) => at.split(":")[0] ?? at;
|
|
@@ -4874,14 +5163,14 @@ function secondSide(finding) {
|
|
|
4874
5163
|
return ` <span class="unit">${label}</span> <code>${escape(shownSide)}</code>`;
|
|
4875
5164
|
}
|
|
4876
5165
|
var HTML_GROUP_LIMIT = 50;
|
|
4877
|
-
function
|
|
5166
|
+
function figure(row) {
|
|
4878
5167
|
return row.unit === "%" && typeof row.value === "number" ? row.value.toFixed(2) : String(row.value);
|
|
4879
5168
|
}
|
|
4880
5169
|
function renderConsole(report) {
|
|
4881
5170
|
const lines = [];
|
|
4882
5171
|
const width = (pick) => Math.max(...report.rows.map((row) => pick(row).length));
|
|
4883
5172
|
const label = (row) => helpFor(row.metric)?.label ?? row.metric;
|
|
4884
|
-
const value = (row) => `${
|
|
5173
|
+
const value = (row) => `${figure(row)} ${row.unit}`;
|
|
4885
5174
|
const labelWidth = width(label);
|
|
4886
5175
|
const valueWidth = width(value);
|
|
4887
5176
|
lines.push(`sentinel static-analysis: ${report.module}`);
|
|
@@ -4945,7 +5234,7 @@ function renderHtml(report) {
|
|
|
4945
5234
|
const help = helpFor(row.metric);
|
|
4946
5235
|
return ` <tr>
|
|
4947
5236
|
<th scope="row">${escape(help?.label ?? row.metric)}</th>
|
|
4948
|
-
<td class="num">${escape(
|
|
5237
|
+
<td class="num">${escape(figure(row))} <span class="unit">${escape(row.unit)}</span></td>
|
|
4949
5238
|
</tr>`;
|
|
4950
5239
|
}).join("\n");
|
|
4951
5240
|
const reserves = report.rows.map((row) => ({ row, help: helpFor(row.metric) })).filter((entry) => entry.help).map(
|
|
@@ -4971,15 +5260,18 @@ function renderHtml(report) {
|
|
|
4971
5260
|
}
|
|
4972
5261
|
const findings = [...groups.entries()].map(([metric, all2]) => {
|
|
4973
5262
|
const label = helpFor(metric)?.label ?? metric;
|
|
4974
|
-
const
|
|
4975
|
-
const
|
|
4976
|
-
const
|
|
4977
|
-
const
|
|
5263
|
+
const weight = (finding) => finding.value ?? finding.lines ?? 0;
|
|
5264
|
+
const sorted = [...all2].sort((left, right) => weight(right) - weight(left));
|
|
5265
|
+
const listed = sorted.slice(0, HTML_GROUP_LIMIT);
|
|
5266
|
+
const rest = sorted.length - listed.length;
|
|
5267
|
+
const rows2 = listed.map(
|
|
4978
5268
|
(finding) => ` <tr>
|
|
4979
5269
|
<td><code>${escape(finding.path)}</code>${secondSide(finding)}</td>
|
|
4980
5270
|
<td>${finding.name ? `<code>${escape(finding.name)}</code>` : ""}</td>
|
|
5271
|
+
<td class="num">${finding.value ?? ""}</td>
|
|
4981
5272
|
<td class="num">${finding.lines ?? ""}</td>
|
|
4982
5273
|
<td class="num">${finding.confidence ?? ""}</td>
|
|
5274
|
+
<td class="read">${finding.reason ? `<span class="reason">${escape(finding.reason)}</span>` : ""}</td>
|
|
4983
5275
|
</tr>`
|
|
4984
5276
|
).join("\n");
|
|
4985
5277
|
return `
|
|
@@ -4987,7 +5279,7 @@ function renderHtml(report) {
|
|
|
4987
5279
|
<section>
|
|
4988
5280
|
<table class="places">
|
|
4989
5281
|
<thead>
|
|
4990
|
-
<tr><th scope="col">File</th><th scope="col">Name</th><th scope="col">Lines</th><th scope="col">Confidence</th></tr>
|
|
5282
|
+
<tr><th scope="col">File</th><th scope="col">Name</th><th scope="col">Value</th><th scope="col">Lines</th><th scope="col">Confidence</th><th scope="col">Why the tool is unsure</th></tr>
|
|
4991
5283
|
</thead>
|
|
4992
5284
|
<tbody>
|
|
4993
5285
|
${rows2}
|
|
@@ -5073,6 +5365,11 @@ ${rest > 0 ? ` <p class="sub">The ${HTML_GROUP_LIMIT} longest are listed; ${r
|
|
|
5073
5365
|
tbody th { color: var(--ink); }
|
|
5074
5366
|
code { font: 0.86em ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink-soft); }
|
|
5075
5367
|
.unit { color: var(--label); font-size: 0.82em; font-weight: 400; }
|
|
5368
|
+
.reason {
|
|
5369
|
+
font: 0.74rem ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
5370
|
+
background: var(--brand-bg); color: var(--brand);
|
|
5371
|
+
padding: 0.1rem 0.4rem; border-radius: 4px; white-space: nowrap;
|
|
5372
|
+
}
|
|
5076
5373
|
.moved {
|
|
5077
5374
|
color: var(--moved); font-size: 0.78rem; font-weight: 500;
|
|
5078
5375
|
white-space: nowrap;
|
|
@@ -5167,118 +5464,20 @@ ${findings}
|
|
|
5167
5464
|
`;
|
|
5168
5465
|
}
|
|
5169
5466
|
|
|
5170
|
-
// src/roles/static-analysis/adapters/jscpd/
|
|
5171
|
-
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5172
|
-
import { tmpdir } from "node:os";
|
|
5173
|
-
import { join as join18 } from "node:path";
|
|
5174
|
-
var OWN_ARTEFACTS = [`**/${STATIC_CONFIG_FILE}`, `**/${REPORT_DIR}/**`];
|
|
5175
|
-
function testGlobs(config) {
|
|
5176
|
-
const relative4 = config.duplication.tests;
|
|
5177
|
-
return {
|
|
5178
|
-
ignore: relative4.map((glob) => `**/${glob}`),
|
|
5179
|
-
// A one-item brace group is avoided rather than trusted: nothing guarantees `{x}` expands.
|
|
5180
|
-
pattern: relative4.length === 1 ? `**/${relative4[0]}` : `**/{${relative4.join(",")}}`
|
|
5181
|
-
};
|
|
5182
|
-
}
|
|
5183
|
-
function exclusions(config, scope = "tests") {
|
|
5184
|
-
const tests = scope === "product" ? testGlobs(config).ignore : [];
|
|
5185
|
-
return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
|
|
5186
|
-
}
|
|
5187
|
-
function fileSettings(config, scope) {
|
|
5188
|
-
return {
|
|
5189
|
-
// The detection window. Stated rather than defaulted, because it decides what is invisible.
|
|
5190
|
-
minTokens: config.duplication.minTokens,
|
|
5191
|
-
minLines: config.duplication.minLines,
|
|
5192
|
-
// `format` is jscpd's name for what we call `languages`. Naming `typescript` alone would
|
|
5193
|
-
// leave out `.tsx`, which is a separate language to it: 300 files of 523 on front/console.
|
|
5194
|
-
format: config.languages,
|
|
5195
|
-
ignore: exclusions(config, scope),
|
|
5196
|
-
// A scan that analyses nothing otherwise exits 0 with only a warning, which reads as a pass.
|
|
5197
|
-
failOnEmpty: true,
|
|
5198
|
-
// The dead-code engine keeps its own section, and its own `minLines`: jscpd's documentation is
|
|
5199
|
-
// explicit that the top-level one is about clones and is never carried into a dead-code run.
|
|
5200
|
-
deadCode: {
|
|
5201
|
-
entry: config.entry,
|
|
5202
|
-
minConfidence: config.deadCode.minConfidence,
|
|
5203
|
-
categories: config.deadCode.categories
|
|
5204
|
-
},
|
|
5205
|
-
// The escape hatch, last so it wins, and empty unless a module asked for it.
|
|
5206
|
-
...config.tools.jscpd
|
|
5207
|
-
};
|
|
5208
|
-
}
|
|
5209
|
-
function duplicationRun(config, paths, scope) {
|
|
5210
|
-
const dir = mkdtempSync(join18(tmpdir(), "sentinel-static-"));
|
|
5211
|
-
const configPath = join18(dir, "jscpd.json");
|
|
5212
|
-
writeFileSync3(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
|
|
5213
|
-
`, "utf8");
|
|
5214
|
-
const args = [
|
|
5215
|
-
"--config",
|
|
5216
|
-
configPath,
|
|
5217
|
-
// The test pass SELECTS its files; the product pass excludes them through `ignore` above. A
|
|
5218
|
-
// flag and not a config key, because `--pattern` is the only documented way in and it takes
|
|
5219
|
-
// exactly one glob, braces expanded. Verified: 478 product files + 87 test files = the 565 of
|
|
5220
|
-
// a blended scan on `apps/front/console`.
|
|
5221
|
-
...scope === "tests" ? ["--pattern", testGlobs(config).pattern] : [],
|
|
5222
|
-
// Always json, and only json, whatever shape the caller asked for. The three shapes are
|
|
5223
|
-
// rendered by this role from the figures, not by the tool: one report lives under
|
|
5224
|
-
// `static-analysis`, assembled from several passes, so a per-pass console table from the tool
|
|
5225
|
-
// would be a fourth voice saying the same numbers differently. `--output` below is jscpd's
|
|
5226
|
-
// output DIRECTORY, not our `--output` shape.
|
|
5227
|
-
"--reporters",
|
|
5228
|
-
"json",
|
|
5229
|
-
"--output",
|
|
5230
|
-
dir,
|
|
5231
|
-
"--no-tips",
|
|
5232
|
-
...paths
|
|
5233
|
-
];
|
|
5234
|
-
return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
|
|
5235
|
-
}
|
|
5236
|
-
function deadCodeRun(config, paths) {
|
|
5237
|
-
const dir = mkdtempSync(join18(tmpdir(), "sentinel-static-"));
|
|
5238
|
-
const configPath = join18(dir, "jscpd.json");
|
|
5239
|
-
writeFileSync3(
|
|
5240
|
-
configPath,
|
|
5241
|
-
`${JSON.stringify(fileSettings(config, "dead-code"), null, 2)}
|
|
5242
|
-
`,
|
|
5243
|
-
"utf8"
|
|
5244
|
-
);
|
|
5245
|
-
return {
|
|
5246
|
-
args: [
|
|
5247
|
-
"--config",
|
|
5248
|
-
configPath,
|
|
5249
|
-
"--dead-code",
|
|
5250
|
-
"--reporters",
|
|
5251
|
-
"json",
|
|
5252
|
-
"--output",
|
|
5253
|
-
dir,
|
|
5254
|
-
"--no-tips",
|
|
5255
|
-
...paths
|
|
5256
|
-
],
|
|
5257
|
-
rawReportDir: dir,
|
|
5258
|
-
dispose: () => rmSync3(dir, { force: true, recursive: true })
|
|
5259
|
-
};
|
|
5260
|
-
}
|
|
5261
|
-
var RAW_REPORT = {
|
|
5262
|
-
duplication: "jscpd-report.json",
|
|
5263
|
-
"dead-code": "basta-report.json"
|
|
5264
|
-
};
|
|
5265
|
-
|
|
5266
|
-
// src/roles/static-analysis/adapters/jscpd/resolve.ts
|
|
5467
|
+
// src/roles/static-analysis/adapters/jscpd/resolve-binary.ts
|
|
5267
5468
|
function resolveJscpd(cwd) {
|
|
5268
5469
|
return resolveBin(cwd, "jscpd") ?? binFromOwnInstall("jscpd", "jscpd");
|
|
5269
5470
|
}
|
|
5270
5471
|
|
|
5271
5472
|
// src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
|
|
5272
|
-
var EXTENSIONS =
|
|
5273
|
-
var SCRIPT = "static-analysis";
|
|
5274
|
-
var SHAPES = ["console", "json", "html"];
|
|
5473
|
+
var EXTENSIONS = extensionsOf(defaults.languages);
|
|
5275
5474
|
var scripts = () => ({
|
|
5276
|
-
[
|
|
5277
|
-
[
|
|
5278
|
-
[
|
|
5475
|
+
[SCRIPTS.console]: `sentinel --run --${TARGET}`,
|
|
5476
|
+
[SCRIPTS.json]: `sentinel --run --${TARGET} --output json`,
|
|
5477
|
+
[SCRIPTS.html]: `sentinel --run --${TARGET} --output html`
|
|
5279
5478
|
});
|
|
5280
5479
|
var targets = () => ({
|
|
5281
|
-
[
|
|
5480
|
+
[TARGET]: { cache: true, inputs: ["default", `{projectRoot}/${STATIC_CONFIG_FILE}`] }
|
|
5282
5481
|
});
|
|
5283
5482
|
function configStub() {
|
|
5284
5483
|
const rows = overridableSettings();
|
|
@@ -5310,7 +5509,7 @@ ${movable.join("\n")}
|
|
|
5310
5509
|
// A list shown as a count is not copyable, and replacing one means writing back what you keep,
|
|
5311
5510
|
// so read the values in full with:
|
|
5312
5511
|
//
|
|
5313
|
-
// pnpm exec sentinel --inspect --${
|
|
5512
|
+
// pnpm exec sentinel --inspect --${TARGET} --json
|
|
5314
5513
|
//
|
|
5315
5514
|
${refused.join("\n")}
|
|
5316
5515
|
//
|
|
@@ -5327,7 +5526,7 @@ function configValue(config, path) {
|
|
|
5327
5526
|
);
|
|
5328
5527
|
}
|
|
5329
5528
|
var JscpdAdapter = class extends BaseAdapter {
|
|
5330
|
-
target =
|
|
5529
|
+
target = TARGET;
|
|
5331
5530
|
runner = "jscpd";
|
|
5332
5531
|
/** Every preset: duplication, dead code and complexity are not stack-specific. */
|
|
5333
5532
|
appliesTo(_preset) {
|
|
@@ -5352,7 +5551,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
5352
5551
|
// repeats the same line in every adopting module.
|
|
5353
5552
|
],
|
|
5354
5553
|
notes: [
|
|
5355
|
-
`added the three \`${
|
|
5554
|
+
`added the three \`${TARGET}\` scripts and ${STATIC_CONFIG_FILE}; declare this module's entry points and journeys there, everything else comes from the role`
|
|
5356
5555
|
]
|
|
5357
5556
|
};
|
|
5358
5557
|
}
|
|
@@ -5404,7 +5603,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
5404
5603
|
}
|
|
5405
5604
|
const { paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: ["-c", "--config"] });
|
|
5406
5605
|
const scanned = paths.length > 0 ? paths : ["."];
|
|
5407
|
-
const product =
|
|
5606
|
+
const product = runDuplication(jscpd, config, scanned, "product", ctx.cwd);
|
|
5408
5607
|
if (product.kind === "failed") return { ok: false, code: product.code };
|
|
5409
5608
|
if (product.kind === "empty") {
|
|
5410
5609
|
process.stderr.write(
|
|
@@ -5413,7 +5612,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
5413
5612
|
);
|
|
5414
5613
|
return { ok: true, code: 0 };
|
|
5415
5614
|
}
|
|
5416
|
-
const tests =
|
|
5615
|
+
const tests = runDuplication(jscpd, config, scanned, "tests", ctx.cwd);
|
|
5417
5616
|
if (tests.kind === "failed") return { ok: false, code: tests.code };
|
|
5418
5617
|
const dead = runDeadCode(jscpd, config, scanned, ctx.cwd);
|
|
5419
5618
|
if (dead.kind === "failed") return { ok: false, code: dead.code };
|
|
@@ -5423,7 +5622,8 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
5423
5622
|
`
|
|
5424
5623
|
);
|
|
5425
5624
|
}
|
|
5426
|
-
const
|
|
5625
|
+
const complexity = scanComplexity(ctx.cwd, config);
|
|
5626
|
+
const report = assembleReport(ctx, config, product, tests, dead, complexity);
|
|
5427
5627
|
if (emitting) emit(report, shape, ctx.cwd);
|
|
5428
5628
|
return { ok: true, code: 0, metrics: metricsOf(report) };
|
|
5429
5629
|
}
|
|
@@ -5510,7 +5710,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
5510
5710
|
function analysedNothing(stderr) {
|
|
5511
5711
|
return typeof stderr === "string" && stderr.includes("--fail-on-empty");
|
|
5512
5712
|
}
|
|
5513
|
-
function
|
|
5713
|
+
function measuredWith(config) {
|
|
5514
5714
|
const moved = (key) => config.moved.some((path) => path === key || path.startsWith(`${key}.`));
|
|
5515
5715
|
const row = (key, value) => ({ key, value, moved: moved(key) });
|
|
5516
5716
|
return [
|
|
@@ -5527,28 +5727,42 @@ function duplicationSettings(config) {
|
|
|
5527
5727
|
// two modules' unreachable share needs the confidence floor as much as the detection window.
|
|
5528
5728
|
row("deadCode.minConfidence", String(config.deadCode.minConfidence)),
|
|
5529
5729
|
row("deadCode.categories", config.deadCode.categories.join(", ")),
|
|
5530
|
-
row("entry", `${config.entry.length} patterns`)
|
|
5730
|
+
row("entry", `${config.entry.length} patterns`),
|
|
5731
|
+
// Not a setting a module can move: it is McCabe's own published figure, and the row says so
|
|
5732
|
+
// rather than letting a reader take it for one of ours.
|
|
5733
|
+
{
|
|
5734
|
+
key: "complexity bands",
|
|
5735
|
+
value: RISK_BANDS.map(
|
|
5736
|
+
(band) => `${band.from}${band.to === Infinity ? "+" : `-${band.to}`}`
|
|
5737
|
+
).join(" / "),
|
|
5738
|
+
moved: false
|
|
5739
|
+
}
|
|
5531
5740
|
];
|
|
5532
5741
|
}
|
|
5742
|
+
function runTool(jscpd, args, cwd, pass) {
|
|
5743
|
+
const result = spawnSync5(jscpd, args, { cwd, encoding: "utf8" });
|
|
5744
|
+
if (result.error) {
|
|
5745
|
+
process.stderr.write(
|
|
5746
|
+
`sentinel static-analysis(jscpd): could not run the ${pass} pass (${result.error.message})
|
|
5747
|
+
`
|
|
5748
|
+
);
|
|
5749
|
+
return { kind: "failed", code: 1 };
|
|
5750
|
+
}
|
|
5751
|
+
if (result.status !== 0) {
|
|
5752
|
+
if (analysedNothing(result.stderr)) return { kind: "empty" };
|
|
5753
|
+
process.stderr.write(
|
|
5754
|
+
`sentinel static-analysis(jscpd): the ${pass} pass exited ${result.status}
|
|
5755
|
+
${result.stderr ?? ""}`
|
|
5756
|
+
);
|
|
5757
|
+
return { kind: "failed", code: result.status ?? 1 };
|
|
5758
|
+
}
|
|
5759
|
+
return { kind: "ran" };
|
|
5760
|
+
}
|
|
5533
5761
|
function runDeadCode(jscpd, config, paths, cwd) {
|
|
5534
5762
|
const { args, rawReportDir, dispose } = deadCodeRun(config, paths);
|
|
5535
5763
|
try {
|
|
5536
|
-
const
|
|
5537
|
-
if (
|
|
5538
|
-
process.stderr.write(
|
|
5539
|
-
`sentinel static-analysis(jscpd): could not run the dead-code pass (${result.error.message})
|
|
5540
|
-
`
|
|
5541
|
-
);
|
|
5542
|
-
return { kind: "failed", code: 1 };
|
|
5543
|
-
}
|
|
5544
|
-
if (result.status !== 0) {
|
|
5545
|
-
if (analysedNothing(result.stderr)) return { kind: "empty" };
|
|
5546
|
-
process.stderr.write(
|
|
5547
|
-
`sentinel static-analysis(jscpd): the dead-code pass exited ${result.status}
|
|
5548
|
-
${result.stderr ?? ""}`
|
|
5549
|
-
);
|
|
5550
|
-
return { kind: "failed", code: result.status ?? 1 };
|
|
5551
|
-
}
|
|
5764
|
+
const ran = runTool(jscpd, args, cwd, "dead-code");
|
|
5765
|
+
if (ran.kind !== "ran") return ran;
|
|
5552
5766
|
const path = join19(rawReportDir, RAW_REPORT["dead-code"]);
|
|
5553
5767
|
if (!existsSync14(path)) {
|
|
5554
5768
|
process.stderr.write(
|
|
@@ -5557,54 +5771,35 @@ ${result.stderr ?? ""}`
|
|
|
5557
5771
|
);
|
|
5558
5772
|
return { kind: "failed", code: 1 };
|
|
5559
5773
|
}
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
5563
|
-
} catch {
|
|
5564
|
-
process.stderr.write(`sentinel static-analysis(jscpd): the dead-code report does not parse
|
|
5565
|
-
`);
|
|
5566
|
-
return { kind: "failed", code: 1 };
|
|
5567
|
-
}
|
|
5568
|
-
const statistics = parsed.statistics;
|
|
5774
|
+
const parsed = safeParseDeadCode(readFileSync15(path, "utf8"));
|
|
5775
|
+
const statistics = parsed?.statistics;
|
|
5569
5776
|
if (!statistics) {
|
|
5570
|
-
process.stderr.write(
|
|
5571
|
-
`)
|
|
5777
|
+
process.stderr.write(
|
|
5778
|
+
`sentinel static-analysis(jscpd): the dead-code report has no readable figures
|
|
5779
|
+
`
|
|
5780
|
+
);
|
|
5572
5781
|
return { kind: "failed", code: 1 };
|
|
5573
5782
|
}
|
|
5574
5783
|
if ((statistics.entryPoints ?? 0) === 0) {
|
|
5575
5784
|
return { kind: "no-entry-point", files: statistics.files ?? 0 };
|
|
5576
5785
|
}
|
|
5577
5786
|
const byCategory = {};
|
|
5578
|
-
for (const finding of parsed
|
|
5787
|
+
for (const finding of parsed?.findings ?? []) {
|
|
5579
5788
|
const category = finding.category ?? "unknown";
|
|
5580
5789
|
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
5581
5790
|
}
|
|
5582
|
-
return { kind: "figures", statistics, byCategory, findings: parsed
|
|
5791
|
+
return { kind: "figures", statistics, byCategory, findings: parsed?.findings ?? [] };
|
|
5583
5792
|
} finally {
|
|
5584
5793
|
dispose();
|
|
5585
5794
|
}
|
|
5586
5795
|
}
|
|
5587
|
-
function
|
|
5796
|
+
function runDuplication(jscpd, config, paths, scope, cwd) {
|
|
5588
5797
|
const { args, rawReportDir, dispose } = duplicationRun(config, paths, scope);
|
|
5589
5798
|
try {
|
|
5590
|
-
const
|
|
5591
|
-
if (
|
|
5592
|
-
process.stderr.write(
|
|
5593
|
-
`sentinel static-analysis(jscpd): could not run the ${scope} pass (${result.error.message})
|
|
5594
|
-
`
|
|
5595
|
-
);
|
|
5596
|
-
return { kind: "failed", code: 1 };
|
|
5597
|
-
}
|
|
5598
|
-
if (result.status !== 0) {
|
|
5599
|
-
if (analysedNothing(result.stderr)) return { kind: "empty" };
|
|
5600
|
-
process.stderr.write(
|
|
5601
|
-
`sentinel static-analysis(jscpd): the ${scope} pass exited ${result.status}
|
|
5602
|
-
${result.stderr ?? ""}`
|
|
5603
|
-
);
|
|
5604
|
-
return { kind: "failed", code: result.status ?? 1 };
|
|
5605
|
-
}
|
|
5799
|
+
const ran = runTool(jscpd, args, cwd, scope);
|
|
5800
|
+
if (ran.kind !== "ran") return ran;
|
|
5606
5801
|
const path = join19(rawReportDir, RAW_REPORT.duplication);
|
|
5607
|
-
const parsed = existsSync14(path) ? safeParse(
|
|
5802
|
+
const parsed = existsSync14(path) ? safeParse(readFileSync15(path, "utf8")) : void 0;
|
|
5608
5803
|
const total = parsed?.statistics?.total;
|
|
5609
5804
|
if (!total) {
|
|
5610
5805
|
process.stderr.write(
|
|
@@ -5630,7 +5825,14 @@ function safeParse(text) {
|
|
|
5630
5825
|
return void 0;
|
|
5631
5826
|
}
|
|
5632
5827
|
}
|
|
5633
|
-
function
|
|
5828
|
+
function safeParseDeadCode(text) {
|
|
5829
|
+
try {
|
|
5830
|
+
return JSON.parse(text);
|
|
5831
|
+
} catch {
|
|
5832
|
+
return void 0;
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
function placesToLook(product, tests, dead, complexity) {
|
|
5634
5836
|
const at = (side) => `${side?.name ?? "(unknown)"}${side?.start === void 0 ? "" : `:${side.start}`}`;
|
|
5635
5837
|
const clones = (outcome, metric) => outcome.kind === "figures" ? outcome.duplicates.map((entry) => ({
|
|
5636
5838
|
metric,
|
|
@@ -5647,16 +5849,25 @@ function placesToLook(product, tests, dead) {
|
|
|
5647
5849
|
return [
|
|
5648
5850
|
...clones(product, "clones"),
|
|
5649
5851
|
...clones(tests, "clonesTests"),
|
|
5852
|
+
// Each one under the band it falls in, so it sits beside the caveat that applies to it.
|
|
5853
|
+
...complexity.complex.map((fn) => ({
|
|
5854
|
+
metric: RISK_BANDS.find((band) => fn.complexity >= band.from && fn.complexity <= band.to)?.metric ?? "complexityMax",
|
|
5855
|
+
path: `${fn.path}:${fn.line}`,
|
|
5856
|
+
name: fn.name,
|
|
5857
|
+
value: fn.complexity
|
|
5858
|
+
})),
|
|
5650
5859
|
...dead.kind === "figures" ? dead.findings.map((finding) => ({
|
|
5651
5860
|
metric: deadMetric[finding.category ?? ""] ?? "deadLinesPercent",
|
|
5652
5861
|
path: finding.path ?? "(unknown)",
|
|
5653
5862
|
...finding.name ? { name: finding.name } : {},
|
|
5654
5863
|
...finding.lines === void 0 ? {} : { lines: finding.lines },
|
|
5655
|
-
...finding.confidence === void 0 ? {} : { confidence: finding.confidence }
|
|
5864
|
+
...finding.confidence === void 0 ? {} : { confidence: finding.confidence },
|
|
5865
|
+
...finding.reasons?.length ? { reason: finding.reasons.join(", ") } : {},
|
|
5866
|
+
...finding.message ? { message: finding.message } : {}
|
|
5656
5867
|
})) : []
|
|
5657
5868
|
];
|
|
5658
5869
|
}
|
|
5659
|
-
function
|
|
5870
|
+
function assembleReport(ctx, config, product, tests, dead, complexity) {
|
|
5660
5871
|
const share = (total) => Number((total.percentage ?? 0).toFixed(2));
|
|
5661
5872
|
const rows = [
|
|
5662
5873
|
{ metric: "duplicatedLinesPercent", value: share(product.total), unit: "%" },
|
|
@@ -5695,6 +5906,20 @@ function assemble(ctx, config, product, tests, dead) {
|
|
|
5695
5906
|
{ metric: "unusedImports", value: dead.byCategory["unused-import"] ?? 0, unit: "imports" }
|
|
5696
5907
|
);
|
|
5697
5908
|
}
|
|
5909
|
+
if (complexity.functions > 0) {
|
|
5910
|
+
rows.push({ metric: "complexityMax", value: complexity.max, unit: "max per function" });
|
|
5911
|
+
for (const band of RISK_BANDS) {
|
|
5912
|
+
const count = complexity.bands[band.metric] ?? 0;
|
|
5913
|
+
if (count === 0) continue;
|
|
5914
|
+
rows.push({
|
|
5915
|
+
metric: band.metric,
|
|
5916
|
+
value: count,
|
|
5917
|
+
unit: `of ${complexity.functions} functions`
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5920
|
+
} else {
|
|
5921
|
+
notes.push("no function to measure, so no complexity figure is reported");
|
|
5922
|
+
}
|
|
5698
5923
|
const byLanguage = Object.fromEntries(
|
|
5699
5924
|
Object.entries(product.formats).map(([language, stats]) => [
|
|
5700
5925
|
language,
|
|
@@ -5710,12 +5935,13 @@ function assemble(ctx, config, product, tests, dead) {
|
|
|
5710
5935
|
passes: [
|
|
5711
5936
|
"duplication",
|
|
5712
5937
|
...tests.kind === "figures" ? ["duplication (tests)"] : [],
|
|
5713
|
-
...dead.kind === "figures" ? ["dead code"] : []
|
|
5938
|
+
...dead.kind === "figures" ? ["dead code"] : [],
|
|
5939
|
+
...complexity.functions > 0 ? ["complexity"] : []
|
|
5714
5940
|
],
|
|
5715
5941
|
rows,
|
|
5716
5942
|
notes,
|
|
5717
|
-
settings:
|
|
5718
|
-
findings: placesToLook(product, tests, dead),
|
|
5943
|
+
settings: measuredWith(config),
|
|
5944
|
+
findings: placesToLook(product, tests, dead, complexity),
|
|
5719
5945
|
byLanguage
|
|
5720
5946
|
};
|
|
5721
5947
|
}
|
|
@@ -5762,7 +5988,7 @@ function registerTest() {
|
|
|
5762
5988
|
|
|
5763
5989
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5764
5990
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5765
|
-
import { existsSync as existsSync15, readFileSync as
|
|
5991
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
|
|
5766
5992
|
import { createRequire } from "node:module";
|
|
5767
5993
|
import { join as join21 } from "node:path";
|
|
5768
5994
|
|
|
@@ -5949,7 +6175,7 @@ function readTsconfigAdoption(cwd) {
|
|
|
5949
6175
|
}
|
|
5950
6176
|
|
|
5951
6177
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
5952
|
-
import { readFileSync as
|
|
6178
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
5953
6179
|
import { join as join20 } from "node:path";
|
|
5954
6180
|
|
|
5955
6181
|
// src/roles/typescript/typecheck-script.ts
|
|
@@ -6043,7 +6269,7 @@ function planAdoption(context) {
|
|
|
6043
6269
|
};
|
|
6044
6270
|
}
|
|
6045
6271
|
const existing = parseJsonc(
|
|
6046
|
-
|
|
6272
|
+
readFileSync16(join20(context.cwd, target.path), "utf8"),
|
|
6047
6273
|
target.path
|
|
6048
6274
|
);
|
|
6049
6275
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -6181,7 +6407,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6181
6407
|
let chain;
|
|
6182
6408
|
try {
|
|
6183
6409
|
const parsed = parseJsonc(
|
|
6184
|
-
|
|
6410
|
+
readFileSync17(join21(cwd, target.path), "utf8"),
|
|
6185
6411
|
target.path
|
|
6186
6412
|
);
|
|
6187
6413
|
chain = parsed.extends;
|
|
@@ -6290,7 +6516,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6290
6516
|
referencedProjects(cwd, config) {
|
|
6291
6517
|
try {
|
|
6292
6518
|
const parsed = parseJsonc(
|
|
6293
|
-
|
|
6519
|
+
readFileSync17(join21(cwd, config), "utf8"),
|
|
6294
6520
|
config
|
|
6295
6521
|
);
|
|
6296
6522
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -6402,11 +6628,11 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6402
6628
|
* errors in the output mean the rule must be on.
|
|
6403
6629
|
*/
|
|
6404
6630
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
6405
|
-
const
|
|
6406
|
-
if (
|
|
6631
|
+
const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
6632
|
+
if (shown.status === 0 && shown.stdout) {
|
|
6407
6633
|
try {
|
|
6408
6634
|
const co = parseJsonc(
|
|
6409
|
-
|
|
6635
|
+
shown.stdout,
|
|
6410
6636
|
"tsconfig(--showConfig)"
|
|
6411
6637
|
).compilerOptions;
|
|
6412
6638
|
if (co) return co.noImplicitAny ?? co.strict === true;
|
|
@@ -6512,7 +6738,7 @@ function replaceLines(current, replacements) {
|
|
|
6512
6738
|
}
|
|
6513
6739
|
|
|
6514
6740
|
// src/core/apply-plan.ts
|
|
6515
|
-
import { existsSync as existsSync16, readFileSync as
|
|
6741
|
+
import { existsSync as existsSync16, readFileSync as readFileSync18, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6516
6742
|
import { resolve as resolve3, sep } from "node:path";
|
|
6517
6743
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6518
6744
|
function resolveWithinRoot(cwd, relativePath) {
|
|
@@ -6524,7 +6750,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
6524
6750
|
return absolutePath;
|
|
6525
6751
|
}
|
|
6526
6752
|
function readIfExists(absolutePath) {
|
|
6527
|
-
return existsSync16(absolutePath) ?
|
|
6753
|
+
return existsSync16(absolutePath) ? readFileSync18(absolutePath, "utf8") : void 0;
|
|
6528
6754
|
}
|
|
6529
6755
|
function* leaves(value, prefix = []) {
|
|
6530
6756
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6642,7 +6868,7 @@ function applyPlan(cwd, plan3) {
|
|
|
6642
6868
|
}
|
|
6643
6869
|
|
|
6644
6870
|
// src/core/config/preset-evidence.ts
|
|
6645
|
-
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as
|
|
6871
|
+
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync19 } from "node:fs";
|
|
6646
6872
|
import { join as join22 } from "node:path";
|
|
6647
6873
|
var PATH_SIGNALS = [
|
|
6648
6874
|
{
|
|
@@ -6682,7 +6908,7 @@ function sourceImports(cwd, pattern) {
|
|
|
6682
6908
|
}
|
|
6683
6909
|
if (!SOURCE_FILE.test(entry.name)) continue;
|
|
6684
6910
|
try {
|
|
6685
|
-
if (pattern.test(
|
|
6911
|
+
if (pattern.test(readFileSync19(join22(dir, entry.name), "utf8"))) return true;
|
|
6686
6912
|
} catch {
|
|
6687
6913
|
continue;
|
|
6688
6914
|
}
|
|
@@ -6695,7 +6921,7 @@ function dependencyNames(cwd) {
|
|
|
6695
6921
|
if (!existsSync17(path)) return [];
|
|
6696
6922
|
try {
|
|
6697
6923
|
const manifest = parseJsonc(
|
|
6698
|
-
|
|
6924
|
+
readFileSync19(path, "utf8"),
|
|
6699
6925
|
path
|
|
6700
6926
|
);
|
|
6701
6927
|
return [
|
|
@@ -6718,7 +6944,7 @@ function reactTsconfigEvidence(cwd) {
|
|
|
6718
6944
|
}
|
|
6719
6945
|
for (const name of entries) {
|
|
6720
6946
|
try {
|
|
6721
|
-
const config = parseJsonc(
|
|
6947
|
+
const config = parseJsonc(readFileSync19(join22(cwd, name), "utf8"), name);
|
|
6722
6948
|
if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
|
|
6723
6949
|
const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
|
|
6724
6950
|
if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
|
|
@@ -15,17 +15,17 @@ var shared_default = {
|
|
|
15
15
|
],
|
|
16
16
|
ignore: [
|
|
17
17
|
"**/node_modules/**",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
18
|
+
"dist/**",
|
|
19
|
+
"build/**",
|
|
20
|
+
"coverage/**",
|
|
21
|
+
".next/**",
|
|
22
22
|
"**/__mocks__/**",
|
|
23
23
|
"**/mocks/**",
|
|
24
24
|
"**/_mock_/**",
|
|
25
25
|
"**/**Mock**/**",
|
|
26
26
|
"**/__fixtures__/**",
|
|
27
27
|
"**/fixtures/**",
|
|
28
|
-
"
|
|
28
|
+
"public/**",
|
|
29
29
|
"**/.husky/**",
|
|
30
30
|
"**/.vscode/**",
|
|
31
31
|
"**/.idea/**",
|
|
@@ -56,6 +56,25 @@ var shared_default = {
|
|
|
56
56
|
|
|
57
57
|
// src/roles/static-analysis/preset-data.ts
|
|
58
58
|
var REPORT_DIR = "static-analysis-report";
|
|
59
|
+
var TARGET = "static-analysis";
|
|
60
|
+
var SHAPES = ["console", "json", "html"];
|
|
61
|
+
var SCRIPTS = {
|
|
62
|
+
console: TARGET,
|
|
63
|
+
json: `${TARGET}:json`,
|
|
64
|
+
html: `${TARGET}:html`
|
|
65
|
+
};
|
|
66
|
+
var EXTENSIONS_BY_LANGUAGE = {
|
|
67
|
+
javascript: [".js", ".mjs", ".cjs"],
|
|
68
|
+
typescript: [".ts"],
|
|
69
|
+
jsx: [".jsx"],
|
|
70
|
+
tsx: [".tsx"],
|
|
71
|
+
css: [".css"],
|
|
72
|
+
scss: [".scss"]
|
|
73
|
+
};
|
|
74
|
+
function extensionsOf(languages) {
|
|
75
|
+
return [...new Set(languages.flatMap((language) => EXTENSIONS_BY_LANGUAGE[language] ?? []))];
|
|
76
|
+
}
|
|
77
|
+
var MAPPED_LANGUAGES = Object.keys(EXTENSIONS_BY_LANGUAGE);
|
|
59
78
|
var defaults = shared_default;
|
|
60
79
|
var DECLARED = {
|
|
61
80
|
entry: "added to the patterns the role ships, never replacing them",
|
|
@@ -135,8 +154,12 @@ function defineStaticAnalysis(input = {}) {
|
|
|
135
154
|
export {
|
|
136
155
|
isPlainObject,
|
|
137
156
|
REPORT_DIR,
|
|
157
|
+
TARGET,
|
|
158
|
+
SHAPES,
|
|
159
|
+
SCRIPTS,
|
|
160
|
+
extensionsOf,
|
|
138
161
|
defaults,
|
|
139
162
|
overridableSettings,
|
|
140
163
|
defineStaticAnalysis
|
|
141
164
|
};
|
|
142
|
-
//# sourceMappingURL=chunk-
|
|
165
|
+
//# sourceMappingURL=chunk-HO2C5D57.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/deep-merge.ts","../src/roles/static-analysis/presets/shared.json","../src/roles/static-analysis/preset-data.ts","../src/roles/static-analysis/configs/define.ts"],"sourcesContent":["/**\n * Is this a plain JSON object?\n *\n * The file is named for a `deepMerge` that no longer exists. It was written when the engine\n * merged JSON in memory and wrote the result; that approach was replaced by editing the\n * document IN PLACE with jsonc-parser, so a project's comments, key order and formatting\n * survive a `merge-json` op. Nothing has called `deepMerge` since, except its own recursion,\n * so it went.\n *\n * What remains is the predicate the in-place merge still needs, to decide whether a value is\n * a branch to walk into or a leaf to set.\n */\n\n/** True for a mergeable plain object (not null, not an array). */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n","{\n \"entry\": [\n \"**/*.integration-spec.ts\",\n \"**/*.steps.ts\",\n \"src/client.tsx\",\n \"src/start.ts\",\n \"src/router.tsx\",\n \"src/RouterContext.tsx\"\n ],\n \"ignore\": [\n \"**/node_modules/**\",\n \"dist/**\",\n \"build/**\",\n \"coverage/**\",\n \".next/**\",\n \"**/__mocks__/**\",\n \"**/mocks/**\",\n \"**/_mock_/**\",\n \"**/**Mock**/**\",\n \"**/__fixtures__/**\",\n \"**/fixtures/**\",\n \"public/**\",\n \"**/.husky/**\",\n \"**/.vscode/**\",\n \"**/.idea/**\",\n \"**/.github/**\",\n \"**/*.d.ts\",\n \"**/*.gen.ts\",\n \"**/*.generated.ts\"\n ],\n \"languages\": [\"javascript\", \"typescript\", \"jsx\", \"tsx\", \"css\", \"scss\"],\n \"journeys\": {},\n \"duplication\": {\n \"minTokens\": 50,\n \"minLines\": 5,\n \"kinds\": [\"exact\"],\n \"tests\": [\n \"*.{test,spec,steps,e2e-spec,integration-spec}.{ts,tsx,js,jsx}\",\n \"{tests,__tests__}/**/*.{ts,tsx,js,jsx}\"\n ]\n },\n \"deadCode\": {\n \"minConfidence\": 60,\n \"categories\": [\"unused-file\", \"unused-export\", \"unused-symbol\", \"unused-import\"]\n },\n \"tools\": {\n \"jscpd\": {}\n }\n}\n","/**\n * The static analysis preset: the role's settings, as data.\n *\n * One shared layer and no per-stack preset yet, because nothing measured so far varies by stack:\n * duplication, dead code and complexity are the same questions in Nest and in React. The split\n * that is already foreseen is `LCOM4`, which is meaningless on a functional front end and real on\n * nest's 16 568 classes; when it arrives it becomes `nest.json` beside this layer, which is why\n * the values live here as JSON rather than inline in the code.\n *\n * ## Why these values are the role's and not the module's\n *\n * A team declares FACTS about its own domain (which journeys exist, which files are entry points);\n * it does not declare the values a number is judged by. Two files with the same measurement\n * getting different verdicts in two modules would make every figure incomparable, and the first\n * thing this role has to be able to say is that a number means the same thing everywhere.\n *\n * That is a default, not a wall: a module CAN move any of them (see `configs/define.ts`), and when\n * it does the report prints `(module)` next to the value. The seam exists from the first commit\n * because retrofitting one later means rewriting every module's file.\n *\n * ⛔ There are no thresholds here, and their absence is a decision rather than an omission.\n *\n * A threshold was written first, at 5% of duplicated lines, and then removed. Measured over all 228\n * nx projects: 5% flags 65 of the 201 measurable modules, a third of the repository, and moving the\n * figure until the count felt manageable is not calibration, it is fitting the instrument to the\n * answer. Calibration is legitimate when a bench is polluted and the noise is removed, or when the\n * reference frame changes; neither applies to a share of duplicated lines.\n *\n * And there is nothing to calibrate AGAINST: of the sources in `docs/static-analysis-study.md`\n * section 5, NDepend, Lattix, ArchUnit, CodeScene and Arcan, none publishes a threshold for\n * duplicated lines. One invented here would travel as if it were published.\n *\n * So the role reports the measurement. What makes a single figure readable is the distribution it\n * sits in, which only a workspace-wide run can know, so that belongs to the sweep and to the\n * migration page, not to a module's own report. The first defensible warning will not be an absolute\n * number either: a module that got WORSE than its own previous measurement is a signal that needs no\n * invented bar, which is what a baseline gives, and that was already deferred for its own reasons.\n *\n * ## Where each value comes from, so nobody has to guess later\n *\n * - `entry` is the single most consequential line in the file. Measured on\n * `apps/nest/microservices/network` with jscpd 5.4.0: with nothing declared the dead-code pass\n * reports **33.4%** of lines dead (79 657 lines, 232 files, 13 entry points); with these two\n * patterns declared it reports **1.0%** (2 462 lines, 207 entry points). The cause is a naming\n * convention and not a bug: our integration tests are `foo.integration-spec.ts` with a HYPHEN,\n * so they do not end in `.spec.ts` and the tool's own test-file detection never sees them, and\n * the 115 `.steps.ts` BDD definitions are tests by no convention it knows. A module ADDS to\n * this list rather than replacing it, precisely so no team can lose these two by declaring a\n * third.\n *\n * ⭐ The four `src/` files are the FRONT bootstrap, and they are the most consequential thing\n * measured on this pass. The tool recognises around fifty frameworks and TanStack Start is not\n * one of them, so nothing in the repository imports these files: the framework finds them by\n * convention at build time. Measured without them declared, `apps/front/console` reports\n * `src/client.tsx`, `src/router.tsx` and `src/start.ts` among its 5 dead files, which are its\n * hydration entry, its router factory and its Start config. `host-admin` reports `client.tsx`\n * too and `career` reports `router.tsx` and `RouterContext.tsx`. Three of console's five\n * findings were its own bootstrap, and the first team to see that stops trusting the tool on\n * the spot. They exist in 2 to 3 front apps each.\n *\n * ⛔ `scripts/**` is deliberately NOT here, although 8 modules have one and\n * `apps/nest/microservices/contract` reports two `scripts/e2e-*.ts` as dead. No `package.json`\n * script invokes them, so whether they run at all is a fact only the team holds. Declaring them\n * would hide a finding that may well be true, and that is the opposite of this role's job.\n *\n * - `languages` is this role's name for what the tool calls `format`, a word that already names\n * another sentinel role and means something else there. Stating it has a measured cost either\n * way: on `apps/front/console`, naming `typescript` alone scans 300 of 523 files because `.tsx`\n * is a SEPARATE language to the tool, naming all four scans 522 (the one left out being\n * `styles.css`), and naming nothing scans every one of its 224 languages, json and markdown\n * included. Stylesheets are in deliberately: measured on `libs/front` they duplicate at 9.64%\n * against 3.50% for the code, a signal that only survives because the report is broken down per\n * language.\n *\n * - `duplication.minTokens` and `minLines` are the detection window, and the window decides what\n * is INVISIBLE: a fragment shorter than it is not reported at all, so a reader comparing two\n * runs has to know both values. They are the tool's own defaults, written out rather than\n * inherited for that reason.\n *\n * - `duplication.kinds` is `exact` alone, and that is a correctness constraint rather than\n * caution. Measured: with a second kind enabled, `duplicatedLines` adds up overlapping clones.\n * Two 9-line files reported 13 duplicated lines out of 18, 72%, because an `exact` clone of\n * lines 1-4 and a `similar` clone of lines 1-9 containing it were both counted; the real extent\n * is 9 of 18, 50%. The tool's own answer is to cap the display at 100%, which does not make a\n * threshold meaningful. Opening another kind therefore requires computing the share ourselves\n * from the UNION of the line ranges.\n *\n * - `duplication.tests` is the glob that splits the pass in two, and the split is the finding that\n * forced it. Measured across 8 modules of the three families, **71% of all duplicated lines are\n * between two test files**: `libs/nest/auth` reports 35.77% and its product code is at 5.96%,\n * `apps/front/host-admin` 7.91% against 3.59%. A single blended figure is therefore mostly a\n * measure of spec duplication wearing the name of the module's duplication, and the number\n * travels while that nuance does not. So the role reports the two side by side, the way it\n * already refuses to blend languages, and neither is hidden: a module whose specs carry 93% of\n * its clones is saying something about its specs that excluding them would erase.\n *\n * ⚠️ The two passes PARTITION the files and therefore lose a clone that spans them: a fragment\n * shared between a test and a product file has no partner in either scan. Measured on the same\n * 8 modules, that is 42 entries of 5117, 0.8%, 650 lines. Stated in the caveat rather than\n * silently accepted, and it is the price of the split.\n *\n * Two patterns and not one, because a suffix convention does not cover a DIRECTORY convention,\n * and the directory one is not a quirk: 635 files across 26 modules live under `tests/` or\n * `__tests__/` without a name this role's suffixes recognise, `apps/nest/microservices/recruitment`\n * alone holding 160. They are step definitions, in-memory doubles and stubs, which is test-side\n * code by any reading. A role default rather than 26 identical overrides.\n *\n * ⚠️ The patterns are RELATIVE here, and the adapter prefixes and composes them, because the\n * tool's `--pattern` takes exactly one glob. Verified on four modules that the two passes\n * partition exactly: console 479 + 87 = 566, contract 399 + 151 = 550, backends-for-frontends/admin\n * 2062 + 773 = 2835, recruitment 936 + 621 = 1557.\n *\n * ⚠️ And a trap that cost a wrong table before it was caught: these globs contain commas, so they\n * survive the config FILE and are destroyed by the `-i` command-line flag, which splits on\n * commas and cuts `{test,spec}` in half. The adapter writes a file, which is one more reason it\n * does.\n *\n * - `deadCode.minConfidence` is the tool's own default of 60. It scores every finding and names\n * what lowered it (a file calling `eval`, an unrecognised decorator, a name appearing in a\n * string literal, a file in the scan that did not parse). Measured on sentinel: 60 yields 20\n * files, 10 exports and 22 symbols; 90 yields 18 files; 100 yields nothing.\n *\n * - `deadCode.categories` leaves out `unused-member` deliberately: without type information\n * `x.render()` could reach any `render` in the project, which the tool itself calls its least\n * certain rule.\n *\n * - `ignore` excludes generated, vendored and fixture code, which duplicates by nature and buries\n * the clones a developer can act on. It is load-bearing rather than tidy: measured on this\n * package, scanning the module root with no ignore list reports 18 186 files and 32 153 clones,\n * **31 969** of them touching `node_modules`; with the list, 408 files and zero.\n *\n * - `tools.jscpd` is passed to the tool verbatim, and it is **sentinel's own** escape hatch, not a\n * module's: `defineStaticAnalysis` excludes it from what a module can override. Everything above\n * uses OUR key names, translated on the way out, so that replacing the tool one day does not\n * oblige a single module to edit its config file, and a module only ever knows the ROLE. This\n * block is where that guarantee stops, which is why it stays on this side and why it is empty:\n * the first thing to put in it would be an option a single preset needs, not a workaround.\n */\nimport sharedData from './presets/shared.json'\n\n/**\n * Where the json and html reports are written, relative to the module.\n *\n * Named for the role rather than left on the tool's default `report/`, and added to `.gitignore`\n * by `--init`. Both halves matter: the format role once shipped a `lint/` directory that nothing\n * ignored, and every adopting module grew an untracked directory nobody could explain.\n */\nexport const REPORT_DIR = 'static-analysis-report'\n\n/** The target this role serves, which is also the npm script a module runs and the nx target. */\nexport const TARGET = 'static-analysis'\n\n/** The output shapes `--run` can produce. `console` writes nothing to disk. */\nexport const SHAPES = ['console', 'json', 'html'] as const\n\n/**\n * The npm scripts a module gets, one per shape.\n *\n * Here rather than in the adapter because the REPORT needs them too, to tell a reader of the\n * console summary where the detail is, and two spellings of one script name is drift nobody\n * notices until a developer types the one that does not exist.\n */\nexport const SCRIPTS = {\n console: TARGET,\n json: `${TARGET}:json`,\n html: `${TARGET}:html`,\n} as const\n\n/**\n * What each language the role scans is called on disk.\n *\n * ⚠️ Two lists that MUST agree, and they were maintained separately: the role declares the\n * languages it asks the tool to scan, and the adapter declared the extensions it looks for when\n * deciding whether a module has anything to measure at all. A module that narrowed `languages`\n * would still be probed for the extensions it had removed, and a language added to the preset\n * would never reach the probe. Derived from one source now, with a test that every language maps.\n */\nconst EXTENSIONS_BY_LANGUAGE: Record<string, string[]> = {\n javascript: ['.js', '.mjs', '.cjs'],\n typescript: ['.ts'],\n jsx: ['.jsx'],\n tsx: ['.tsx'],\n css: ['.css'],\n scss: ['.scss'],\n}\n\n/** The file extensions of the languages given, for deciding whether a module has anything to read. */\nexport function extensionsOf(languages: readonly string[]): string[] {\n return [...new Set(languages.flatMap((language) => EXTENSIONS_BY_LANGUAGE[language] ?? []))]\n}\n\n/** Every language the mapping knows, so a test can hold it against the preset. */\nexport const MAPPED_LANGUAGES = Object.keys(EXTENSIONS_BY_LANGUAGE)\n\nexport type StaticDefaults = {\n entry: string[]\n ignore: string[]\n languages: string[]\n journeys: Record<string, string[]>\n duplication: { minTokens: number; minLines: number; kinds: string[]; tests: string[] }\n deadCode: { minConfidence: number; categories: string[] }\n tools: { jscpd: Record<string, unknown> }\n}\n\n/** The role's settings. Read-only here; `defineStaticAnalysis` resolves a module against them. */\nexport const defaults: StaticDefaults = sharedData as StaticDefaults\n\n/**\n * Keys a module declares DIRECTLY rather than through `overrides`, and how each one combines.\n *\n * They are apart because the verb differs: these two are added to and merged, so a team cannot\n * lose what the role ships by declaring its own. Everything else replaces.\n */\nconst DECLARED: Record<string, string> = {\n entry: 'added to the patterns the role ships, never replacing them',\n journeys: \"merged by name with the role's\",\n}\n\n/**\n * ⛔ Not a module's to set, and the catalogue says so rather than leaving it absent: a key that is\n * simply missing from a list reads as an oversight, and someone will try it. Passing it is dropped\n * with a message, see `configs/define.ts`.\n */\nconst NOT_YOURS: Record<string, string> = {\n tools: \"sentinel's own passthrough to the tool underneath; a module never names the vendor\",\n}\n\n/** A value as a reader compares it: short lists in full, long ones as a count. */\nfunction render(value: unknown): string {\n if (Array.isArray(value)) {\n const joined = value.join(', ')\n return value.length <= 6 && joined.length <= 72 ? joined : `${value.length} patterns`\n }\n if (value && typeof value === 'object') return `${Object.keys(value).length} entries`\n return String(value)\n}\n\n/** One documented key: its path, the role's value, and the note a reader needs. */\nexport interface OverridableSetting {\n /** The dotted path a module writes, e.g. `duplication.minTokens`. */\n key: string\n /** The role's current value, rendered. */\n value: string\n /** How it combines, or why it is not available. */\n note: string\n /** Where it goes: directly in the config, inside `overrides`, or nowhere. */\n where: 'declared' | 'overrides' | 'refused'\n}\n\n/**\n * Every setting a module can touch, DERIVED from the values rather than written beside them.\n *\n * Héla's ask, and the reason it is generated: a hand-written list is a second copy of the preset,\n * and the first setting somebody adds makes it a lie that nobody notices, because a stale comment\n * fails no test. Here a new key appears in the next module's config file on its own, with its real\n * value, and `tests/role-layout.test.ts` refuses a key that reaches none of the three groups.\n *\n * It is a snapshot: `--init` leaves an EXISTING config alone, by design, so a module that adopted\n * earlier keeps the catalogue of that day. The report is the live answer, since it prints the value\n * every figure was produced with.\n */\nexport function overridableSettings(from: StaticDefaults = defaults): OverridableSetting[] {\n const rows: OverridableSetting[] = []\n\n for (const [key, value] of Object.entries(from)) {\n if (key in NOT_YOURS) {\n rows.push({ key, value: render(value), note: NOT_YOURS[key] ?? '', where: 'refused' })\n continue\n }\n if (key in DECLARED) {\n rows.push({ key, value: render(value), note: DECLARED[key] ?? '', where: 'declared' })\n continue\n }\n // A group is documented by its leaves, because `duplication` whole is not what anyone writes.\n const leaves =\n value && typeof value === 'object' && !Array.isArray(value)\n ? Object.entries(value).map(([leaf, own]) => ({ key: `${key}.${leaf}`, own }))\n : [{ key, own: value }]\n for (const leaf of leaves) {\n rows.push({ key: leaf.key, value: render(leaf.own), note: 'replaced', where: 'overrides' })\n }\n }\n\n return rows\n}\n","/**\n * `defineStaticAnalysis`: what a module imports to declare its own static analysis settings.\n *\n * ```js\n * // static-analysis.config.mjs\n * import { defineStaticAnalysis } from '@hublo/sentinel/static-analysis'\n *\n * export default defineStaticAnalysis({\n * root: import.meta.dirname,\n * entry: ['**\\/*.steps.ts'],\n * journeys: { planning: ['src/planning/**'] },\n * })\n * ```\n *\n * ## Why a function and not `{ ...baseConfig, ... }`\n *\n * A spread is SHALLOW, so a module writing `deadCode: { minConfidence: 80 }` would replace the\n * whole block and silently lose `categories`. And the flattening happens in the module's file,\n * before this package ever sees the object, so nothing here could tell \"set one key, keep the\n * rest\" from \"set one key, drop the rest\". A function receives the module's INTENT, the keys it\n * actually passed, and merges them here. Second reason: the defaults are never copied into a\n * module, so changing one reaches every module at the next release instead of leaving 108 stale\n * copies behind.\n *\n * Same shape as `reactTestConfig` in the test role, for the same reasons.\n *\n * ## Two semantics, one obvious and one deliberate\n *\n * `entry` and `journeys` are FACTS a team declares, and they ADD to what this role ships: no\n * module can lose `**\\/*.integration-spec.ts` by declaring a third pattern, which matters\n * because that one pattern is the difference between 33.4% and 1.0% of dead code on a real\n * service (see `defaults.ts`).\n *\n * `overrides` REPLACES, including those lists. It is the seam for the day a team has a real\n * reason, it is not documented anywhere a developer would meet it, and every path it sets is\n * recorded in `moved` so the report can print `(module)` next to the value. Without that trace\n * two modules stop being comparable and nobody knows. It reaches every setting EXCEPT the vendor\n * passthrough, for the reason stated on the field itself.\n */\nimport { isPlainObject } from '../../../shared/deep-merge.js'\nimport { defaults, type StaticDefaults } from '../preset-data.js'\n\ntype DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }\n\n/** What a module's config file passes. Everything is optional, including the file itself. */\nexport interface StaticAnalysisInput {\n /** The module's own directory, normally `import.meta.dirname`. */\n root?: string\n /** Files that run although nothing imports them. ADDED to the shipped patterns. */\n entry?: string[]\n /** Named journeys, as `{ name: [globs] }`. MERGED by name with any shipped ones. */\n journeys?: Record<string, string[]>\n /**\n * The seam. Replaces whatever it names, and every path it sets lands in `moved`.\n *\n * ⛔ `tools` is excluded, and the reason is the whole point of translating the vocabulary: a\n * module knows the ROLE, not the tool underneath it. A key named after the vendor would mean\n * nothing to the person reading their own config file, and showing it would invite them to learn\n * a name we are working to keep out of their world. The passthrough still exists, on sentinel's\n * side, for a preset that needs a vendor option; a module that genuinely needs one gets a key in\n * OUR vocabulary, which is the right friction because it goes through a review instead of\n * spreading silently.\n */\n overrides?: DeepPartial<Omit<StaticDefaults, 'tools'>>\n}\n\nexport interface ResolvedStaticConfig extends StaticDefaults {\n /** The module's directory, or null when its config file did not say. */\n root: string | null\n /**\n * Dotted paths the module set through `overrides`, so the report can mark them. Sorted, so\n * two runs on the same config produce the same report.\n */\n moved: string[]\n}\n\n/**\n * Merge `patch` into `base`, recursively, patch winning.\n *\n * Not the `deepMerge` this repo once had: that one existed for WRITING a module's json and was\n * replaced by an in-place jsonc edit, which is why only `isPlainObject` survived. This is the\n * read side, which has no file to preserve and only has to resolve two objects into one.\n *\n * Arrays are replaced, never concatenated. Additive behaviour is a decision about a specific\n * key, which `defineStaticAnalysis` makes explicitly for `entry`, rather than a property of the\n * merge: a merge that silently concatenates cannot express \"replace this list\", and a team\n * reaching for `overrides` is asking for exactly that.\n */\nfunction mergeInto<T>(base: T, patch: unknown): T {\n if (!isPlainObject(patch)) return base\n if (!isPlainObject(base)) return patch as T\n const out: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n out[key] = isPlainObject(value) ? mergeInto(out[key], value) : value\n }\n return out as T\n}\n\n/** Every leaf path in `patch`, dotted, so a reader can be told what the module decided. */\nfunction leafPaths(patch: unknown, prefix = ''): string[] {\n if (!isPlainObject(patch)) return prefix ? [prefix] : []\n return Object.entries(patch).flatMap(([key, value]) =>\n leafPaths(value, prefix ? `${prefix}.${key}` : key),\n )\n}\n\n/** The same list with duplicates dropped, keeping the first occurrence's order. */\nfunction unique(values: string[]): string[] {\n return [...new Set(values)]\n}\n\n/**\n * Drop the vendor passthrough from what a module sent.\n *\n * ⚠️ Enforced here and not by the type, because the type cannot: a module's config file is `.mjs`,\n * so nothing type-checks it, and `Omit` on the input would be documentation that compiles away. A\n * `tools` key written there is dropped and NAMED on stderr rather than ignored, so a developer who\n * found the key somewhere learns why it did nothing instead of concluding the role is broken.\n */\nfunction withoutVendorKeys(overrides: unknown): unknown {\n if (!isPlainObject(overrides) || !('tools' in overrides)) return overrides\n const { tools: _vendor, ...rest } = overrides\n process.stderr.write(\n 'sentinel static-analysis: ignoring `overrides.tools` from this module. The tool underneath ' +\n \"the role is not part of a module's surface; if a setting is missing, it needs a key of \" +\n 'its own.\\n',\n )\n return rest\n}\n\n/** Resolve a module's declaration against this role's defaults. */\nexport function defineStaticAnalysis(input: StaticAnalysisInput = {}): ResolvedStaticConfig {\n const declared: StaticDefaults = {\n ...defaults,\n entry: unique([...defaults.entry, ...(input.entry ?? [])]),\n journeys: { ...defaults.journeys, ...(input.journeys ?? {}) },\n }\n const overrides = withoutVendorKeys(input.overrides)\n\n return {\n ...mergeInto(declared, overrides),\n root: input.root ?? null,\n moved: leafPaths(overrides).sort(),\n }\n}\n"],"mappings":";AAcO,SAAS,cAAc,OAAkD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;AChBA;AAAA,EACE,OAAS;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,QAAU;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,WAAa,CAAC,cAAc,cAAc,OAAO,OAAO,OAAO,MAAM;AAAA,EACrE,UAAY,CAAC;AAAA,EACb,aAAe;AAAA,IACb,WAAa;AAAA,IACb,UAAY;AAAA,IACZ,OAAS,CAAC,OAAO;AAAA,IACjB,OAAS;AAAA,MACP;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,UAAY;AAAA,IACV,eAAiB;AAAA,IACjB,YAAc,CAAC,eAAe,iBAAiB,iBAAiB,eAAe;AAAA,EACjF;AAAA,EACA,OAAS;AAAA,IACP,OAAS,CAAC;AAAA,EACZ;AACF;;;ACmGO,IAAM,aAAa;AAGnB,IAAM,SAAS;AAGf,IAAM,SAAS,CAAC,WAAW,QAAQ,MAAM;AASzC,IAAM,UAAU;AAAA,EACrB,SAAS;AAAA,EACT,MAAM,GAAG,MAAM;AAAA,EACf,MAAM,GAAG,MAAM;AACjB;AAWA,IAAM,yBAAmD;AAAA,EACvD,YAAY,CAAC,OAAO,QAAQ,MAAM;AAAA,EAClC,YAAY,CAAC,KAAK;AAAA,EAClB,KAAK,CAAC,MAAM;AAAA,EACZ,KAAK,CAAC,MAAM;AAAA,EACZ,KAAK,CAAC,MAAM;AAAA,EACZ,MAAM,CAAC,OAAO;AAChB;AAGO,SAAS,aAAa,WAAwC;AACnE,SAAO,CAAC,GAAG,IAAI,IAAI,UAAU,QAAQ,CAAC,aAAa,uBAAuB,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC;AAC7F;AAGO,IAAM,mBAAmB,OAAO,KAAK,sBAAsB;AAa3D,IAAM,WAA2B;AAQxC,IAAM,WAAmC;AAAA,EACvC,OAAO;AAAA,EACP,UAAU;AACZ;AAOA,IAAM,YAAoC;AAAA,EACxC,OAAO;AACT;AAGA,SAAS,OAAO,OAAwB;AACtC,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAM,SAAS,MAAM,KAAK,IAAI;AAC9B,WAAO,MAAM,UAAU,KAAK,OAAO,UAAU,KAAK,SAAS,GAAG,MAAM,MAAM;AAAA,EAC5E;AACA,MAAI,SAAS,OAAO,UAAU,SAAU,QAAO,GAAG,OAAO,KAAK,KAAK,EAAE,MAAM;AAC3E,SAAO,OAAO,KAAK;AACrB;AA0BO,SAAS,oBAAoB,OAAuB,UAAgC;AACzF,QAAM,OAA6B,CAAC;AAEpC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,QAAI,OAAO,WAAW;AACpB,WAAK,KAAK,EAAE,KAAK,OAAO,OAAO,KAAK,GAAG,MAAM,UAAU,GAAG,KAAK,IAAI,OAAO,UAAU,CAAC;AACrF;AAAA,IACF;AACA,QAAI,OAAO,UAAU;AACnB,WAAK,KAAK,EAAE,KAAK,OAAO,OAAO,KAAK,GAAG,MAAM,SAAS,GAAG,KAAK,IAAI,OAAO,WAAW,CAAC;AACrF;AAAA,IACF;AAEA,UAAM,SACJ,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IACtD,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,MAAM,GAAG,OAAO,EAAE,KAAK,GAAG,GAAG,IAAI,IAAI,IAAI,IAAI,EAAE,IAC3E,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC;AAC1B,eAAW,QAAQ,QAAQ;AACzB,WAAK,KAAK,EAAE,KAAK,KAAK,KAAK,OAAO,OAAO,KAAK,GAAG,GAAG,MAAM,YAAY,OAAO,YAAY,CAAC;AAAA,IAC5F;AAAA,EACF;AAEA,SAAO;AACT;;;ACpMA,SAAS,UAAa,MAAS,OAAmB;AAChD,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,MAAI,CAAC,cAAc,IAAI,EAAG,QAAO;AACjC,QAAM,MAA+B,EAAE,GAAG,KAAK;AAC/C,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,GAAG,IAAI,cAAc,KAAK,IAAI,UAAU,IAAI,GAAG,GAAG,KAAK,IAAI;AAAA,EACjE;AACA,SAAO;AACT;AAGA,SAAS,UAAU,OAAgB,SAAS,IAAc;AACxD,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO,SAAS,CAAC,MAAM,IAAI,CAAC;AACvD,SAAO,OAAO,QAAQ,KAAK,EAAE;AAAA,IAAQ,CAAC,CAAC,KAAK,KAAK,MAC/C,UAAU,OAAO,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK,GAAG;AAAA,EACpD;AACF;AAGA,SAAS,OAAO,QAA4B;AAC1C,SAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAC5B;AAUA,SAAS,kBAAkB,WAA6B;AACtD,MAAI,CAAC,cAAc,SAAS,KAAK,EAAE,WAAW,WAAY,QAAO;AACjE,QAAM,EAAE,OAAO,SAAS,GAAG,KAAK,IAAI;AACpC,UAAQ,OAAO;AAAA,IACb;AAAA,EAGF;AACA,SAAO;AACT;AAGO,SAAS,qBAAqB,QAA6B,CAAC,GAAyB;AAC1F,QAAM,WAA2B;AAAA,IAC/B,GAAG;AAAA,IACH,OAAO,OAAO,CAAC,GAAG,SAAS,OAAO,GAAI,MAAM,SAAS,CAAC,CAAE,CAAC;AAAA,IACzD,UAAU,EAAE,GAAG,SAAS,UAAU,GAAI,MAAM,YAAY,CAAC,EAAG;AAAA,EAC9D;AACA,QAAM,YAAY,kBAAkB,MAAM,SAAS;AAEnD,SAAO;AAAA,IACL,GAAG,UAAU,UAAU,SAAS;AAAA,IAChC,MAAM,MAAM,QAAQ;AAAA,IACpB,OAAO,UAAU,SAAS,EAAE,KAAK;AAAA,EACnC;AACF;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
registerAdapters,
|
|
7
7
|
resolve,
|
|
8
8
|
setDefaultRunner
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-5EXXX2US.js";
|
|
10
10
|
import {
|
|
11
11
|
BaseAdapter
|
|
12
12
|
} from "./chunk-KLEXJFJ6.js";
|
|
13
13
|
import "./chunk-YQRDKWSH.js";
|
|
14
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-HO2C5D57.js";
|
|
15
15
|
import "./chunk-O7REVMOC.js";
|
|
16
16
|
import "./chunk-QXFCZON7.js";
|
|
17
17
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.5.0-alpha.
|
|
3
|
+
"version": "1.5.0-alpha.14",
|
|
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",
|
|
@@ -160,6 +160,13 @@
|
|
|
160
160
|
},
|
|
161
161
|
"format:fix": {
|
|
162
162
|
"cache": false
|
|
163
|
+
},
|
|
164
|
+
"static-analysis": {
|
|
165
|
+
"cache": true,
|
|
166
|
+
"inputs": [
|
|
167
|
+
"default",
|
|
168
|
+
"{projectRoot}/static-analysis.config.mjs"
|
|
169
|
+
]
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
172
|
},
|
|
@@ -190,6 +197,9 @@
|
|
|
190
197
|
"docs:traces": "pnpm build && bash scripts/gen-traces.sh",
|
|
191
198
|
"docs:traces:check": "pnpm build && bash scripts/gen-traces.sh --check",
|
|
192
199
|
"format:fix": "node dist/bin/sentinel.js --run --format --fix",
|
|
193
|
-
"bench": "node scripts/bench.mjs"
|
|
200
|
+
"bench": "node scripts/bench.mjs",
|
|
201
|
+
"static-analysis": "sentinel --run --static-analysis",
|
|
202
|
+
"static-analysis:json": "sentinel --run --static-analysis --output json",
|
|
203
|
+
"static-analysis:html": "sentinel --run --static-analysis --output html"
|
|
194
204
|
}
|
|
195
205
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shared/deep-merge.ts","../src/roles/static-analysis/presets/shared.json","../src/roles/static-analysis/preset-data.ts","../src/roles/static-analysis/configs/define.ts"],"sourcesContent":["/**\n * Is this a plain JSON object?\n *\n * The file is named for a `deepMerge` that no longer exists. It was written when the engine\n * merged JSON in memory and wrote the result; that approach was replaced by editing the\n * document IN PLACE with jsonc-parser, so a project's comments, key order and formatting\n * survive a `merge-json` op. Nothing has called `deepMerge` since, except its own recursion,\n * so it went.\n *\n * What remains is the predicate the in-place merge still needs, to decide whether a value is\n * a branch to walk into or a leaf to set.\n */\n\n/** True for a mergeable plain object (not null, not an array). */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n","{\n \"entry\": [\n \"**/*.integration-spec.ts\",\n \"**/*.steps.ts\",\n \"src/client.tsx\",\n \"src/start.ts\",\n \"src/router.tsx\",\n \"src/RouterContext.tsx\"\n ],\n \"ignore\": [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/coverage/**\",\n \"**/.next/**\",\n \"**/__mocks__/**\",\n \"**/mocks/**\",\n \"**/_mock_/**\",\n \"**/**Mock**/**\",\n \"**/__fixtures__/**\",\n \"**/fixtures/**\",\n \"**/public/**\",\n \"**/.husky/**\",\n \"**/.vscode/**\",\n \"**/.idea/**\",\n \"**/.github/**\",\n \"**/*.d.ts\",\n \"**/*.gen.ts\",\n \"**/*.generated.ts\"\n ],\n \"languages\": [\"javascript\", \"typescript\", \"jsx\", \"tsx\", \"css\", \"scss\"],\n \"journeys\": {},\n \"duplication\": {\n \"minTokens\": 50,\n \"minLines\": 5,\n \"kinds\": [\"exact\"],\n \"tests\": [\n \"*.{test,spec,steps,e2e-spec,integration-spec}.{ts,tsx,js,jsx}\",\n \"{tests,__tests__}/**/*.{ts,tsx,js,jsx}\"\n ]\n },\n \"deadCode\": {\n \"minConfidence\": 60,\n \"categories\": [\"unused-file\", \"unused-export\", \"unused-symbol\", \"unused-import\"]\n },\n \"tools\": {\n \"jscpd\": {}\n }\n}\n","/**\n * The static analysis preset: the role's settings, as data.\n *\n * One shared layer and no per-stack preset yet, because nothing measured so far varies by stack:\n * duplication, dead code and complexity are the same questions in Nest and in React. The split\n * that is already foreseen is `LCOM4`, which is meaningless on a functional front end and real on\n * nest's 16 568 classes; when it arrives it becomes `nest.json` beside this layer, which is why\n * the values live here as JSON rather than inline in the code.\n *\n * ## Why these values are the role's and not the module's\n *\n * A team declares FACTS about its own domain (which journeys exist, which files are entry points);\n * it does not declare the values a number is judged by. Two files with the same measurement\n * getting different verdicts in two modules would make every figure incomparable, and the first\n * thing this role has to be able to say is that a number means the same thing everywhere.\n *\n * That is a default, not a wall: a module CAN move any of them (see `configs/define.ts`), and when\n * it does the report prints `(module)` next to the value. The seam exists from the first commit\n * because retrofitting one later means rewriting every module's file.\n *\n * ⛔ There are no thresholds here, and their absence is a decision rather than an omission.\n *\n * A threshold was written first, at 5% of duplicated lines, and then removed. Measured over all 228\n * nx projects: 5% flags 65 of the 201 measurable modules, a third of the repository, and moving the\n * figure until the count felt manageable is not calibration, it is fitting the instrument to the\n * answer. Calibration is legitimate when a bench is polluted and the noise is removed, or when the\n * reference frame changes; neither applies to a share of duplicated lines.\n *\n * And there is nothing to calibrate AGAINST: of the sources in `docs/static-analysis-study.md`\n * section 5, NDepend, Lattix, ArchUnit, CodeScene and Arcan, none publishes a threshold for\n * duplicated lines. One invented here would travel as if it were published.\n *\n * So the role reports the measurement. What makes a single figure readable is the distribution it\n * sits in, which only a workspace-wide run can know, so that belongs to the sweep and to the\n * migration page, not to a module's own report. The first defensible warning will not be an absolute\n * number either: a module that got WORSE than its own previous measurement is a signal that needs no\n * invented bar, which is what a baseline gives, and that was already deferred for its own reasons.\n *\n * ## Where each value comes from, so nobody has to guess later\n *\n * - `entry` is the single most consequential line in the file. Measured on\n * `apps/nest/microservices/network` with jscpd 5.4.0: with nothing declared the dead-code pass\n * reports **33.4%** of lines dead (79 657 lines, 232 files, 13 entry points); with these two\n * patterns declared it reports **1.0%** (2 462 lines, 207 entry points). The cause is a naming\n * convention and not a bug: our integration tests are `foo.integration-spec.ts` with a HYPHEN,\n * so they do not end in `.spec.ts` and the tool's own test-file detection never sees them, and\n * the 115 `.steps.ts` BDD definitions are tests by no convention it knows. A module ADDS to\n * this list rather than replacing it, precisely so no team can lose these two by declaring a\n * third.\n *\n * ⭐ The four `src/` files are the FRONT bootstrap, and they are the most consequential thing\n * measured on this pass. The tool recognises around fifty frameworks and TanStack Start is not\n * one of them, so nothing in the repository imports these files: the framework finds them by\n * convention at build time. Measured without them declared, `apps/front/console` reports\n * `src/client.tsx`, `src/router.tsx` and `src/start.ts` among its 5 dead files, which are its\n * hydration entry, its router factory and its Start config. `host-admin` reports `client.tsx`\n * too and `career` reports `router.tsx` and `RouterContext.tsx`. Three of console's five\n * findings were its own bootstrap, and the first team to see that stops trusting the tool on\n * the spot. They exist in 2 to 3 front apps each.\n *\n * ⛔ `scripts/**` is deliberately NOT here, although 8 modules have one and\n * `apps/nest/microservices/contract` reports two `scripts/e2e-*.ts` as dead. No `package.json`\n * script invokes them, so whether they run at all is a fact only the team holds. Declaring them\n * would hide a finding that may well be true, and that is the opposite of this role's job.\n *\n * - `languages` is this role's name for what the tool calls `format`, a word that already names\n * another sentinel role and means something else there. Stating it has a measured cost either\n * way: on `apps/front/console`, naming `typescript` alone scans 300 of 523 files because `.tsx`\n * is a SEPARATE language to the tool, naming all four scans 522 (the one left out being\n * `styles.css`), and naming nothing scans every one of its 224 languages, json and markdown\n * included. Stylesheets are in deliberately: measured on `libs/front` they duplicate at 9.64%\n * against 3.50% for the code, a signal that only survives because the report is broken down per\n * language.\n *\n * - `duplication.minTokens` and `minLines` are the detection window, and the window decides what\n * is INVISIBLE: a fragment shorter than it is not reported at all, so a reader comparing two\n * runs has to know both values. They are the tool's own defaults, written out rather than\n * inherited for that reason.\n *\n * - `duplication.kinds` is `exact` alone, and that is a correctness constraint rather than\n * caution. Measured: with a second kind enabled, `duplicatedLines` adds up overlapping clones.\n * Two 9-line files reported 13 duplicated lines out of 18, 72%, because an `exact` clone of\n * lines 1-4 and a `similar` clone of lines 1-9 containing it were both counted; the real extent\n * is 9 of 18, 50%. The tool's own answer is to cap the display at 100%, which does not make a\n * threshold meaningful. Opening another kind therefore requires computing the share ourselves\n * from the UNION of the line ranges.\n *\n * - `duplication.tests` is the glob that splits the pass in two, and the split is the finding that\n * forced it. Measured across 8 modules of the three families, **71% of all duplicated lines are\n * between two test files**: `libs/nest/auth` reports 35.77% and its product code is at 5.96%,\n * `apps/front/host-admin` 7.91% against 3.59%. A single blended figure is therefore mostly a\n * measure of spec duplication wearing the name of the module's duplication, and the number\n * travels while that nuance does not. So the role reports the two side by side, the way it\n * already refuses to blend languages, and neither is hidden: a module whose specs carry 93% of\n * its clones is saying something about its specs that excluding them would erase.\n *\n * ⚠️ The two passes PARTITION the files and therefore lose a clone that spans them: a fragment\n * shared between a test and a product file has no partner in either scan. Measured on the same\n * 8 modules, that is 42 entries of 5117, 0.8%, 650 lines. Stated in the caveat rather than\n * silently accepted, and it is the price of the split.\n *\n * Two patterns and not one, because a suffix convention does not cover a DIRECTORY convention,\n * and the directory one is not a quirk: 635 files across 26 modules live under `tests/` or\n * `__tests__/` without a name this role's suffixes recognise, `apps/nest/microservices/recruitment`\n * alone holding 160. They are step definitions, in-memory doubles and stubs, which is test-side\n * code by any reading. A role default rather than 26 identical overrides.\n *\n * ⚠️ The patterns are RELATIVE here, and the adapter prefixes and composes them, because the\n * tool's `--pattern` takes exactly one glob. Verified on four modules that the two passes\n * partition exactly: console 479 + 87 = 566, contract 399 + 151 = 550, backends-for-frontends/admin\n * 2062 + 773 = 2835, recruitment 936 + 621 = 1557.\n *\n * ⚠️ And a trap that cost a wrong table before it was caught: these globs contain commas, so they\n * survive the config FILE and are destroyed by the `-i` command-line flag, which splits on\n * commas and cuts `{test,spec}` in half. The adapter writes a file, which is one more reason it\n * does.\n *\n * - `deadCode.minConfidence` is the tool's own default of 60. It scores every finding and names\n * what lowered it (a file calling `eval`, an unrecognised decorator, a name appearing in a\n * string literal, a file in the scan that did not parse). Measured on sentinel: 60 yields 20\n * files, 10 exports and 22 symbols; 90 yields 18 files; 100 yields nothing.\n *\n * - `deadCode.categories` leaves out `unused-member` deliberately: without type information\n * `x.render()` could reach any `render` in the project, which the tool itself calls its least\n * certain rule.\n *\n * - `ignore` excludes generated, vendored and fixture code, which duplicates by nature and buries\n * the clones a developer can act on. It is load-bearing rather than tidy: measured on this\n * package, scanning the module root with no ignore list reports 18 186 files and 32 153 clones,\n * **31 969** of them touching `node_modules`; with the list, 408 files and zero.\n *\n * - `tools.jscpd` is passed to the tool verbatim, and it is **sentinel's own** escape hatch, not a\n * module's: `defineStaticAnalysis` excludes it from what a module can override. Everything above\n * uses OUR key names, translated on the way out, so that replacing the tool one day does not\n * oblige a single module to edit its config file, and a module only ever knows the ROLE. This\n * block is where that guarantee stops, which is why it stays on this side and why it is empty:\n * the first thing to put in it would be an option a single preset needs, not a workaround.\n */\nimport sharedData from './presets/shared.json'\n\n/**\n * Where the json and html reports are written, relative to the module.\n *\n * Named for the role rather than left on the tool's default `report/`, and added to `.gitignore`\n * by `--init`. Both halves matter: the format role once shipped a `lint/` directory that nothing\n * ignored, and every adopting module grew an untracked directory nobody could explain.\n */\nexport const REPORT_DIR = 'static-analysis-report'\n\nexport type StaticDefaults = {\n entry: string[]\n ignore: string[]\n languages: string[]\n journeys: Record<string, string[]>\n duplication: { minTokens: number; minLines: number; kinds: string[]; tests: string[] }\n deadCode: { minConfidence: number; categories: string[] }\n tools: { jscpd: Record<string, unknown> }\n}\n\n/** The role's settings. Read-only here; `defineStaticAnalysis` resolves a module against them. */\nexport const defaults: StaticDefaults = sharedData as StaticDefaults\n\n/**\n * Keys a module declares DIRECTLY rather than through `overrides`, and how each one combines.\n *\n * They are apart because the verb differs: these two are added to and merged, so a team cannot\n * lose what the role ships by declaring its own. Everything else replaces.\n */\nconst DECLARED: Record<string, string> = {\n entry: 'added to the patterns the role ships, never replacing them',\n journeys: \"merged by name with the role's\",\n}\n\n/**\n * ⛔ Not a module's to set, and the catalogue says so rather than leaving it absent: a key that is\n * simply missing from a list reads as an oversight, and someone will try it. Passing it is dropped\n * with a message, see `configs/define.ts`.\n */\nconst NOT_YOURS: Record<string, string> = {\n tools: \"sentinel's own passthrough to the tool underneath; a module never names the vendor\",\n}\n\n/** A value as a reader compares it: short lists in full, long ones as a count. */\nfunction render(value: unknown): string {\n if (Array.isArray(value)) {\n const joined = value.join(', ')\n return value.length <= 6 && joined.length <= 72 ? joined : `${value.length} patterns`\n }\n if (value && typeof value === 'object') return `${Object.keys(value).length} entries`\n return String(value)\n}\n\n/** One documented key: its path, the role's value, and the note a reader needs. */\nexport interface OverridableSetting {\n /** The dotted path a module writes, e.g. `duplication.minTokens`. */\n key: string\n /** The role's current value, rendered. */\n value: string\n /** How it combines, or why it is not available. */\n note: string\n /** Where it goes: directly in the config, inside `overrides`, or nowhere. */\n where: 'declared' | 'overrides' | 'refused'\n}\n\n/**\n * Every setting a module can touch, DERIVED from the values rather than written beside them.\n *\n * Héla's ask, and the reason it is generated: a hand-written list is a second copy of the preset,\n * and the first setting somebody adds makes it a lie that nobody notices, because a stale comment\n * fails no test. Here a new key appears in the next module's config file on its own, with its real\n * value, and `tests/role-layout.test.ts` refuses a key that reaches none of the three groups.\n *\n * It is a snapshot: `--init` leaves an EXISTING config alone, by design, so a module that adopted\n * earlier keeps the catalogue of that day. The report is the live answer, since it prints the value\n * every figure was produced with.\n */\nexport function overridableSettings(from: StaticDefaults = defaults): OverridableSetting[] {\n const rows: OverridableSetting[] = []\n\n for (const [key, value] of Object.entries(from)) {\n if (key in NOT_YOURS) {\n rows.push({ key, value: render(value), note: NOT_YOURS[key] ?? '', where: 'refused' })\n continue\n }\n if (key in DECLARED) {\n rows.push({ key, value: render(value), note: DECLARED[key] ?? '', where: 'declared' })\n continue\n }\n // A group is documented by its leaves, because `duplication` whole is not what anyone writes.\n const leaves =\n value && typeof value === 'object' && !Array.isArray(value)\n ? Object.entries(value).map(([leaf, own]) => ({ key: `${key}.${leaf}`, own }))\n : [{ key, own: value }]\n for (const leaf of leaves) {\n rows.push({ key: leaf.key, value: render(leaf.own), note: 'replaced', where: 'overrides' })\n }\n }\n\n return rows\n}\n","/**\n * `defineStaticAnalysis`: what a module imports to declare its own static analysis settings.\n *\n * ```js\n * // static-analysis.config.mjs\n * import { defineStaticAnalysis } from '@hublo/sentinel/static-analysis'\n *\n * export default defineStaticAnalysis({\n * root: import.meta.dirname,\n * entry: ['**\\/*.steps.ts'],\n * journeys: { planning: ['src/planning/**'] },\n * })\n * ```\n *\n * ## Why a function and not `{ ...baseConfig, ... }`\n *\n * A spread is SHALLOW, so a module writing `deadCode: { minConfidence: 80 }` would replace the\n * whole block and silently lose `categories`. And the flattening happens in the module's file,\n * before this package ever sees the object, so nothing here could tell \"set one key, keep the\n * rest\" from \"set one key, drop the rest\". A function receives the module's INTENT, the keys it\n * actually passed, and merges them here. Second reason: the defaults are never copied into a\n * module, so changing one reaches every module at the next release instead of leaving 108 stale\n * copies behind.\n *\n * Same shape as `reactTestConfig` in the test role, for the same reasons.\n *\n * ## Two semantics, one obvious and one deliberate\n *\n * `entry` and `journeys` are FACTS a team declares, and they ADD to what this role ships: no\n * module can lose `**\\/*.integration-spec.ts` by declaring a third pattern, which matters\n * because that one pattern is the difference between 33.4% and 1.0% of dead code on a real\n * service (see `defaults.ts`).\n *\n * `overrides` REPLACES, including those lists. It is the seam for the day a team has a real\n * reason, it is not documented anywhere a developer would meet it, and every path it sets is\n * recorded in `moved` so the report can print `(module)` next to the value. Without that trace\n * two modules stop being comparable and nobody knows. It reaches every setting EXCEPT the vendor\n * passthrough, for the reason stated on the field itself.\n */\nimport { isPlainObject } from '../../../shared/deep-merge.js'\nimport { defaults, type StaticDefaults } from '../preset-data.js'\n\ntype DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }\n\n/** What a module's config file passes. Everything is optional, including the file itself. */\nexport interface StaticAnalysisInput {\n /** The module's own directory, normally `import.meta.dirname`. */\n root?: string\n /** Files that run although nothing imports them. ADDED to the shipped patterns. */\n entry?: string[]\n /** Named journeys, as `{ name: [globs] }`. MERGED by name with any shipped ones. */\n journeys?: Record<string, string[]>\n /**\n * The seam. Replaces whatever it names, and every path it sets lands in `moved`.\n *\n * ⛔ `tools` is excluded, and the reason is the whole point of translating the vocabulary: a\n * module knows the ROLE, not the tool underneath it. A key named after the vendor would mean\n * nothing to the person reading their own config file, and showing it would invite them to learn\n * a name we are working to keep out of their world. The passthrough still exists, on sentinel's\n * side, for a preset that needs a vendor option; a module that genuinely needs one gets a key in\n * OUR vocabulary, which is the right friction because it goes through a review instead of\n * spreading silently.\n */\n overrides?: DeepPartial<Omit<StaticDefaults, 'tools'>>\n}\n\nexport interface ResolvedStaticConfig extends StaticDefaults {\n /** The module's directory, or null when its config file did not say. */\n root: string | null\n /**\n * Dotted paths the module set through `overrides`, so the report can mark them. Sorted, so\n * two runs on the same config produce the same report.\n */\n moved: string[]\n}\n\n/**\n * Merge `patch` into `base`, recursively, patch winning.\n *\n * Not the `deepMerge` this repo once had: that one existed for WRITING a module's json and was\n * replaced by an in-place jsonc edit, which is why only `isPlainObject` survived. This is the\n * read side, which has no file to preserve and only has to resolve two objects into one.\n *\n * Arrays are replaced, never concatenated. Additive behaviour is a decision about a specific\n * key, which `defineStaticAnalysis` makes explicitly for `entry`, rather than a property of the\n * merge: a merge that silently concatenates cannot express \"replace this list\", and a team\n * reaching for `overrides` is asking for exactly that.\n */\nfunction mergeInto<T>(base: T, patch: unknown): T {\n if (!isPlainObject(patch)) return base\n if (!isPlainObject(base)) return patch as T\n const out: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n out[key] = isPlainObject(value) ? mergeInto(out[key], value) : value\n }\n return out as T\n}\n\n/** Every leaf path in `patch`, dotted, so a reader can be told what the module decided. */\nfunction leafPaths(patch: unknown, prefix = ''): string[] {\n if (!isPlainObject(patch)) return prefix ? [prefix] : []\n return Object.entries(patch).flatMap(([key, value]) =>\n leafPaths(value, prefix ? `${prefix}.${key}` : key),\n )\n}\n\n/** The same list with duplicates dropped, keeping the first occurrence's order. */\nfunction unique(values: string[]): string[] {\n return [...new Set(values)]\n}\n\n/**\n * Drop the vendor passthrough from what a module sent.\n *\n * ⚠️ Enforced here and not by the type, because the type cannot: a module's config file is `.mjs`,\n * so nothing type-checks it, and `Omit` on the input would be documentation that compiles away. A\n * `tools` key written there is dropped and NAMED on stderr rather than ignored, so a developer who\n * found the key somewhere learns why it did nothing instead of concluding the role is broken.\n */\nfunction withoutVendorKeys(overrides: unknown): unknown {\n if (!isPlainObject(overrides) || !('tools' in overrides)) return overrides\n const { tools: _vendor, ...rest } = overrides\n process.stderr.write(\n 'sentinel static-analysis: ignoring `overrides.tools` from this module. The tool underneath ' +\n \"the role is not part of a module's surface; if a setting is missing, it needs a key of \" +\n 'its own.\\n',\n )\n return rest\n}\n\n/** Resolve a module's declaration against this role's defaults. */\nexport function defineStaticAnalysis(input: StaticAnalysisInput = {}): ResolvedStaticConfig {\n const declared: StaticDefaults = {\n ...defaults,\n entry: unique([...defaults.entry, ...(input.entry ?? [])]),\n journeys: { ...defaults.journeys, ...(input.journeys ?? {}) },\n }\n const overrides = withoutVendorKeys(input.overrides)\n\n return {\n ...mergeInto(declared, overrides),\n root: input.root ?? null,\n moved: leafPaths(overrides).sort(),\n }\n}\n"],"mappings":"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","names":[]}
|