@hublo/sentinel 1.5.0-alpha.5 → 1.5.0-alpha.7
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-DVKTH3RY.js → chunk-BRAW65YY.js} +1 -2
- package/dist/chunk-BRAW65YY.js.map +1 -0
- package/dist/{chunk-JNXDJ2QD.js → chunk-W2VLS6U2.js} +82 -41
- package/dist/index.js +2 -2
- package/dist/roles/static-analysis/config.js +1 -1
- package/docs/typescript-traces.md +8 -0
- package/package.json +1 -1
- package/dist/chunk-DVKTH3RY.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-W2VLS6U2.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-BRAW65YY.js";
|
|
30
30
|
import "../chunk-O7REVMOC.js";
|
|
31
31
|
import "../chunk-QXFCZON7.js";
|
|
32
32
|
|
|
@@ -12,7 +12,6 @@ var shared_default = {
|
|
|
12
12
|
"**/build/**",
|
|
13
13
|
"**/coverage/**",
|
|
14
14
|
"**/.next/**",
|
|
15
|
-
"**/static-analysis-report/**",
|
|
16
15
|
"**/__mocks__/**",
|
|
17
16
|
"**/mocks/**",
|
|
18
17
|
"**/_mock_/**",
|
|
@@ -94,4 +93,4 @@ export {
|
|
|
94
93
|
REPORT_DIR,
|
|
95
94
|
defineStaticAnalysis
|
|
96
95
|
};
|
|
97
|
-
//# sourceMappingURL=chunk-
|
|
96
|
+
//# sourceMappingURL=chunk-BRAW65YY.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\": [\"**/*.integration-spec.ts\", \"**/*.steps.ts\"],\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 },\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 * - `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 * - `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[] }\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 * `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,CAAC,4BAA4B,eAAe;AAAA,EACrD,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,EACnB;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;;;ACiEO,IAAM,aAAa;AAanB,IAAM,WAA2B;;;AC3BxC,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":[]}
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
REPORT_DIR,
|
|
66
66
|
defineStaticAnalysis,
|
|
67
67
|
isPlainObject
|
|
68
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-BRAW65YY.js";
|
|
69
69
|
import {
|
|
70
70
|
parseJsonc
|
|
71
71
|
} from "./chunk-QXFCZON7.js";
|
|
@@ -4788,9 +4788,9 @@ function adopt(exported) {
|
|
|
4788
4788
|
var metricsHelp = {
|
|
4789
4789
|
duplicatedLinesPercent: {
|
|
4790
4790
|
label: "Duplicated lines",
|
|
4791
|
-
what: "The share of lines jscpd found in at least one clone, over the lines it
|
|
4792
|
-
read: "Higher is worse. It is computed per module, so two modules compare directly; the repository-wide figure is not the average of theirs, because modules differ in size.",
|
|
4793
|
-
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.",
|
|
4791
|
+
what: "The share of lines jscpd found in at least one clone, over the lines of the files it was able to scan.",
|
|
4792
|
+
read: "Higher is worse. It is computed per module, so two modules compare directly as long as they measured with the same settings, which is what the settings printed under the figures are for; the repository-wide figure is not the average of theirs, because modules differ in size.",
|
|
4793
|
+
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%.",
|
|
4794
4794
|
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."
|
|
4795
4795
|
},
|
|
4796
4796
|
clones: {
|
|
@@ -4806,22 +4806,28 @@ function helpFor(key) {
|
|
|
4806
4806
|
}
|
|
4807
4807
|
|
|
4808
4808
|
// src/roles/static-analysis/report.ts
|
|
4809
|
-
function hasFinding(report) {
|
|
4810
|
-
return report.rows.some((row) => row.over);
|
|
4811
|
-
}
|
|
4812
4809
|
function renderConsole(report) {
|
|
4813
4810
|
const lines = [];
|
|
4814
4811
|
const width = (pick) => Math.max(...report.rows.map((row) => pick(row).length));
|
|
4815
4812
|
const label = (row) => helpFor(row.metric)?.label ?? row.metric;
|
|
4816
4813
|
const value = (row) => `${row.value} ${row.unit}`;
|
|
4817
|
-
const limit = (row) => row.threshold === null ? "no threshold" : `max ${row.threshold}${row.moved ? " (module)" : ""}`;
|
|
4818
4814
|
const labelWidth = width(label);
|
|
4819
4815
|
const valueWidth = width(value);
|
|
4820
4816
|
lines.push(`sentinel static-analysis: ${report.module}`);
|
|
4821
4817
|
for (const row of report.rows) {
|
|
4822
|
-
lines.push(
|
|
4823
|
-
|
|
4824
|
-
|
|
4818
|
+
lines.push(` ${label(row).padEnd(labelWidth)} ${value(row).padStart(valueWidth)}`);
|
|
4819
|
+
}
|
|
4820
|
+
if (report.settings.length > 0) {
|
|
4821
|
+
const keyWidth = Math.max(...report.settings.map((setting) => setting.key.length));
|
|
4822
|
+
lines.push("", " measured with");
|
|
4823
|
+
for (const setting of report.settings) {
|
|
4824
|
+
lines.push(
|
|
4825
|
+
` ${setting.key.padEnd(keyWidth)} ${setting.value}${setting.moved ? " (module)" : ""}`
|
|
4826
|
+
);
|
|
4827
|
+
}
|
|
4828
|
+
if (report.settings.some((setting) => setting.moved)) {
|
|
4829
|
+
lines.push(` (module) = overridden in ${report.module}'s static-analysis.config.mjs`);
|
|
4830
|
+
}
|
|
4825
4831
|
}
|
|
4826
4832
|
if (report.byLanguage && Object.keys(report.byLanguage).length > 0) {
|
|
4827
4833
|
lines.push("", " per language, where a clone was found");
|
|
@@ -4830,7 +4836,7 @@ function renderConsole(report) {
|
|
|
4830
4836
|
` ${language.padEnd(12)} ${stats.percentage.toFixed(2)}% (${stats.duplicatedLines} lines over ${stats.files} files)`
|
|
4831
4837
|
);
|
|
4832
4838
|
}
|
|
4833
|
-
lines.push(" a language
|
|
4839
|
+
lines.push(" a language is absent when no file of it was long enough to scan");
|
|
4834
4840
|
}
|
|
4835
4841
|
const caveats = report.rows.map((row) => ({ row, help: helpFor(row.metric) })).filter(
|
|
4836
4842
|
(entry) => Boolean(entry.help)
|
|
@@ -4856,13 +4862,19 @@ function escape(text) {
|
|
|
4856
4862
|
function renderHtml(report) {
|
|
4857
4863
|
const rows = report.rows.map((row) => {
|
|
4858
4864
|
const help = helpFor(row.metric);
|
|
4859
|
-
return ` <tr
|
|
4865
|
+
return ` <tr>
|
|
4860
4866
|
<th scope="row">${escape(help?.label ?? row.metric)}</th>
|
|
4861
4867
|
<td class="num">${escape(String(row.value))} <span class="unit">${escape(row.unit)}</span></td>
|
|
4862
|
-
<td class="num">${row.threshold === null ? '<span class="unit">none</span>' : escape(String(row.threshold))}${row.moved ? ' <span class="moved">module</span>' : ""}</td>
|
|
4863
4868
|
<td class="caveat">${escape(help?.caveat ?? "")}</td>
|
|
4864
4869
|
</tr>`;
|
|
4865
4870
|
}).join("\n");
|
|
4871
|
+
const settings = report.settings.map(
|
|
4872
|
+
(setting) => ` <tr>
|
|
4873
|
+
<th scope="row"><code>${escape(setting.key)}</code></th>
|
|
4874
|
+
<td class="num">${escape(setting.value)}</td>
|
|
4875
|
+
<td>${setting.moved ? '<span class="moved">overridden in this module</span>' : ""}</td>
|
|
4876
|
+
</tr>`
|
|
4877
|
+
).join("\n");
|
|
4866
4878
|
return `<!doctype html>
|
|
4867
4879
|
<html lang="en">
|
|
4868
4880
|
<head>
|
|
@@ -4878,14 +4890,16 @@ function renderHtml(report) {
|
|
|
4878
4890
|
main { max-width: 60rem; margin: 0 auto; }
|
|
4879
4891
|
h1 { font-size: 1.25rem; margin: 0 0 0.25rem; }
|
|
4880
4892
|
p.sub { color: var(--dim); margin: 0 0 1.5rem; }
|
|
4893
|
+
h2 { font-size: 0.95rem; margin: 2.5rem 0 0.5rem; }
|
|
4881
4894
|
table { width: 100%; border-collapse: collapse; }
|
|
4882
4895
|
th, td { text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--rule);
|
|
4883
4896
|
vertical-align: top; }
|
|
4884
4897
|
thead th { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--dim); }
|
|
4885
4898
|
td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
4886
|
-
|
|
4899
|
+
code { font: 0.9em ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
4900
|
+
.unit { color: var(--dim); font-size: 0.85em; }
|
|
4901
|
+
.moved { color: var(--warn); font-size: 0.85em; }
|
|
4887
4902
|
.caveat { color: var(--dim); font-size: 0.9em; }
|
|
4888
|
-
tr.over th, tr.over td.num { color: var(--warn); font-weight: 600; }
|
|
4889
4903
|
</style>
|
|
4890
4904
|
</head>
|
|
4891
4905
|
<body>
|
|
@@ -4894,12 +4908,21 @@ function renderHtml(report) {
|
|
|
4894
4908
|
<p class="sub">Passes: ${escape(report.passes.join(", "))}</p>
|
|
4895
4909
|
<table>
|
|
4896
4910
|
<thead>
|
|
4897
|
-
<tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">
|
|
4911
|
+
<tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">What it does not say</th></tr>
|
|
4898
4912
|
</thead>
|
|
4899
4913
|
<tbody>
|
|
4900
4914
|
${rows}
|
|
4901
4915
|
</tbody>
|
|
4902
4916
|
</table>
|
|
4917
|
+
<h2>Measured with</h2>
|
|
4918
|
+
<table>
|
|
4919
|
+
<thead>
|
|
4920
|
+
<tr><th scope="col">Setting</th><th scope="col">Value</th><th scope="col">Origin</th></tr>
|
|
4921
|
+
</thead>
|
|
4922
|
+
<tbody>
|
|
4923
|
+
${settings}
|
|
4924
|
+
</tbody>
|
|
4925
|
+
</table>
|
|
4903
4926
|
</main>
|
|
4904
4927
|
</body>
|
|
4905
4928
|
</html>
|
|
@@ -4910,6 +4933,10 @@ ${rows}
|
|
|
4910
4933
|
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4911
4934
|
import { tmpdir } from "node:os";
|
|
4912
4935
|
import { join as join18 } from "node:path";
|
|
4936
|
+
var OWN_ARTEFACTS = [`**/${STATIC_CONFIG_FILE}`, `**/${REPORT_DIR}/**`];
|
|
4937
|
+
function exclusions(config) {
|
|
4938
|
+
return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS])];
|
|
4939
|
+
}
|
|
4913
4940
|
function fileSettings(config) {
|
|
4914
4941
|
return {
|
|
4915
4942
|
// The detection window. Stated rather than defaulted, because it decides what is invisible.
|
|
@@ -4918,7 +4945,7 @@ function fileSettings(config) {
|
|
|
4918
4945
|
// `format` is jscpd's name for what we call `languages`. Naming `typescript` alone would
|
|
4919
4946
|
// leave out `.tsx`, which is a separate language to it: 300 files of 523 on front/console.
|
|
4920
4947
|
format: config.languages,
|
|
4921
|
-
ignore: config
|
|
4948
|
+
ignore: exclusions(config),
|
|
4922
4949
|
// A scan that analyses nothing otherwise exits 0 with only a warning, which reads as a pass.
|
|
4923
4950
|
failOnEmpty: true,
|
|
4924
4951
|
// The dead-code engine keeps its own section, and its own `minLines`: jscpd's documentation is
|
|
@@ -4984,6 +5011,14 @@ export default defineStaticAnalysis({
|
|
|
4984
5011
|
// Named product journeys, as { 'the name product uses': ['src/some/path/**'] }. Paths are
|
|
4985
5012
|
// relative to this module, because one run measures one module.
|
|
4986
5013
|
journeys: {},
|
|
5014
|
+
|
|
5015
|
+
// Everything else comes from the role, and this is the door when a module must move one of its
|
|
5016
|
+
// values. Anything set here is printed under "measured with" and marked \`(module)\`, so nobody
|
|
5017
|
+
// compares this module's figures with its neighbour's without seeing that they differ:
|
|
5018
|
+
//
|
|
5019
|
+
// overrides: {
|
|
5020
|
+
// duplication: { minTokens: 100 },
|
|
5021
|
+
// },
|
|
4987
5022
|
})
|
|
4988
5023
|
`;
|
|
4989
5024
|
var JscpdAdapter = class extends BaseAdapter {
|
|
@@ -5087,7 +5122,7 @@ ${result.stderr ?? ""}`
|
|
|
5087
5122
|
);
|
|
5088
5123
|
return { ok: false, code: result.status ?? 1 };
|
|
5089
5124
|
}
|
|
5090
|
-
const report = buildReport(ctx, rawReportDir);
|
|
5125
|
+
const report = buildReport(ctx, rawReportDir, config);
|
|
5091
5126
|
if (!report) {
|
|
5092
5127
|
process.stderr.write(`sentinel static-analysis(jscpd): jscpd wrote no report
|
|
5093
5128
|
`);
|
|
@@ -5135,7 +5170,20 @@ ${result.stderr ?? ""}`
|
|
|
5135
5170
|
function analysedNothing(stderr) {
|
|
5136
5171
|
return typeof stderr === "string" && stderr.includes("--fail-on-empty");
|
|
5137
5172
|
}
|
|
5138
|
-
function
|
|
5173
|
+
function duplicationSettings(config) {
|
|
5174
|
+
const moved = (key) => config.moved.some((path) => path === key || path.startsWith(`${key}.`));
|
|
5175
|
+
const row = (key, value) => ({ key, value, moved: moved(key) });
|
|
5176
|
+
return [
|
|
5177
|
+
row("duplication.minTokens", String(config.duplication.minTokens)),
|
|
5178
|
+
row("duplication.minLines", String(config.duplication.minLines)),
|
|
5179
|
+
row("duplication.kinds", config.duplication.kinds.join(", ")),
|
|
5180
|
+
row("languages", `${config.languages.length}: ${config.languages.join(", ")}`),
|
|
5181
|
+
// The count the SCAN received, not the module's list: the role unions its own artefacts in,
|
|
5182
|
+
// and printing 20 while measuring with 22 is the kind of small lie that costs an afternoon.
|
|
5183
|
+
row("ignore", `${exclusions(config).length} patterns`)
|
|
5184
|
+
];
|
|
5185
|
+
}
|
|
5186
|
+
function buildReport(ctx, reportDir, config) {
|
|
5139
5187
|
const path = join19(reportDir, "jscpd-report.json");
|
|
5140
5188
|
if (!existsSync14(path)) return void 0;
|
|
5141
5189
|
let parsed;
|
|
@@ -5148,25 +5196,8 @@ function buildReport(ctx, reportDir) {
|
|
|
5148
5196
|
if (!total) return void 0;
|
|
5149
5197
|
const percent = Number((total.percentage ?? 0).toFixed(2));
|
|
5150
5198
|
const rows = [
|
|
5151
|
-
{
|
|
5152
|
-
|
|
5153
|
-
value: percent,
|
|
5154
|
-
unit: "%",
|
|
5155
|
-
// No threshold, and none is coming from here: see `preset-data.ts`. The figure is reported as
|
|
5156
|
-
// measured, and what makes it readable is the distribution it sits in, which one module
|
|
5157
|
-
// cannot know.
|
|
5158
|
-
threshold: null,
|
|
5159
|
-
over: false,
|
|
5160
|
-
moved: false
|
|
5161
|
-
},
|
|
5162
|
-
{
|
|
5163
|
-
metric: "clones",
|
|
5164
|
-
value: total.clones ?? 0,
|
|
5165
|
-
unit: "entries",
|
|
5166
|
-
threshold: null,
|
|
5167
|
-
over: false,
|
|
5168
|
-
moved: false
|
|
5169
|
-
}
|
|
5199
|
+
{ metric: "duplicatedLinesPercent", value: percent, unit: "%" },
|
|
5200
|
+
{ metric: "clones", value: total.clones ?? 0, unit: "entries" }
|
|
5170
5201
|
];
|
|
5171
5202
|
const formats = parsed.statistics?.formats ?? {};
|
|
5172
5203
|
const byLanguage = Object.fromEntries(
|
|
@@ -5179,7 +5210,13 @@ function buildReport(ctx, reportDir) {
|
|
|
5179
5210
|
}
|
|
5180
5211
|
])
|
|
5181
5212
|
);
|
|
5182
|
-
return {
|
|
5213
|
+
return {
|
|
5214
|
+
module: ctx.module,
|
|
5215
|
+
passes: ["duplication"],
|
|
5216
|
+
rows,
|
|
5217
|
+
settings: duplicationSettings(config),
|
|
5218
|
+
byLanguage
|
|
5219
|
+
};
|
|
5183
5220
|
}
|
|
5184
5221
|
function emit(report, shape, cwd) {
|
|
5185
5222
|
if (shape === "console") {
|
|
@@ -5197,8 +5234,12 @@ function emit(report, shape, cwd) {
|
|
|
5197
5234
|
function metricsOf(report) {
|
|
5198
5235
|
return {
|
|
5199
5236
|
passes: report.passes,
|
|
5200
|
-
finding: hasFinding(report),
|
|
5201
5237
|
...Object.fromEntries(report.rows.map((row) => [row.metric, row.value])),
|
|
5238
|
+
// Carried into `--json` and the CI comment for the same reason the console prints them: a
|
|
5239
|
+
// figure whose settings are unknown cannot be compared with the next module's.
|
|
5240
|
+
settings: Object.fromEntries(
|
|
5241
|
+
report.settings.map((s) => [s.key, { value: s.value, moved: s.moved }])
|
|
5242
|
+
),
|
|
5202
5243
|
byLanguage: report.byLanguage
|
|
5203
5244
|
};
|
|
5204
5245
|
}
|
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-W2VLS6U2.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-BRAW65YY.js";
|
|
15
15
|
import "./chunk-O7REVMOC.js";
|
|
16
16
|
import "./chunk-QXFCZON7.js";
|
|
17
17
|
export {
|
|
@@ -1171,6 +1171,14 @@ $ sentinel --init --dry-run
|
|
|
1171
1171
|
+ // Named product journeys, as { 'the name product uses': ['src/some/path/**'] }. Paths are
|
|
1172
1172
|
+ // relative to this module, because one run measures one module.
|
|
1173
1173
|
+ journeys: {},
|
|
1174
|
+
+
|
|
1175
|
+
+ // Everything else comes from the role, and this is the door when a module must move one of its
|
|
1176
|
+
+ // values. Anything set here is printed under "measured with" and marked `(module)`, so nobody
|
|
1177
|
+
+ // compares this module's figures with its neighbour's without seeing that they differ:
|
|
1178
|
+
+ //
|
|
1179
|
+
+ // overrides: {
|
|
1180
|
+
+ // duplication: { minTokens: 100 },
|
|
1181
|
+
+ // },
|
|
1174
1182
|
+ })
|
|
1175
1183
|
|
|
1176
1184
|
update package.json
|
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.7",
|
|
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",
|
|
@@ -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\": [\"**/*.integration-spec.ts\", \"**/*.steps.ts\"],\n \"ignore\": [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/coverage/**\",\n \"**/.next/**\",\n \"**/static-analysis-report/**\",\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 },\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 * - `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 * - `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[] }\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 * `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,CAAC,4BAA4B,eAAe;AAAA,EACrD,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,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,EACnB;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;;;ACgEO,IAAM,aAAa;AAanB,IAAM,WAA2B;;;AC3BxC,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":[]}
|