bareloop 0.11.0 → 0.13.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 +252 -0
- package/README.md +3 -2
- package/bareloop.context.md +387 -9
- package/package.json +1 -1
- package/src/authorflow.js +176 -33
- package/src/authoring.js +170 -25
- package/src/authorjob.js +340 -26
- package/src/bridges.js +237 -10
- package/src/calibrate.js +597 -0
- package/src/cardauthor.js +479 -0
- package/src/declaredclose.js +108 -10
- package/src/index.js +100 -9
- package/src/job.js +27 -13
- package/src/judged.js +765 -0
- package/src/kinds.js +390 -6
- package/src/ledger.js +102 -0
- package/src/plan.js +43 -2
- package/src/planrun.js +492 -26
- package/src/readshim.js +538 -0
- package/src/reuse.js +322 -90
- package/src/reviewdoor.js +246 -0
- package/src/run.js +95 -6
- package/src/selection.js +22 -6
- package/src/tools.js +34 -2
- package/types/authorflow.d.ts +147 -9
- package/types/authoring.d.ts +54 -8
- package/types/authorjob.d.ts +36 -6
- package/types/bridges.d.ts +95 -1
- package/types/calibrate.d.ts +191 -0
- package/types/cardauthor.d.ts +352 -0
- package/types/declaredclose.d.ts +34 -5
- package/types/index.d.ts +12 -7
- package/types/job.d.ts +22 -9
- package/types/judged.d.ts +280 -0
- package/types/kinds.d.ts +151 -4
- package/types/ledger.d.ts +58 -0
- package/types/plan.d.ts +19 -2
- package/types/planrun.d.ts +91 -2
- package/types/readshim.d.ts +117 -0
- package/types/reuse.d.ts +146 -3
- package/types/reviewdoor.d.ts +81 -0
- package/types/run.d.ts +63 -1
- package/types/tools.d.ts +17 -1
package/src/authorflow.js
CHANGED
|
@@ -75,11 +75,12 @@
|
|
|
75
75
|
import { createRequire } from 'node:module';
|
|
76
76
|
import { realpathSync } from 'node:fs';
|
|
77
77
|
import { join, relative, isAbsolute } from 'node:path';
|
|
78
|
-
import { seedRead as runSeedReadStages, makeSeedTrees, AGGREGATES, SIGNS, MAX_TERMS } from './kinds.js';
|
|
78
|
+
import { seedRead as runSeedReadStages, makeSeedTrees, AGGREGATES, SIGNS, MAX_TERMS, MAX_JUDGED_PATHS } from './kinds.js';
|
|
79
|
+
import { JUDGE_RULE_IDS } from './judged.js';
|
|
79
80
|
import {
|
|
80
81
|
KIND_CATALOGUE, MAX_STAGES, DIRECTIONS, BASELINES,
|
|
81
82
|
GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
82
|
-
validateDeclaration, envCapableKind, VERDICT_CLASSES, LIVE_CLASSES,
|
|
83
|
+
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES,
|
|
83
84
|
} from './authoring.js';
|
|
84
85
|
import { buildSeedListing, cleanEntry, SURVEY_CAUSES } from './authorscout.js';
|
|
85
86
|
import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop } from './text.js';
|
|
@@ -223,11 +224,47 @@ export const HITL_QUESTIONS = Object.freeze({
|
|
|
223
224
|
});
|
|
224
225
|
|
|
225
226
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
227
|
+
* THE SOFTGREEN SET (module 3, design record §4.6) — the green questions BYTE
|
|
228
|
+
* FOR BYTE, plus TWO.
|
|
229
|
+
*
|
|
230
|
+
* Same discipline as the hitl set, and for the same reason: a softgreen close is
|
|
231
|
+
* *mechanical stages first, the judged stage after* (§4.5), so a softgreen job
|
|
232
|
+
* needs every mechanical fact a green one does. No renumbering of the green set,
|
|
233
|
+
* no genre question, nothing reworded.
|
|
234
|
+
*
|
|
235
|
+
* The two additions are the only things the green set cannot supply, and neither
|
|
236
|
+
* is a flourish — they are the judged floor's two REQUIRED inputs:
|
|
237
|
+
* - Q6 becomes the RUBRIC CARD (§4.3): the person's own pass/fail lines,
|
|
238
|
+
* compiled into enumerated items, signed, and enumerated in the spec hash.
|
|
239
|
+
* The documented ceiling rides with it — the judge catches violations of
|
|
240
|
+
* STATED card items only, and the fix for a miss is a new card line and a
|
|
241
|
+
* re-sign, never a smarter judge.
|
|
242
|
+
* - Q7 becomes the frozen CALIBRATION SET (§4.4): the whole pipe must grade it
|
|
243
|
+
* correctly before the close is signable.
|
|
244
|
+
* Both are asked in the person's own terms — an example they would pass and one
|
|
245
|
+
* they would fail, not a rubric they are asked to write — because compiling a lay
|
|
246
|
+
* answer into extractable items is the LLM's job (D5, proposed-and-signed) and
|
|
247
|
+
* asking the person to do it is the SWE tax this product refuses.
|
|
248
|
+
*
|
|
249
|
+
* What the two answers COMPILE INTO is module 4. This module carries them
|
|
250
|
+
* exactly as the hitl set carries its own last answer: they ride in `answers`,
|
|
251
|
+
* they reach the composer verbatim through the prompt's interview block, and
|
|
252
|
+
* nothing here derives anything from them.
|
|
253
|
+
*/
|
|
254
|
+
export const SOFTGREEN_QUESTIONS = Object.freeze({
|
|
255
|
+
...GREEN_QUESTIONS,
|
|
256
|
+
6: 'When you judge the result yourself, what separates a pass from a fail? Name the few things you actually look for.',
|
|
257
|
+
7: 'Give one example you\'d pass and one you\'d fail, and say why.',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The three sets, all LIVE as of softgreen module 3. The locked shape stays
|
|
262
|
+
* spelled out in the readers below rather than deleted with the last locked
|
|
263
|
+
* class: `questions: null` is ABSENT, never `{}` — an empty set would read as
|
|
264
|
+
* "this class needs no questions", a different and false statement (F59's
|
|
265
|
+
* distinction, at the interview layer) — and a locked class refuses at ADMISSION,
|
|
266
|
+
* before its questions run, so the two readers throw rather than hand back
|
|
267
|
+
* nothing. A fourth class arrives locked, and the machinery is here for it.
|
|
231
268
|
* @type {Record<string, {locked: boolean, questions: Record<string|number, string>|null, required: readonly number[]|null}>}
|
|
232
269
|
*/
|
|
233
270
|
export const QUESTION_SETS = Object.freeze({
|
|
@@ -236,7 +273,11 @@ export const QUESTION_SETS = Object.freeze({
|
|
|
236
273
|
questions: GREEN_QUESTIONS,
|
|
237
274
|
required: Object.freeze(Object.keys(GREEN_QUESTIONS).map(Number).sort((a, b) => a - b)),
|
|
238
275
|
}),
|
|
239
|
-
'soft-green': Object.freeze({
|
|
276
|
+
'soft-green': Object.freeze({
|
|
277
|
+
locked: false,
|
|
278
|
+
questions: SOFTGREEN_QUESTIONS,
|
|
279
|
+
required: Object.freeze(Object.keys(SOFTGREEN_QUESTIONS).map(Number).sort((a, b) => a - b)),
|
|
280
|
+
}),
|
|
240
281
|
hitl: Object.freeze({
|
|
241
282
|
locked: false,
|
|
242
283
|
questions: HITL_QUESTIONS,
|
|
@@ -259,7 +300,18 @@ export const CLASS_STATEMENTS = Object.freeze({
|
|
|
259
300
|
+ 'to decide it — no judgement, no score, and no person in the loop. Every stage you compose is a mechanical '
|
|
260
301
|
+ 'measurement of this repository. If part of what they asked for cannot be measured that way, leave it out and '
|
|
261
302
|
+ 'say so in your notes rather than approximating it with a stage that means something else.',
|
|
262
|
-
'
|
|
303
|
+
// softgreen module 3. Stated as an ORDER for the same reason hitl's is: the
|
|
304
|
+
// close is first-red-wins, so the cheap mechanical stages shield the one stage
|
|
305
|
+
// that spends money, and the judge is never the first filter. The last sentence
|
|
306
|
+
// is the documented ceiling (§4.3) said to the composer in its own terms — the
|
|
307
|
+
// judge only ever checks lines the card names, so an unstated expectation is
|
|
308
|
+
// not a stricter judge, it is a line nobody wrote.
|
|
309
|
+
'soft-green': 'The person declared this job SOFT-GREEN: part of what "done" means here needs JUDGEMENT — reading '
|
|
310
|
+
+ 'the result and deciding whether it is any good — and no command can render it. Compose the mechanical part '
|
|
311
|
+
+ 'first: everything about this repository a command can decide is still a mechanical stage, exactly as it would '
|
|
312
|
+
+ 'be for a green job. Then put ONE judged-floor stage AFTER them, carrying the few things the person said they '
|
|
313
|
+
+ 'actually look for and the real files the work lands in. A judged stage decides only the lines its card names, '
|
|
314
|
+
+ 'so name each thing they look for as its own line, and leave nothing to be inferred.',
|
|
263
315
|
// N4 slice 1. The mechanical-first composition law is stated as an ORDER
|
|
264
316
|
// because first-red-wins makes the order the mechanism: the cheap stages shield
|
|
265
317
|
// the expensive one, and the person is never the first filter.
|
|
@@ -408,6 +460,39 @@ export const PARAM_SCHEMAS = Object.freeze({
|
|
|
408
460
|
extensions: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
409
461
|
allowPrefixes: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
410
462
|
requireNonEmpty: { const: true },
|
|
463
|
+
// SOFTGREEN's two. `rule` is an ENUM off the judge's own rulebook — where only a
|
|
464
|
+
// closed set can satisfy a field, the illegal value is made inexpressible rather
|
|
465
|
+
// than rejected after the fact (`check-passes(name)`, again) — and `text` is the
|
|
466
|
+
// only free string, carrying the signer's words so a red can be explained in them.
|
|
467
|
+
card: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
description: 'the rubric, as enumerated items: one line per thing the person actually looks for, in the order '
|
|
470
|
+
+ 'they look for them. The rule is SELECTED from the arbiter\'s own rulebook; the text is theirs.',
|
|
471
|
+
properties: {
|
|
472
|
+
items: {
|
|
473
|
+
type: 'array',
|
|
474
|
+
minItems: 1,
|
|
475
|
+
items: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
rule: { enum: [...JUDGE_RULE_IDS], description: 'which owned rule this line asserts' },
|
|
479
|
+
text: { type: 'string', minLength: 1, description: 'what this line means, in the person\'s own words' },
|
|
480
|
+
},
|
|
481
|
+
required: ['rule', 'text'],
|
|
482
|
+
additionalProperties: false,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
required: ['items'],
|
|
487
|
+
additionalProperties: false,
|
|
488
|
+
},
|
|
489
|
+
paths: {
|
|
490
|
+
type: 'array',
|
|
491
|
+
minItems: 1,
|
|
492
|
+
maxItems: MAX_JUDGED_PATHS,
|
|
493
|
+
items: { type: 'string' },
|
|
494
|
+
description: 'the AUTHORITATIVE artifacts to judge, repository-relative, read off the listing. One paid call each.',
|
|
495
|
+
},
|
|
411
496
|
ask: {
|
|
412
497
|
type: 'string',
|
|
413
498
|
minLength: 1,
|
|
@@ -451,19 +536,27 @@ export function schemaCoverage(catalogue = KIND_CATALOGUE) {
|
|
|
451
536
|
* and they arrive as validation reds through the revise loop — a schema that
|
|
452
537
|
* tried to restate them would be a second validator.
|
|
453
538
|
*
|
|
539
|
+
* `verdictType` narrows the branches to the kinds the picked class may express
|
|
540
|
+
* (softgreen module 3, `classMenu`): a green job cannot SPELL `judged-floor`,
|
|
541
|
+
* rather than spelling it and being told afterwards. COVERAGE is still computed
|
|
542
|
+
* over the WHOLE catalogue — the class filter must never make a parameter look
|
|
543
|
+
* orphaned and turn a menu narrowing into a catalogue-drift alarm.
|
|
544
|
+
*
|
|
454
545
|
* THROWS on a catalogue it cannot cover: a malformed catalogue is a bareloop bug,
|
|
455
546
|
* and failing loudly beats emitting a schema that quietly cannot say something
|
|
456
547
|
* the validator will then demand.
|
|
457
548
|
* @param {Record<string, any>} [catalogue]
|
|
549
|
+
* @param {{verdictType?: string|null}} [o]
|
|
458
550
|
*/
|
|
459
|
-
export function declarationSchema(catalogue = KIND_CATALOGUE) {
|
|
551
|
+
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null } = {}) {
|
|
460
552
|
const cov = schemaCoverage(catalogue);
|
|
461
553
|
if (!cov.ok) {
|
|
462
554
|
throw new Error('[authorflow] the declaration schema does not cover the catalogue — '
|
|
463
555
|
+ `missing: ${cov.missing.join(', ') || 'none'}; orphaned: ${cov.orphan.join(', ') || 'none'}`);
|
|
464
556
|
}
|
|
557
|
+
const menu = classMenu(verdictType, catalogue);
|
|
465
558
|
const branches = Object.entries(catalogue)
|
|
466
|
-
.filter(([, spec]) => !spec.locked)
|
|
559
|
+
.filter(([kind, spec]) => !spec.locked && menu.includes(kind))
|
|
467
560
|
.map(([kind, spec]) => {
|
|
468
561
|
/** @type {Record<string, any>} */
|
|
469
562
|
const properties = {};
|
|
@@ -511,13 +604,13 @@ export function declarationSchema(catalogue = KIND_CATALOGUE) {
|
|
|
511
604
|
* The output channel. It records and acknowledges; it takes no action, which is
|
|
512
605
|
* why handing it to a toolless authoring call does not reopen D3.
|
|
513
606
|
* @param {{calls: any[]}} box
|
|
514
|
-
* @param {{catalogue?: Record<string, any
|
|
607
|
+
* @param {{catalogue?: Record<string, any>, verdictType?: string|null}} [o]
|
|
515
608
|
*/
|
|
516
|
-
export function declarationTool(box, { catalogue = KIND_CATALOGUE } = {}) {
|
|
609
|
+
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null } = {}) {
|
|
517
610
|
return {
|
|
518
611
|
name: DECLARATION_TOOL_NAME,
|
|
519
612
|
description: 'Deliver the finished close. Call this exactly once, with the whole declaration as its arguments.',
|
|
520
|
-
parameters: declarationSchema(catalogue),
|
|
613
|
+
parameters: declarationSchema(catalogue, { verdictType }),
|
|
521
614
|
execute: async (/** @type {any} */ args) => { box.calls.push(args); return DECLARATION_ACK; },
|
|
522
615
|
};
|
|
523
616
|
}
|
|
@@ -528,10 +621,19 @@ export function declarationTool(box, { catalogue = KIND_CATALOGUE } = {}) {
|
|
|
528
621
|
* The catalogue, as the authoring call sees it — rendered FROM the catalogue, so
|
|
529
622
|
* the prompt and the validator judge one set of facts. `shape` and `asserts` are
|
|
530
623
|
* M2's own strings.
|
|
624
|
+
*
|
|
625
|
+
* `verdictType` narrows it to the picked class's menu, exactly as it narrows the
|
|
626
|
+
* schema — the prompt and the tool must offer ONE set of kinds, or the composer
|
|
627
|
+
* reads about a kind it cannot call. A kind above the ceiling is omitted rather
|
|
628
|
+
* than rendered as unavailable: a LOCKED row says "this does not run in v1",
|
|
629
|
+
* which is a fact about the build; "not for your class" is a fact about a choice
|
|
630
|
+
* the person already made, and printing it invites the composer to argue with it.
|
|
531
631
|
* @param {Record<string, any>} [catalogue]
|
|
632
|
+
* @param {{verdictType?: string|null}} [o]
|
|
532
633
|
*/
|
|
533
|
-
export function catalogueBlock(catalogue = KIND_CATALOGUE) {
|
|
534
|
-
const
|
|
634
|
+
export function catalogueBlock(catalogue = KIND_CATALOGUE, { verdictType = null } = {}) {
|
|
635
|
+
const menu = classMenu(verdictType, catalogue);
|
|
636
|
+
const rows = Object.entries(catalogue).filter(([name]) => menu.includes(name)).map(([name, spec]) => (spec.locked
|
|
535
637
|
? `- ${name} — LOCKED. Not available in v1; it will not run.`
|
|
536
638
|
: `- ${name}\n ${spec.shape}\n required: ${spec.required.join(', ')}`
|
|
537
639
|
+ `${spec.optional.length ? ` optional: ${spec.optional.join(', ')}` : ''}\n asserts: ${spec.asserts}`));
|
|
@@ -719,7 +821,9 @@ measure anything.`;
|
|
|
719
821
|
`THE INTERVIEW — the person's own words\n\n${interview}`,
|
|
720
822
|
`THE FACTS OBJECT — from a read-only survey of the repository\n\n${JSON.stringify(facts, null, 2)}`
|
|
721
823
|
+ (listingBlock ? `\n\n${listingBlock}` : ''),
|
|
722
|
-
|
|
824
|
+
// CLASS-SCOPED (softgreen module 3): the composer for a green job is never
|
|
825
|
+
// shown a kind whose pick would red as `class-ceiling` after the call.
|
|
826
|
+
catalogueBlock(catalogue, { verdictType }),
|
|
723
827
|
lawsBlock(),
|
|
724
828
|
`THE GENRE TEMPLATE — fixed by the job's genre (${lang}), not yours to decide\n\n`
|
|
725
829
|
+ 'This job\'s genre carries a fixed close SHAPE. The following policy is law for your declaration. It names\n'
|
|
@@ -1058,13 +1162,23 @@ function canonical(x) {
|
|
|
1058
1162
|
|
|
1059
1163
|
/**
|
|
1060
1164
|
* ONE structured ask, with the malformed-emission retry bounded on its OWN axis.
|
|
1061
|
-
* A reply that carries no
|
|
1165
|
+
* A reply that carries no artifact is an ARTIFACT-RED — name the axis, retry
|
|
1062
1166
|
* under a cap, write nothing — and it never consumes a revision, because a
|
|
1063
1167
|
* transport fault is not a defect in a close.
|
|
1168
|
+
*
|
|
1169
|
+
* THE CHANNEL IS A PARAMETER, and that is softgreen module 4's only change here:
|
|
1170
|
+
* the rubric card and the calibration set are a SECOND schema-forced emission
|
|
1171
|
+
* with the same failure modes, and a second ladder is a second place for the cap,
|
|
1172
|
+
* the reject block and the re-ask to drift. The declaration channel below is the
|
|
1173
|
+
* one this loop has always used, spelled out rather than inlined — nothing about
|
|
1174
|
+
* the authoring path's behaviour moves.
|
|
1064
1175
|
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
1065
|
-
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
1176
|
+
* label: string, book: ReturnType<typeof makeCostBook>,
|
|
1177
|
+
* channel: {name: string, instruction: string, textPath?: string, tool: (box: {calls: any[]}) => any}}} o
|
|
1178
|
+
* @returns {Promise<{artifact: any, attempts: number, convo: any[], raw: string,
|
|
1179
|
+
* providerError: string|null, red: Red|null, budget: 'cap-halt'|'pricing-red'|null}>}
|
|
1066
1180
|
*/
|
|
1067
|
-
async function
|
|
1181
|
+
export async function askStructured({ messages, generate, mode, retries, label, book, channel }) {
|
|
1068
1182
|
/** @type {any[]} */
|
|
1069
1183
|
const convo = [...messages];
|
|
1070
1184
|
let attempts = 0;
|
|
@@ -1080,49 +1194,78 @@ async function askDeclaration({ messages, generate, mode, retries, label, book,
|
|
|
1080
1194
|
// unbounded. `attempts` is returned as-is so the caller can tell a cap that
|
|
1081
1195
|
// tripped before this ask began (0) from one that cut a retry ladder short.
|
|
1082
1196
|
const halt = book.capStop();
|
|
1083
|
-
if (halt) return {
|
|
1197
|
+
if (halt) return { artifact: null, attempts, convo, raw: '', providerError: null, red: null, budget: halt };
|
|
1084
1198
|
|
|
1085
1199
|
/** @type {{calls: any[]}} */
|
|
1086
1200
|
const box = { calls: [] };
|
|
1087
|
-
|
|
1201
|
+
// the tool's schema is CLASS-SCOPED, like the prompt's catalogue block: the
|
|
1202
|
+
// two must offer one set of kinds, or the composer reads about a kind it
|
|
1203
|
+
// cannot call — or worse, calls one the ceiling will refuse after we paid.
|
|
1204
|
+
const tools = mode === 'tool' ? [channel.tool(box)] : [];
|
|
1088
1205
|
const r = await generate(convo, tools, {});
|
|
1089
1206
|
attempts += 1;
|
|
1090
1207
|
book.add(attempt === 0 ? label : `${label}#${attempt + 1}`, r, attempts);
|
|
1091
1208
|
const raw = redactSecrets(String(r?.text ?? ''));
|
|
1092
1209
|
|
|
1093
1210
|
if (r?.error) {
|
|
1094
|
-
return {
|
|
1211
|
+
return { artifact: null, attempts, convo, raw, providerError: String(r.error), red: null, budget: null };
|
|
1095
1212
|
}
|
|
1096
1213
|
|
|
1097
1214
|
if (mode === 'tool') {
|
|
1098
|
-
if (box.calls.length === 1) return {
|
|
1215
|
+
if (box.calls.length === 1) return { artifact: box.calls[0], attempts, convo, raw, providerError: null, red: null, budget: null };
|
|
1099
1216
|
red = {
|
|
1100
1217
|
code: 'artifact-red',
|
|
1101
|
-
path:
|
|
1218
|
+
path: channel.name,
|
|
1102
1219
|
detail: box.calls.length === 0
|
|
1103
|
-
? `the reply delivered no ${
|
|
1104
|
-
: `the reply delivered ${box.calls.length} ${
|
|
1220
|
+
? `the reply delivered no ${channel.name} call — structure is enforced, prose is never parsed`
|
|
1221
|
+
: `the reply delivered ${box.calls.length} ${channel.name} calls; exactly one is expected`,
|
|
1105
1222
|
axis: box.calls.length === 0 ? 'no-declaration-tool-call' : 'multiple-declaration-tool-calls',
|
|
1106
1223
|
};
|
|
1107
1224
|
} else {
|
|
1108
1225
|
// THE FALLBACK PATH — providers without tool mode only. The ONE shipped
|
|
1109
1226
|
// parser, never a second one.
|
|
1227
|
+
const at = channel.textPath ?? channel.name;
|
|
1110
1228
|
const { code, red: extractRed } = extractArtifact(raw);
|
|
1111
1229
|
if (code) {
|
|
1112
|
-
try { return {
|
|
1113
|
-
catch (e) { red = { code: 'artifact-red', path:
|
|
1230
|
+
try { return { artifact: JSON.parse(code), attempts, convo, raw, providerError: null, red: null, budget: null }; }
|
|
1231
|
+
catch (e) { red = { code: 'artifact-red', path: at, detail: `the ${at} did not parse as JSON: ${String(/** @type {any} */ (e)?.message ?? e)}`, axis: 'unparseable-json' }; }
|
|
1114
1232
|
} else {
|
|
1115
|
-
red = { code: 'artifact-red', path:
|
|
1233
|
+
red = { code: 'artifact-red', path: at, detail: `no artifact in the reply: ${extractRed}`, axis: 'no-artifact' };
|
|
1116
1234
|
}
|
|
1117
1235
|
}
|
|
1118
1236
|
|
|
1119
1237
|
if (attempt < retries) {
|
|
1120
1238
|
const block = renderRejectBlock({ kind: 'artifact', reds: [/** @type {Red} */ (red).detail] });
|
|
1121
1239
|
convo.push({ role: 'assistant', content: raw.trim() || '(the reply carried no text)' });
|
|
1122
|
-
convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ?
|
|
1240
|
+
convo.push({ role: 'user', content: `${block}\n\n${mode === 'tool' ? channel.instruction : STRUCTURE_INSTRUCTION_TEXT}` });
|
|
1123
1241
|
}
|
|
1124
1242
|
}
|
|
1125
|
-
return {
|
|
1243
|
+
return { artifact: null, attempts, convo, raw: '', providerError: null, red, budget: null };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** THE DECLARATION CHANNEL — the one this loop has always used. Its schema is
|
|
1247
|
+
* CLASS-SCOPED, like the prompt's catalogue block: the two must offer one set of
|
|
1248
|
+
* kinds, or the composer reads about a kind it cannot call.
|
|
1249
|
+
* @param {{catalogue: Record<string, any>, verdictType: string}} o */
|
|
1250
|
+
const declarationChannel = ({ catalogue, verdictType }) => ({
|
|
1251
|
+
name: DECLARATION_TOOL_NAME,
|
|
1252
|
+
instruction: STRUCTURE_INSTRUCTION_TOOL,
|
|
1253
|
+
textPath: 'declaration',
|
|
1254
|
+
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType }),
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
/** The declaration ask, over the shared ladder. `declaration` is `artifact`
|
|
1258
|
+
* under this channel's own name — the flow below reads a declaration, not an
|
|
1259
|
+
* anonymous artifact, and one word for two things is how a reader loses which.
|
|
1260
|
+
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
1261
|
+
* label: string, book: ReturnType<typeof makeCostBook>, catalogue: Record<string, any>,
|
|
1262
|
+
* verdictType: string}} o
|
|
1263
|
+
*/
|
|
1264
|
+
async function askDeclaration({ messages, generate, mode, retries, label, book, catalogue, verdictType }) {
|
|
1265
|
+
const r = await askStructured({
|
|
1266
|
+
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType }),
|
|
1267
|
+
});
|
|
1268
|
+
return { ...r, declaration: r.artifact };
|
|
1126
1269
|
}
|
|
1127
1270
|
|
|
1128
1271
|
/**
|
|
@@ -1391,7 +1534,7 @@ export async function authorClose({
|
|
|
1391
1534
|
// a readout quoting a number the loop does not obey is a second
|
|
1392
1535
|
// instrument, and this file's own history is what that costs.
|
|
1393
1536
|
onPhase('author-call', { call: label, i, of: revisionCap });
|
|
1394
|
-
const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue });
|
|
1537
|
+
const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue, verdictType });
|
|
1395
1538
|
messages = ask.convo;
|
|
1396
1539
|
|
|
1397
1540
|
// The cap tripped BEFORE this ask made any call at all, so there is no
|
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 */
|
|
@@ -95,9 +96,15 @@ import { isObj, isNonEmptyString, hasNestedQuantifier, globToPrefix } from './va
|
|
|
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
98
|
/** declared-but-locked: selecting one is COUNTED demand, never an admission.
|
|
98
|
-
* `hitl` LEFT this list at N4 slice 1
|
|
99
|
-
*
|
|
100
|
-
|
|
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([]);
|
|
101
108
|
/** the classes v1 actually builds */
|
|
102
109
|
export const LIVE_CLASSES = Object.freeze(VERDICT_CLASSES.filter((c) => !LOCKED_CLASSES.includes(c)));
|
|
103
110
|
|
|
@@ -117,10 +124,11 @@ const CLASS_RANK = Object.freeze({ green: 0, 'soft-green': 1, hitl: 2 });
|
|
|
117
124
|
* `CLASS_BY_VERDICT` exist to stop, defeated from ABOVE by never letting the
|
|
118
125
|
* illegal pair be formed.
|
|
119
126
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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.
|
|
124
132
|
*
|
|
125
133
|
* A kind the catalogue does not know contributes NOTHING — it already carries an
|
|
126
134
|
* `unknown-kind` red, and guessing a class for it would be deriving the promise
|
|
@@ -154,6 +162,49 @@ export function closeCeiling(declaration, catalogue = KIND_CATALOGUE) {
|
|
|
154
162
|
return top;
|
|
155
163
|
}
|
|
156
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
|
+
|
|
157
208
|
// ── 1. THE KIND CATALOGUE ────────────────────────────────────────────────────
|
|
158
209
|
|
|
159
210
|
/**
|
|
@@ -229,15 +280,33 @@ export const KIND_CATALOGUE = Object.freeze({
|
|
|
229
280
|
shape: 'allowPrefixes: string[], requireNonEmpty: true',
|
|
230
281
|
asserts: 'the set of files this run changed must be non-empty and must lie wholly inside allowPrefixes.',
|
|
231
282
|
}),
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
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.
|
|
236
300
|
'judged-floor': Object.freeze({
|
|
237
301
|
verdictClass: 'soft-green',
|
|
238
|
-
required: Object.freeze([
|
|
239
|
-
|
|
240
|
-
|
|
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.',
|
|
241
310
|
}),
|
|
242
311
|
// LIVE at N4 slice 1, and the ONLY kind that does not measure anything: it
|
|
243
312
|
// does not RUN, it PAUSES the run and hands the tree to a person (ruling 1).
|
|
@@ -651,7 +720,15 @@ const MECHANICAL_GUARDS = Object.freeze([
|
|
|
651
720
|
|
|
652
721
|
export const CLASS_BATTERIES = Object.freeze({
|
|
653
722
|
green: Object.freeze({ locked: false, guards: MECHANICAL_GUARDS }),
|
|
654
|
-
|
|
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 }),
|
|
655
732
|
// OPEN-1, RULED (hamr, in-turn, 2026-08-13): **hitl inherits the green
|
|
656
733
|
// mechanical guards and adds nothing a human stage cannot see.** D5 makes the
|
|
657
734
|
// battery mandatory, shown-and-fixed and un-removable, and PRD v1.57 §2 keys it
|
|
@@ -948,7 +1025,29 @@ function scopeOfJob(declaration, idx, guards) {
|
|
|
948
1025
|
* stage — applied to the one population that decides whether the law fires at
|
|
949
1026
|
* all.
|
|
950
1027
|
*/
|
|
951
|
-
const AT_MOST_ONCE_KINDS = Object.freeze(['files-changed', 'human-confirms']);
|
|
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
|
+
});
|
|
952
1051
|
|
|
953
1052
|
/** the kinds NO ONE runs as an in-run ruler — ruling 5's law, as data. A
|
|
954
1053
|
* declaration that offers one is a red rather than a normalization: the signer
|
|
@@ -1156,14 +1255,12 @@ export function validateDeclaration(declaration, opts = {}) {
|
|
|
1156
1255
|
if (AT_MOST_ONCE_KINDS.includes(s.kind)) {
|
|
1157
1256
|
const owner = kindOwners.get(s.kind);
|
|
1158
1257
|
if (owner !== undefined) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
+ '
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
+ 'which is itself a prefix set and already says the same thing in one stage; what a second one does add is a '
|
|
1166
|
-
+ '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 });
|
|
1167
1264
|
} else kindOwners.set(s.kind, label);
|
|
1168
1265
|
}
|
|
1169
1266
|
|
|
@@ -1192,6 +1289,30 @@ export function validateDeclaration(declaration, opts = {}) {
|
|
|
1192
1289
|
+ 'evidence package and the mechanical stages behind them would be silently deleted (the composition law: '
|
|
1193
1290
|
+ 'mechanical first, human last)', { kind: s.kind, stage: label, at: i, last: stages.length - 1 });
|
|
1194
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
|
+
}
|
|
1195
1316
|
|
|
1196
1317
|
const p = s.params;
|
|
1197
1318
|
if (!isObj(p)) { red('missing-field', `${at}.params`, `an object carrying: ${spec.required.join(', ')}`); return; }
|
|
@@ -1405,6 +1526,30 @@ function checkKind({ kind, params: p, at, red, scoped, label, populations, envOw
|
|
|
1405
1526
|
+ 'result — a human stage with nothing to ask is the bare "approve?" ruling 2 forbids');
|
|
1406
1527
|
}
|
|
1407
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
|
+
}
|
|
1408
1553
|
default:
|
|
1409
1554
|
red('unknown-kind', `${at}.kind`, `"${kind}" is in the catalogue but has no checker`, { kind });
|
|
1410
1555
|
}
|