@binary-balance/seshat 0.1.0 → 0.3.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 (3) hide show
  1. package/README.md +118 -92
  2. package/bin/seshat.mjs +36 -35
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -1,129 +1,155 @@
1
1
  # Seshat
2
2
 
3
- Seshat is a native CLI for TypeScript and TSX complexity analysis and mutation
4
- testing. `check` runs both assessments, `crap` runs complexity and coverage
5
- analysis, and `mutate` runs mutation testing from the original test baseline.
3
+ > Seshat was the ancient Egyptian goddess of writing, wisdom, and knowledge. Seen as a scribe and record keeper, she became identified as the goddess of measurement, accounting, architecture, science, astronomy, mathematics, geometry, history and surveying.
6
4
 
7
- ## Install
8
-
9
- Install the entry package in the project you want to assess:
10
-
11
- ```sh
12
- npm install --save-dev @binary-balance/seshat
13
- ```
5
+ A native code-assurance tool for TypeScript and TSX, written in Rust. Seshat
6
+ combines function-level [CRAP](https://testing.googleblog.com/2011/02/this-code-is-crap.html)
7
+ analysis with [comparison-operator mutation testing](https://en.wikipedia.org/wiki/Mutation_testing) to find complex and/or
8
+ insufficiently tested code.
14
9
 
15
- The package declares and verifies Node.js 24.20.0 exactly. It selects one
16
- native payload for the current platform from these five verified targets:
10
+ ## What Seshat does
17
11
 
18
- - Linux x64 (glibc; Debian 11 userspace, glibc 2.31)
19
- - Linux ARM64 (glibc; Ubuntu 22.04, glibc 2.35)
20
- - macOS x64 (macOS 15.0 minimum)
21
- - macOS ARM64 (macOS 15.0 minimum)
22
- - Windows x64 (Windows Server 2022 verification)
12
+ - Calculates function complexity and CRAP scores from branch coverage.
13
+ - Changes comparison operators one at a time and checks whether tests detect them.
14
+ - Runs tests in isolated copies of the captured project.
15
+ - Produces readable terminal output and versioned JSON, with optional CI thresholds.
16
+ - Supports explicit source selection, multiple test setups and parallel mutation workers.
23
17
 
24
- These are the known verification floors; see the [native platform support
25
- matrix](https://github.com/Binary-Balance/seshat/blob/main/docs/platform-support.md)
26
- for the detailed target evidence.
27
-
28
- ## Minimal mutation example
29
-
30
- For a small Node test setup, install TypeScript in the project first:
18
+ ## Install
31
19
 
32
20
  ```sh
33
- npm install --save-dev typescript
21
+ npm install --save-dev @binary-balance/seshat
34
22
  ```
35
23
 
36
- Create `src/rules.mts`:
37
-
38
- ```ts
39
- export function classify(value: number): 'positive' | 'negative' {
40
- return value >= 0 ? 'positive' : 'negative';
41
- }
42
- ```
24
+ Seshat needs Node `>=24.20.0 <25`. npm installs a prebuilt binary for Linux
25
+ x64/ARM64 (glibc), macOS x64/ARM64 or Windows x64, so Rust is not required.
26
+ Alpine Linux, which uses musl instead of glibc, is not supported. See the
27
+ [platform support matrix](https://github.com/Binary-Balance/seshat/blob/main/docs/platform-support.md) for details.
43
28
 
44
- Create `tests/rules.test.mjs`:
29
+ The project being assessed must already have its own dependencies installed,
30
+ including TypeScript, its test runner and a coverage tool.
45
31
 
46
- ```js
47
- import assert from 'node:assert/strict';
48
- import {test} from 'node:test';
49
- import {classify} from '../src/rules.mts';
32
+ ## Supported test runners
50
33
 
51
- test('classifies zero and positive values', () => {
52
- assert.equal(classify(0), 'positive');
53
- assert.equal(classify(1), 'positive');
54
- });
55
- ```
34
+ | Runner | Supported versions |
35
+ | --- | --- |
36
+ | Node's built-in test runner | Node `>=24.20.0 <25` |
37
+ | Vitest | `>=5.0.0 <6` |
38
+ | Jest with Expo | Jest `29.7.0` with jest-expo `57.0.5` |
56
39
 
57
- Create `tsconfig.json`:
40
+ Seshat checks the runner version on every run and stops if it is not
41
+ supported. See the [runner compatibility guide](https://github.com/Binary-Balance/seshat/blob/main/docs/runner-compatibility.md).
58
42
 
59
- ```json
60
- {
61
- "compilerOptions": {
62
- "strict": true,
63
- "noEmit": true,
64
- "skipLibCheck": true,
65
- "target": "ES2022",
66
- "module": "NodeNext",
67
- "moduleResolution": "NodeNext",
68
- "allowImportingTsExtensions": true
69
- },
70
- "include": ["src/**/*.mts"]
71
- }
72
- ```
43
+ ## Configure
73
44
 
74
- Create `seshat.json`:
45
+ Add a `seshat.json` to the project root. This one is for Node's built-in test
46
+ runner:
75
47
 
76
48
  ```json
77
49
  {
78
- "source": {"include": ["src/**/*.mts"]},
50
+ "source": {
51
+ "include": ["src/**/*.ts"]
52
+ },
79
53
  "capture": [
80
- "package.json",
81
- "package-lock.json",
82
- "tsconfig.json",
83
- "src",
84
- "tests",
85
- "node_modules"
54
+ "package.json", "package-lock.json", "tsconfig.json", "collect-node.mjs",
55
+ "src", "tests", "node_modules"
86
56
  ],
87
57
  "setups": [{
88
58
  "name": "node",
89
59
  "runner": "node",
90
60
  "cwd": ".",
91
- "typecheck": [
92
- "node",
93
- "node_modules/typescript/bin/tsc",
94
- "--project",
95
- "tsconfig.json"
96
- ],
61
+ "typecheck": ["node", "node_modules/typescript/bin/tsc", "--project", "tsconfig.json"],
97
62
  "test": [
98
- "node",
99
- "--test",
100
- "--test-concurrency=1",
101
- "--test-reporter={seshatReporter}",
102
- "tests/rules.test.mjs"
63
+ "node", "--test", "--test-concurrency=1",
64
+ "--test-reporter={seshatReporter}", "tests/rules.test.mjs"
103
65
  ],
104
66
  "coverage": {
105
- "command": ["node", "-e", "process.exit(0)"],
106
- "report": "coverage/unused.json"
67
+ "command": ["node", "collect-node.mjs", "tests/rules.test.mjs"],
68
+ "report": "coverage/coverage-final.json"
107
69
  }
108
70
  }]
109
71
  }
110
72
  ```
111
73
 
112
- Run mutation testing and save its JSON report:
74
+ - `source` selects the files to assess, using glob patterns.
75
+ - `capture` lists the files and directories copied into each isolated run. It
76
+ must include the selected source and everything the commands need.
77
+ - `setups` gives the typecheck, test and coverage commands. The coverage
78
+ command must write an Istanbul JSON report; `collect-node.mjs` is a small
79
+ adapter from the [Node example](https://github.com/Binary-Balance/seshat/tree/main/examples/node).
80
+
81
+ The [examples](https://github.com/Binary-Balance/seshat/blob/main/examples/README.md) include complete Node, npm workspace, Vitest
82
+ and Jest/Expo projects. The [configuration guide](https://github.com/Binary-Balance/seshat/blob/main/docs/configuration.md) covers
83
+ every field, including `workers` for parallel mutation runs.
84
+
85
+ ## Use
113
86
 
114
87
  ```sh
115
- ./node_modules/.bin/seshat mutate --config ./seshat.json \
116
- --json --no-progress > seshat-report.json
88
+ npx @binary-balance/seshat check # CRAP and mutation testing
89
+ npx @binary-balance/seshat crap # coverage and CRAP only
90
+ npx @binary-balance/seshat mutate # mutation testing only
91
+ ```
92
+
93
+ Use the full package name with `npx`. A bare `npx seshat` can fetch an
94
+ unrelated package of that name.
95
+
96
+ Each command reads `./seshat.json`; use `--config PATH` for another file. Add
97
+ `--json` to write a report to stdout, as described in the
98
+ [report format](https://github.com/Binary-Balance/seshat/blob/main/docs/report-format.md). `--help` lists all options.
99
+
100
+ Run only trusted test commands. Seshat's copies protect the project checkout,
101
+ but tests can still access external files, services, databases and credentials.
102
+
103
+ ## Understand the results
104
+
105
+ CRAP uses complexity and branch coverage, a fraction from 0 to 1. Functions
106
+ with no recorded branches use statement coverage instead:
107
+
108
+ ```text
109
+ complexity^2 * (1 - coverage)^3 + complexity
110
+ ```
111
+
112
+ For complexity 10, zero coverage gives `110`, 50% coverage gives `22.5` and
113
+ full coverage gives `10`.
114
+
115
+ Statement coverage counts code that ran; branch coverage counts the recorded
116
+ outcomes of decisions. For example, testing only the member price in
117
+ `if (member) total *= 0.9; return total;` can run every statement while
118
+ leaving the non-member outcome untested. Its statement coverage is 100%,
119
+ but its branch coverage is 50%. Both measurements appear in the report.
120
+
121
+ The mutation score is the percentage of mutants that made a test fail. A mutant
122
+ is one changed comparison, such as `>` to `>=`. If tests catch two of three
123
+ mutants, the score is `66.67%`. A timeout or execution error leaves the run
124
+ incomplete and withholds the score. Neither score proves correctness.
125
+
126
+ ## Use in CI
127
+
128
+ Add optional thresholds to `seshat.json`:
129
+
130
+ ```json
131
+ "thresholds": {
132
+ "maxCrap": 30,
133
+ "minMutationScore": 80
134
+ }
117
135
  ```
118
136
 
119
- The `coverage` block is required by the configuration schema but is not run by
120
- `mutate`; the no-op command above keeps this example dependency-light. `check`
121
- and `crap` do run coverage and need a command that writes a fresh Istanbul JSON
122
- report. Use the complete [Node example](https://github.com/Binary-Balance/seshat/tree/main/examples/node),
123
- [Vitest example](https://github.com/Binary-Balance/seshat/tree/main/examples/vitest),
124
- or [Jest/Expo example](https://github.com/Binary-Balance/seshat/tree/main/examples/jest-expo)
125
- for those commands.
126
-
127
- See the [full configuration guide](https://github.com/Binary-Balance/seshat/blob/main/docs/configuration.md)
128
- for all fields and runner setup, and the [JSON report format](https://github.com/Binary-Balance/seshat/blob/main/docs/report-format.md)
129
- for the `--json` output contract.
137
+ | Exit status | Meaning |
138
+ | ---: | --- |
139
+ | `0` | Complete run, thresholds met. |
140
+ | `1` | Complete run, a threshold was not met. |
141
+ | `2` | Invalid input, failed baseline or incomplete run. |
142
+ | `130` / `143` | Cancelled by `SIGINT` / `SIGTERM` on Unix. |
143
+
144
+ See [quality thresholds](https://github.com/Binary-Balance/seshat/blob/main/docs/configuration.md#quality-thresholds) for the full
145
+ rules.
146
+
147
+ ## Development
148
+
149
+ See the [architecture](https://github.com/Binary-Balance/seshat/blob/main/docs/architecture.md), the
150
+ [build and regression guide](https://github.com/Binary-Balance/seshat/blob/main/benchmarks/proofs/README.md) and the
151
+ [packaging guide](https://github.com/Binary-Balance/seshat/blob/main/packaging/README.md).
152
+
153
+ ## License
154
+
155
+ [MIT](https://github.com/Binary-Balance/seshat/blob/main/LICENSE).
package/bin/seshat.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {spawn} from 'node:child_process';
4
4
  import {createRequire} from 'node:module';
5
+ import {constants} from 'node:os';
5
6
  import {existsSync, readFileSync, statSync} from 'node:fs';
6
7
  import {basename, dirname, join} from 'node:path';
7
8
  import {fileURLToPath} from 'node:url';
@@ -11,6 +12,8 @@ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
12
  const packageManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
12
13
  const version = packageManifest.version;
13
14
  const args = process.argv.slice(2);
15
+ // Match native argument errors: no -- separator or --flag=value syntax exists,
16
+ // and path values cannot start with --. A literal --json always requests JSON.
14
17
  const jsonRequested = args.includes('--json');
15
18
  const commands = new Set(['check', 'crap', 'mutate']);
16
19
  const started = performance.now();
@@ -87,10 +90,6 @@ function payloadExecutable(target) {
87
90
  return executable;
88
91
  }
89
92
 
90
- function signalNumber(signal) {
91
- return {SIGINT: 2, SIGTERM: 15}[signal] ?? 1;
92
- }
93
-
94
93
  const host = targetForHost();
95
94
  if (host.error) {
96
95
  fail(host.error);
@@ -102,37 +101,39 @@ if (host.error) {
102
101
  fail(error instanceof Error ? error.message : String(error));
103
102
  }
104
103
  if (executable) {
105
- const child = spawn(executable, args, {
106
- cwd: process.cwd(),
107
- env: {...process.env},
108
- shell: false,
109
- windowsHide: false,
110
- stdio: 'inherit',
111
- });
112
- let spawnFailed = false;
113
- const forward = signal => {
114
- if (child.exitCode === null) child.kill(signal);
115
- };
116
- // A CTRL_BREAK_EVENT is broadcast to the console process group. Keep the
117
- // launcher alive on Windows so the native child can handle that event and
118
- // return its cancellation report through inherited stdio.
119
- const preserveConsole = () => {};
120
- process.on('SIGINT', forward);
121
- process.on('SIGTERM', forward);
122
- if (process.platform === 'win32') process.on('SIGBREAK', preserveConsole);
123
- const cleanup = () => {
124
- process.removeListener('SIGINT', forward);
125
- process.removeListener('SIGTERM', forward);
126
- if (process.platform === 'win32') process.removeListener('SIGBREAK', preserveConsole);
127
- };
128
- child.once('error', error => {
129
- spawnFailed = true;
130
- cleanup();
104
+ let child;
105
+ try {
106
+ child = spawn(executable, args, {
107
+ cwd: process.cwd(),
108
+ env: {...process.env},
109
+ shell: false,
110
+ windowsHide: false,
111
+ stdio: 'inherit',
112
+ });
113
+ } catch (error) {
131
114
  fail(`cannot start native payload: ${error.message}`);
132
- });
133
- child.once('close', (code, signal) => {
134
- cleanup();
135
- if (!spawnFailed) process.exitCode = signal ? 128 + signalNumber(signal) : code ?? 1;
136
- });
115
+ }
116
+ if (child) {
117
+ let spawnFailed = false;
118
+ const forward = signal => {
119
+ if (!spawnFailed && child.exitCode === null && child.signalCode === null) child.kill(signal);
120
+ };
121
+ // Windows delivers Ctrl+C/Break to the shared console. Forwarding SIGINT
122
+ // with child.kill() force-terminates the native child before it can clean up.
123
+ const preserveConsole = () => {};
124
+ const interrupt = process.platform === 'win32' ? preserveConsole : forward;
125
+ process.on('SIGINT', interrupt);
126
+ process.on('SIGTERM', forward);
127
+ if (process.platform === 'win32') process.on('SIGBREAK', preserveConsole);
128
+ // Keep handlers until launcher exit: a second console/group signal can arrive
129
+ // after child close and must not replace the child's already chosen exit code.
130
+ child.once('error', error => {
131
+ spawnFailed = true;
132
+ fail(`cannot start native payload: ${error.message}`);
133
+ });
134
+ child.once('close', (code, signal) => {
135
+ if (!spawnFailed) process.exitCode = signal ? 128 + (constants.signals[signal] ?? 1) : code ?? 1;
136
+ });
137
+ }
137
138
  }
138
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binary-balance/seshat",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A native CLI for TypeScript and TSX complexity analysis and mutation testing.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "type": "module",
11
11
  "engines": {
12
- "node": "24.20.0"
12
+ "node": ">=24.20.0 <25"
13
13
  },
14
14
  "bin": {
15
15
  "seshat": "bin/seshat.mjs"
@@ -20,10 +20,10 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "optionalDependencies": {
23
- "@binary-balance/seshat-linux-x64": "0.1.0",
24
- "@binary-balance/seshat-linux-arm64": "0.1.0",
25
- "@binary-balance/seshat-darwin-x64": "0.1.0",
26
- "@binary-balance/seshat-darwin-arm64": "0.1.0",
27
- "@binary-balance/seshat-win32-x64": "0.1.0"
23
+ "@binary-balance/seshat-linux-x64": "0.3.0",
24
+ "@binary-balance/seshat-linux-arm64": "0.3.0",
25
+ "@binary-balance/seshat-darwin-x64": "0.3.0",
26
+ "@binary-balance/seshat-darwin-arm64": "0.3.0",
27
+ "@binary-balance/seshat-win32-x64": "0.3.0"
28
28
  }
29
29
  }