unitbob 0.6.0 → 0.6.1
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.
|
@@ -52,6 +52,7 @@ export async function validateWorkerCheckpoints(config, _args = [], deps = { std
|
|
|
52
52
|
errors.push(`${label}: written path ${pathValue} is not an owned path`);
|
|
53
53
|
}
|
|
54
54
|
validateCompactFacts(checkpoint.facts, label, errors);
|
|
55
|
+
validateSurfaceCoverage(checkpoint.surface_coverage, item, label, errors);
|
|
55
56
|
stringArray(checkpoint.decisions, `${label}: decisions`, errors);
|
|
56
57
|
stringArray(checkpoint.known_problems, `${label}: known_problems`, errors);
|
|
57
58
|
}
|
|
@@ -68,6 +69,16 @@ function stringArray(value, label, errors) {
|
|
|
68
69
|
}
|
|
69
70
|
return value;
|
|
70
71
|
}
|
|
72
|
+
// A fact says how it was established, and the vocabulary is two words wide:
|
|
73
|
+
// `read` when the `source_refs` are what establishes it, `ran: <command>` when
|
|
74
|
+
// something was executed and its result observed.
|
|
75
|
+
//
|
|
76
|
+
// a2time, 2026-08-17. A seeded fact claimed a dismissed employee cannot sign in.
|
|
77
|
+
// It came from reading one method and remembering another, it reached sixteen
|
|
78
|
+
// workers marked as verified, and it was false. Every fact that run established
|
|
79
|
+
// by running the application held; the one that was not, did not — and nothing in
|
|
80
|
+
// the checkpoint told the two apart, so no reader could weigh them differently.
|
|
81
|
+
const ESTABLISHED_BY = /^(read|ran: \S.*)$/;
|
|
71
82
|
function validateCompactFacts(value, label, errors) {
|
|
72
83
|
if (!Array.isArray(value)) {
|
|
73
84
|
errors.push(`${label}: facts must be an array`);
|
|
@@ -75,7 +86,7 @@ function validateCompactFacts(value, label, errors) {
|
|
|
75
86
|
}
|
|
76
87
|
for (const [index, entry] of value.entries()) {
|
|
77
88
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
78
|
-
errors.push(`${label}: $.facts[${index}] must be an object with fact and
|
|
89
|
+
errors.push(`${label}: $.facts[${index}] must be an object with fact, source_refs and established_by; got ${jsonType(entry)}`);
|
|
79
90
|
continue;
|
|
80
91
|
}
|
|
81
92
|
const fact = entry;
|
|
@@ -84,11 +95,50 @@ function validateCompactFacts(value, label, errors) {
|
|
|
84
95
|
if (!Array.isArray(fact.source_refs) || fact.source_refs.some((ref) => typeof ref !== 'string' || !ref.trim())) {
|
|
85
96
|
errors.push(`${label}: facts[${index}].source_refs must be compact source references`);
|
|
86
97
|
}
|
|
98
|
+
if (typeof fact.established_by !== 'string' || !ESTABLISHED_BY.test(fact.established_by)) {
|
|
99
|
+
errors.push(`${label}: facts[${index}].established_by must be "read" or "ran: <command>"`);
|
|
100
|
+
}
|
|
87
101
|
if ('source' in fact || 'transcript' in fact || 'suite' in fact) {
|
|
88
102
|
errors.push(`${label}: facts[${index}] may not embed source, transcript, or suite copies`);
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
}
|
|
106
|
+
// Which addresses a Scenario drives is knowable in one place — the step file the
|
|
107
|
+
// worker just wrote — and until now it travelled nowhere. The coordinator owes
|
|
108
|
+
// the server one `surface_coverage` entry per Scenario, so on a2time, 2026-08-17,
|
|
109
|
+
// it assembled that join out of the workers' closing prose and its own plan. The
|
|
110
|
+
// independent reviewer read the step code instead, the two disagreed on six
|
|
111
|
+
// Scenarios, and the server refused the publication. The join now rides with the
|
|
112
|
+
// work that produced it, and the coordinator copies it instead of interpreting.
|
|
113
|
+
//
|
|
114
|
+
// Behavioral only: this is a join between Gherkin Scenarios and surfaces, and the
|
|
115
|
+
// structural branch has neither. Requiring the key there would refuse honest
|
|
116
|
+
// slices over a field that would mean nothing if they filled it in.
|
|
117
|
+
function validateSurfaceCoverage(value, item, label, errors) {
|
|
118
|
+
if (value === undefined && item.branch !== 'behavioral')
|
|
119
|
+
return;
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
errors.push(`${label}: surface_coverage must be an array of {capability_id, scenario, surfaces} entries, one per Scenario written`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const [index, entry] of value.entries()) {
|
|
125
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
126
|
+
errors.push(`${label}: surface_coverage[${index}] must be an object with capability_id, scenario and surfaces; got ${jsonType(entry)}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const record = entry;
|
|
130
|
+
if (typeof record.capability_id !== 'string' || !item.capability_ids.includes(record.capability_id)) {
|
|
131
|
+
errors.push(`${label}: surface_coverage[${index}].capability_id ${String(record.capability_id)} is not in this plan item`);
|
|
132
|
+
}
|
|
133
|
+
if (typeof record.scenario !== 'string' || !record.scenario.trim()) {
|
|
134
|
+
errors.push(`${label}: surface_coverage[${index}].scenario must name the exact Scenario it covers`);
|
|
135
|
+
}
|
|
136
|
+
if (!Array.isArray(record.surfaces) || record.surfaces.length === 0
|
|
137
|
+
|| record.surfaces.some((surface) => typeof surface !== 'string' || !surface.trim())) {
|
|
138
|
+
errors.push(`${label}: surface_coverage[${index}].surfaces must name at least one surface the Scenario drives`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
92
142
|
function jsonType(value) {
|
|
93
143
|
if (value === null)
|
|
94
144
|
return 'null';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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": {
|
|
@@ -21,7 +21,7 @@ markers, or paths. Do not edit production code, host-owned shared files, the
|
|
|
21
21
|
connector-owned harness, or another slice.
|
|
22
22
|
|
|
23
23
|
After every owned edit, run
|
|
24
|
-
`npx -y --loglevel=error unitbob@0.6.
|
|
24
|
+
`npx -y --loglevel=error unitbob@0.6.1 run-local <branch>` and inspect the machine
|
|
25
25
|
report. Look only at examples or scenarios matching your owned paths or case
|
|
26
26
|
markers. Do not require a green exit code from the whole branch: foreign failures
|
|
27
27
|
and an already-confirmed product red do not widen your scope. Repeat the bounded
|
|
@@ -48,9 +48,13 @@ No strict JSON handoff is required.
|
|
|
48
48
|
Update the same checkpoint as promises complete. Keep facts compact and
|
|
49
49
|
source-referenced. The normative JSON shape of one facts entry is:
|
|
50
50
|
```json
|
|
51
|
-
{"fact":"The route creates an order.","source_refs":["app/orders.rb:12"]}
|
|
51
|
+
{"fact":"The route creates an order.","source_refs":["app/orders.rb:12"],"established_by":"read"}
|
|
52
52
|
```
|
|
53
|
-
Every facts entry is an object in that shape, never a string
|
|
53
|
+
Every facts entry is an object in that shape, never a string; `established_by` is
|
|
54
|
+
`read` or `ran: <command>`, and a failure you reproduced is the second kind. On
|
|
55
|
+
the behavioral branch, when you rename a Scenario or change what its steps drive,
|
|
56
|
+
update that Scenario's `surface_coverage` entry in the same breath — the
|
|
57
|
+
coordinator publishes those entries and does not reread your steps. Before handoff,
|
|
54
58
|
make one final read of the checkpoint and confirm every `facts` entry is an
|
|
55
59
|
object in the normative shape above. Do not delegate repair or auto-resume after
|
|
56
60
|
the fuse. Preserve files and checkpoint for the coordinator's existing
|
|
@@ -21,21 +21,46 @@ nothing to put in them: `written_paths` (only your own `owned_paths`),
|
|
|
21
21
|
unresolved harness problems). A missing array is not an empty one — the gate
|
|
22
22
|
that reads this checkpoint refuses it either way.
|
|
23
23
|
|
|
24
|
-
Facts are short statements with source references
|
|
25
|
-
The normative JSON shape of one facts entry is:
|
|
24
|
+
Facts are short statements with source references, and each one says how it was
|
|
25
|
+
established. The normative JSON shape of one facts entry is:
|
|
26
26
|
```json
|
|
27
|
-
{"fact":"The route creates an order.","source_refs":["app/orders.rb:12"]}
|
|
27
|
+
{"fact":"The route creates an order.","source_refs":["app/orders.rb:12"],"established_by":"read"}
|
|
28
28
|
```
|
|
29
|
-
Every facts entry is an object in that shape, never a string.
|
|
30
|
-
|
|
29
|
+
Every facts entry is an object in that shape, never a string. `established_by` is
|
|
30
|
+
`read` when the references are what establishes it, or `ran: <command>` when
|
|
31
|
+
something was executed and its result observed. You run nothing, so every fact
|
|
32
|
+
you add yourself is `read`; a `ran:` fact is one the coordinator established
|
|
33
|
+
before fan-out, and that is exactly what makes it worth more than a fact anybody
|
|
34
|
+
read. Never embed source files, suite copies, or transcript.
|
|
35
|
+
|
|
36
|
+
On the behavioral branch your checkpoint also carries `surface_coverage`: one
|
|
37
|
+
entry per Scenario you write, recorded as you write it.
|
|
38
|
+
```json
|
|
39
|
+
{"capability_id":"<one of your plan item's ids>","scenario":"<exact Scenario name>","surfaces":["POST /orders"]}
|
|
40
|
+
```
|
|
41
|
+
`surfaces` names the addresses and jobs the Scenario's `When` really reaches — not
|
|
42
|
+
the ones its capability was assigned, and not the ones you meant to reach. Only
|
|
43
|
+
you can know this: the coordinator publishes this join and never reopens your step
|
|
44
|
+
files. On a2time, 2026-08-17, it had to reconstruct the join from what the workers
|
|
45
|
+
said about their work; the independent reviewer read the steps instead, six
|
|
46
|
+
Scenarios claimed addresses their steps never drove, and the server refused the
|
|
47
|
+
publication.
|
|
31
48
|
|
|
32
49
|
Write first, then find out. Start with the planned cases your seeded facts
|
|
33
50
|
already support and get them onto disk; go reading only for what you still lack
|
|
34
51
|
after that. The opposite order — survey the sources, then write — is what spent
|
|
35
52
|
seven of eight workers' entire ceilings on a2time, 2026-08-10, and produced no
|
|
36
|
-
file at all.
|
|
37
|
-
|
|
38
|
-
it.
|
|
53
|
+
file at all.
|
|
54
|
+
|
|
55
|
+
A fact already in your checkpoint is settled: do not establish it a second time.
|
|
56
|
+
A `read` fact is settled the same way — until a file you had to open anyway says
|
|
57
|
+
otherwise. Then check that one fact against its own `source_refs`, which is two
|
|
58
|
+
or three lines and not a fresh survey; if it is wrong, correct the entry and say
|
|
59
|
+
so in `known_problems`. On a2time, 2026-08-17, a seeded fact said a dismissed
|
|
60
|
+
employee cannot sign in — one method read, another remembered — and sixteen
|
|
61
|
+
workers got it as verified. One of them looked, disagreed, and kept its scenario
|
|
62
|
+
honest, which is the only reason that access hole came back red instead of green.
|
|
63
|
+
Nothing mechanical enforces any of this; it holds because you keep it.
|
|
39
64
|
|
|
40
65
|
Read only the `source_paths` and dependencies your finite planned cases need.
|
|
41
66
|
Ask closed questions with the files to look in. For a closed missing fact, use
|