@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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/package.json +51 -0
  4. package/src/audit-structure.mjs +103 -0
  5. package/src/bundle-codebase.mjs +626 -0
  6. package/src/check-images.mjs +125 -0
  7. package/src/coverage/clean.mjs +32 -0
  8. package/src/coverage/merge-istanbul.mjs +161 -0
  9. package/src/coverage/next-start-cov.mjs +38 -0
  10. package/src/coverage/report-global.mjs +90 -0
  11. package/src/coverage/report-suite.mjs +148 -0
  12. package/src/coverage/src-filter.mjs +50 -0
  13. package/src/dashboard/collectors/code.mjs +104 -0
  14. package/src/dashboard/collectors/composition-meta.mjs +295 -0
  15. package/src/dashboard/collectors/composition-transitions.mjs +0 -0
  16. package/src/dashboard/collectors/composition.mjs +360 -0
  17. package/src/dashboard/collectors/coverage.mjs +98 -0
  18. package/src/dashboard/collectors/deps.mjs +187 -0
  19. package/src/dashboard/collectors/entities.mjs +147 -0
  20. package/src/dashboard/collectors/graph.mjs +105 -0
  21. package/src/dashboard/collectors/lint.mjs +117 -0
  22. package/src/dashboard/collectors/routing.mjs +82 -0
  23. package/src/dashboard/collectors/security.mjs +182 -0
  24. package/src/dashboard/collectors/storybook.mjs +33 -0
  25. package/src/dashboard/config.mjs +15 -0
  26. package/src/dashboard/render/client.mjs +178 -0
  27. package/src/dashboard/render/components.mjs +247 -0
  28. package/src/dashboard/render/composition.mjs +192 -0
  29. package/src/dashboard/render/styles.mjs +217 -0
  30. package/src/dashboard/render/template.mjs +283 -0
  31. package/src/dashboard/utils/exec.mjs +29 -0
  32. package/src/dashboard/utils/format.mjs +32 -0
  33. package/src/dashboard/utils/fs.mjs +48 -0
  34. package/src/e2e-server-guard.mjs +283 -0
  35. package/src/optimize-images.mjs +231 -0
  36. package/src/quality-dashboard.mjs +291 -0
  37. package/src/security-scan.mjs +267 -0
  38. 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);