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/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
- export const LOCKED_CLASSES = Object.freeze(['soft-green', 'hitl']);
99
- /** the classes v1 actually builds — exactly one */
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
- * INERT IN v1 BY CONSTRUCTION, which is why it is cheap now: every LIVE kind is
119
- * mechanical, so no declaration a v1 catalogue can express reaches above green.
120
- * It is written before the second class exists so the first soft-green kind is
121
- * not the thing that discovers the question.
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
- // LOCKED: named so declaring one is COUNTED DEMAND rather than an unknown-kind
231
- // typo (D13 forward-compat point 2). Disclosure is not admission — these do
232
- // not run in v1, and `close[]` on disk stays predicate-only and shape-enforced
233
- // (src/job.js:442), so the inexpressibility that is already free stays free.
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([]), optional: Object.freeze([]), pathParams: Object.freeze([]), locked: true,
237
- shape: 'LOCKED — not available in v1',
238
- asserts: 'a judged score must clear a floor. Declaring it is recorded as demand; it will not run.',
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([]), optional: Object.freeze([]), pathParams: Object.freeze([]), locked: true,
243
- shape: 'LOCKED — not available in v1',
244
- asserts: 'a person renders the verdict. Declaring it is recorded as demand; it will not run.',
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
- export const CLASS_BATTERIES = Object.freeze({
617
- green: Object.freeze({
618
- locked: false,
619
- guards: Object.freeze([
620
- Object.freeze({
621
- name: 'changed-from-seed',
622
- kind: 'files-changed',
623
- params: Object.freeze({ requireNonEmpty: true }),
624
- compose: Object.freeze([]),
625
- fill: Object.freeze(['allowPrefixes']),
626
- }),
627
- Object.freeze({
628
- name: 'no-suppressions',
629
- kind: 'pattern-absent-in-diff',
630
- // NO scope: the scan covers every changed file of these extensions. A
631
- // narrowing here is the round-3 arm A defect and is a red, not a taste.
632
- params: Object.freeze({}),
633
- compose: Object.freeze(['patterns', 'extensions']),
634
- fill: Object.freeze([]),
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
- 'soft-green': Object.freeze({ locked: true, guards: null }),
639
- hitl: Object.freeze({ locked: true, guards: null }),
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
- red('duplicate-kind', `${at}.kind`, `stage "${label}" is a second ${s.kind} stage — "${owner}" already declares `
1132
- + 'which files this job may change. Stages are ANDed, so two of them mean the intersection of two prefix sets, '
1133
- + 'which is itself a prefix set and already says the same thing in one stage; what a second one does add is a '
1134
- + 'second answer to what this job\'s scope IS', { kind: s.kind, stage: label, twin: owner });
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
  }