@mifort-solutions/qmetrix 1.0.0
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/LICENSE +21 -0
- package/README.md +75 -0
- package/package.json +51 -0
- package/src/audit-structure.mjs +103 -0
- package/src/bundle-codebase.mjs +626 -0
- package/src/check-images.mjs +125 -0
- package/src/coverage/clean.mjs +32 -0
- package/src/coverage/merge-istanbul.mjs +161 -0
- package/src/coverage/next-start-cov.mjs +38 -0
- package/src/coverage/report-global.mjs +90 -0
- package/src/coverage/report-suite.mjs +148 -0
- package/src/coverage/src-filter.mjs +50 -0
- package/src/dashboard/collectors/code.mjs +104 -0
- package/src/dashboard/collectors/composition-meta.mjs +295 -0
- package/src/dashboard/collectors/composition-transitions.mjs +0 -0
- package/src/dashboard/collectors/composition.mjs +360 -0
- package/src/dashboard/collectors/coverage.mjs +98 -0
- package/src/dashboard/collectors/deps.mjs +187 -0
- package/src/dashboard/collectors/entities.mjs +147 -0
- package/src/dashboard/collectors/graph.mjs +105 -0
- package/src/dashboard/collectors/lint.mjs +117 -0
- package/src/dashboard/collectors/routing.mjs +82 -0
- package/src/dashboard/collectors/security.mjs +182 -0
- package/src/dashboard/collectors/storybook.mjs +33 -0
- package/src/dashboard/config.mjs +15 -0
- package/src/dashboard/render/client.mjs +178 -0
- package/src/dashboard/render/components.mjs +247 -0
- package/src/dashboard/render/composition.mjs +192 -0
- package/src/dashboard/render/styles.mjs +217 -0
- package/src/dashboard/render/template.mjs +283 -0
- package/src/dashboard/utils/exec.mjs +29 -0
- package/src/dashboard/utils/format.mjs +32 -0
- package/src/dashboard/utils/fs.mjs +48 -0
- package/src/e2e-server-guard.mjs +283 -0
- package/src/optimize-images.mjs +231 -0
- package/src/quality-dashboard.mjs +291 -0
- package/src/security-scan.mjs +267 -0
- package/src/test-outline.mjs +98 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Kolonitsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# QMetriX
|
|
2
|
+
|
|
3
|
+
> `@mifort-solutions/qmetrix` — build & quality tooling, extracted from `ai.mifort.com`'s `dev/scripts/`.
|
|
4
|
+
|
|
5
|
+
QMetriX is a set of plain-ESM Node scripts that compute quality signals over a
|
|
6
|
+
**consuming repository**: coverage merge & reporting, a quality dashboard, an image
|
|
7
|
+
budget check / optimizer, a structural-duplication audit, a security scan, an
|
|
8
|
+
e2e-server guard, and a single-file codebase bundler.
|
|
9
|
+
|
|
10
|
+
It is **dev tooling, not a library** — you install it as a `devDependency` and invoke its
|
|
11
|
+
`bin` executables from your project's `package.json` scripts. The scripts read and write
|
|
12
|
+
the **consuming repo's** tree (`src/`, `content/`, `public/`, `dist/reports/*`,
|
|
13
|
+
`package.json`, `node_modules`, git history).
|
|
14
|
+
|
|
15
|
+
## The cwd contract (important)
|
|
16
|
+
|
|
17
|
+
Every QMetriX bin anchors the project root on **`process.cwd()`** — i.e. the directory the
|
|
18
|
+
verb runs from. **Run every bin from your repository root** (which is what `npm run …` does
|
|
19
|
+
by default). Running a bin from a subdirectory will make its `src/`, `dist/`, `package.json`
|
|
20
|
+
reads target the wrong tree.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- **Node `22.x`** (closed range — the package's `engines` pins the same major as the host app).
|
|
25
|
+
- A few bins shell out to system tools that are not npm dependencies:
|
|
26
|
+
`qmetrix-security-scan` uses `curl` / `tar` / the CodeQL CLI; `qmetrix-quality-dashboard`
|
|
27
|
+
expects a built Storybook under `dist/site/storybook` when run for a full dashboard.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install --save-dev @mifort-solutions/qmetrix
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Bins
|
|
36
|
+
|
|
37
|
+
| Bin | What it does |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `qmetrix-check-images` | Fail the build when committed images exceed the byte/pixel budget (`sharp`). |
|
|
40
|
+
| `qmetrix-optimize-images` | Resize / re-encode committed assets (`sharp`). |
|
|
41
|
+
| `qmetrix-audit-structure` | Structural (AST-shape) copy-paste audit via `jsinspect-plus` → `dist/reports/jsinspect.json`. Advisory (exits 0). |
|
|
42
|
+
| `qmetrix-security-scan` | Local SAST/SCA (Snyk + CodeQL) → SARIF under `dist/reports/`. |
|
|
43
|
+
| `qmetrix-quality-dashboard` | Build the quality dashboard HTML from `dist/reports/*` + the repo tree. |
|
|
44
|
+
| `qmetrix-bundle-codebase` | Bundle the codebase into one self-contained HTML file. |
|
|
45
|
+
| `qmetrix-e2e-server-guard` | Guard the e2e port against a stale `next start` / poisoned prerender cache. |
|
|
46
|
+
| `qmetrix-coverage-clean` | Clean per-suite / global coverage scratch under `dist/reports/coverage`. |
|
|
47
|
+
| `qmetrix-coverage-report-suite` | Build a per-suite coverage report from raw V8 / istanbul data. |
|
|
48
|
+
| `qmetrix-coverage-report-global` | Merge per-suite coverage into a global line-union `lcov.info`. |
|
|
49
|
+
| `qmetrix-coverage-next-start` | Start Next.js in-process with V8 coverage enabled (used by Playwright). |
|
|
50
|
+
|
|
51
|
+
Arguments pass through verbatim, e.g.:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
qmetrix-coverage-clean e2e
|
|
55
|
+
qmetrix-e2e-server-guard --pre-build
|
|
56
|
+
qmetrix-coverage-report-suite storybook
|
|
57
|
+
qmetrix-quality-dashboard --no-coverage
|
|
58
|
+
qmetrix-optimize-images public/images --format webp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Example wiring (`package.json`)
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
{
|
|
65
|
+
"scripts": {
|
|
66
|
+
"images:check": "qmetrix-check-images",
|
|
67
|
+
"audit:structure": "qmetrix-audit-structure",
|
|
68
|
+
"coverage:global": "qmetrix-coverage-clean global && qmetrix-coverage-report-global"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mifort-solutions/qmetrix",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "QMetriX — build & quality tooling (coverage merge, quality dashboard, image budget, security scan, structural duplication audit, e2e server guard, codebase bundler). Operates on the consuming repo via its cwd.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alex Kolonitsky <alex.kolonitsky@gmail.com>",
|
|
8
|
+
"homepage": "https://github.com/AlexKolonitskyMifort/qmetrix#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/AlexKolonitskyMifort/qmetrix.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/AlexKolonitskyMifort/qmetrix/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"quality",
|
|
18
|
+
"coverage",
|
|
19
|
+
"dashboard",
|
|
20
|
+
"tooling",
|
|
21
|
+
"code-quality",
|
|
22
|
+
"ci"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"qmetrix-audit-structure": "src/audit-structure.mjs",
|
|
26
|
+
"qmetrix-bundle-codebase": "src/bundle-codebase.mjs",
|
|
27
|
+
"qmetrix-check-images": "src/check-images.mjs",
|
|
28
|
+
"qmetrix-optimize-images": "src/optimize-images.mjs",
|
|
29
|
+
"qmetrix-quality-dashboard": "src/quality-dashboard.mjs",
|
|
30
|
+
"qmetrix-security-scan": "src/security-scan.mjs",
|
|
31
|
+
"qmetrix-e2e-server-guard": "src/e2e-server-guard.mjs",
|
|
32
|
+
"qmetrix-coverage-clean": "src/coverage/clean.mjs",
|
|
33
|
+
"qmetrix-coverage-report-suite": "src/coverage/report-suite.mjs",
|
|
34
|
+
"qmetrix-coverage-report-global": "src/coverage/report-global.mjs",
|
|
35
|
+
"qmetrix-coverage-next-start": "src/coverage/next-start-cov.mjs"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"jsinspect-plus": "^3.1.3",
|
|
42
|
+
"monocart-coverage-reports": "^2.12.12",
|
|
43
|
+
"sharp": "^0.35.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": "22.x"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Structural copy-paste audit — finds renamed / restructured duplication that the
|
|
4
|
+
* token-based detector (`npm run cpd`, jscpd) misses.
|
|
5
|
+
*
|
|
6
|
+
* jscpd matches token *values*, so the moment identifiers or literals change it
|
|
7
|
+
* stops seeing the clone. jsinspect-plus walks the AST and matches node *shape*,
|
|
8
|
+
* so two blocks with the same structure but different variable names still flag.
|
|
9
|
+
* Threshold / ignore live in `.config/.jsinspectrc`; identifier matching is turned
|
|
10
|
+
* off here on the CLI (`-I`) because jsinspect ignores that key from the config
|
|
11
|
+
* file (see the note in .jsinspectrc). Literals stay on as anchors.
|
|
12
|
+
*
|
|
13
|
+
* This is **advisory**: structural detectors are noisy by nature, so a finding is
|
|
14
|
+
* a review prompt, not a build failure. It always exits 0 unless `--strict` is
|
|
15
|
+
* passed. Part of the periodic `audit` phase (see `npm run audit`), not the
|
|
16
|
+
* per-push `verify` gate.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node dev/scripts/audit-structure.mjs [paths...] [--strict]
|
|
20
|
+
* npm run audit:structure
|
|
21
|
+
*
|
|
22
|
+
* Writes a machine-readable report to dist/reports/jsinspect.json.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
|
|
30
|
+
// Anchored on the consuming repo's cwd — every verb runs from the app root.
|
|
31
|
+
const ROOT = process.cwd();
|
|
32
|
+
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
const strict = argv.includes('--strict');
|
|
35
|
+
const targets = argv.filter((a) => !a.startsWith('--'));
|
|
36
|
+
const paths = targets.length ? targets : ['src'];
|
|
37
|
+
|
|
38
|
+
// Resolve jsinspect-plus relative to THIS package (hoist-proof): works whether the
|
|
39
|
+
// dep hoists to the app root or nests under node_modules/@mifort-solutions/qmetrix/node_modules.
|
|
40
|
+
const BIN = path.join(
|
|
41
|
+
path.dirname(createRequire(import.meta.url).resolve('jsinspect-plus/package.json')),
|
|
42
|
+
'bin',
|
|
43
|
+
'jsinspect',
|
|
44
|
+
);
|
|
45
|
+
const CONFIG = path.join('.config', '.jsinspectrc');
|
|
46
|
+
const REPORT = path.join('dist', 'reports', 'jsinspect.json');
|
|
47
|
+
|
|
48
|
+
const posix = (p) => p.replace(/^\.[\\/]/, '').replace(/\\/g, '/');
|
|
49
|
+
|
|
50
|
+
/* ── Run jsinspect-plus via its bundled CLI (cross-platform: node + bin.js) ── */
|
|
51
|
+
const { stdout, stderr, status, error } = spawnSync(
|
|
52
|
+
process.execPath,
|
|
53
|
+
[BIN, '-c', CONFIG, '-I', '-r', 'json', ...paths],
|
|
54
|
+
{ cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (error) {
|
|
58
|
+
console.error(`audit:structure — failed to launch jsinspect-plus: ${error.message}`);
|
|
59
|
+
process.exit(strict ? 1 : 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// jsinspect prints per-file parser failures to stderr but keeps going — surface them.
|
|
63
|
+
const parseErrors = (stderr || '').trim();
|
|
64
|
+
if (parseErrors) {
|
|
65
|
+
console.warn('audit:structure — some files could not be parsed:');
|
|
66
|
+
console.warn(parseErrors);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let matches;
|
|
70
|
+
try {
|
|
71
|
+
matches = JSON.parse(stdout || '[]');
|
|
72
|
+
} catch {
|
|
73
|
+
console.error('audit:structure — could not parse jsinspect output (exit', status, ')');
|
|
74
|
+
console.error(stdout);
|
|
75
|
+
process.exit(strict ? 1 : 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ── Persist the artifact (dashboard / history can pick this up later) ── */
|
|
79
|
+
mkdirSync(path.join(ROOT, 'dist', 'reports'), { recursive: true });
|
|
80
|
+
writeFileSync(path.join(ROOT, REPORT), JSON.stringify(matches, null, 2));
|
|
81
|
+
|
|
82
|
+
/* ── Human summary, worst (most lines duplicated) first ── */
|
|
83
|
+
const linesOf = (m) => m.instances.reduce((n, i) => n + (i.lines[1] - i.lines[0] + 1), 0);
|
|
84
|
+
const ranked = [...matches].sort((a, b) => linesOf(b) - linesOf(a));
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
`\nStructural duplication audit (jsinspect-plus, AST shape) — ${matches.length} clone group(s)\n`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (matches.length === 0) {
|
|
91
|
+
console.log('No structural clones above the configured threshold. ✓\n');
|
|
92
|
+
} else {
|
|
93
|
+
ranked.forEach((m, i) => {
|
|
94
|
+
const span = m.instances[0].lines[1] - m.instances[0].lines[0] + 1;
|
|
95
|
+
const locs = m.instances.map((inst) => `${posix(inst.path)}:${inst.lines[0]}-${inst.lines[1]}`);
|
|
96
|
+
console.log(`#${i + 1} ${m.instances.length}× (~${span} lines each)`);
|
|
97
|
+
locs.forEach((loc) => console.log(` ${loc}`));
|
|
98
|
+
});
|
|
99
|
+
console.log(`\nReport: ${posix(REPORT)}`);
|
|
100
|
+
console.log('Advisory — these are candidates for extraction, not build failures.\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
process.exit(strict && matches.length ? 5 : 0);
|