bareloop 0.10.0 → 0.12.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.
- package/CHANGELOG.md +510 -0
- package/README.md +18 -7
- package/bareloop.context.md +456 -19
- package/package.json +3 -3
- package/src/authorflow.js +290 -49
- package/src/authoring.js +258 -46
- package/src/authorjob.js +390 -32
- package/src/authorscout.js +13 -1
- package/src/bridges.js +238 -11
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/clock.js +21 -3
- package/src/declaredclose.js +188 -13
- package/src/index.js +90 -7
- package/src/job.js +28 -9
- package/src/judged.js +765 -0
- package/src/kinds.js +578 -13
- package/src/ledger.js +24 -1
- package/src/planrun.js +517 -14
- package/src/ralph.js +9 -0
- package/src/reuse.js +514 -96
- package/src/reviewdoor.js +246 -0
- package/src/root.js +1 -1
- package/src/run.js +81 -8
- package/src/selection.js +23 -7
- package/types/authorflow.d.ts +233 -21
- package/types/authoring.d.ts +99 -39
- package/types/authorjob.d.ts +44 -6
- package/types/authorscout.d.ts +7 -1
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/clock.d.ts +5 -2
- package/types/declaredclose.d.ts +93 -6
- package/types/index.d.ts +11 -7
- package/types/job.d.ts +22 -4
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +227 -8
- package/types/planrun.d.ts +73 -3
- package/types/reuse.d.ts +218 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +60 -2
package/src/authoring.js
CHANGED
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
// flagged a `runClose` naming collision between the kind executor and
|
|
73
73
|
// src/ralph.js; the public surface is settled once at M4, not piecemeal.
|
|
74
74
|
|
|
75
|
-
import { normalizeParser } from './kinds.js';
|
|
75
|
+
import { normalizeParser, MAX_JUDGED_PATHS, JUDGED_FLOOR_KIND } from './kinds.js';
|
|
76
|
+
import { JUDGE_RULE_IDS, validateCard } from './judged.js';
|
|
76
77
|
import { isObj, isNonEmptyString, hasNestedQuantifier, globToPrefix } from './validate.js';
|
|
77
78
|
|
|
78
79
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -94,9 +95,17 @@ import { isObj, isNonEmptyString, hasNestedQuantifier, globToPrefix } from './va
|
|
|
94
95
|
|
|
95
96
|
/** the whole menu, in ascending order of what it takes to render the verdict */
|
|
96
97
|
export const VERDICT_CLASSES = Object.freeze(['green', 'soft-green', 'hitl']);
|
|
97
|
-
/** declared-but-locked: selecting one is COUNTED demand, never an admission
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/** declared-but-locked: selecting one is COUNTED demand, never an admission.
|
|
99
|
+
* `hitl` LEFT this list at N4 slice 1 and `soft-green` at softgreen module 3
|
|
100
|
+
* (see `src/job.js`'s twin, `LOCKED_VERDICTS`, which moved in the same commit) —
|
|
101
|
+
* the judged floor its lock was waiting on is built, wired and calibrated.
|
|
102
|
+
*
|
|
103
|
+
* EMPTY is a true statement about a closed menu, not F59's absent-as-empty: the
|
|
104
|
+
* mechanism is read at call time from this constant, so re-locking a class is
|
|
105
|
+
* one edit and every counted refusal comes back with it. */
|
|
106
|
+
/** @type {readonly string[]} */
|
|
107
|
+
export const LOCKED_CLASSES = Object.freeze([]);
|
|
108
|
+
/** the classes v1 actually builds */
|
|
100
109
|
export const LIVE_CLASSES = Object.freeze(VERDICT_CLASSES.filter((c) => !LOCKED_CLASSES.includes(c)));
|
|
101
110
|
|
|
102
111
|
/** the hierarchy as a comparable number. `green` is the FLOOR: a mechanical
|
|
@@ -115,10 +124,11 @@ const CLASS_RANK = Object.freeze({ green: 0, 'soft-green': 1, hitl: 2 });
|
|
|
115
124
|
* `CLASS_BY_VERDICT` exist to stop, defeated from ABOVE by never letting the
|
|
116
125
|
* illegal pair be formed.
|
|
117
126
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
127
|
+
* NO LONGER INERT: `human-confirms` (N4 slice 1) and `judged-floor` (softgreen
|
|
128
|
+
* module 2) are both live and both above green, so a green declaration naming
|
|
129
|
+
* one is a real red rather than a hypothetical. `classMenu` below is the other
|
|
130
|
+
* half of the same rule, applied one step earlier — the composer is never handed
|
|
131
|
+
* a kind this would refuse.
|
|
122
132
|
*
|
|
123
133
|
* A kind the catalogue does not know contributes NOTHING — it already carries an
|
|
124
134
|
* `unknown-kind` red, and guessing a class for it would be deriving the promise
|
|
@@ -152,6 +162,49 @@ export function closeCeiling(declaration, catalogue = KIND_CATALOGUE) {
|
|
|
152
162
|
return top;
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
/**
|
|
166
|
+
* THE MENU A JOB OF THIS CLASS MAY EXPRESS — the ceiling rule, moved one step
|
|
167
|
+
* EARLIER, to the moment the composer is handed its vocabulary.
|
|
168
|
+
*
|
|
169
|
+
* `closeCeiling` refuses an above-ceiling kind AFTER a paid call has composed a
|
|
170
|
+
* declaration around it. That is a late red for an illegal pick the composer was
|
|
171
|
+
* shown in its own catalogue and its own tool schema — and the standing rule is
|
|
172
|
+
* the opposite: wherever only a closed set can satisfy a field, hand over the
|
|
173
|
+
* enumerated set, so the illegal value is INEXPRESSIBLE rather than rejected
|
|
174
|
+
* after the fact (the `check-passes(name)` model). A green job is therefore never
|
|
175
|
+
* shown `judged-floor` or `human-confirms` at all.
|
|
176
|
+
*
|
|
177
|
+
* ONLY THE MENU NARROWS. The VALIDATOR keeps the whole catalogue, deliberately:
|
|
178
|
+
* a model that names a kind it was never offered must be told which RULE it
|
|
179
|
+
* broke (`class-ceiling`), not that the kind does not exist (`unknown-kind`) —
|
|
180
|
+
* one is the truth and the other invites it to invent a spelling.
|
|
181
|
+
*
|
|
182
|
+
* `null` means UNFILTERED and exists for callers inspecting the catalogue itself
|
|
183
|
+
* (the schema/coverage suite). Every composing call site passes the user's own
|
|
184
|
+
* picked class; `authorPrompt` already throws without one.
|
|
185
|
+
* @param {string|null} verdictType
|
|
186
|
+
* @param {Record<string, KindSpec>} [catalogue]
|
|
187
|
+
* @returns {string[]} kind names at or below the picked class, in catalogue order
|
|
188
|
+
*/
|
|
189
|
+
export function classMenu(verdictType, catalogue = KIND_CATALOGUE) {
|
|
190
|
+
const names = Object.keys(catalogue);
|
|
191
|
+
if (verdictType === null || verdictType === undefined) return names;
|
|
192
|
+
const picked = String(verdictType);
|
|
193
|
+
if (!Object.hasOwn(CLASS_RANK, picked)) {
|
|
194
|
+
throw new Error(`[authoring] no verdict class "${picked}" — the menu a composer may be handed is keyed by the `
|
|
195
|
+
+ `class the user picked, one of ${VERDICT_CLASSES.join(', ')}, and guessing one would compose under a promise `
|
|
196
|
+
+ 'nobody made');
|
|
197
|
+
}
|
|
198
|
+
return names.filter((k) => {
|
|
199
|
+
const spec = catalogue[k];
|
|
200
|
+
if (!spec || !Object.hasOwn(CLASS_RANK, spec.verdictClass)) {
|
|
201
|
+
throw new Error(`[authoring] catalogue kind "${k}" declares no verdict class — every kind states the class of `
|
|
202
|
+
+ `verdict it can honestly render (one of ${VERDICT_CLASSES.join(', ')})`);
|
|
203
|
+
}
|
|
204
|
+
return CLASS_RANK[spec.verdictClass] <= CLASS_RANK[picked];
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
155
208
|
// ── 1. THE KIND CATALOGUE ────────────────────────────────────────────────────
|
|
156
209
|
|
|
157
210
|
/**
|
|
@@ -227,21 +280,50 @@ export const KIND_CATALOGUE = Object.freeze({
|
|
|
227
280
|
shape: 'allowPrefixes: string[], requireNonEmpty: true',
|
|
228
281
|
asserts: 'the set of files this run changed must be non-empty and must lie wholly inside allowPrefixes.',
|
|
229
282
|
}),
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
283
|
+
// LIVE at softgreen module 2, and the only kind that BUYS its measurement: a
|
|
284
|
+
// pinned toolless judge extracts facts WITH QUOTES from each named artifact and
|
|
285
|
+
// an arbiter-owned rulebook renders the verdict over them (src/judged.js). The
|
|
286
|
+
// judge never says whether anything passed — a model asked "did this pass?" is
|
|
287
|
+
// a model that can be argued with, and bareguard's A/B measured that
|
|
288
|
+
// `judgeLocate` is not injectable where `judgeVerdict` is.
|
|
289
|
+
//
|
|
290
|
+
// `card` is the signer's own rubric, compiled to ENUMERATED items: `rule`
|
|
291
|
+
// SELECTS from the rulebook we implement, so a rule we do not own is
|
|
292
|
+
// inexpressible rather than rejected late, and `text` is the signer's words —
|
|
293
|
+
// what a red is explained WITH, never what a red is decided BY. `paths` names
|
|
294
|
+
// the AUTHORITATIVE artifacts, selected from the seed listing like every other
|
|
295
|
+
// path, never the worker's summary of what it did.
|
|
296
|
+
//
|
|
297
|
+
// No `timeoutMs` and no `env`: this kind spawns nothing. Its own bounds are the
|
|
298
|
+
// arbiter's (the retry ladder and the artifact ceiling live in src/kinds.js),
|
|
299
|
+
// because a declaration that could widen them is a declaration that can spend.
|
|
234
300
|
'judged-floor': Object.freeze({
|
|
235
301
|
verdictClass: 'soft-green',
|
|
236
|
-
required: Object.freeze([
|
|
237
|
-
|
|
238
|
-
|
|
302
|
+
required: Object.freeze(['card', 'paths']),
|
|
303
|
+
optional: Object.freeze([]),
|
|
304
|
+
pathParams: Object.freeze(['paths']),
|
|
305
|
+
shape: `card: {items: [{rule: ${JUDGE_RULE_IDS.join('|')}, text: string}]}, paths: string[] (at most ${MAX_JUDGED_PATHS})`,
|
|
306
|
+
asserts: 'a pinned judge extracts facts and VERBATIM quotes from each named artifact, and an arbiter-owned rulebook '
|
|
307
|
+
+ 'renders pass/fail per card item over those facts — first red wins, and anything it cannot be sure of is a red. '
|
|
308
|
+
+ 'Name one card line per thing you actually look for, in the order you look for them, and name the real files the '
|
|
309
|
+
+ 'work lands in. It is never offered to the agent as an in-run check.',
|
|
239
310
|
}),
|
|
311
|
+
// LIVE at N4 slice 1, and the ONLY kind that does not measure anything: it
|
|
312
|
+
// does not RUN, it PAUSES the run and hands the tree to a person (ruling 1).
|
|
313
|
+
// `ask` is the whole parameter surface — no cmd, no timeoutMs, no env — so a
|
|
314
|
+
// human stage cannot spawn, cannot be env-capable, and cannot be given a
|
|
315
|
+
// second job. `offer` is never one of its fields either: ruling 5's law is a
|
|
316
|
+
// LAW, checked by the validator, not a knob the composer sets.
|
|
240
317
|
'human-confirms': Object.freeze({
|
|
241
318
|
verdictClass: 'hitl',
|
|
242
|
-
required: Object.freeze([]),
|
|
243
|
-
|
|
244
|
-
|
|
319
|
+
required: Object.freeze(['ask']),
|
|
320
|
+
optional: Object.freeze([]),
|
|
321
|
+
pathParams: Object.freeze([]),
|
|
322
|
+
shape: 'ask: string',
|
|
323
|
+
asserts: 'a PERSON renders this verdict, at the end of the run, and nothing about it is measured. Write `ask` as '
|
|
324
|
+
+ 'the plain question they answer while looking at the finished result. It is always the LAST stage, it is '
|
|
325
|
+
+ 'never offered to the agent as an in-run check, and every part of the job a command can decide belongs in a '
|
|
326
|
+
+ 'mechanical stage before it.',
|
|
245
327
|
}),
|
|
246
328
|
});
|
|
247
329
|
|
|
@@ -613,30 +695,48 @@ function language(lang) {
|
|
|
613
695
|
* than hand back nothing if anything ever does.
|
|
614
696
|
* @type {Record<string, {locked: boolean, guards: readonly any[]|null}>}
|
|
615
697
|
*/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
]),
|
|
698
|
+
/** THE MECHANICAL BATTERY — the two guards a machine can always render, named
|
|
699
|
+
* once because TWO classes carry exactly them (OPEN-1's ruling below). It is
|
|
700
|
+
* shared by reference and never mutated: `classGuards` deep-copies before it
|
|
701
|
+
* hands anything back, so one battery cannot be weakened through another class. */
|
|
702
|
+
const MECHANICAL_GUARDS = Object.freeze([
|
|
703
|
+
Object.freeze({
|
|
704
|
+
name: 'changed-from-seed',
|
|
705
|
+
kind: 'files-changed',
|
|
706
|
+
params: Object.freeze({ requireNonEmpty: true }),
|
|
707
|
+
compose: Object.freeze([]),
|
|
708
|
+
fill: Object.freeze(['allowPrefixes']),
|
|
709
|
+
}),
|
|
710
|
+
Object.freeze({
|
|
711
|
+
name: 'no-suppressions',
|
|
712
|
+
kind: 'pattern-absent-in-diff',
|
|
713
|
+
// NO scope: the scan covers every changed file of these extensions. A
|
|
714
|
+
// narrowing here is the round-3 arm A defect and is a red, not a taste.
|
|
715
|
+
params: Object.freeze({}),
|
|
716
|
+
compose: Object.freeze(['patterns', 'extensions']),
|
|
717
|
+
fill: Object.freeze([]),
|
|
637
718
|
}),
|
|
638
|
-
|
|
639
|
-
|
|
719
|
+
]);
|
|
720
|
+
|
|
721
|
+
export const CLASS_BATTERIES = Object.freeze({
|
|
722
|
+
green: Object.freeze({ locked: false, guards: MECHANICAL_GUARDS }),
|
|
723
|
+
// RULING 4 (hamr, 2026-08-18, selected): **softgreen INHERITS green's
|
|
724
|
+
// battery** — the same precedent hitl was built on. A softgreen job is a green
|
|
725
|
+
// job's agent-authored plan under the same arbiter with one judged stage at the
|
|
726
|
+
// end; the mandatory guards are about what the WORKER may not do, and nothing
|
|
727
|
+
// about a judged final stage relaxes any of them. It is the SAME OBJECT rather
|
|
728
|
+
// than a second spelling of the same prose: a fix to green's battery must flow
|
|
729
|
+
// here with no second edit, and `classGuards` deep-copies on the way out so
|
|
730
|
+
// sharing cannot become weakening.
|
|
731
|
+
'soft-green': Object.freeze({ locked: false, guards: MECHANICAL_GUARDS }),
|
|
732
|
+
// OPEN-1, RULED (hamr, in-turn, 2026-08-13): **hitl inherits the green
|
|
733
|
+
// mechanical guards and adds nothing a human stage cannot see.** D5 makes the
|
|
734
|
+
// battery mandatory, shown-and-fixed and un-removable, and PRD v1.57 §2 keys it
|
|
735
|
+
// to the CLASS; the mechanical-first composition law makes every hitl close
|
|
736
|
+
// mostly mechanical anyway, so the green battery is the right FLOOR. A guard a
|
|
737
|
+
// person cannot verify by looking would be a guard nobody checks — the one
|
|
738
|
+
// thing D5 exists to make unsayable.
|
|
739
|
+
hitl: Object.freeze({ locked: false, guards: MECHANICAL_GUARDS }),
|
|
640
740
|
});
|
|
641
741
|
|
|
642
742
|
/**
|
|
@@ -925,7 +1025,34 @@ function scopeOfJob(declaration, idx, guards) {
|
|
|
925
1025
|
* stage — applied to the one population that decides whether the law fires at
|
|
926
1026
|
* all.
|
|
927
1027
|
*/
|
|
928
|
-
const AT_MOST_ONCE_KINDS = Object.freeze(['files-changed']);
|
|
1028
|
+
const AT_MOST_ONCE_KINDS = Object.freeze(['files-changed', 'human-confirms', 'judged-floor']);
|
|
1029
|
+
|
|
1030
|
+
/** why each at-most-once kind is at most once, in the author's own terms. A map
|
|
1031
|
+
* rather than a chain of conditionals: a kind added to the list above with no
|
|
1032
|
+
* reason here is a red nobody can act on, and this fails loudly instead.
|
|
1033
|
+
* @type {Record<string, (label: string, owner: string) => string>} */
|
|
1034
|
+
const AT_MOST_ONCE_REASON = Object.freeze({
|
|
1035
|
+
'files-changed': (label, owner) => `stage "${label}" is a second files-changed stage — "${owner}" already declares `
|
|
1036
|
+
+ 'which files this job may change. Stages are ANDed, so two of them mean the intersection of two prefix sets, '
|
|
1037
|
+
+ 'which is itself a prefix set and already says the same thing in one stage; what a second one does add is a '
|
|
1038
|
+
+ 'second answer to what this job\'s scope IS',
|
|
1039
|
+
'human-confirms': (label, owner) => `stage "${label}" is a second human-confirms stage — "${owner}" already asks the `
|
|
1040
|
+
+ 'person. One signer, one judge (ruling 4), asked ONCE at the end: a second door is a second verdict, and the run '
|
|
1041
|
+
+ 'has no way to say which of them was the one it stopped for',
|
|
1042
|
+
// softgreen module 3. The judged floor is the close's ONE judged ruler: two of
|
|
1043
|
+
// them are two bars for one verdict class, each buying its own paid calls, and
|
|
1044
|
+
// first-red-wins means the second only ever runs when the first was satisfied —
|
|
1045
|
+
// so its only reachable effect is to spend money re-judging a tree that already
|
|
1046
|
+
// cleared the bar the signer signed. One card, one calibration set, one ruler.
|
|
1047
|
+
'judged-floor': (label, owner) => `stage "${label}" is a second judged-floor stage — "${owner}" is already this `
|
|
1048
|
+
+ 'close\'s judged ruler. One card and one calibration set decide one bar: a second judged stage is a second bar '
|
|
1049
|
+
+ 'for the same verdict, and it buys its own paid calls to re-judge a tree the first one already passed',
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
/** the kinds NO ONE runs as an in-run ruler — ruling 5's law, as data. A
|
|
1053
|
+
* declaration that offers one is a red rather than a normalization: the signer
|
|
1054
|
+
* must not read one thing and have the runner store another. */
|
|
1055
|
+
export const NEVER_OFFERED_KINDS = Object.freeze(['human-confirms', 'judged-floor']);
|
|
929
1056
|
|
|
930
1057
|
/** @param {any} v */
|
|
931
1058
|
const strArray = (v) => Array.isArray(v) && v.length > 0 && v.every(isNonEmptyString);
|
|
@@ -1128,13 +1255,65 @@ export function validateDeclaration(declaration, opts = {}) {
|
|
|
1128
1255
|
if (AT_MOST_ONCE_KINDS.includes(s.kind)) {
|
|
1129
1256
|
const owner = kindOwners.get(s.kind);
|
|
1130
1257
|
if (owner !== undefined) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1258
|
+
const why = AT_MOST_ONCE_REASON[s.kind];
|
|
1259
|
+
if (!why) {
|
|
1260
|
+
throw new Error(`[authoring] "${s.kind}" is declared at-most-once with no stated reason — a red the author `
|
|
1261
|
+
+ 'cannot act on is worse than no rule');
|
|
1262
|
+
}
|
|
1263
|
+
red('duplicate-kind', `${at}.kind`, why(label, owner), { kind: s.kind, stage: label, twin: owner });
|
|
1135
1264
|
} else kindOwners.set(s.kind, label);
|
|
1136
1265
|
}
|
|
1137
1266
|
|
|
1267
|
+
// ── RULING 5, AS LAW: judged and human stages are `offer: false`, and the
|
|
1268
|
+
// law is checked rather than applied. `checkMenu` filters `offer !== false`,
|
|
1269
|
+
// so a declaration that says nothing is silent about a rule the runner
|
|
1270
|
+
// enforces anyway (`declaredStages` stamps it) — that part is the arbiter's
|
|
1271
|
+
// own bookkeeping. What must never happen is a declaration that says
|
|
1272
|
+
// something ELSE: `offer: true` on a human stage is the composer asking to
|
|
1273
|
+
// use a person as an in-run ruler, and a silent normalization would leave
|
|
1274
|
+
// the signer reading one thing while the runner stored another.
|
|
1275
|
+
if (NEVER_OFFERED_KINDS.includes(s.kind) && Object.hasOwn(s, 'offer') && s.offer !== false) {
|
|
1276
|
+
red('human-stage-offered', `${at}.offer`, `stage "${label}" is a ${s.kind} stage and offers itself as an in-run `
|
|
1277
|
+
+ 'check. A person (or a paid judge) is never an in-run ruler (ruling 5): the agent may never use one to '
|
|
1278
|
+
+ 'decide a step, and this is refused rather than quietly rewritten to offer: false',
|
|
1279
|
+
{ kind: s.kind, stage: label });
|
|
1280
|
+
}
|
|
1281
|
+
// ── THE COMPOSITION LAW's ordering half (2026-08-07: *mechanical first,
|
|
1282
|
+
// judge minimal, human LAST*), enforced where it is cheap. First-red-wins
|
|
1283
|
+
// means anything after the human stage never runs — so a human stage in the
|
|
1284
|
+
// middle silently deletes the mechanical stages behind it AND empties the
|
|
1285
|
+
// evidence package ruling 2 requires the person to be shown.
|
|
1286
|
+
if (s.kind === 'human-confirms' && i !== stages.length - 1) {
|
|
1287
|
+
red('human-stage-not-last', `${at}.kind`, `stage "${label}" asks a person and is not the LAST stage. The close `
|
|
1288
|
+
+ 'is first-red-wins, so every stage after this one would never run: the person would be shown an incomplete '
|
|
1289
|
+
+ 'evidence package and the mechanical stages behind them would be silently deleted (the composition law: '
|
|
1290
|
+
+ 'mechanical first, human last)', { kind: s.kind, stage: label, at: i, last: stages.length - 1 });
|
|
1291
|
+
}
|
|
1292
|
+
// ── THE SAME LAW's judged half (design record 2026-08-17 §4.5: *mechanical
|
|
1293
|
+
// stages FIRST, the judged stage AFTER, first-red-wins*). The human rule is
|
|
1294
|
+
// stated as "last" because a person is the top of the hierarchy; the judged
|
|
1295
|
+
// rule is stated as "after the mechanical ones", because a human stage may
|
|
1296
|
+
// legally follow it — mechanical → judge → person is the full composition.
|
|
1297
|
+
//
|
|
1298
|
+
// What it costs to admit is the same both ways round: first-red-wins means a
|
|
1299
|
+
// judged stage in front of a mechanical one BUYS PAID CALLS to grade a tree
|
|
1300
|
+
// the free stages would have refused for nothing — and if the judge passes it
|
|
1301
|
+
// the mechanical red still lands, so the money bought no verdict either.
|
|
1302
|
+
// A SECOND judged stage is NOT this red (it is `duplicate-kind`, emitted
|
|
1303
|
+
// above): one problem, one red, in one vocabulary.
|
|
1304
|
+
if (s.kind === JUDGED_FLOOR_KIND) {
|
|
1305
|
+
const behind = stages.slice(i + 1).filter((/** @type {any} */ x) => isObj(x) && isNonEmptyString(x.kind)
|
|
1306
|
+
&& catalogue[x.kind] && CLASS_RANK[catalogue[x.kind].verdictClass] < CLASS_RANK['soft-green']);
|
|
1307
|
+
if (behind.length) {
|
|
1308
|
+
red('judged-stage-order', `${at}.kind`, `stage "${label}" is judged and runs BEFORE the mechanical `
|
|
1309
|
+
+ `stage${behind.length > 1 ? 's' : ''} ${behind.map((/** @type {any} */ x) => `"${x.name}"`).join(', ')}. `
|
|
1310
|
+
+ 'The close is first-red-wins and a judged stage buys a paid call per artifact: put every stage a command '
|
|
1311
|
+
+ 'can decide in front of it, so the free rulers shield the expensive one (the composition law: mechanical '
|
|
1312
|
+
+ 'first, judge after)',
|
|
1313
|
+
{ kind: s.kind, stage: label, at: i, behind: behind.map((/** @type {any} */ x) => x.name) });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1138
1317
|
const p = s.params;
|
|
1139
1318
|
if (!isObj(p)) { red('missing-field', `${at}.params`, `an object carrying: ${spec.required.join(', ')}`); return; }
|
|
1140
1319
|
for (const k of spec.required) {
|
|
@@ -1338,6 +1517,39 @@ function checkKind({ kind, params: p, at, red, scoped, label, populations, envOw
|
|
|
1338
1517
|
if (!strArray(p.allowPrefixes)) red('invalid-value', `${at}.params.allowPrefixes`, 'a non-empty array of path prefixes');
|
|
1339
1518
|
if (p.requireNonEmpty !== true) red('invalid-value', `${at}.params.requireNonEmpty`, 'literally true');
|
|
1340
1519
|
break;
|
|
1520
|
+
case 'human-confirms':
|
|
1521
|
+
// the whole surface: the question the signer answers. It is shown to a
|
|
1522
|
+
// person verbatim, so an empty one is a bare "approve?" — ruling 2's
|
|
1523
|
+
// named failure mode, refused here rather than rendered.
|
|
1524
|
+
if (!isNonEmptyString(p.ask)) {
|
|
1525
|
+
red('invalid-value', `${at}.params.ask`, 'the plain question the signer answers while looking at the finished '
|
|
1526
|
+
+ 'result — a human stage with nothing to ask is the bare "approve?" ruling 2 forbids');
|
|
1527
|
+
}
|
|
1528
|
+
break;
|
|
1529
|
+
case 'judged-floor': {
|
|
1530
|
+
// ONE spelling of what a card is, the JUDGE's own (`validateCard`, src/judged.js) —
|
|
1531
|
+
// the same rule `normalizeParser` follows: the validator and the grader must agree
|
|
1532
|
+
// about what a card MEANS, or the one that grades is the one nobody validated.
|
|
1533
|
+
const cv = validateCard(p.card);
|
|
1534
|
+
for (const detail of cv.reds) red('invalid-value', `${at}.params.card`, detail);
|
|
1535
|
+
// the artifact list. The listing rule (`checkPaths`) judges whether each element
|
|
1536
|
+
// EXISTS; this judges the shape and the bill — one paid call per artifact, so an
|
|
1537
|
+
// unbounded list is a bill rather than a red (the MAX_TERMS sibling).
|
|
1538
|
+
if (!strArray(p.paths)) {
|
|
1539
|
+
red('invalid-value', `${at}.params.paths`, 'a non-empty array of repository-relative files to judge — the '
|
|
1540
|
+
+ 'AUTHORITATIVE artifacts, read off the seed listing, never the worker\'s summary of what it did');
|
|
1541
|
+
} else if (new Set(p.paths.map(String)).size !== p.paths.length) {
|
|
1542
|
+
// one artifact is one population, and a list naming it twice buys two paid
|
|
1543
|
+
// calls over identical bytes — deduplicating it silently would leave the
|
|
1544
|
+
// signer reading one list while the runner spent on another
|
|
1545
|
+
red('invalid-value', `${at}.params.paths`, 'the same artifact is named twice — one artifact is one population, '
|
|
1546
|
+
+ 'and a duplicate buys a second paid call over identical bytes');
|
|
1547
|
+
} else if (p.paths.length > MAX_JUDGED_PATHS) {
|
|
1548
|
+
red('bounds', `${at}.params.paths`, `${p.paths.length} artifacts exceeds the ceiling of ${MAX_JUDGED_PATHS} — a `
|
|
1549
|
+
+ 'judged stage buys one call per artifact, and the ceiling is the arbiter\'s, not the declaration\'s');
|
|
1550
|
+
}
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1341
1553
|
default:
|
|
1342
1554
|
red('unknown-kind', `${at}.kind`, `"${kind}" is in the catalogue but has no checker`, { kind });
|
|
1343
1555
|
}
|