unitbob 0.3.5 → 0.3.6
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/verbs/validateBuild.js +64 -4
- package/package.json +1 -1
|
@@ -199,16 +199,25 @@ function checkSurfaceCoverage(row, id, expected, suiteText, add) {
|
|
|
199
199
|
// here in the server's own shape; refusing it locally would refuse an answer
|
|
200
200
|
// the server takes, which is the one direction of drift that costs a branch.
|
|
201
201
|
const declaredUnreachable = collectUnreachable(row, id, reached, add);
|
|
202
|
-
const
|
|
202
|
+
const deferred = collectDeferred(row, id, reached, declaredUnreachable, add);
|
|
203
|
+
// Spec 34-3, criterion 6. Cheap here and expensive later: over the ceiling is
|
|
204
|
+
// one of the answers the server rejects, and finding it after the suite has
|
|
205
|
+
// been written, run and reviewed costs the whole cycle.
|
|
206
|
+
const budget = expected.surfaceBudget;
|
|
207
|
+
if (budget !== undefined && reached.size > budget) {
|
|
208
|
+
add(`${id} guards ${reached.size} surfaces, over the surface_budget of ${budget}` +
|
|
209
|
+
' — guard the most important ones up to that number and list the rest in deferred_surfaces.');
|
|
210
|
+
}
|
|
211
|
+
const missed = expected.surfaces.filter((surface) => !reached.has(surface) && !declaredUnreachable.has(surface) && !deferred.has(surface));
|
|
203
212
|
if (missed.length > 0) {
|
|
204
213
|
add(`${id} surface_coverage accounts for no scenario at ${missed.join(', ')}` +
|
|
205
|
-
' — drive it,
|
|
214
|
+
' — drive it, declare it unreachable with a business reason, or defer it under the surface budget.');
|
|
206
215
|
}
|
|
207
216
|
// No check here for "declares everything unreachable and drives nothing": the
|
|
208
217
|
// caller already returned when the capability's marker appears in no suite
|
|
209
218
|
// file, so a capability with no Scenario never reaches this function at all.
|
|
210
219
|
// The server refuses that state for the same reason, one rule earlier.
|
|
211
|
-
const foreign = [...reached, ...declaredUnreachable].filter((surface) => !expected.surfaces.includes(surface));
|
|
220
|
+
const foreign = [...reached, ...declaredUnreachable, ...deferred].filter((surface) => !expected.surfaces.includes(surface));
|
|
212
221
|
if (foreign.length > 0) {
|
|
213
222
|
add(`${id} surface_coverage names ${foreign.join(', ')}, which this branch's assignment does not carry.`);
|
|
214
223
|
}
|
|
@@ -262,6 +271,49 @@ function collectUnreachable(row, id, reached, add) {
|
|
|
262
271
|
}
|
|
263
272
|
return surfaces;
|
|
264
273
|
}
|
|
274
|
+
// Spec 34-3, criterion 6. The addresses this run did not take, because the
|
|
275
|
+
// capability carried more than `surface_budget` of them. Mirrored here for the
|
|
276
|
+
// same reason the unreachable list is, and more urgently: refusing this answer
|
|
277
|
+
// locally does not merely disagree with the server, it hands the host an error
|
|
278
|
+
// message pointing at `unreachable_surfaces` — the one place these must never
|
|
279
|
+
// go, because "nothing can cause this request" and "there were better ones" are
|
|
280
|
+
// different sentences and only one of them is true.
|
|
281
|
+
//
|
|
282
|
+
// Plain surface ids, with no reason each. That asymmetry with the unreachable
|
|
283
|
+
// list is deliberate: there the sentence is the guard, because an address you
|
|
284
|
+
// cannot write a sentence about is not really unreachable. Here the reason is
|
|
285
|
+
// the same for every entry and already known — the ceiling.
|
|
286
|
+
function collectDeferred(row, id, reached, unreachable, add) {
|
|
287
|
+
const declared = row.deferred_surfaces;
|
|
288
|
+
if (declared === undefined)
|
|
289
|
+
return new Set();
|
|
290
|
+
if (!Array.isArray(declared) || declared.length === 0) {
|
|
291
|
+
add(`${id} deferred_surfaces must be a non-empty array of surface ids when it is present.`);
|
|
292
|
+
return new Set();
|
|
293
|
+
}
|
|
294
|
+
const surfaces = new Set();
|
|
295
|
+
for (const [index, item] of declared.entries()) {
|
|
296
|
+
const surface = typeof item === 'string' ? item.trim() : '';
|
|
297
|
+
if (!surface) {
|
|
298
|
+
add(`${id} deferred_surfaces[${index}] names no surface.`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (reached.has(surface)) {
|
|
302
|
+
add(`${id} both drives ${surface} in a scenario and defers it — it is one or the other.`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (unreachable.has(surface)) {
|
|
306
|
+
add(`${id} declares ${surface} both unreachable and deferred — cannot be reached and was not taken this time are different answers.`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (surfaces.has(surface)) {
|
|
310
|
+
add(`${id} defers ${surface} more than once.`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
surfaces.add(surface);
|
|
314
|
+
}
|
|
315
|
+
return surfaces;
|
|
316
|
+
}
|
|
265
317
|
// Scenario names carrying one marker, by shape rather than by grammar. See the
|
|
266
318
|
// caller for why this stays deliberately timid.
|
|
267
319
|
function scenarioNamesTagged(suiteText, marker) {
|
|
@@ -333,6 +385,7 @@ function hasContent(assignment) {
|
|
|
333
385
|
// case, wherever the shape happens to nest it.
|
|
334
386
|
function assignedCases(assignment) {
|
|
335
387
|
const found = [];
|
|
388
|
+
let surfaceBudget;
|
|
336
389
|
const walk = (value) => {
|
|
337
390
|
if (Array.isArray(value)) {
|
|
338
391
|
value.forEach(walk);
|
|
@@ -341,6 +394,11 @@ function assignedCases(assignment) {
|
|
|
341
394
|
if (!value || typeof value !== 'object')
|
|
342
395
|
return;
|
|
343
396
|
const row = value;
|
|
397
|
+
// Found by the same walk rather than by knowing where the server put it, for
|
|
398
|
+
// the same reason the cases are: the assignment body is opaque here.
|
|
399
|
+
if (typeof row.surface_budget === 'number' && Number.isFinite(row.surface_budget)) {
|
|
400
|
+
surfaceBudget = row.surface_budget;
|
|
401
|
+
}
|
|
344
402
|
const key = row.contract_key;
|
|
345
403
|
const marker = row.case_marker;
|
|
346
404
|
if (typeof key === 'string' && key.startsWith(CONTRACT_PREFIX) && typeof marker === 'string') {
|
|
@@ -356,7 +414,9 @@ function assignedCases(assignment) {
|
|
|
356
414
|
Object.values(row).forEach(walk);
|
|
357
415
|
};
|
|
358
416
|
walk(assignment);
|
|
359
|
-
|
|
417
|
+
// Stamped after the walk, never during it: nothing promises the ceiling is
|
|
418
|
+
// visited before the cases that answer to it.
|
|
419
|
+
return found.map((entry) => ({ ...entry, surfaceBudget }));
|
|
360
420
|
}
|
|
361
421
|
// Every byte of the branch's suite, main file and support files together, for
|
|
362
422
|
// the "is the marker actually in there" check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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": {
|