unitbob 0.1.6 → 0.1.8
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 +45 -0
- package/dist/files/guardrails.js +5 -0
- package/dist/link.js +3 -3
- package/dist/runner/rspec.js +22 -5
- package/dist/verbs/run.js +5 -1
- package/dist/verbs/show.js +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Unitbob
|
|
2
|
+
|
|
3
|
+
A living map of your app's business parts. Each important seam gets an automatic
|
|
4
|
+
test — a "lamp" on the map. Green means fine, red means something broke.
|
|
5
|
+
|
|
6
|
+
You work through Claude Code. You need: Node 18+, Python 3.10+.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Install (once)
|
|
11
|
+
|
|
12
|
+
**With a prompt:**
|
|
13
|
+
```
|
|
14
|
+
Add the Unitbob plugin marketplace: sergeygershun/unitbob-connector
|
|
15
|
+
Install the unitbob plugin
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**With commands (in the terminal):**
|
|
19
|
+
```
|
|
20
|
+
claude plugin marketplace add sergeygershun/unitbob-connector
|
|
21
|
+
claude plugin install unitbob@unitbob
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Restart the session so the commands load.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Full cycle
|
|
29
|
+
|
|
30
|
+
| Step | With a prompt (just type in chat) | With a command |
|
|
31
|
+
|------|-----------------------------------|----------------|
|
|
32
|
+
| 1. Build the map | `Build my Unitbob map` | `/unitbob:map` |
|
|
33
|
+
| 2. Generate tests | `Generate the guardrail tests` | `/unitbob:suite` |
|
|
34
|
+
| 3. Run the checks | `Run the checks` | `/unitbob:check` |
|
|
35
|
+
| 4. Fix a red lamp | `Fix guardrail <id>` | `/unitbob:fix <id>` |
|
|
36
|
+
| 5. Open the map | `Open my Unitbob map` | `/unitbob:show` |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## How to read it
|
|
41
|
+
|
|
42
|
+
- **Green lamp** — the behavior works.
|
|
43
|
+
- **Red lamp** — something the structure relied on broke. Copy its `id` and run
|
|
44
|
+
step 4.
|
|
45
|
+
- The project links itself by folder name — nothing to set up by hand.
|
package/dist/files/guardrails.js
CHANGED
|
@@ -7,6 +7,11 @@ export const HELPER_FILE = 'unitbob_helper.rb';
|
|
|
7
7
|
// the project's own .rspec (stray --require lines, extra stdout formatters)
|
|
8
8
|
// out of guardrail runs, whose JSON output must stay parseable.
|
|
9
9
|
export const OPTIONS_FILE = 'rspec.opts';
|
|
10
|
+
// RSpec writes its JSON report here via `--out`, not to stdout: the app under
|
|
11
|
+
// test can print to stdout during a run (Rails logging, deprecation notices, a
|
|
12
|
+
// stray `puts`), which corrupts a stdout-parsed JSON document. A dedicated file
|
|
13
|
+
// isolates the report from anything else the process emits.
|
|
14
|
+
export const RESULT_FILE = 'rspec_result.json';
|
|
10
15
|
// The boot file the generated suite requires (spec 29). Connector-owned and
|
|
11
16
|
// versioned with it — never scaffolded into the project, never part of the
|
|
12
17
|
// suite digest (that covers `spec_rb` bytes only). Delegates to the project's
|
package/dist/link.js
CHANGED
|
@@ -7,9 +7,9 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
8
8
|
import { CONFIG_FILE, readLocalRepoId, writeConfigFile } from "./config.js";
|
|
9
9
|
import { registerRepo, WireError } from "./wire.js";
|
|
10
|
-
//
|
|
11
|
-
// server
|
|
12
|
-
export const DEFAULT_SERVER = '
|
|
10
|
+
// Public Unitbob brain used by default. Local development can still pass an
|
|
11
|
+
// explicit server URL or edit `.unitbob.json`.
|
|
12
|
+
export const DEFAULT_SERVER = 'https://unitbob-73a4082838d3.herokuapp.com';
|
|
13
13
|
// Make sure this project is linked, resolving the repo by name on every run
|
|
14
14
|
// (cheap — the server find-or-creates) and reconciling with the local file:
|
|
15
15
|
// - no working link (file missing / repo_id 0 / non-int) → register, write
|
package/dist/runner/rspec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { runProcess } from "../proc.js";
|
|
4
|
-
import { GUARDRAILS_DIR, OPTIONS_FILE, SUITE_FILE } from "../files/guardrails.js";
|
|
4
|
+
import { GUARDRAILS_DIR, OPTIONS_FILE, RESULT_FILE, SUITE_FILE } from "../files/guardrails.js";
|
|
5
5
|
export const RSPEC_TIMEOUT_MS = 10 * 60 * 1000;
|
|
6
6
|
// Spec 26: the Unitbob file runs in a defined order with a fixed seed so a
|
|
7
7
|
// green→red flip can never come from run-order nondeterminism. It does not inherit
|
|
@@ -11,11 +11,14 @@ export const RSPEC_SEED = '1';
|
|
|
11
11
|
// never the project's full suite — under RAILS_ENV=test with a fixed order/seed.
|
|
12
12
|
// --options points at the materialized empty file so the project's own .rspec
|
|
13
13
|
// (a --require of a helper we replaced, an extra stdout formatter) can neither
|
|
14
|
-
// break the boot nor corrupt the JSON output.
|
|
14
|
+
// break the boot nor corrupt the JSON output. The JSON report goes to `--out`
|
|
15
|
+
// (a file), not stdout, so the app's own stdout writes during the run can never
|
|
16
|
+
// corrupt it.
|
|
15
17
|
export async function runRspecSuite(projectRoot) {
|
|
16
18
|
const suitePath = join(GUARDRAILS_DIR, SUITE_FILE);
|
|
17
19
|
const optionsPath = join(GUARDRAILS_DIR, OPTIONS_FILE);
|
|
18
|
-
|
|
20
|
+
const resultPath = join(GUARDRAILS_DIR, RESULT_FILE);
|
|
21
|
+
const { result, command, args } = await invokeRspec(projectRoot, [
|
|
19
22
|
suitePath,
|
|
20
23
|
'--options',
|
|
21
24
|
optionsPath,
|
|
@@ -25,7 +28,21 @@ export async function runRspecSuite(projectRoot) {
|
|
|
25
28
|
RSPEC_SEED,
|
|
26
29
|
'--format',
|
|
27
30
|
'json',
|
|
31
|
+
'--out',
|
|
32
|
+
resultPath,
|
|
28
33
|
]);
|
|
34
|
+
return { ...result, command, args, jsonReport: readReport(join(projectRoot, resultPath)) };
|
|
35
|
+
}
|
|
36
|
+
// Read the `--out` report file back verbatim. A missing or unreadable file is a
|
|
37
|
+
// clean empty string, not a throw: the caller falls back to stdout/stderr and
|
|
38
|
+
// reports a suite error, exactly as it would for unparseable output.
|
|
39
|
+
function readReport(path) {
|
|
40
|
+
try {
|
|
41
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
29
46
|
}
|
|
30
47
|
// Prefer the project's own `bin/rspec`; fall back to `bundle exec rspec`. Every
|
|
31
48
|
// run sets RAILS_ENV=test so guardrails execute against the Rails test
|
|
@@ -40,7 +57,7 @@ async function invokeRspec(projectRoot, rspecArgs) {
|
|
|
40
57
|
timeoutMs: RSPEC_TIMEOUT_MS,
|
|
41
58
|
env: { ...process.env, RAILS_ENV: 'test', UNITBOB_REPO_ROOT: projectRoot },
|
|
42
59
|
});
|
|
43
|
-
return {
|
|
60
|
+
return { result, command, args };
|
|
44
61
|
}
|
|
45
62
|
function executable(path) {
|
|
46
63
|
try {
|
package/dist/verbs/run.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function run(config, _args, deps) {
|
|
|
32
32
|
code: null,
|
|
33
33
|
command: 'rspec',
|
|
34
34
|
args: [],
|
|
35
|
+
jsonReport: '',
|
|
35
36
|
}));
|
|
36
37
|
const payload = rawRunPayload(suite.suite_digest, result);
|
|
37
38
|
const summary = await d.postRun(payload);
|
|
@@ -39,8 +40,11 @@ export async function run(config, _args, deps) {
|
|
|
39
40
|
if (summary.map_url)
|
|
40
41
|
d.stdout.write(`${summary.map_url}\n`);
|
|
41
42
|
}
|
|
43
|
+
// The RSpec JSON report comes from the `--out` file (`jsonReport`), which the app
|
|
44
|
+
// under test cannot pollute. Stdout is only a fallback for a runner double that
|
|
45
|
+
// emits its report there; anything unparseable is a genuine suite error.
|
|
42
46
|
function rawRunPayload(suiteDigest, result) {
|
|
43
|
-
const parsed = parseJsonObject(result.stdout);
|
|
47
|
+
const parsed = parseJsonObject(result.jsonReport) ?? parseJsonObject(result.stdout);
|
|
44
48
|
if (parsed)
|
|
45
49
|
return { suite_digest: suiteDigest, rspec_json: boundFailures(parsed) };
|
|
46
50
|
return {
|
package/dist/verbs/show.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Unitbob connector — thin local hands for the Unitbob Rails brain. Owns no domain logic: it runs tools, relays bytes over the wire, and prints what the server returns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|