unitbob 0.2.7 → 0.2.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/dist/cli.js +57 -6
- package/dist/verbs/putSuiteBuild.js +42 -2
- package/dist/verbs/run.js +34 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
// trace. This module decides exit codes but never acts on them: importing it must
|
|
4
4
|
// stay free of side effects so tests can drive `main` directly. `bin.ts` is the
|
|
5
5
|
// executable that owns process startup and exit.
|
|
6
|
+
//
|
|
7
|
+
// One verb is more than a dispatch: `put-suite-build` composes two hands-verbs
|
|
8
|
+
// into a single user operation, so `publishAndRun` at the bottom of this file
|
|
9
|
+
// holds that sequence and the exit code it implies (spec 32-4). Every other verb
|
|
10
|
+
// keeps its whole flow in its own module under `verbs/`.
|
|
6
11
|
import { ensureLinked } from "./link.js";
|
|
7
12
|
import { recipe } from "./verbs/recipe.js";
|
|
8
13
|
import { show } from "./verbs/show.js";
|
|
9
|
-
import { run } from "./verbs/run.js";
|
|
14
|
+
import { run, runOnly } from "./verbs/run.js";
|
|
10
15
|
import { init } from "./verbs/init.js";
|
|
11
16
|
import { mapPrepare } from "./verbs/mapPrepare.js";
|
|
12
17
|
import { putMapBuild } from "./verbs/putMapBuild.js";
|
|
13
18
|
import { suitePrepare } from "./verbs/suitePrepare.js";
|
|
14
|
-
import { putSuiteBuild } from "./verbs/putSuiteBuild.js";
|
|
19
|
+
import { classifyPublication, putSuiteBuild } from "./verbs/putSuiteBuild.js";
|
|
15
20
|
import { fixPrepare } from "./verbs/fixPrepare.js";
|
|
16
21
|
import { contractPrompt } from "./verbs/contractPrompt.js";
|
|
17
22
|
import { suiteReviewPrepare } from "./verbs/suiteReviewPrepare.js";
|
|
@@ -27,7 +32,8 @@ Verbs:
|
|
|
27
32
|
put-map-build Internal: upload the host-built map and graph.
|
|
28
33
|
suite-prepare Internal: fetch the recipe and capability assignment, write the host suite-build request.
|
|
29
34
|
suite-review-prepare Internal: bind an independent BDD quality review to the built behavioral candidate.
|
|
30
|
-
put-suite-build Internal: upload the host-built guardrail suite (whole spec file + test_metadata)
|
|
35
|
+
put-suite-build Internal: upload the host-built guardrail suite (whole spec file + test_metadata),
|
|
36
|
+
then run every branch it published and report the server's results.
|
|
31
37
|
fix-prepare <id> Internal: fetch the per-capability repair packet for one red guard (by interface_id).
|
|
32
38
|
contract-prompt <digest> <test_id> [fix|accept]
|
|
33
39
|
Internal: fetch the fix/accept brief for one red check on either map.
|
|
@@ -37,7 +43,8 @@ Verbs:
|
|
|
37
43
|
Pipeline: map and suite are built on your machine. \`*-prepare\` writes a request
|
|
38
44
|
packet (with the recipe and paths); you build the artifact at the packet's
|
|
39
45
|
output_path from your local source; \`put-*\` uploads only the structured result.
|
|
40
|
-
\`
|
|
46
|
+
\`put-suite-build\` then runs what it published, so a suite never sits with nothing
|
|
47
|
+
to show. \`check\` re-runs the guardrails locally and reports results to the server.
|
|
41
48
|
Graph extraction is keyless: the connector needs no LLM API key. Inference is the
|
|
42
49
|
host-LLM's job; any semantic graph enrichment is host-LLM work (the /graphify skill).
|
|
43
50
|
|
|
@@ -73,8 +80,7 @@ export async function main(argv, deps = { ensureLinked }) {
|
|
|
73
80
|
await suiteReviewPrepare(await deps.ensureLinked(), args);
|
|
74
81
|
return 0;
|
|
75
82
|
case 'put-suite-build':
|
|
76
|
-
await
|
|
77
|
-
return 0;
|
|
83
|
+
return await publishAndRun(await deps.ensureLinked(), args);
|
|
78
84
|
case 'fix-prepare':
|
|
79
85
|
await fixPrepare(await deps.ensureLinked(), args);
|
|
80
86
|
return 0;
|
|
@@ -95,3 +101,48 @@ export async function main(argv, deps = { ensureLinked }) {
|
|
|
95
101
|
return 1;
|
|
96
102
|
}
|
|
97
103
|
}
|
|
104
|
+
// Publishing a suite and running it the first time are one user operation
|
|
105
|
+
// (spec 32-4). They used to be two commands, and the second one was a step the
|
|
106
|
+
// host LLM could simply not take: the suite was stored, nothing had ever run it,
|
|
107
|
+
// and the user was told about results that did not exist. Now the connector owns
|
|
108
|
+
// the sequence, so no instruction-following can drop half of it.
|
|
109
|
+
//
|
|
110
|
+
// This is a composition, not a transaction. The two server requests stay separate:
|
|
111
|
+
// if the run cannot finish, the published suite and any earlier results for the
|
|
112
|
+
// same identity survive untouched, and a standalone `unitbob check` completes the
|
|
113
|
+
// loop. The two outputs also keep separate authorities — which branch published
|
|
114
|
+
// comes from the build response, and what the results are comes only from the
|
|
115
|
+
// server's answer to the run.
|
|
116
|
+
export async function publishAndRun(config, args, deps) {
|
|
117
|
+
const d = {
|
|
118
|
+
putSuiteBuild: (cfg, a) => putSuiteBuild(cfg, a),
|
|
119
|
+
runOnly: (cfg, digests) => runOnly(cfg, digests),
|
|
120
|
+
stdout: process.stdout,
|
|
121
|
+
stderr: process.stderr,
|
|
122
|
+
...deps,
|
|
123
|
+
};
|
|
124
|
+
const { digests, unpublished } = classifyPublication(await d.putSuiteBuild(config, args));
|
|
125
|
+
// Nothing is current, so there is nothing honest to run. A branch that failed
|
|
126
|
+
// to publish must never fall back to the suite it was meant to replace.
|
|
127
|
+
if (digests.length === 0) {
|
|
128
|
+
d.stderr.write('No suite was published, so nothing was run. Fix the problems reported above and generate the Unitbob guardrails again.\n');
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
// Said between the two halves, because it is a fact about publication and the
|
|
132
|
+
// run summaries have not been printed yet. Reading a peer's results onto a
|
|
133
|
+
// branch that was never published is the exact mistake this whole spec exists
|
|
134
|
+
// to stop, so the output names the gap instead of leaving it to be inferred.
|
|
135
|
+
if (unpublished.length > 0) {
|
|
136
|
+
d.stdout.write(`Partial success. No run summary below covers: ${unpublished.join(', ')}.\n`);
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await d.runOnly(config, digests);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// The upload never happened, so no results were stored — partially or
|
|
143
|
+
// otherwise. Everything published above is still valid and still current.
|
|
144
|
+
d.stderr.write(`${err.message}\nThe suite is published. Run the Unitbob checks to finish.\n`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
@@ -6,6 +6,9 @@ import { Wire } from "../wire.js";
|
|
|
6
6
|
// the host cannot claim a different map than each branch was given. A branch the
|
|
7
7
|
// host could not build is uploaded as a `build_error`, which never rolls back the
|
|
8
8
|
// peer branch. If a branch's answer is unparseable, nothing is uploaded.
|
|
9
|
+
//
|
|
10
|
+
// Returns the server's per-branch results so the caller can compose the first run
|
|
11
|
+
// on top of them (spec 32-4) without parsing the lines printed here.
|
|
9
12
|
export async function putSuiteBuild(config, _args = [], deps) {
|
|
10
13
|
const request = readSuiteBuildRequest(config.projectRoot);
|
|
11
14
|
const outputs = readHostSuiteOutputs(request.output_path, request);
|
|
@@ -57,10 +60,37 @@ export async function putSuiteBuild(config, _args = [], deps) {
|
|
|
57
60
|
for (const result of results) {
|
|
58
61
|
d.stdout.write(`${printResult(result)}\n`);
|
|
59
62
|
}
|
|
63
|
+
return results;
|
|
60
64
|
}
|
|
65
|
+
// The three outcomes that leave a branch published and current: a new version, an
|
|
66
|
+
// identical version already stored, or a reactivated one. Each returns the
|
|
67
|
+
// identity to run. Everything else — a rejected branch, a branch the host could
|
|
68
|
+
// not build, or a status this connector has never seen — fails closed and is
|
|
69
|
+
// never run, so a newer server can never trick an older connector into running
|
|
70
|
+
// something it does not understand.
|
|
71
|
+
const PUBLISHED = new Set(['created', 'unchanged', 'restored']);
|
|
72
|
+
export function classifyPublication(results) {
|
|
73
|
+
const split = { digests: [], unpublished: [] };
|
|
74
|
+
for (const result of results) {
|
|
75
|
+
if (!PUBLISHED.has(result.status)) {
|
|
76
|
+
split.unpublished.push(result.suite_kind);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!result.suite_digest) {
|
|
80
|
+
throw new Error(`The server accepted the ${result.suite_kind} suite as "${result.status}" but returned no identity ` +
|
|
81
|
+
'to run it by. The suite is published; run the Unitbob checks to finish.');
|
|
82
|
+
}
|
|
83
|
+
split.digests.push(result.suite_digest);
|
|
84
|
+
}
|
|
85
|
+
return split;
|
|
86
|
+
}
|
|
87
|
+
// Asks `PUBLISHED` rather than naming the failing statuses again: this line and
|
|
88
|
+
// the run that follows it must agree about what "published" means, or a branch the
|
|
89
|
+
// command skipped gets a line that reads like a success — digest and all — right
|
|
90
|
+
// above "no suite was published".
|
|
61
91
|
function printResult(result) {
|
|
62
|
-
if (
|
|
63
|
-
return `${result.suite_kind}: not published — ${result
|
|
92
|
+
if (!PUBLISHED.has(result.status)) {
|
|
93
|
+
return `${result.suite_kind}: not published — ${unpublishedReason(result)}.`;
|
|
64
94
|
}
|
|
65
95
|
const tallies = result.counts
|
|
66
96
|
? Object.entries(result.counts)
|
|
@@ -70,3 +100,13 @@ function printResult(result) {
|
|
|
70
100
|
const digest = result.suite_digest ? ` (${result.suite_digest})` : '';
|
|
71
101
|
return `${result.suite_kind}: ${result.status}${digest}${tallies ? ` — ${tallies}` : ''}.`;
|
|
72
102
|
}
|
|
103
|
+
// The server's own words when it sent any; otherwise the best true thing that can
|
|
104
|
+
// be said. A status this connector does not know is quoted rather than guessed
|
|
105
|
+
// at — claiming the host could not build it would invent a cause.
|
|
106
|
+
function unpublishedReason(result) {
|
|
107
|
+
if (result.error)
|
|
108
|
+
return result.error;
|
|
109
|
+
if (result.status === 'build_error')
|
|
110
|
+
return 'the host could not build this suite';
|
|
111
|
+
return `the server answered "${result.status}"`;
|
|
112
|
+
}
|
package/dist/verbs/run.js
CHANGED
|
@@ -8,9 +8,22 @@ import { runBddSuite } from "../runner/bdd.js";
|
|
|
8
8
|
import { boundReport } from "../runner/boundReport.js";
|
|
9
9
|
import { Wire } from "../wire.js";
|
|
10
10
|
const OUTPUT_TAIL_CHARS = 2000;
|
|
11
|
+
// `check`/`run`: execute every ready peer. This is the standalone flow the user
|
|
12
|
+
// asks for by name, and the recovery path after an interrupted first run.
|
|
11
13
|
export async function run(config, _args, deps) {
|
|
14
|
+
return execute(config, resolve(config, deps), null);
|
|
15
|
+
}
|
|
16
|
+
// The first run that `put-suite-build` performs itself (spec 32-4): execute
|
|
17
|
+
// exactly the suite identities publication just returned, or none of them. Every
|
|
18
|
+
// requested identity must still be the current one — see `select`. Callers pass a
|
|
19
|
+
// non-empty list; "nothing was published" is decided and reported one level up,
|
|
20
|
+
// where the publication results that explain it are still in hand.
|
|
21
|
+
export async function runOnly(config, digests, deps) {
|
|
22
|
+
return execute(config, resolve(config, deps), digests);
|
|
23
|
+
}
|
|
24
|
+
function resolve(config, deps) {
|
|
12
25
|
const wire = new Wire(config);
|
|
13
|
-
|
|
26
|
+
return {
|
|
14
27
|
getSuites: () => wire.getSuites(),
|
|
15
28
|
postRunsBatch: (runs) => wire.postRunsBatch(runs),
|
|
16
29
|
materializeStructural: (projectRoot, item) => materializeGuardrails(projectRoot, {
|
|
@@ -25,14 +38,17 @@ export async function run(config, _args, deps) {
|
|
|
25
38
|
stdout: process.stdout,
|
|
26
39
|
...deps,
|
|
27
40
|
};
|
|
41
|
+
}
|
|
42
|
+
async function execute(config, d, only) {
|
|
28
43
|
const suites = await d.getSuites();
|
|
29
44
|
const ready = suites.filter((item) => item.status === 'ready');
|
|
30
|
-
|
|
45
|
+
const selected = only === null ? ready : select(ready, only);
|
|
46
|
+
if (selected.length === 0) {
|
|
31
47
|
d.stdout.write('No Unitbob suites exist yet. Generate them first, then run the Unitbob checks again.\n');
|
|
32
48
|
return;
|
|
33
49
|
}
|
|
34
50
|
const runs = [];
|
|
35
|
-
for (const item of
|
|
51
|
+
for (const item of selected) {
|
|
36
52
|
runs.push(await buildRunPayload(config, d, item));
|
|
37
53
|
}
|
|
38
54
|
const { results, map_url } = await d.postRunsBatch(runs);
|
|
@@ -41,6 +57,21 @@ export async function run(config, _args, deps) {
|
|
|
41
57
|
if (map_url)
|
|
42
58
|
d.stdout.write(`${map_url}\n`);
|
|
43
59
|
}
|
|
60
|
+
// All-or-nothing. Publication and this fetch are two requests, so another client
|
|
61
|
+
// can republish in between. Running whatever is current instead would file honest
|
|
62
|
+
// results against a version the user never asked about, and silently substituting
|
|
63
|
+
// an older suite for a branch that failed to publish would be worse still. So a
|
|
64
|
+
// requested identity that is no longer current stops the whole run.
|
|
65
|
+
function select(ready, wanted) {
|
|
66
|
+
const byDigest = new Map(ready.map((item) => [item.suite_digest ?? '', item]));
|
|
67
|
+
const missing = wanted.filter((digest) => !byDigest.has(digest));
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
throw new Error(`The suite this project just published (${missing.join(', ')}) is no longer the current one — ` +
|
|
70
|
+
'something else replaced it while this command was running. Nothing was run, so no results were ' +
|
|
71
|
+
'filed against the wrong version.');
|
|
72
|
+
}
|
|
73
|
+
return wanted.map((digest) => byDigest.get(digest));
|
|
74
|
+
}
|
|
44
75
|
// One branch's run payload. A stack mismatch, a materialize failure, or a runner
|
|
45
76
|
// that produced no report all become this branch's structured suite error — the
|
|
46
77
|
// peer branch is unaffected. This connector never installs anything: a missing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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": {
|