@piklv/ftaql-cli 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.
@@ -0,0 +1,140 @@
1
+ declare module "@piklv/ftaql-cli" {
2
+ /**
3
+ * Полная структура метрик и анализа для одного исходного файла (1:1 с Rust FileData).
4
+ */
5
+ export type AnalyzedFile = {
6
+ /**
7
+ * Полный путь или имя исходного файла.
8
+ */
9
+ file_name: string;
10
+ /**
11
+ * Метрики размера файла.
12
+ */
13
+ size_metrics: {
14
+ /** Количество строк кода в файле (без пустых строк и, опционально, комментариев). */
15
+ line_count: number;
16
+ };
17
+ /**
18
+ * Метрики сложности файла.
19
+ */
20
+ complexity_metrics: {
21
+ /** Цикломатическая сложность (количество независимых путей исполнения). */
22
+ cyclomatic: number;
23
+ /** Метрики Холстеда — количественная оценка сложности кода. */
24
+ halstead: {
25
+ uniq_operators: number;
26
+ uniq_operands: number;
27
+ total_operators: number;
28
+ total_operands: number;
29
+ program_length: number;
30
+ vocabulary_size: number;
31
+ volume: number;
32
+ difficulty: number;
33
+ effort: number;
34
+ time: number;
35
+ bugs: number;
36
+ };
37
+ };
38
+ /**
39
+ * Метрики связанности (coupling) для файла. Может отсутствовать или быть null, если не применимо.
40
+ */
41
+ coupling_metrics?: {
42
+ /** Входящая связанность (afferent coupling, Ca): сколько других файлов зависят от этого файла. */
43
+ afferent_coupling: number;
44
+ /** Исходящая связанность (efferent coupling, Ce): от скольких файлов зависит этот файл. */
45
+ efferent_coupling: number;
46
+ /** Instability: Ce / (Ca + Ce). Чем ближе к 1, тем менее устойчив модуль. */
47
+ instability: number;
48
+ /** Сила зависимости: карта "путь к файлу" → "количество импортируемых идентификаторов". */
49
+ dependency_strength: Record<string, number>;
50
+ /** Информация о циклах в графе зависимостей. */
51
+ cycles?: {
52
+ /** ID цикла, в котором участвует файл. Ссылается на `project_analysis.cycles`. */
53
+ cycle_id?: number;
54
+ /** ID runtime-цикла, если цикл существует во время выполнения. Ссылается на `project_analysis.runtime_cycles`. */
55
+ runtime_cycle_id?: number;
56
+ };
57
+ } | null;
58
+ /**
59
+ * Итоговые оценки файла.
60
+ */
61
+ scores: {
62
+ /** Итоговый File Score (чем ниже, тем лучше). */
63
+ file_score: number;
64
+ /** Итоговый Coupling Score (чем ниже, тем лучше). */
65
+ coupling_score: number;
66
+ };
67
+ };
68
+
69
+ /**
70
+ * Информация о циклических зависимостях (сильно связанных компонентах) в проекте.
71
+ */
72
+ export type CycleInfo = {
73
+ /** Уникальный идентификатор цикла. */
74
+ id: number;
75
+ /** Количество файлов в цикле. */
76
+ size: number;
77
+ /**
78
+ * Граф цикла, где ключи верхнего и вложенного уровней - это индексы из
79
+ * `project_analysis.cycle_members`, а значения - вес связи между файлами.
80
+ */
81
+ graph: Record<string, Record<string, number>>;
82
+ };
83
+
84
+ /**
85
+ * Глобальные метрики и анализ для всего проекта.
86
+ */
87
+ export type ProjectAnalysis = {
88
+ /** Общий отсортированный список файлов, участвующих хотя бы в одном цикле. */
89
+ cycle_members: string[];
90
+ /** Список всех обнаруженных в проекте циклических зависимостей. */
91
+ cycles: CycleInfo[];
92
+ /** Список циклов, которые сохраняются во время выполнения. */
93
+ runtime_cycles: CycleInfo[];
94
+ };
95
+
96
+ /**
97
+ * Корневой объект, возвращаемый FtaQl при анализе в формате JSON.
98
+ */
99
+ export type FtaQlJsonOutput = {
100
+ /** Глобальный анализ проекта. */
101
+ project_analysis: ProjectAnalysis;
102
+ /** Список проанализированных файлов. */
103
+ findings: AnalyzedFile[];
104
+ };
105
+
106
+ /**
107
+ * Options for persisting a project analysis snapshot into SQLite.
108
+ *
109
+ * `dbPath` is required. The remaining fields are stored as metadata on the analysis run.
110
+ */
111
+ export type FtaQlRunOptions = {
112
+ /**
113
+ * Path to the SQLite database file that should receive the snapshot.
114
+ */
115
+ dbPath: string;
116
+ /**
117
+ * Optional custom path to `ftaql.json`.
118
+ */
119
+ configPath?: string;
120
+ /**
121
+ * Optional revision identifier, for example a git commit SHA.
122
+ */
123
+ revision?: string;
124
+ /**
125
+ * Optional human-readable label such as branch or tag.
126
+ */
127
+ ref?: string;
128
+ };
129
+
130
+ /**
131
+ * Runs the project analysis for the given project and persists the result into SQLite.
132
+ *
133
+ * @param projectPath - The path to the root of the project to analyze
134
+ * @param options - SQLite persistence options. `dbPath` is required.
135
+ */
136
+ export function runFtaQl(
137
+ projectPath: string,
138
+ options: FtaQlRunOptions
139
+ ): string;
140
+ }
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # FtaQl
2
+
3
+ [English](https://github.com/pikulev/ftaql/blob/main/README.md) | [Русский](https://github.com/pikulev/ftaql/blob/main/README.ru.md)
4
+
5
+ FtaQl helps you see where risk is actually accumulating in a TS/JS project. It walks the repository, stores a snapshot in SQLite, and turns code analysis into plain SQL instead of guesswork.
6
+
7
+ After one run you can ask which files stay expensive across revisions, where coupling keeps growing, and when runtime cycles first appeared. That makes it useful for refactors, CI, and history analysis when you need an accumulated dataset instead of a one-off report.
8
+
9
+ The core is written in Rust, so repeated runs across large codebases and revision history stay practical. On Apple M1 hardware FtaQl can analyze up to **3000 files per second**.
10
+
11
+ ## What FtaQl Collects
12
+
13
+ For project-level analysis through the native CLI and Node wrapper, FtaQl stores:
14
+
15
+ | Metric / artifact | What it means |
16
+ | --- | --- |
17
+ | `file_score` | A composite score for a file based on its own metrics. |
18
+ | `coupling_score` | A composite score for relationship risk in the context of the whole project. |
19
+ | Cyclomatic complexity | How many independent execution paths the code contains. |
20
+ | Halstead metrics | Operator/operand metrics that help estimate code volume and complexity. |
21
+ | Afferent and efferent coupling | Who depends on the file, and how many files the file depends on. |
22
+ | Dependency strength | How tight module-to-module relationships are. |
23
+ | Full-graph cycles | Cycles in the whole project dependency graph, including type-only edges. |
24
+ | `runtime` cycles | Cycles over dependencies that actually participate in execution. |
25
+ | SQLite snapshots | Normalized runs for SQL queries and historical comparisons. |
26
+
27
+ Detailed formulas, caveats, and interpretation notes are documented in [`docs/scoring/en.md`](https://github.com/pikulev/ftaql/blob/main/docs/scoring/en.md).
28
+
29
+ A `runtime` cycle is a cyclic dependency through runtime entities. A full-graph cycle can include type-only dependencies, which may hurt tooling and architecture even when runtime is unaffected.
30
+
31
+ ## Quickstart: run -> persist -> query
32
+
33
+ Project-level analysis is available through the native CLI and the Node.js wrapper. The WASM build analyzes a single file and returns JSON for that file only.
34
+ In WASM, `coupling_score` is always `0.0`, and there is no project-level dependency graph or cycle analysis.
35
+
36
+ The basic loop is simple: persist a snapshot to SQLite, attach `revision` and `ref` when needed, then query the accumulated runs with SQL.
37
+
38
+ Persist the current checkout:
39
+
40
+ ```bash
41
+ npx @piklv/ftaql-cli path/to/project --db ./ftaql.sqlite
42
+ ```
43
+
44
+ Append revision metadata as you collect more snapshots:
45
+
46
+ ```bash
47
+ npx @piklv/ftaql-cli path/to/project \
48
+ --db ./ftaql.sqlite \
49
+ --revision "$(git rev-parse HEAD)" \
50
+ --ref main
51
+ ```
52
+
53
+ Use a custom config file when needed:
54
+
55
+ ```bash
56
+ npx @piklv/ftaql-cli path/to/project \
57
+ --db ./ftaql.sqlite \
58
+ --config-path ./path/to/ftaql.json
59
+ ```
60
+
61
+ Main options:
62
+
63
+ - `--db`
64
+ - `--config-path`
65
+ - `--revision`
66
+ - `--ref`
67
+
68
+ When you append multiple runs into the same SQLite database:
69
+
70
+ - `--revision` stores the exact snapshot identifier, usually a commit SHA
71
+ - `--ref` stores a human-readable branch, tag, or channel label such as `main`, `release/1.2`, or `nightly`
72
+ - using both lets you compare exact snapshots while still grouping runs by branch or release line
73
+
74
+ ## Querying the Snapshot History
75
+
76
+ Once snapshots are in SQLite, the interesting part starts.
77
+
78
+ Worst files in the latest snapshot:
79
+
80
+ ```sql
81
+ SELECT file_path, file_score, coupling_score
82
+ FROM files
83
+ WHERE run_id = (SELECT MAX(id) FROM analysis_runs)
84
+ ORDER BY file_score DESC, coupling_score DESC
85
+ LIMIT 20;
86
+ ```
87
+
88
+ If your team prefers buckets right away, you can layer empirical ranks on top of the same columns. This is not an official FtaQl scale, just one example of a project-level interpretation, so the thresholds should be tuned to your own codebase:
89
+
90
+ ```sql
91
+ SELECT
92
+ file_path,
93
+ file_score,
94
+ CASE
95
+ WHEN file_score <= 50 THEN 'OK'
96
+ WHEN file_score <= 60 THEN 'Could be better'
97
+ ELSE 'Needs improvement'
98
+ END AS file_rank,
99
+ coupling_score,
100
+ CASE
101
+ WHEN coupling_score <= 100 THEN 'OK'
102
+ WHEN coupling_score <= 200 THEN 'Could be better'
103
+ ELSE 'Needs improvement'
104
+ END AS coupling_rank
105
+ FROM files
106
+ WHERE run_id = (SELECT MAX(id) FROM analysis_runs)
107
+ ORDER BY file_score DESC, coupling_score DESC
108
+ LIMIT 20;
109
+ ```
110
+
111
+ Compare average `file_score` across revisions on one branch:
112
+
113
+ ```sql
114
+ SELECT ar.revision, AVG(f.file_score) AS avg_file_score
115
+ FROM analysis_runs ar
116
+ JOIN files f ON f.run_id = ar.id
117
+ WHERE ar.ref_label = 'main'
118
+ GROUP BY ar.revision
119
+ ORDER BY ar.created_at;
120
+ ```
121
+
122
+ Inspect runtime cycles for a specific revision:
123
+
124
+ ```sql
125
+ SELECT cf.cycle_id, cf.file_path
126
+ FROM cycle_files cf
127
+ JOIN analysis_runs ar ON ar.id = cf.run_id
128
+ WHERE ar.revision = 'abc123'
129
+ AND cf.cycle_kind = 'runtime'
130
+ ORDER BY cf.cycle_id, cf.file_path;
131
+ ```
132
+
133
+ Shape coupling hotspots into JSON for downstream tooling:
134
+
135
+ ```sql
136
+ SELECT json_group_array(
137
+ json_object(
138
+ 'file_path', hotspot.file_path,
139
+ 'file_score', hotspot.file_score,
140
+ 'coupling_score', hotspot.coupling_score
141
+ )
142
+ ) AS hotspots
143
+ FROM (
144
+ SELECT file_path, file_score, coupling_score
145
+ FROM files
146
+ WHERE run_id = (SELECT MAX(id) FROM analysis_runs)
147
+ ORDER BY coupling_score DESC, file_score DESC
148
+ LIMIT 10
149
+ ) AS hotspot;
150
+ ```
151
+
152
+ ## Typical Use Cases
153
+
154
+ - Snapshot a large TS/JS monorepo before and after a refactor, then query the worst files instead of guessing where the risk lives.
155
+ - Append runs for many commits or CI builds into the same SQLite database and compare trends by `revision` or `ref`.
156
+ - Use SQL as the analysis layer itself: aggregate hotspots, inspect cycles, or shape rows into JSON payloads for scripts and dashboards.
157
+
158
+ ## What FtaQl Measures
159
+
160
+ The table above is the fastest way to understand the main metrics, while formulas and caveats live in [`docs/scoring/en.md`](https://github.com/pikulev/ftaql/blob/main/docs/scoring/en.md). In practice, most teams start from three views:
161
+
162
+ - `file_score`, when they want the quickest list of the heaviest files
163
+ - `coupling_score`, when they want to see where project relationships create the most risk
164
+ - full-graph cycles and `runtime` cycles, when they need to separate architectural smells from runtime trouble
165
+
166
+ ## What Gets Persisted
167
+
168
+ FtaQl stores normalized project snapshots in SQLite. The core tables are:
169
+
170
+ - `analysis_runs` for run metadata and resolved config
171
+ - `files` for per-file metrics
172
+ - `file_dependencies` for dependency edges between analyzed files
173
+ - `cycles`, `cycle_files`, and `cycle_edges` for normalized cycle data
174
+
175
+ That layout is enough to accumulate many revisions in one database and query them with plain SQL. For the full contract, see [`docs/sqlite-output/en.md`](https://github.com/pikulev/ftaql/blob/main/docs/sqlite-output/en.md).
176
+
177
+ ## Using FtaQl In Package Scripts
178
+
179
+ Install `@piklv/ftaql-cli`:
180
+
181
+ ```bash
182
+ yarn add -D @piklv/ftaql-cli
183
+ # or
184
+ npm install --save-dev @piklv/ftaql-cli
185
+ # or
186
+ pnpm add -D @piklv/ftaql-cli
187
+ ```
188
+
189
+ Then add a script:
190
+
191
+ ```json
192
+ {
193
+ "scripts": {
194
+ "ftaql": "ftaql . --db ./ftaql.sqlite"
195
+ }
196
+ }
197
+ ```
198
+
199
+ ## Using FtaQl From Code
200
+
201
+ `@piklv/ftaql-cli` also exports `runFtaQl(projectPath, options)`.
202
+
203
+ ```javascript
204
+ import { runFtaQl } from "@piklv/ftaql-cli";
205
+ // CommonJS alternative:
206
+ // const { runFtaQl } = require("@piklv/ftaql-cli");
207
+
208
+ const output = runFtaQl("path/to/project", {
209
+ dbPath: "./ftaql.sqlite",
210
+ revision: process.env.GIT_SHA,
211
+ ref: process.env.GIT_BRANCH,
212
+ });
213
+
214
+ console.log(output);
215
+ ```
216
+
217
+ Important notes about the Node.js wrapper:
218
+
219
+ - `options.dbPath` is required
220
+ - the wrapper persists a snapshot and returns the CLI summary from stdout
221
+ - `configPath`, `revision`, and `ref` are forwarded to the native binary
222
+
223
+ ## Configuration
224
+
225
+ By default, the native CLI looks for `ftaql.json` in the analyzed project root. You can override that path with `--config-path`.
226
+
227
+ The `ftaql.json` file controls analysis behavior such as:
228
+
229
+ - `extensions`
230
+ - `exclude_filenames`
231
+ - `exclude_directories`
232
+ - `score_cap`
233
+ - `include_comments`
234
+ - `exclude_under`
235
+
236
+ FtaQl also auto-detects `tsconfig.json` and `jsconfig.json` files when resolving imports. It supports:
237
+
238
+ - `compilerOptions.paths`
239
+ - `compilerOptions.baseUrl`
240
+ - inherited configs via `extends`
241
+ - project references discovered through the resolver
242
+
243
+ For monorepo and nested-config examples, see [`docs/usage-patterns/en.md`](https://github.com/pikulev/ftaql/blob/main/docs/usage-patterns/en.md). For the exact config contract, see [`docs/configuration/en.md`](https://github.com/pikulev/ftaql/blob/main/docs/configuration/en.md).
244
+
245
+ ## WebAssembly
246
+
247
+ The `@piklv/ftaql-wasm` package is intended for browser usage and analyzes one source file at a time:
248
+
249
+ - input: source code string
250
+ - output: JSON string for a single file
251
+ - no filesystem access
252
+ - no project-level coupling analysis
253
+
254
+ ## Docs
255
+
256
+ Read the full documentation in [`docs/`](https://github.com/pikulev/ftaql/tree/main/docs), especially [`docs/overview/en.md`](https://github.com/pikulev/ftaql/blob/main/docs/overview/en.md).
257
+
258
+ ## License
259
+
260
+ MIT
@@ -0,0 +1 @@
1
+ This directory must contain the ftaql crate binaries at publish time
package/check.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const { exeTargets, plainTargets } = require("./targets");
7
+
8
+ let missingBinaries = [];
9
+
10
+ function checkBinaryFile(target, path) {
11
+ try {
12
+ fs.readFileSync(path);
13
+ } catch (e) {
14
+ missingBinaries.push(target);
15
+ }
16
+ }
17
+
18
+ for (let i = 0; i < exeTargets.length; i += 1) {
19
+ const bin = path.join(__dirname, "binaries", exeTargets[i], "ftaql.exe");
20
+ checkBinaryFile(exeTargets[i], bin);
21
+ }
22
+
23
+ for (let i = 0; i < plainTargets.length; i += 1) {
24
+ const bin = path.join(__dirname, "binaries", plainTargets[i], "ftaql");
25
+ checkBinaryFile(plainTargets[i], bin);
26
+ }
27
+
28
+ if (missingBinaries.length > 0) {
29
+ console.log("The following binaries are missing: \n");
30
+ missingBinaries.forEach((target) => {
31
+ console.log("- " + target);
32
+ });
33
+ console.log("\n");
34
+ throw new Error("Check failed");
35
+ }
36
+
37
+ console.log("All binaries were located");
package/index.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execFileSync } = require("node:child_process");
4
+ const path = require("node:path");
5
+ const fs = require("node:fs");
6
+
7
+ const platform = process.platform;
8
+ const architecture = process.arch;
9
+
10
+ function getBinaryPath() {
11
+ const targetDirectory = path.join(__dirname, "binaries");
12
+
13
+ switch (platform) {
14
+ case "win32":
15
+ if (architecture === "x64") {
16
+ return path.join(
17
+ targetDirectory,
18
+ "ftaql-x86_64-pc-windows-msvc",
19
+ "ftaql.exe"
20
+ );
21
+ } else if (architecture === "arm64") {
22
+ return path.join(
23
+ targetDirectory,
24
+ "ftaql-aarch64-pc-windows-msvc",
25
+ "ftaql.exe"
26
+ );
27
+ }
28
+ case "darwin":
29
+ if (architecture === "x64") {
30
+ return path.join(targetDirectory, "ftaql-x86_64-apple-darwin", "ftaql");
31
+ } else if (architecture === "arm64") {
32
+ return path.join(targetDirectory, "ftaql-aarch64-apple-darwin", "ftaql");
33
+ }
34
+ case "linux":
35
+ if (architecture === "x64") {
36
+ return path.join(
37
+ targetDirectory,
38
+ "ftaql-x86_64-unknown-linux-musl",
39
+ "ftaql"
40
+ );
41
+ } else if (architecture === "arm64") {
42
+ return path.join(
43
+ targetDirectory,
44
+ "ftaql-aarch64-unknown-linux-musl",
45
+ "ftaql"
46
+ );
47
+ } else if (architecture === "arm") {
48
+ return path.join(
49
+ targetDirectory,
50
+ "ftaql-arm-unknown-linux-musleabi",
51
+ "ftaql"
52
+ );
53
+ }
54
+ break;
55
+ default:
56
+ throw new Error("Unsupported platform: " + platform);
57
+ }
58
+
59
+ throw new Error("Binary not found for the current platform");
60
+ }
61
+
62
+ function setUnixPerms(binaryPath) {
63
+ if (platform === "darwin" || platform === "linux") {
64
+ try {
65
+ fs.chmodSync(binaryPath, "755");
66
+ } catch (e) {
67
+ console.warn("Could not chmod ftaql binary: ", e);
68
+ }
69
+ }
70
+ }
71
+
72
+ // Run the binary from code
73
+ // We build arguments that get sent to the binary
74
+ function runFtaQl(project, options) {
75
+ if (!options || !options.dbPath) {
76
+ throw new Error("runFtaQl(project, options) requires options.dbPath");
77
+ }
78
+
79
+ const binaryPath = getBinaryPath();
80
+ setUnixPerms(binaryPath);
81
+ const binaryArgs = [project, "--db", options.dbPath];
82
+
83
+ if (options.configPath) {
84
+ binaryArgs.push("--config-path", options.configPath);
85
+ }
86
+ if (options.revision) {
87
+ binaryArgs.push("--revision", options.revision);
88
+ }
89
+ if (options.ref) {
90
+ binaryArgs.push("--ref", options.ref);
91
+ }
92
+
93
+ const result = execFileSync(binaryPath, binaryArgs);
94
+ return result.toString();
95
+ }
96
+
97
+ // Run the binary directly if executed as a standalone script
98
+ // Arguments are directly forwarded to the binary
99
+ if (require.main === module) {
100
+ const args = process.argv.slice(2); // Exclude the first two arguments (node binary and project path)
101
+ const binaryPath = getBinaryPath();
102
+ setUnixPerms(binaryPath);
103
+
104
+ execFileSync(binaryPath, args, { stdio: "inherit" });
105
+ }
106
+
107
+ module.exports.runFtaQl = runFtaQl;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@piklv/ftaql-cli",
3
+ "version": "1.0.0",
4
+ "description": "FtaQl is a super-fast TypeScript static analysis tool written in Rust",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/pikulev/ftaql.git"
8
+ },
9
+ "author": "vasya pikulev",
10
+ "homepage": "https://github.com/pikulev/ftaql#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/pikulev/ftaql/issues"
13
+ },
14
+ "license": "MIT",
15
+ "main": "index.js",
16
+ "bin": {
17
+ "ftaql": "index.js"
18
+ },
19
+ "types": "@types/ftaql-cli.d.ts",
20
+ "files": [
21
+ "index.js",
22
+ "check.js",
23
+ "targets.js",
24
+ "README.md",
25
+ "@types",
26
+ "binaries"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "prepublishOnly": "node check.js"
33
+ }
34
+ }
package/targets.js ADDED
@@ -0,0 +1,18 @@
1
+ // Windows
2
+ const exeTargets = [
3
+ "ftaql-aarch64-pc-windows-msvc",
4
+ "ftaql-x86_64-pc-windows-msvc",
5
+ ];
6
+
7
+ const plainTargets = [
8
+ // macOS
9
+ "ftaql-x86_64-apple-darwin",
10
+ "ftaql-aarch64-apple-darwin",
11
+ // Linux
12
+ "ftaql-x86_64-unknown-linux-musl",
13
+ "ftaql-aarch64-unknown-linux-musl",
14
+ "ftaql-arm-unknown-linux-musleabi",
15
+ ];
16
+
17
+ module.exports.exeTargets = exeTargets;
18
+ module.exports.plainTargets = plainTargets;