@binary-balance/seshat 0.1.0-rc.1 → 0.2.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 +145 -5
  2. package/bin/seshat.mjs +36 -35
  3. package/package.json +12 -8
package/README.md CHANGED
@@ -1,8 +1,148 @@
1
1
  # Seshat
2
2
 
3
- This package installs the Seshat command and selects the native payload for
4
- the current Node platform. The native payload packages are optional so npm can
5
- skip the four platforms that do not match the consumer.
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
- Seshat needs Node 24.20.0. Run `seshat check`, `seshat crap`, or `seshat mutate`
8
- with a project `seshat.json`. Add `--json` for the versioned report.
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.
9
+
10
+ ## What Seshat does
11
+
12
+ - Calculates function complexity and CRAP scores from statement 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.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install --save-dev @binary-balance/seshat
22
+ ```
23
+
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.
28
+
29
+ The project being assessed must already have its own dependencies installed,
30
+ including TypeScript, its test runner and a coverage tool.
31
+
32
+ ## Supported test runners
33
+
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` |
39
+
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).
42
+
43
+ ## Configure
44
+
45
+ Add a `seshat.json` to the project root. This one is for Node's built-in test
46
+ runner:
47
+
48
+ ```json
49
+ {
50
+ "source": {
51
+ "include": ["src/**/*.ts"]
52
+ },
53
+ "capture": [
54
+ "package.json", "package-lock.json", "tsconfig.json", "collect-node.mjs",
55
+ "src", "tests", "node_modules"
56
+ ],
57
+ "setups": [{
58
+ "name": "node",
59
+ "runner": "node",
60
+ "cwd": ".",
61
+ "typecheck": ["node", "node_modules/typescript/bin/tsc", "--project", "tsconfig.json"],
62
+ "test": [
63
+ "node", "--test", "--test-concurrency=1",
64
+ "--test-reporter={seshatReporter}", "tests/rules.test.mjs"
65
+ ],
66
+ "coverage": {
67
+ "command": ["node", "collect-node.mjs", "tests/rules.test.mjs"],
68
+ "report": "coverage/coverage-final.json"
69
+ }
70
+ }]
71
+ }
72
+ ```
73
+
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
86
+
87
+ ```sh
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 statement coverage (a fraction from 0 to 1):
106
+
107
+ ```text
108
+ complexity^2 * (1 - coverage)^3 + complexity
109
+ ```
110
+
111
+ For complexity 10, zero coverage gives `110`, 50% coverage gives `22.5` and
112
+ full coverage gives `10`.
113
+
114
+ The mutation score is the percentage of mutants that made a test fail. A mutant
115
+ is one changed comparison, such as `>` to `>=`. If tests catch two of three
116
+ mutants, the score is `66.67%`. A timeout or execution error leaves the run
117
+ incomplete and withholds the score. Neither score proves correctness.
118
+
119
+ ## Use in CI
120
+
121
+ Add optional thresholds to `seshat.json`:
122
+
123
+ ```json
124
+ "thresholds": {
125
+ "maxCrap": 30,
126
+ "minMutationScore": 80
127
+ }
128
+ ```
129
+
130
+ | Exit status | Meaning |
131
+ | ---: | --- |
132
+ | `0` | Complete run, thresholds met. |
133
+ | `1` | Complete run, a threshold was not met. |
134
+ | `2` | Invalid input, failed baseline or incomplete run. |
135
+ | `130` / `143` | Cancelled by `SIGINT` / `SIGTERM` on Unix. |
136
+
137
+ See [quality thresholds](https://github.com/Binary-Balance/seshat/blob/main/docs/configuration.md#quality-thresholds) for the full
138
+ rules.
139
+
140
+ ## Development
141
+
142
+ See the [architecture](https://github.com/Binary-Balance/seshat/blob/main/docs/architecture.md), the
143
+ [build and regression guide](https://github.com/Binary-Balance/seshat/blob/main/benchmarks/proofs/README.md) and the
144
+ [packaging guide](https://github.com/Binary-Balance/seshat/blob/main/packaging/README.md).
145
+
146
+ ## License
147
+
148
+ [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,11 +1,15 @@
1
1
  {
2
2
  "name": "@binary-balance/seshat",
3
- "version": "0.1.0-rc.1",
4
- "description": "Native TypeScript code assurance",
3
+ "version": "0.2.0",
4
+ "description": "A native CLI for TypeScript and TSX complexity analysis and mutation testing.",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Binary-Balance/seshat.git"
9
+ },
6
10
  "type": "module",
7
11
  "engines": {
8
- "node": "24.20.0"
12
+ "node": ">=24.20.0 <25"
9
13
  },
10
14
  "bin": {
11
15
  "seshat": "bin/seshat.mjs"
@@ -16,10 +20,10 @@
16
20
  "LICENSE"
17
21
  ],
18
22
  "optionalDependencies": {
19
- "@binary-balance/seshat-linux-x64": "0.1.0-rc.1",
20
- "@binary-balance/seshat-linux-arm64": "0.1.0-rc.1",
21
- "@binary-balance/seshat-darwin-x64": "0.1.0-rc.1",
22
- "@binary-balance/seshat-darwin-arm64": "0.1.0-rc.1",
23
- "@binary-balance/seshat-win32-x64": "0.1.0-rc.1"
23
+ "@binary-balance/seshat-linux-x64": "0.2.0",
24
+ "@binary-balance/seshat-linux-arm64": "0.2.0",
25
+ "@binary-balance/seshat-darwin-x64": "0.2.0",
26
+ "@binary-balance/seshat-darwin-arm64": "0.2.0",
27
+ "@binary-balance/seshat-win32-x64": "0.2.0"
24
28
  }
25
29
  }