@agentvibes/guardrails 0.1.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/README.md +239 -0
- package/candidates/README.md +30 -0
- package/candidates/r10-as-cast.yml +24 -0
- package/candidates/r1d-scattered-numeric-const-in-component.yml +13 -0
- package/candidates/r2-helper-fn-in-tsx.yml +22 -0
- package/candidates/r3-hooks-outside-layout.yml +21 -0
- package/candidates/r6-jsx-comment-smell.yml +14 -0
- package/candidates/r7-raw-hsl-outside-tokens.yml +17 -0
- package/candidates/r8-inline-style-prop.yml +13 -0
- package/candidates/r9-optional-store-arg.yml +24 -0
- package/configs/biome.json +96 -0
- package/configs/tsconfig.base.json +21 -0
- package/dist/astGrep.d.ts +16 -0
- package/dist/astGrep.js +48 -0
- package/dist/astGrep.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +34 -0
- package/dist/deploy.js.map +1 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +102 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fileWalk.d.ts +2 -0
- package/dist/fileWalk.js +45 -0
- package/dist/fileWalk.js.map +1 -0
- package/dist/findings.d.ts +12 -0
- package/dist/findings.js +8 -0
- package/dist/findings.js.map +1 -0
- package/dist/gateRed.d.ts +1 -0
- package/dist/gateRed.js +93 -0
- package/dist/gateRed.js.map +1 -0
- package/dist/gitDiff.d.ts +18 -0
- package/dist/gitDiff.js +90 -0
- package/dist/gitDiff.js.map +1 -0
- package/dist/hookPostedit.d.ts +1 -0
- package/dist/hookPostedit.js +117 -0
- package/dist/hookPostedit.js.map +1 -0
- package/dist/hookStop.d.ts +1 -0
- package/dist/hookStop.js +56 -0
- package/dist/hookStop.js.map +1 -0
- package/dist/hookStopTest.d.ts +1 -0
- package/dist/hookStopTest.js +71 -0
- package/dist/hookStopTest.js.map +1 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +172 -0
- package/dist/init.js.map +1 -0
- package/dist/initTest.d.ts +1 -0
- package/dist/initTest.js +88 -0
- package/dist/initTest.js.map +1 -0
- package/dist/iterate.d.ts +9 -0
- package/dist/iterate.js +79 -0
- package/dist/iterate.js.map +1 -0
- package/dist/iterateTest.d.ts +1 -0
- package/dist/iterateTest.js +91 -0
- package/dist/iterateTest.js.map +1 -0
- package/dist/leakPatterns.d.ts +5 -0
- package/dist/leakPatterns.js +22 -0
- package/dist/leakPatterns.js.map +1 -0
- package/dist/leaks.d.ts +8 -0
- package/dist/leaks.js +211 -0
- package/dist/leaks.js.map +1 -0
- package/dist/leaksRed.d.ts +1 -0
- package/dist/leaksRed.js +62 -0
- package/dist/leaksRed.js.map +1 -0
- package/dist/metrics.d.ts +10 -0
- package/dist/metrics.js +113 -0
- package/dist/metrics.js.map +1 -0
- package/dist/metricsAggregate.d.ts +5 -0
- package/dist/metricsAggregate.js +78 -0
- package/dist/metricsAggregate.js.map +1 -0
- package/dist/metricsBaseline.d.ts +21 -0
- package/dist/metricsBaseline.js +88 -0
- package/dist/metricsBaseline.js.map +1 -0
- package/dist/metricsCollect.d.ts +8 -0
- package/dist/metricsCollect.js +295 -0
- package/dist/metricsCollect.js.map +1 -0
- package/dist/metricsCycle.d.ts +1 -0
- package/dist/metricsCycle.js +79 -0
- package/dist/metricsCycle.js.map +1 -0
- package/dist/metricsTypes.d.ts +102 -0
- package/dist/metricsTypes.js +2 -0
- package/dist/metricsTypes.js.map +1 -0
- package/dist/packagePaths.d.ts +7 -0
- package/dist/packagePaths.js +11 -0
- package/dist/packagePaths.js.map +1 -0
- package/dist/pluginApi.d.ts +24 -0
- package/dist/pluginApi.js +12 -0
- package/dist/pluginApi.js.map +1 -0
- package/dist/pluginResolve.d.ts +6 -0
- package/dist/pluginResolve.js +48 -0
- package/dist/pluginResolve.js.map +1 -0
- package/dist/screenScope.d.ts +21 -0
- package/dist/screenScope.js +81 -0
- package/dist/screenScope.js.map +1 -0
- package/dist/screenScopeTest.d.ts +1 -0
- package/dist/screenScopeTest.js +116 -0
- package/dist/screenScopeTest.js.map +1 -0
- package/dist/severity.d.ts +9 -0
- package/dist/severity.js +62 -0
- package/dist/severity.js.map +1 -0
- package/dist/severityTest.d.ts +1 -0
- package/dist/severityTest.js +63 -0
- package/dist/severityTest.js.map +1 -0
- package/dist/sourceLines.d.ts +9 -0
- package/dist/sourceLines.js +52 -0
- package/dist/sourceLines.js.map +1 -0
- package/dist/structure.d.ts +2 -0
- package/dist/structure.js +98 -0
- package/dist/structure.js.map +1 -0
- package/dist/testRules.d.ts +1 -0
- package/dist/testRules.js +285 -0
- package/dist/testRules.js.map +1 -0
- package/dist/textGrep.d.ts +2 -0
- package/dist/textGrep.js +52 -0
- package/dist/textGrep.js.map +1 -0
- package/dist/tomlTable.d.ts +2 -0
- package/dist/tomlTable.js +26 -0
- package/dist/tomlTable.js.map +1 -0
- package/dist/toolResolve.d.ts +18 -0
- package/dist/toolResolve.js +50 -0
- package/dist/toolResolve.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +49 -0
- package/dist/verify.js.map +1 -0
- package/dist/verifyDiff.d.ts +18 -0
- package/dist/verifyDiff.js +126 -0
- package/dist/verifyDiff.js.map +1 -0
- package/dist/workspaces.d.ts +6 -0
- package/dist/workspaces.js +84 -0
- package/dist/workspaces.js.map +1 -0
- package/mise.toml +9 -0
- package/package.json +62 -0
- package/rules/__fixtures__/as-any-escape/bad.ts +8 -0
- package/rules/__fixtures__/as-any-escape/good.ts +14 -0
- package/rules/__fixtures__/catch-empty/bad.ts +7 -0
- package/rules/__fixtures__/catch-empty/good.ts +14 -0
- package/rules/__fixtures__/direct-store-import/bad.tsx +23 -0
- package/rules/__fixtures__/direct-store-import/good.tsx +34 -0
- package/rules/__fixtures__/direct-store-import/showcase/ignored.tsx +9 -0
- package/rules/__fixtures__/discriminator-ternary/bad.tsx +10 -0
- package/rules/__fixtures__/discriminator-ternary/good.tsx +23 -0
- package/rules/__fixtures__/hardcoded-url-in-component/components/bad.tsx +8 -0
- package/rules/__fixtures__/hardcoded-url-in-component/components/good.tsx +14 -0
- package/rules/__fixtures__/inline-map-row/bad.tsx +72 -0
- package/rules/__fixtures__/inline-map-row/good.tsx +51 -0
- package/rules/__fixtures__/instanceof-map-set/bad.ts +7 -0
- package/rules/__fixtures__/instanceof-map-set/good.ts +8 -0
- package/rules/__fixtures__/literal-union-in-component/components/bad.ts +8 -0
- package/rules/__fixtures__/literal-union-in-component/components/good.ts +23 -0
- package/rules/__fixtures__/missing-observer/bad.tsx +66 -0
- package/rules/__fixtures__/missing-observer/good.tsx +69 -0
- package/rules/__fixtures__/no-local-kit-clone/bad.ts +30 -0
- package/rules/__fixtures__/no-local-kit-clone/bad.tsx +30 -0
- package/rules/__fixtures__/no-local-kit-clone/good.ts +31 -0
- package/rules/__fixtures__/no-local-kit-clone/good.tsx +31 -0
- package/rules/__fixtures__/non-exhaustive-match/bad.ts +8 -0
- package/rules/__fixtures__/non-exhaustive-match/good.ts +14 -0
- package/rules/__fixtures__/page-store-bare-usememo/bad.ts +15 -0
- package/rules/__fixtures__/page-store-bare-usememo/bad.tsx +13 -0
- package/rules/__fixtures__/page-store-bare-usememo/good.ts +17 -0
- package/rules/__fixtures__/page-store-bare-usememo/good.tsx +14 -0
- package/rules/__fixtures__/screen-file-styling/bad.screen.tsx +10 -0
- package/rules/__fixtures__/screen-file-styling/good.screen.tsx +8 -0
- package/rules/__fixtures__/state-loading-boolean-shape/bad.ts +36 -0
- package/rules/__fixtures__/state-loading-boolean-shape/bad.tsx +36 -0
- package/rules/__fixtures__/state-loading-boolean-shape/good.ts +44 -0
- package/rules/__fixtures__/state-loading-boolean-shape/good.tsx +44 -0
- package/rules/__fixtures__/store-async-method/bad.ts +37 -0
- package/rules/__fixtures__/store-async-method/bad.tsx +37 -0
- package/rules/__fixtures__/store-async-method/good.ts +73 -0
- package/rules/__fixtures__/store-async-method/good.tsx +73 -0
- package/rules/__fixtures__/store-context-provider/bad.ts +16 -0
- package/rules/__fixtures__/store-context-provider/bad.tsx +24 -0
- package/rules/__fixtures__/store-context-provider/good.ts +13 -0
- package/rules/__fixtures__/store-context-provider/good.tsx +11 -0
- package/rules/__fixtures__/store-delegation-getter/bad.ts +30 -0
- package/rules/__fixtures__/store-delegation-getter/bad.tsx +30 -0
- package/rules/__fixtures__/store-delegation-getter/good.ts +65 -0
- package/rules/__fixtures__/store-delegation-getter/good.tsx +65 -0
- package/rules/__fixtures__/store-new-map/bad.ts +22 -0
- package/rules/__fixtures__/store-new-map/bad.tsx +22 -0
- package/rules/__fixtures__/store-new-map/good.ts +41 -0
- package/rules/__fixtures__/store-new-map/good.tsx +41 -0
- package/rules/__fixtures__/store-no-runinaction/bad.ts +67 -0
- package/rules/__fixtures__/store-no-runinaction/bad.tsx +67 -0
- package/rules/__fixtures__/store-no-runinaction/good.ts +39 -0
- package/rules/__fixtures__/store-no-runinaction/good.tsx +39 -0
- package/rules/__fixtures__/store-no-setinterval-poll/bad.ts +19 -0
- package/rules/__fixtures__/store-no-setinterval-poll/bad.tsx +19 -0
- package/rules/__fixtures__/store-no-setinterval-poll/good.ts +29 -0
- package/rules/__fixtures__/store-no-setinterval-poll/good.tsx +29 -0
- package/rules/__fixtures__/ts-pattern-dangling-match/bad.ts +20 -0
- package/rules/__fixtures__/ts-pattern-dangling-match/good.ts +35 -0
- package/rules/__fixtures__/ui-imports-app-store/src/ui/bad.tsx +13 -0
- package/rules/__fixtures__/ui-imports-app-store/src/ui/good.tsx +23 -0
- package/rules/__fixtures__/view-file-logic/bad.view.tsx +16 -0
- package/rules/__fixtures__/view-file-logic/good.view.tsx +7 -0
- package/rules/__fixtures__/view-imports-store/bad.view.tsx +13 -0
- package/rules/__fixtures__/view-imports-store/good.view.tsx +23 -0
- package/rules/__fixtures__/zod-optional-nullable/bad.ts +10 -0
- package/rules/__fixtures__/zod-optional-nullable/bad.tsx +17 -0
- package/rules/__fixtures__/zod-optional-nullable/good.ts +16 -0
- package/rules/__fixtures__/zod-optional-nullable/good.tsx +20 -0
- package/rules/as-any-escape-tsx.yml +53 -0
- package/rules/as-any-escape.yml +29 -0
- package/rules/catch-empty-tsx.yml +55 -0
- package/rules/catch-empty.yml +31 -0
- package/rules/classname-not-composed.yml +28 -0
- package/rules/classname-visual-identity.yml +34 -0
- package/rules/demo-mode-by-default-ts.yml +76 -0
- package/rules/demo-mode-by-default.yml +54 -0
- package/rules/direct-store-import.yml +118 -0
- package/rules/discriminator-ternary.yml +70 -0
- package/rules/hardcoded-url-in-component.yml +44 -0
- package/rules/inline-map-row.yml +107 -0
- package/rules/instanceof-map-set-tsx.yml +46 -0
- package/rules/instanceof-map-set.yml +22 -0
- package/rules/json-roundtrip.yml +9 -0
- package/rules/jsx-cond-and.yml +16 -0
- package/rules/jsx-ternary.yml +35 -0
- package/rules/kind-if-without-match.yml +13 -0
- package/rules/literal-union-in-component.yml +71 -0
- package/rules/match-bool-to-null.yml +15 -0
- package/rules/missing-observer.yml +189 -0
- package/rules/mobx-effect-observable-dep.yml +23 -0
- package/rules/mobx-effect-store-write.yml +21 -0
- package/rules/mobx-usestate-from-store.yml +20 -0
- package/rules/no-local-kit-clone-tsx.yml +104 -0
- package/rules/no-local-kit-clone.yml +93 -0
- package/rules/non-exhaustive-match.yml +34 -0
- package/rules/page-store-bare-usememo-tsx.yml +47 -0
- package/rules/page-store-bare-usememo.yml +43 -0
- package/rules/screen-file-styling.yml +29 -0
- package/rules/silent-default-return.yml +17 -0
- package/rules/state-loading-boolean-shape-tsx.yml +102 -0
- package/rules/state-loading-boolean-shape.yml +91 -0
- package/rules/store-async-method-tsx.yml +108 -0
- package/rules/store-async-method.yml +98 -0
- package/rules/store-context-provider-tsx.yml +68 -0
- package/rules/store-context-provider.yml +78 -0
- package/rules/store-delegation-getter-tsx.yml +104 -0
- package/rules/store-delegation-getter.yml +93 -0
- package/rules/store-new-map-tsx.yml +84 -0
- package/rules/store-new-map.yml +73 -0
- package/rules/store-no-runinaction-tsx.yml +71 -0
- package/rules/store-no-runinaction.yml +60 -0
- package/rules/store-no-setinterval-poll-tsx.yml +62 -0
- package/rules/store-no-setinterval-poll.yml +51 -0
- package/rules/ts-pattern-dangling-match-tsx.yml +41 -0
- package/rules/ts-pattern-dangling-match.yml +63 -0
- package/rules/ui-imports-app-store.yml +49 -0
- package/rules/view-file-logic.yml +53 -0
- package/rules/view-imports-store.yml +64 -0
- package/rules/zod-optional-nullable-tsx.yml +56 -0
- package/rules/zod-optional-nullable.yml +29 -0
- package/sgconfig.yml +8 -0
- package/structure/rules/component-decl.yml +48 -0
- package/structure/sgconfig.yml +7 -0
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { aggregate } from "./metricsAggregate.js";
|
|
4
|
+
import { appendSnapshot, checkAgainstBaseline, GATED_KEYS, readBaseline, updateGated, writeBaseline, } from "./metricsBaseline.js";
|
|
5
|
+
import { collectMetrics } from "./metricsCollect.js";
|
|
6
|
+
function headCommit(cwd) {
|
|
7
|
+
const res = spawnSync("git", ["-C", cwd, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
8
|
+
if (res.status !== 0 || res.error)
|
|
9
|
+
return null;
|
|
10
|
+
return res.stdout.trim();
|
|
11
|
+
}
|
|
12
|
+
function printReport(metrics) {
|
|
13
|
+
const { gated, observed } = metrics;
|
|
14
|
+
console.log(`guardrails metrics — ${observed.componentCount} component(s) in ${observed.fileCount} file(s)`);
|
|
15
|
+
console.log("gated (ratchet set, lower = better):");
|
|
16
|
+
for (const key of GATED_KEYS)
|
|
17
|
+
console.log(` ${key.padEnd(22)} ${gated[key]}`);
|
|
18
|
+
console.log("observed:");
|
|
19
|
+
console.log(` component_loc p50/p90/max ${observed.p50ComponentLoc}/${gated.p90ComponentLoc}/${observed.maxComponentLoc} (${observed.pctComponentsLe80}% ≤80)`);
|
|
20
|
+
console.log(` hooks total useState=${observed.hooksTotal.useState} useEffect=${observed.hooksTotal.useEffect} useMemo=${observed.hooksTotal.useMemo} useCallback=${observed.hooksTotal.useCallback} useRef=${observed.hooksTotal.useRef}`);
|
|
21
|
+
console.log(` observer_wrapped ${observed.observerWrappedPct}% avg props ${observed.avgPropsCount} max JSX depth ${observed.maxJsxDepth}`);
|
|
22
|
+
console.log(` exhaustive_ratio ${observed.exhaustiveRatio ?? "n/a (no match sites)"} (${observed.matchSites} match sites, ${observed.otherwiseCount} .otherwise)`);
|
|
23
|
+
console.log(` jsx branching ternary=${observed.ternaryInJsxCount} &&=${observed.andInJsxCount}`);
|
|
24
|
+
console.log(` file_sloc p50 ${observed.p50FileSloc}`);
|
|
25
|
+
}
|
|
26
|
+
export function runMetrics(opts) {
|
|
27
|
+
if (opts.check && opts.updateBaseline) {
|
|
28
|
+
console.error("guardrails metrics: --check and --update-baseline are mutually exclusive");
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
const baselinePath = opts.baselinePath ?? join(cwd, ".guardrails", "metrics.json");
|
|
33
|
+
const snapshotPath = join(cwd, ".guardrails", "metrics-snapshots.jsonl");
|
|
34
|
+
const targets = opts.targets.length > 0 ? opts.targets : ["."];
|
|
35
|
+
const metrics = aggregate(collectMetrics(targets));
|
|
36
|
+
const commit = headCommit(cwd);
|
|
37
|
+
if (opts.snapshot) {
|
|
38
|
+
appendSnapshot(snapshotPath, metrics.gated, metrics.observed, commit);
|
|
39
|
+
}
|
|
40
|
+
if (opts.updateBaseline) {
|
|
41
|
+
const old = readBaseline(baselinePath);
|
|
42
|
+
const result = updateGated(old?.gated, metrics.gated, opts.force);
|
|
43
|
+
writeBaseline(baselinePath, result.next, metrics.observed, commit);
|
|
44
|
+
if (opts.json) {
|
|
45
|
+
console.log(JSON.stringify({
|
|
46
|
+
command: "metrics",
|
|
47
|
+
action: "update-baseline",
|
|
48
|
+
baselinePath,
|
|
49
|
+
...result,
|
|
50
|
+
current: metrics.gated,
|
|
51
|
+
}, null, 2));
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
console.log(old === undefined
|
|
55
|
+
? `guardrails metrics: baseline created at ${baselinePath}`
|
|
56
|
+
: `guardrails metrics: baseline updated at ${baselinePath}${opts.force ? " (--force: set to current values)" : ""}`);
|
|
57
|
+
for (const k of result.tightened)
|
|
58
|
+
console.log(` tightened ${k} → ${result.next[k]}`);
|
|
59
|
+
for (const k of result.withinHysteresis) {
|
|
60
|
+
console.log(` kept ${k} (improved <2% — hysteresis holds the old ceiling)`);
|
|
61
|
+
}
|
|
62
|
+
for (const k of result.worse) {
|
|
63
|
+
console.log(` kept ${k} (current ${metrics.gated[k]} is WORSE than baseline ${result.next[k]} — not loosening without --force)`);
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
if (opts.check) {
|
|
68
|
+
const baseline = readBaseline(baselinePath);
|
|
69
|
+
if (baseline === undefined) {
|
|
70
|
+
console.error(`guardrails metrics --check: no baseline at ${baselinePath} — create one with \`guardrails metrics --update-baseline\` and commit it.`);
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
const regressions = checkAgainstBaseline(metrics.gated, baseline.gated);
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
command: "metrics",
|
|
77
|
+
action: "check",
|
|
78
|
+
baselinePath,
|
|
79
|
+
ok: regressions.length === 0,
|
|
80
|
+
regressions,
|
|
81
|
+
current: metrics.gated,
|
|
82
|
+
baseline: baseline.gated,
|
|
83
|
+
}, null, 2));
|
|
84
|
+
return regressions.length === 0 ? 0 : 1;
|
|
85
|
+
}
|
|
86
|
+
if (regressions.length === 0) {
|
|
87
|
+
console.log(`guardrails metrics --check: OK — no gated metric regressed vs ${baselinePath} (baseline commit ${baseline.commit?.slice(0, 12) ?? "unknown"})`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
console.log("guardrails metrics --check: RATCHET VIOLATION — gated metrics regressed:");
|
|
91
|
+
for (const r of regressions) {
|
|
92
|
+
console.log(` ${r.metric.padEnd(22)} baseline ${r.baseline} → current ${r.current}`);
|
|
93
|
+
}
|
|
94
|
+
console.log("Improve the code back under the baseline. Loosening the baseline itself requires\n`guardrails metrics --update-baseline --force` and belongs in a reviewed commit.");
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
console.log(JSON.stringify({
|
|
99
|
+
command: "metrics",
|
|
100
|
+
targets,
|
|
101
|
+
gated: metrics.gated,
|
|
102
|
+
observed: metrics.observed,
|
|
103
|
+
components: metrics.components,
|
|
104
|
+
files: metrics.files,
|
|
105
|
+
}, null, 2));
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
printReport(metrics);
|
|
109
|
+
if (opts.snapshot)
|
|
110
|
+
console.log(`snapshot appended to ${snapshotPath}`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CollectedMetrics } from "./metricsCollect.js";
|
|
2
|
+
import type { ProjectMetrics } from "./metricsTypes.js";
|
|
3
|
+
/** Nearest-rank percentile over an unsorted sample; 0 for an empty sample. */
|
|
4
|
+
export declare function percentile(values: number[], p: number): number;
|
|
5
|
+
export declare function aggregate(collected: CollectedMetrics): ProjectMetrics;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Nearest-rank percentile over an unsorted sample; 0 for an empty sample. */
|
|
2
|
+
export function percentile(values, p) {
|
|
3
|
+
if (values.length === 0)
|
|
4
|
+
return 0;
|
|
5
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6
|
+
const rank = Math.max(1, Math.ceil((p / 100) * sorted.length));
|
|
7
|
+
return sorted[rank - 1] ?? 0;
|
|
8
|
+
}
|
|
9
|
+
const round = (n, places = 4) => {
|
|
10
|
+
const f = 10 ** places;
|
|
11
|
+
return Math.round(n * f) / f;
|
|
12
|
+
};
|
|
13
|
+
export function aggregate(collected) {
|
|
14
|
+
const { components, files, store, inlineMapRowCount } = collected;
|
|
15
|
+
const locs = components.map((c) => c.componentLoc);
|
|
16
|
+
const contextCosts = files.map((f) => f.contextCost);
|
|
17
|
+
const n = components.length;
|
|
18
|
+
const hooksTotal = {
|
|
19
|
+
useState: 0,
|
|
20
|
+
useEffect: 0,
|
|
21
|
+
useMemo: 0,
|
|
22
|
+
useCallback: 0,
|
|
23
|
+
useRef: 0,
|
|
24
|
+
};
|
|
25
|
+
let propsTotal = 0;
|
|
26
|
+
let observerWrapped = 0;
|
|
27
|
+
let maxJsxDepth = 0;
|
|
28
|
+
let matchSites = 0;
|
|
29
|
+
let exhaustive = 0;
|
|
30
|
+
let otherwise = 0;
|
|
31
|
+
let ternaryInJsx = 0;
|
|
32
|
+
let andInJsx = 0;
|
|
33
|
+
for (const c of components) {
|
|
34
|
+
for (const k of Object.keys(hooksTotal))
|
|
35
|
+
hooksTotal[k] += c.hooks[k];
|
|
36
|
+
propsTotal += c.propsCount;
|
|
37
|
+
if (c.observerWrapped)
|
|
38
|
+
observerWrapped += 1;
|
|
39
|
+
if (c.jsxMaxDepth > maxJsxDepth)
|
|
40
|
+
maxJsxDepth = c.jsxMaxDepth;
|
|
41
|
+
matchSites += c.branching.match;
|
|
42
|
+
exhaustive += c.branching.exhaustive;
|
|
43
|
+
otherwise += c.branching.otherwise;
|
|
44
|
+
ternaryInJsx += c.branching.ternaryInJsx;
|
|
45
|
+
andInJsx += c.branching.andInJsx;
|
|
46
|
+
}
|
|
47
|
+
const gated = {
|
|
48
|
+
p90ComponentLoc: percentile(locs, 90),
|
|
49
|
+
useStateDensity: n === 0 ? 0 : round(hooksTotal.useState / n),
|
|
50
|
+
inlineMapRowCount,
|
|
51
|
+
p90ContextCost: percentile(contextCosts, 90),
|
|
52
|
+
runInActionCount: store.runInActionCount,
|
|
53
|
+
asyncInStore: store.asyncInStore,
|
|
54
|
+
newMapInStore: store.newMapInStore,
|
|
55
|
+
reactionsTotal: store.reactionsTotal,
|
|
56
|
+
loadingBooleanShapes: store.loadingBooleanShapes,
|
|
57
|
+
};
|
|
58
|
+
const observed = {
|
|
59
|
+
componentCount: n,
|
|
60
|
+
fileCount: files.length,
|
|
61
|
+
p50ComponentLoc: percentile(locs, 50),
|
|
62
|
+
maxComponentLoc: locs.length === 0 ? 0 : Math.max(...locs),
|
|
63
|
+
pctComponentsLe80: n === 0 ? 100 : round((100 * locs.filter((l) => l <= 80).length) / n, 1),
|
|
64
|
+
observerWrappedPct: n === 0 ? 0 : round((100 * observerWrapped) / n, 1),
|
|
65
|
+
hooksTotal,
|
|
66
|
+
avgPropsCount: n === 0 ? 0 : round(propsTotal / n),
|
|
67
|
+
maxJsxDepth,
|
|
68
|
+
exhaustiveRatio: matchSites === 0 ? null : round(exhaustive / matchSites),
|
|
69
|
+
matchSites,
|
|
70
|
+
otherwiseCount: otherwise,
|
|
71
|
+
ternaryInJsxCount: ternaryInJsx,
|
|
72
|
+
andInJsxCount: andInJsx,
|
|
73
|
+
p50FileSloc: percentile(files.map((f) => f.fileSloc), 50),
|
|
74
|
+
p90ContextCostObserved: percentile(contextCosts, 90),
|
|
75
|
+
};
|
|
76
|
+
return { gated, observed, components, files };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=metricsAggregate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsAggregate.js","sourceRoot":"","sources":["../src/metricsAggregate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Baseline, GatedMetrics, ObservedMetrics } from "./metricsTypes.js";
|
|
2
|
+
export declare const HYSTERESIS = 0.02;
|
|
3
|
+
export declare const GATED_KEYS: (keyof GatedMetrics)[];
|
|
4
|
+
export interface Regression {
|
|
5
|
+
metric: keyof GatedMetrics;
|
|
6
|
+
baseline: number;
|
|
7
|
+
current: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function readBaseline(path: string): Baseline | undefined;
|
|
10
|
+
export declare function checkAgainstBaseline(current: GatedMetrics, baseline: GatedMetrics): Regression[];
|
|
11
|
+
export interface UpdateResult {
|
|
12
|
+
next: GatedMetrics;
|
|
13
|
+
tightened: (keyof GatedMetrics)[];
|
|
14
|
+
/** improved, but by less than the hysteresis — kept the old ceiling */
|
|
15
|
+
withinHysteresis: (keyof GatedMetrics)[];
|
|
16
|
+
/** currently worse than baseline — kept the old ceiling (no silent loosening) */
|
|
17
|
+
worse: (keyof GatedMetrics)[];
|
|
18
|
+
}
|
|
19
|
+
export declare function updateGated(old: GatedMetrics | undefined, current: GatedMetrics, force: boolean): UpdateResult;
|
|
20
|
+
export declare function writeBaseline(path: string, gated: GatedMetrics, observed: ObservedMetrics, commit: string | null): void;
|
|
21
|
+
export declare function appendSnapshot(path: string, gated: GatedMetrics, observed: ObservedMetrics, commit: string | null): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
// Ratchet mechanics (D7). Direction for every gated metric is LOWER = BETTER.
|
|
4
|
+
// `--check` always RECOMPUTES the facts and compares against the committed
|
|
5
|
+
// baseline file — it never believes the file's own numbers. Tightening happens
|
|
6
|
+
// only through `--update-baseline`, and only when a metric improved by more
|
|
7
|
+
// than the hysteresis (~2%), so a noise-level fluctuation cannot latch an
|
|
8
|
+
// accidental ceiling; `--force` overrides in both directions (loosening a
|
|
9
|
+
// baseline is a reviewed decision visible in the diff, not something the tool
|
|
10
|
+
// does on its own).
|
|
11
|
+
export const HYSTERESIS = 0.02;
|
|
12
|
+
const EPS = 1e-9;
|
|
13
|
+
export const GATED_KEYS = [
|
|
14
|
+
"p90ComponentLoc",
|
|
15
|
+
"useStateDensity",
|
|
16
|
+
"inlineMapRowCount",
|
|
17
|
+
"p90ContextCost",
|
|
18
|
+
"runInActionCount",
|
|
19
|
+
"asyncInStore",
|
|
20
|
+
"newMapInStore",
|
|
21
|
+
"reactionsTotal",
|
|
22
|
+
"loadingBooleanShapes",
|
|
23
|
+
];
|
|
24
|
+
export function readBaseline(path) {
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return undefined;
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
28
|
+
if (parsed.version !== 1 || typeof parsed.gated !== "object") {
|
|
29
|
+
throw new Error(`baseline at ${path} has an unrecognised shape — regenerate with --update-baseline --force`);
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
export function checkAgainstBaseline(current, baseline) {
|
|
34
|
+
const regressions = [];
|
|
35
|
+
for (const key of GATED_KEYS) {
|
|
36
|
+
const base = baseline[key] ?? 0;
|
|
37
|
+
const cur = current[key];
|
|
38
|
+
if (cur > base + EPS + Math.abs(base) * EPS) {
|
|
39
|
+
regressions.push({ metric: key, baseline: base, current: cur });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return regressions;
|
|
43
|
+
}
|
|
44
|
+
export function updateGated(old, current, force) {
|
|
45
|
+
if (old === undefined || force) {
|
|
46
|
+
return {
|
|
47
|
+
next: { ...current },
|
|
48
|
+
tightened: force ? [] : GATED_KEYS,
|
|
49
|
+
withinHysteresis: [],
|
|
50
|
+
worse: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const next = { ...old };
|
|
54
|
+
const tightened = [];
|
|
55
|
+
const withinHysteresis = [];
|
|
56
|
+
const worse = [];
|
|
57
|
+
for (const key of GATED_KEYS) {
|
|
58
|
+
const base = old[key] ?? 0;
|
|
59
|
+
const cur = current[key];
|
|
60
|
+
if (cur < base * (1 - HYSTERESIS) - EPS) {
|
|
61
|
+
next[key] = cur;
|
|
62
|
+
tightened.push(key);
|
|
63
|
+
}
|
|
64
|
+
else if (cur > base + EPS) {
|
|
65
|
+
worse.push(key);
|
|
66
|
+
}
|
|
67
|
+
else if (cur < base - EPS) {
|
|
68
|
+
withinHysteresis.push(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { next, tightened, withinHysteresis, worse };
|
|
72
|
+
}
|
|
73
|
+
export function writeBaseline(path, gated, observed, commit) {
|
|
74
|
+
const baseline = {
|
|
75
|
+
version: 1,
|
|
76
|
+
updatedAt: new Date().toISOString(),
|
|
77
|
+
commit,
|
|
78
|
+
gated,
|
|
79
|
+
observed,
|
|
80
|
+
};
|
|
81
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
+
writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
83
|
+
}
|
|
84
|
+
export function appendSnapshot(path, gated, observed, commit) {
|
|
85
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
86
|
+
appendFileSync(path, `${JSON.stringify({ timestamp: new Date().toISOString(), commit, gated, observed })}\n`);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=metricsBaseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsBaseline.js","sourceRoot":"","sources":["../src/metricsBaseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentMetrics, FileMetrics, StoreMetrics } from "./metricsTypes.js";
|
|
2
|
+
export interface CollectedMetrics {
|
|
3
|
+
components: ComponentMetrics[];
|
|
4
|
+
files: FileMetrics[];
|
|
5
|
+
store: StoreMetrics;
|
|
6
|
+
inlineMapRowCount: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function collectMetrics(targets: string[]): CollectedMetrics;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { scan } from "./astGrep.js";
|
|
5
|
+
import { collectFiles } from "./fileWalk.js";
|
|
6
|
+
import { rulesConfig, structureConfig } from "./packagePaths.js";
|
|
7
|
+
import { classifyLines, codeLinesInRange } from "./sourceLines.js";
|
|
8
|
+
// Production code only — same exclusions the vault audit used (§1: "тесты/
|
|
9
|
+
// showcase исключены"), so numbers stay comparable to the audit baselines.
|
|
10
|
+
const EXCLUDED_FILE = /\.(test|spec|stories)\.[jt]sx?$|__tests__|\/showcase\/|\.d\.ts$/;
|
|
11
|
+
const HOOK_NAMES = ["useState", "useEffect", "useMemo", "useCallback", "useRef"];
|
|
12
|
+
// Async-progress flag names. Deliberately NOT every boolean (sidebarVisible,
|
|
13
|
+
// dialogOpen are legitimate UI state) — only the loading-machine flags that a
|
|
14
|
+
// Resource<T>/QueryState union should own.
|
|
15
|
+
const PROGRESS_FLAG = /^(is)?\w*([lL]oading|[pP]ending|[fF]etching|[sS]aving|[bB]usy|[rR]efreshing|[sS]canning|[eE]xporting|[iI]mporting|[sS]yncing|[sS]ubmitting|[pP]rocessing|[gG]enerating|[bB]uilding|[cC]onnecting)$/;
|
|
16
|
+
/**
|
|
17
|
+
* Metrics describe the PROJECT — its tracked source — so gitignored files
|
|
18
|
+
* (generated code, local build output) are dropped. Without this the numbers
|
|
19
|
+
* depend on which machine ran them: a fresh CI checkout has no generated
|
|
20
|
+
* files, a dev tree does, and the p90s disagree (observed: p90ContextCost
|
|
21
|
+
* 20 locally vs 21 in CI on the same commit). The ast-grep-backed counters
|
|
22
|
+
* were already deterministic — ast-grep honors .gitignore natively.
|
|
23
|
+
*/
|
|
24
|
+
function dropGitignored(files) {
|
|
25
|
+
if (files.length === 0)
|
|
26
|
+
return files;
|
|
27
|
+
const res = spawnSync("git", ["check-ignore", "--stdin"], {
|
|
28
|
+
input: files.join("\n"),
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
31
|
+
});
|
|
32
|
+
// Outside a repo (or git missing) there is no ignore standard to apply.
|
|
33
|
+
if (res.error || res.status === null || res.status > 1)
|
|
34
|
+
return files;
|
|
35
|
+
const ignored = new Set(res.stdout.split("\n").filter((l) => l !== ""));
|
|
36
|
+
return files.filter((f) => !ignored.has(f));
|
|
37
|
+
}
|
|
38
|
+
function parse(file, text) {
|
|
39
|
+
const kind = file.endsWith(".tsx") || file.endsWith(".jsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
40
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
|
|
41
|
+
}
|
|
42
|
+
function lineOf(sf, node) {
|
|
43
|
+
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
44
|
+
}
|
|
45
|
+
function calleeName(node) {
|
|
46
|
+
if (ts.isIdentifier(node.expression))
|
|
47
|
+
return node.expression.text;
|
|
48
|
+
if (ts.isPropertyAccessExpression(node.expression))
|
|
49
|
+
return node.expression.name.text;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Unwrap `observer(fn)`, `memo(observer(fn))` … down to the function itself. */
|
|
53
|
+
function componentFunction(node) {
|
|
54
|
+
if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
55
|
+
return node;
|
|
56
|
+
}
|
|
57
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
58
|
+
const first = node.arguments[0];
|
|
59
|
+
if (first !== undefined)
|
|
60
|
+
return componentFunction(first);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function isObserverWrapped(node) {
|
|
65
|
+
if (!ts.isCallExpression(node))
|
|
66
|
+
return false;
|
|
67
|
+
if (calleeName(node) === "observer")
|
|
68
|
+
return true;
|
|
69
|
+
const first = node.arguments[0];
|
|
70
|
+
return first !== undefined && isObserverWrapped(first);
|
|
71
|
+
}
|
|
72
|
+
function propsCountOf(fn) {
|
|
73
|
+
const param = fn?.parameters[0];
|
|
74
|
+
if (param === undefined || !ts.isObjectBindingPattern(param.name))
|
|
75
|
+
return 0;
|
|
76
|
+
return param.name.elements.length;
|
|
77
|
+
}
|
|
78
|
+
function walkComponent(root) {
|
|
79
|
+
const hooks = { useState: 0, useEffect: 0, useMemo: 0, useCallback: 0, useRef: 0 };
|
|
80
|
+
const branching = {
|
|
81
|
+
match: 0,
|
|
82
|
+
exhaustive: 0,
|
|
83
|
+
otherwise: 0,
|
|
84
|
+
ternaryInJsx: 0,
|
|
85
|
+
andInJsx: 0,
|
|
86
|
+
};
|
|
87
|
+
let jsxMaxDepth = 0;
|
|
88
|
+
const visit = (node, jsxDepth, inJsxExpr) => {
|
|
89
|
+
let depth = jsxDepth;
|
|
90
|
+
let inExpr = inJsxExpr;
|
|
91
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
|
|
92
|
+
depth += 1;
|
|
93
|
+
if (depth > jsxMaxDepth)
|
|
94
|
+
jsxMaxDepth = depth;
|
|
95
|
+
inExpr = false;
|
|
96
|
+
}
|
|
97
|
+
else if (ts.isJsxExpression(node)) {
|
|
98
|
+
inExpr = true;
|
|
99
|
+
}
|
|
100
|
+
if (ts.isCallExpression(node)) {
|
|
101
|
+
const name = calleeName(node);
|
|
102
|
+
if (name !== undefined) {
|
|
103
|
+
for (const h of HOOK_NAMES) {
|
|
104
|
+
if (name === h)
|
|
105
|
+
hooks[h] += 1;
|
|
106
|
+
}
|
|
107
|
+
if (name === "match" && ts.isIdentifier(node.expression))
|
|
108
|
+
branching.match += 1;
|
|
109
|
+
if (name === "exhaustive" && ts.isPropertyAccessExpression(node.expression)) {
|
|
110
|
+
branching.exhaustive += 1;
|
|
111
|
+
}
|
|
112
|
+
if (name === "otherwise" && ts.isPropertyAccessExpression(node.expression)) {
|
|
113
|
+
branching.otherwise += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (ts.isConditionalExpression(node) && inExpr)
|
|
118
|
+
branching.ternaryInJsx += 1;
|
|
119
|
+
if (ts.isBinaryExpression(node) &&
|
|
120
|
+
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
121
|
+
inExpr) {
|
|
122
|
+
branching.andInJsx += 1;
|
|
123
|
+
}
|
|
124
|
+
node.forEachChild((child) => visit(child, depth, inExpr));
|
|
125
|
+
};
|
|
126
|
+
visit(root, 0, false);
|
|
127
|
+
return { hooks, branching, jsxMaxDepth };
|
|
128
|
+
}
|
|
129
|
+
/** Find the declaration node for a component-decl marker match (by name, then line). */
|
|
130
|
+
function declNodeFor(sf, name, line) {
|
|
131
|
+
let byName;
|
|
132
|
+
let byLine;
|
|
133
|
+
const consider = (node, declName) => {
|
|
134
|
+
if (declName !== name)
|
|
135
|
+
return;
|
|
136
|
+
byName ??= node;
|
|
137
|
+
if (lineOf(sf, node) === line)
|
|
138
|
+
byLine ??= node;
|
|
139
|
+
};
|
|
140
|
+
for (const stmt of sf.statements) {
|
|
141
|
+
if (ts.isFunctionDeclaration(stmt))
|
|
142
|
+
consider(stmt, stmt.name?.text);
|
|
143
|
+
if (ts.isVariableStatement(stmt)) {
|
|
144
|
+
for (const d of stmt.declarationList.declarations) {
|
|
145
|
+
if (ts.isIdentifier(d.name))
|
|
146
|
+
consider(d, d.name.text);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return byLine ?? byName;
|
|
151
|
+
}
|
|
152
|
+
function fileImportedIdentifiers(sf) {
|
|
153
|
+
let count = 0;
|
|
154
|
+
for (const stmt of sf.statements) {
|
|
155
|
+
if (!ts.isImportDeclaration(stmt))
|
|
156
|
+
continue;
|
|
157
|
+
const clause = stmt.importClause;
|
|
158
|
+
if (clause === undefined)
|
|
159
|
+
continue;
|
|
160
|
+
if (clause.name !== undefined)
|
|
161
|
+
count += 1;
|
|
162
|
+
const bindings = clause.namedBindings;
|
|
163
|
+
if (bindings === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
if (ts.isNamespaceImport(bindings))
|
|
166
|
+
count += 1;
|
|
167
|
+
else
|
|
168
|
+
count += bindings.elements.length;
|
|
169
|
+
}
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
function importLineSet(sf) {
|
|
173
|
+
const lines = new Set();
|
|
174
|
+
for (const stmt of sf.statements) {
|
|
175
|
+
if (!ts.isImportDeclaration(stmt))
|
|
176
|
+
continue;
|
|
177
|
+
const start = sf.getLineAndCharacterOfPosition(stmt.getStart(sf)).line;
|
|
178
|
+
const end = sf.getLineAndCharacterOfPosition(stmt.getEnd()).line;
|
|
179
|
+
for (let l = start; l <= end; l++)
|
|
180
|
+
lines.add(l + 1);
|
|
181
|
+
}
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
// runInAction / async-method / new-Map counters moved to the canon rule ids
|
|
185
|
+
// (store-no-runinaction, store-async-method, store-new-map) — see
|
|
186
|
+
// collectMetrics. Only what no rule covers yet stays as a direct AST count.
|
|
187
|
+
function collectStoreMetrics(sf, store) {
|
|
188
|
+
const visit = (node) => {
|
|
189
|
+
if (ts.isCallExpression(node)) {
|
|
190
|
+
const name = calleeName(node);
|
|
191
|
+
// No canon rule counts reactions yet — direct AST count until one lands.
|
|
192
|
+
if ((name === "reaction" || name === "autorun") && ts.isIdentifier(node.expression)) {
|
|
193
|
+
store.reactionsTotal += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// A boolean-shaped async-progress DECLARATION (`loading: boolean`,
|
|
197
|
+
// `uploading = false`) — the §14 antipattern Resource<T>/QueryState
|
|
198
|
+
// replaces. Declarations, not write sites: the number of flags measures
|
|
199
|
+
// the state design; counting every `this.loading = …` would just scale
|
|
200
|
+
// with method count.
|
|
201
|
+
if ((ts.isPropertySignature(node) || ts.isPropertyDeclaration(node)) &&
|
|
202
|
+
ts.isIdentifier(node.name) &&
|
|
203
|
+
PROGRESS_FLAG.test(node.name.text) &&
|
|
204
|
+
(node.type?.kind === ts.SyntaxKind.BooleanKeyword ||
|
|
205
|
+
(ts.isPropertyDeclaration(node) &&
|
|
206
|
+
node.type === undefined &&
|
|
207
|
+
node.initializer !== undefined &&
|
|
208
|
+
(node.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
209
|
+
node.initializer.kind === ts.SyntaxKind.FalseKeyword)))) {
|
|
210
|
+
store.loadingBooleanShapes += 1;
|
|
211
|
+
}
|
|
212
|
+
node.forEachChild(visit);
|
|
213
|
+
};
|
|
214
|
+
visit(sf);
|
|
215
|
+
}
|
|
216
|
+
export function collectMetrics(targets) {
|
|
217
|
+
const allFiles = dropGitignored(collectFiles(targets, [".ts", ".tsx", ".jsx"]).filter((f) => !EXCLUDED_FILE.test(f)));
|
|
218
|
+
// Component spans come from the same ast-grep marker rule the structure
|
|
219
|
+
// check uses — one definition of "a component" across the whole toolkit.
|
|
220
|
+
const decls = scan(structureConfig, targets).filter((r) => r.ruleId === "component-decl" && !EXCLUDED_FILE.test(r.file));
|
|
221
|
+
const declsByFile = new Map();
|
|
222
|
+
for (const d of decls) {
|
|
223
|
+
const list = declsByFile.get(d.file) ?? [];
|
|
224
|
+
list.push(d);
|
|
225
|
+
declsByFile.set(d.file, list);
|
|
226
|
+
}
|
|
227
|
+
// One scan of the full rule canon feeds every rule-backed counter. The store
|
|
228
|
+
// counters use the canon rules' definition of "a store" (a class calling
|
|
229
|
+
// make(Auto)Observable) rather than a name or path heuristic — the metric
|
|
230
|
+
// counts exactly what verify gates.
|
|
231
|
+
const ruleRows = scan(rulesConfig, targets).filter((r) => !EXCLUDED_FILE.test(r.file));
|
|
232
|
+
const ruleCount = (...ids) => ruleRows.filter((r) => ids.includes(r.ruleId)).length;
|
|
233
|
+
const inlineMapRowCount = ruleCount("inline-map-row");
|
|
234
|
+
const components = [];
|
|
235
|
+
const files = [];
|
|
236
|
+
const store = {
|
|
237
|
+
runInActionCount: ruleCount("store-no-runinaction", "store-no-runinaction-tsx"),
|
|
238
|
+
asyncInStore: ruleCount("store-async-method", "store-async-method-tsx"),
|
|
239
|
+
newMapInStore: ruleCount("store-new-map", "store-new-map-tsx"),
|
|
240
|
+
reactionsTotal: 0,
|
|
241
|
+
loadingBooleanShapes: 0,
|
|
242
|
+
};
|
|
243
|
+
for (const file of allFiles) {
|
|
244
|
+
let text;
|
|
245
|
+
try {
|
|
246
|
+
text = readFileSync(file, "utf8");
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const sf = parse(file, text);
|
|
252
|
+
const kinds = classifyLines(text);
|
|
253
|
+
const importLines = importLineSet(sf);
|
|
254
|
+
let fileSloc = 0;
|
|
255
|
+
kinds.forEach((k, i) => {
|
|
256
|
+
if (k === "code" && !importLines.has(i + 1))
|
|
257
|
+
fileSloc += 1;
|
|
258
|
+
});
|
|
259
|
+
collectStoreMetrics(sf, store);
|
|
260
|
+
const fileDecls = declsByFile.get(file) ?? [];
|
|
261
|
+
let filePropsTotal = 0;
|
|
262
|
+
for (const decl of fileDecls) {
|
|
263
|
+
const name = decl.metaText("N") ?? "?";
|
|
264
|
+
const node = declNodeFor(sf, name, decl.startLine);
|
|
265
|
+
if (node === undefined)
|
|
266
|
+
continue;
|
|
267
|
+
const target = ts.isVariableDeclaration(node) ? (node.initializer ?? node) : node;
|
|
268
|
+
const fn = componentFunction(target);
|
|
269
|
+
const props = propsCountOf(fn);
|
|
270
|
+
filePropsTotal += props;
|
|
271
|
+
const { hooks, branching, jsxMaxDepth } = walkComponent(target);
|
|
272
|
+
components.push({
|
|
273
|
+
file,
|
|
274
|
+
name,
|
|
275
|
+
line: decl.startLine,
|
|
276
|
+
componentLoc: codeLinesInRange(kinds, decl.startLine, decl.startLine + decl.spanLines - 1),
|
|
277
|
+
hooks,
|
|
278
|
+
propsCount: props,
|
|
279
|
+
observerWrapped: isObserverWrapped(target),
|
|
280
|
+
jsxMaxDepth,
|
|
281
|
+
branching,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const importedIdentifiers = fileImportedIdentifiers(sf);
|
|
285
|
+
files.push({
|
|
286
|
+
file,
|
|
287
|
+
fileSloc,
|
|
288
|
+
componentsPerFile: fileDecls.length,
|
|
289
|
+
importedIdentifiers,
|
|
290
|
+
contextCost: importedIdentifiers + filePropsTotal,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return { components, files, store, inlineMapRowCount };
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=metricsCollect.js.map
|