bareloop 0.41.0 → 0.42.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 +63 -1
- package/README.md +13 -21
- package/bareloop.context.md +44 -4
- package/package.json +4 -3
- package/src/authorflow.js +337 -29
- package/src/authoring.js +92 -7
- package/src/authorjob.js +78 -16
- package/src/authorreadout.js +89 -5
- package/src/authorrun.js +14 -5
- package/src/authorscout.js +108 -0
- package/src/declaredclose.js +23 -12
- package/src/facts.js +317 -0
- package/src/factwords.js +129 -0
- package/src/inputlook.js +301 -0
- package/src/job.js +2 -1
- package/src/jobblock.js +7 -2
- package/src/kinds.js +84 -3
- package/src/labelscan.js +170 -0
- package/src/pagefetch.js +125 -0
- package/src/panel/authorroutes.js +15 -2
- package/src/panel/authorsession.js +99 -22
- package/src/panel/index.html +33 -7
- package/src/panel/jobplan.js +34 -11
- package/src/panel/server.js +19 -3
- package/src/planrun.js +3 -2
- package/src/rubricstep.js +804 -0
- package/src/source.js +166 -1
- package/src/u-readout.js +1 -1
- package/src/userrun.js +14 -2
- package/types/authorflow.d.ts +267 -13
- package/types/authoring.d.ts +29 -1
- package/types/authorjob.d.ts +7 -5
- package/types/authorreadout.d.ts +27 -2
- package/types/authorscout.d.ts +70 -0
- package/types/declaredclose.d.ts +4 -2
- package/types/facts.d.ts +71 -0
- package/types/factwords.d.ts +42 -0
- package/types/inputlook.d.ts +238 -0
- package/types/jobblock.d.ts +8 -1
- package/types/labelscan.d.ts +38 -0
- package/types/pagefetch.d.ts +71 -0
- package/types/panel/authorsession.d.ts +18 -2
- package/types/panel/jobplan.d.ts +12 -4
- package/types/panel/server.d.ts +4 -2
- package/types/rubricstep.d.ts +286 -0
- package/types/source.d.ts +94 -0
- package/types/userrun.d.ts +11 -0
package/src/authoring.js
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
import { normalizeParser, MAX_JUDGED_PATHS, JUDGED_FLOOR_KIND } from './kinds.js';
|
|
76
76
|
import { JUDGE_RULE_IDS, validateCard } from './judged.js';
|
|
77
|
+
import { rubricStepShapeReds, checkRubricParts, checkRubricRules } from './rubricstep.js';
|
|
77
78
|
import { isObj, isNonEmptyString, hasNestedQuantifier, globToPrefix } from './validate.js';
|
|
78
79
|
|
|
79
80
|
/** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
|
|
@@ -340,6 +341,36 @@ export const KIND_CATALOGUE = Object.freeze({
|
|
|
340
341
|
+ 'Name one card line per thing you actually look for, in the order you look for them, and name the real files the '
|
|
341
342
|
+ 'work lands in. It is never offered to the agent as an in-run check.',
|
|
342
343
|
}),
|
|
344
|
+
// PART 1a (docs/product/ITEM33-BUILD.md, "Addendum 2026-10-12"): the rubric job on LOCAL FILES. One stage per numbered job
|
|
345
|
+
// line, reading the one file that line leaves behind (output/steps/N.json). It measures nothing with a command and buys
|
|
346
|
+
// nothing: bareguard's rubric primitive grades the file against the person's own counted limits and quotes. It exists only in
|
|
347
|
+
// the text genre's menu (`catalogueForLang`) — a code job never sees it. `parts` is the person's constraints on the line,
|
|
348
|
+
// each SORTED by the drafter into a class; only `countable` parts become checks, and the validator reds the rest in plain
|
|
349
|
+
// words (`checkRubricParts`), so no signed stage carries a part this build cannot run.
|
|
350
|
+
'rubric-step': Object.freeze({
|
|
351
|
+
verdictClass: 'green',
|
|
352
|
+
required: Object.freeze(['step', 'parts']),
|
|
353
|
+
optional: Object.freeze([]),
|
|
354
|
+
pathParams: Object.freeze([]),
|
|
355
|
+
shape: 'step: integer (the job line number), parts: [{text: string, class: "countable" | "fact" | "judged" | "guard" | "none", '
|
|
356
|
+
+ 'check?: "maxWords" | "minWords" | "maxLines" | "sections" | "sectionWords" | "cited" | "minClaims" | "maxClaims", value?: integer, names?: string[], '
|
|
357
|
+
+ 'wordsPerSection?: integer, source?: string (an input label), domains?: string[], covers?: integer[], rules?: integer[], '
|
|
358
|
+
+ 'fact parts: facts?: [{name, shape: "money"|"number"|"date"|"time"|"phrase"|"text", unit?|currency?, phrase?, anchor?, until?, '
|
|
359
|
+
+ 'rule?: {op, value | lo+hi}, aiProposal?: boolean}], count?: {min?, max?}}]',
|
|
360
|
+
asserts: 'the file output/<step>.json under output/steps/ exists, is a JSON object, and its "text" is present and within the '
|
|
361
|
+
+ 'limits the person asked for. Write ONE rubric-step stage per numbered job line (step = that line number, fromLine = [that line]). '
|
|
362
|
+
+ 'In "parts" list each constraint the person put on that line, in their own words, and SORT it: "countable" when a machine can '
|
|
363
|
+
+ 'count it (check maxWords/minWords/maxLines with value; sections with names; sectionWords with names and wordsPerSection) or '
|
|
364
|
+
+ 'when claims must be backed by a quote from a named input (check cited, source = the input label), or when a number of '
|
|
365
|
+
+ 'quoted claims is asked for (minClaims / maxClaims with value and source: it counts the claims listed under that input); '
|
|
366
|
+
+ 'a line that FINDS, LISTS or LOOKS UP something gets ONE part of class "fact" instead (source = the input label, '
|
|
367
|
+
+ 'facts = the required facts, count = how many results): its step file holds only filled facts and no free text, so '
|
|
368
|
+
+ 'give such a line no word, section or claim-count parts; list in "rules" the number of every ~ rule a part carries out; '
|
|
369
|
+
+ 'on every countable part that carries out a CONFIRMED check, list that check\'s number in covers (a confirmed check no part '
|
|
370
|
+
+ 'covers is moved to Not checked before signing); "judged" when only reading '
|
|
371
|
+
+ 'it could decide; "guard" for a rule about web sources (domains); "none" when you cannot sort it. Never turn a part you cannot '
|
|
372
|
+
+ 'count or quote into a countable one by guessing a number.',
|
|
373
|
+
}),
|
|
343
374
|
// LIVE at N4 slice 1, and the ONLY kind that does not measure anything: it
|
|
344
375
|
// does not RUN, it PAUSES the run and hands the tree to a person (ruling 1).
|
|
345
376
|
// `ask` is the whole parameter surface — no cmd, no timeoutMs, no env — so a
|
|
@@ -381,6 +412,21 @@ export const LOCKED_KINDS = Object.freeze(CATALOGUE_KINDS.filter((k) => KIND_CAT
|
|
|
381
412
|
* that the catalogue never names is a capability nothing can reach. */
|
|
382
413
|
export const CATALOGUE_LIVE_KINDS = Object.freeze(CATALOGUE_KINDS.filter((k) => !KIND_CATALOGUE[k].locked));
|
|
383
414
|
|
|
415
|
+
/**
|
|
416
|
+
* The catalogue a close of this language may use. A rubric job on local files (`RUBRIC_LANG`) sees only
|
|
417
|
+
* {@link RUBRIC_LANG_KINDS}; every code language sees the whole catalogue MINUS the rubric-only kinds, so a code job's
|
|
418
|
+
* composer, schema and validator can never reach for a stage that reads step files. One filter, both directions.
|
|
419
|
+
* @param {string|null|undefined} lang
|
|
420
|
+
* @param {Record<string, KindSpec>} [catalogue]
|
|
421
|
+
* @returns {Record<string, KindSpec>}
|
|
422
|
+
*/
|
|
423
|
+
export function catalogueForLang(lang, catalogue = KIND_CATALOGUE) {
|
|
424
|
+
const keep = lang === RUBRIC_LANG
|
|
425
|
+
? (/** @type {string} */ k) => RUBRIC_LANG_KINDS.includes(k)
|
|
426
|
+
: (/** @type {string} */ k) => !RUBRIC_ONLY_KINDS.includes(k);
|
|
427
|
+
return Object.freeze(Object.fromEntries(Object.entries(catalogue).filter(([k]) => keep(k))));
|
|
428
|
+
}
|
|
429
|
+
|
|
384
430
|
export const DIRECTIONS = Object.freeze(['lower-is-better', 'higher-is-better']);
|
|
385
431
|
/** D12: the close stores the COUNTING RULE, never the number. "seed" is measured
|
|
386
432
|
* at this run's own seed every run; 0 is literally zero. A frozen constant (the
|
|
@@ -516,6 +562,28 @@ export const TYPES_GENRE_TEMPLATE = `1. The STRICT form of the language's type c
|
|
|
516
562
|
|
|
517
563
|
export const GENRE_LANGUAGES = Object.freeze(['js', 'python']);
|
|
518
564
|
|
|
565
|
+
/**
|
|
566
|
+
* PART 1a: the "language" of a RUBRIC job on local text files. It is not a code language and carries no suppression
|
|
567
|
+
* battery, no environment and no instruments — it only lets such a close declare which genre-shaped guards apply to it
|
|
568
|
+
* (`classGuards`) without pretending the file is JavaScript. `GENRE_LANGUAGES` stays the code-language menu (it is what a
|
|
569
|
+
* repo job's language detection and the confirm turn offer); this is the one extra value a close may DECLARE.
|
|
570
|
+
*/
|
|
571
|
+
export const RUBRIC_LANG = 'text';
|
|
572
|
+
/** the genre label a close over local text files carries (`closeDecl.genre`); the code languages' is `TYPES_GENRE.name`. Never the other's. */
|
|
573
|
+
export const TEXT_GENRE = 'TEXT';
|
|
574
|
+
/** @param {unknown} lang @returns {string} the genre a close of this language is authored under */
|
|
575
|
+
export function genreForLang(lang) { return lang === RUBRIC_LANG ? TEXT_GENRE : TYPES_GENRE.name; }
|
|
576
|
+
/** every `closeDecl.lang` a close may declare: the code languages plus the rubric-files one */
|
|
577
|
+
export const DECLARABLE_LANGUAGES = Object.freeze([...GENRE_LANGUAGES, RUBRIC_LANG]);
|
|
578
|
+
/** the kinds that exist only for a rubric job on local files — a code job's menu, schema and validator never offer them */
|
|
579
|
+
export const RUBRIC_ONLY_KINDS = Object.freeze(['rubric-step']);
|
|
580
|
+
/** the whole kind menu of a rubric job on local files: the one machine guard that applies to a file tree, plus its own stage */
|
|
581
|
+
export const RUBRIC_LANG_KINDS = Object.freeze(['files-changed', 'rubric-step']);
|
|
582
|
+
/** the rubric-files language's data, all declared-empty (never an absent field that would read as "not looked into", F59) */
|
|
583
|
+
const TEXT_LANGUAGE = Object.freeze({
|
|
584
|
+
suppressions: Object.freeze([]), extensions: Object.freeze([]), env: Object.freeze([]), instruments: Object.freeze([]),
|
|
585
|
+
});
|
|
586
|
+
|
|
519
587
|
/**
|
|
520
588
|
* The js `any` suppression pattern's regex, ONE spelling: the TYPES js battery
|
|
521
589
|
* below composes it into the signed guard, and `anySuppressionWarning` keys on
|
|
@@ -733,6 +801,7 @@ export const TYPES_GENRE = Object.freeze({
|
|
|
733
801
|
|
|
734
802
|
/** @param {string} lang */
|
|
735
803
|
function language(lang) {
|
|
804
|
+
if (lang === RUBRIC_LANG) return /** @type {any} */ (TEXT_LANGUAGE);
|
|
736
805
|
const l = TYPES_GENRE.languages[/** @type {'js'|'python'} */ (lang)];
|
|
737
806
|
// An unknown language THROWS rather than returning an empty battery: a genre
|
|
738
807
|
// we have not admitted must not read as "this genre needs no guards" (F59's
|
|
@@ -858,7 +927,15 @@ export function classGuards({ verdictType, lang }) {
|
|
|
858
927
|
+ 'admission refuses it before any battery is built; a locked class must never resolve to an empty one');
|
|
859
928
|
}
|
|
860
929
|
const l = language(lang);
|
|
861
|
-
|
|
930
|
+
// PART 1a: a rubric job on local TEXT files keeps every guard a machine can render over a file tree (`changed-from-seed`:
|
|
931
|
+
// something changed, and only inside the fence) and drops only the guards COMPOSED from a code language's tool data — a
|
|
932
|
+
// suppression scan over source code has nothing to scan in a text file, and composing it from no data would be the
|
|
933
|
+
// empty-guard hazard the throw below exists for. Only the soft-green battery has a text form.
|
|
934
|
+
if (lang === RUBRIC_LANG && String(verdictType) !== 'soft-green') {
|
|
935
|
+
throw new Error(`the "${RUBRIC_LANG}" genre is the rubric-files one: it carries a battery for soft-green only, not "${verdictType}"`);
|
|
936
|
+
}
|
|
937
|
+
const batteryGuards = lang === RUBRIC_LANG ? battery.guards.filter((g) => g.compose.length === 0) : battery.guards;
|
|
938
|
+
const guards = batteryGuards.map((g) => {
|
|
862
939
|
/** @type {Record<string, any>} */
|
|
863
940
|
const params = structuredClone(g.params);
|
|
864
941
|
for (const p of g.compose) {
|
|
@@ -1199,7 +1276,8 @@ function readPath(p, dotted) {
|
|
|
1199
1276
|
* @param {{catalogue?: Record<string, KindSpec>, listing?: string[]|null,
|
|
1200
1277
|
* guards?: {name: string, kind: string, params: Record<string, any>, fill: string[]}[]|null,
|
|
1201
1278
|
* envOwned?: string[]|null, envInjected?: Record<string, string>|null, deferListing?: boolean,
|
|
1202
|
-
* verdictType?: string|null, writeScope?: string[]|null, jobLines?: {n: number}[]|null}} [opts]
|
|
1279
|
+
* verdictType?: string|null, writeScope?: string[]|null, jobLines?: {n: number}[]|null, inputLabels?: string[]|null}} [opts]
|
|
1280
|
+
* `inputLabels` — the labels of the person's inputs (Part 1a). Present, a rubric-step `cited` part must name one of them.
|
|
1203
1281
|
* `jobLines` — the person's numbered job lines (P7). Present, every stage must name the line it serves and every
|
|
1204
1282
|
* line needs a stage or a refusal ({@link checkJobLines}); absent (every spec that predates the job block), the
|
|
1205
1283
|
* rule is not run.
|
|
@@ -1221,7 +1299,7 @@ function readPath(p, dotted) {
|
|
|
1221
1299
|
export function validateDeclaration(declaration, opts = {}) {
|
|
1222
1300
|
const {
|
|
1223
1301
|
catalogue = KIND_CATALOGUE, listing = null, guards = null, envOwned = null, envInjected = null,
|
|
1224
|
-
verdictType = null, writeScope = null, jobLines = null,
|
|
1302
|
+
verdictType = null, writeScope = null, jobLines = null, inputLabels = null,
|
|
1225
1303
|
} = opts;
|
|
1226
1304
|
const deferListing = opts.deferListing === true;
|
|
1227
1305
|
/** @type {Red[]} */
|
|
@@ -1447,6 +1525,10 @@ export function validateDeclaration(declaration, opts = {}) {
|
|
|
1447
1525
|
|
|
1448
1526
|
if (haveGuards) checkGuards({ declaration, guards: /** @type {any[]} */ (guards), red });
|
|
1449
1527
|
checkJobLines({ declaration, stages, jobLines, guards: haveGuards ? /** @type {any[]} */ (guards) : [], red });
|
|
1528
|
+
// PART 1a (R11): the sorting rules for the rubric-step stages — plain-words reds, judged and web parts refused in this build
|
|
1529
|
+
checkRubricParts(stages, { inputLabels, red });
|
|
1530
|
+
// PART 1b (RF16, RF17): every ~ rule is a check or Not checked; the AI's own numbers are marked and a time range is a range
|
|
1531
|
+
checkRubricRules(stages, { jobLines, refused: declaration.refused, red });
|
|
1450
1532
|
|
|
1451
1533
|
const ok = reds.length === 0;
|
|
1452
1534
|
return { ok, reds, declaration: ok ? normalizeDeclaration(declaration) : null, grounded: haveListing, scoped, ceiling };
|
|
@@ -1489,15 +1571,15 @@ function checkJobLines({ declaration, stages, jobLines, guards, red }) {
|
|
|
1489
1571
|
const refused = new Set();
|
|
1490
1572
|
if (declaration.refused !== undefined) {
|
|
1491
1573
|
if (!Array.isArray(declaration.refused)) {
|
|
1492
|
-
red('invalid-value', 'refused', 'an array of {line, reason} — the job lines no stage can check, each with a reason');
|
|
1574
|
+
red('invalid-value', 'refused', 'an array of {line, rule?, reason} — the job lines (or one ~ rule of a line) no stage can check, each with a reason');
|
|
1493
1575
|
} else {
|
|
1494
1576
|
declaration.refused.forEach((/** @type {any} */ r, /** @type {number} */ j) => {
|
|
1495
1577
|
const at = `refused[${j}]`;
|
|
1496
|
-
if (!isObj(r) || !Number.isInteger(r.line) || r.line < 1 || !isNonEmptyString(r.reason)) {
|
|
1497
|
-
red('invalid-value', at, '{line, reason}: a job line number and a plain-words reason');
|
|
1578
|
+
if (!isObj(r) || !Number.isInteger(r.line) || r.line < 1 || !isNonEmptyString(r.reason) || (Object.hasOwn(r, 'rule') && !(Number.isInteger(r.rule) && r.rule >= 1))) {
|
|
1579
|
+
red('invalid-value', at, '{line, rule?, reason}: a job line number, optionally the number of one of its ~ rules, and a plain-words reason');
|
|
1498
1580
|
} else if (known !== null && !known.includes(r.line)) {
|
|
1499
1581
|
red('invalid-value', `${at}.line`, `refused line ${r.line}, but the job has lines ${known.join(', ')} only`);
|
|
1500
|
-
} else refused.add(r.line);
|
|
1582
|
+
} else if (!Object.hasOwn(r, 'rule')) refused.add(r.line);
|
|
1501
1583
|
});
|
|
1502
1584
|
}
|
|
1503
1585
|
}
|
|
@@ -1689,6 +1771,9 @@ function checkKind({ kind, params: p, at, red, scoped, label, populations, envOw
|
|
|
1689
1771
|
if (!strArray(p.allowPrefixes)) red('invalid-value', `${at}.params.allowPrefixes`, 'a non-empty array of path prefixes');
|
|
1690
1772
|
if (p.requireNonEmpty !== true) red('invalid-value', `${at}.params.requireNonEmpty`, 'literally true');
|
|
1691
1773
|
break;
|
|
1774
|
+
case 'rubric-step':
|
|
1775
|
+
for (const r of rubricStepShapeReds(p, `${at}.params`)) red(r.code, r.path, r.detail);
|
|
1776
|
+
break;
|
|
1692
1777
|
case 'human-confirms':
|
|
1693
1778
|
// the whole surface: the question the signer answers. It is shown to a
|
|
1694
1779
|
// person verbatim, so an empty one is a bare "approve?" — ruling 2's
|
package/src/authorjob.js
CHANGED
|
@@ -100,21 +100,25 @@
|
|
|
100
100
|
|
|
101
101
|
import { jobSpecHash, validateJob } from './job.js';
|
|
102
102
|
import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, judgedStages, closeJudges } from './kinds.js';
|
|
103
|
+
import { inputLabelsOf, readFrozenInputs } from './rubricstep.js';
|
|
104
|
+
import { proverOf } from './factwords.js';
|
|
103
105
|
// SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
|
|
104
106
|
// and the gate that decides whether the ruler they describe is a ruler.
|
|
105
107
|
import { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts } from './cardauthor.js';
|
|
106
108
|
import { runCalibration } from './calibrate.js';
|
|
107
109
|
import { CALIBRATION_SIZE } from './judged.js';
|
|
108
110
|
import {
|
|
109
|
-
GENRE_LANGUAGES, LOCKED_KINDS, TYPES_GENRE, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES,
|
|
111
|
+
GENRE_LANGUAGES, RUBRIC_LANG, DECLARABLE_LANGUAGES, genreForLang, LOCKED_KINDS, TYPES_GENRE, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES,
|
|
110
112
|
UNLISTED_CLASSES, MENU_CLASSES,
|
|
111
113
|
} from './authoring.js';
|
|
112
114
|
import {
|
|
113
115
|
QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook,
|
|
114
|
-
runConfirmTurn, LANGUAGE_PICK_FIELD,
|
|
116
|
+
runConfirmTurn, LANGUAGE_PICK_FIELD, normalizeQuestions,
|
|
115
117
|
} from './authorflow.js';
|
|
118
|
+
import { ruleQuestions } from './inputlook.js';
|
|
116
119
|
import { thinkingCallOptions, assertThinkingMode } from './thinking.js';
|
|
117
|
-
import {
|
|
120
|
+
import { pageContentChars } from './pagefetch.js';
|
|
121
|
+
import { runAuthorScout, runInputLook, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
118
122
|
import {
|
|
119
123
|
DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
|
|
120
124
|
} from './declaredclose.js';
|
|
@@ -229,14 +233,14 @@ export function refusalEvents(refusal) {
|
|
|
229
233
|
* makes between a locked verdict and an invalid one).
|
|
230
234
|
*
|
|
231
235
|
* @param {{answers: Record<string|number, any>, verdictType?: string|null,
|
|
232
|
-
* repoPath?: string|null, questions?: Record<string|number, string>|null}} o
|
|
233
|
-
* `verdictType` and `repoPath` are STRUCTURED input from the UI, never parsed
|
|
236
|
+
* repoPath?: string|null, questions?: Record<string|number, string>|null, required?: (string|number)[]|null}} o
|
|
237
|
+
* `required` — the answer keys that must be present; absent = the picked class's whole set. `verdictType` and `repoPath` are STRUCTURED input from the UI, never parsed
|
|
234
238
|
* out of the prose of an answer: a class is picked off a radio and a path is
|
|
235
239
|
* read off the machine, or neither exists.
|
|
236
240
|
* @returns {{ok: boolean, answers: Record<string|number, string>,
|
|
237
241
|
* verdictType: string|null, repoPath: string|null, refusal: Refusal|null, reds: Red[]}}
|
|
238
242
|
*/
|
|
239
|
-
export function runInterview({ answers, verdictType = null, repoPath = null, questions = null }) {
|
|
243
|
+
export function runInterview({ answers, verdictType = null, repoPath = null, questions = null, required: requiredOverride = null }) {
|
|
240
244
|
/** @type {Red[]} */
|
|
241
245
|
const reds = [];
|
|
242
246
|
const out = (/** @type {Partial<any>} */ over) => ({
|
|
@@ -290,7 +294,8 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
|
|
|
290
294
|
}
|
|
291
295
|
|
|
292
296
|
const set = questions ?? questionsFor(picked);
|
|
293
|
-
|
|
297
|
+
// PART 1a: a rubric job on local files asks one question (the job lines); the caller names the keys it needs
|
|
298
|
+
const required = Array.isArray(requiredOverride) ? requiredOverride : requiredAnswersFor(picked);
|
|
294
299
|
|
|
295
300
|
if (!isObj(answers)) {
|
|
296
301
|
reds.push({ code: 'missing-field', path: 'answers', detail: 'the interview answers, keyed by question number' });
|
|
@@ -424,7 +429,7 @@ function composerRefusal(reds) {
|
|
|
424
429
|
* rates?: {in: number, out: number}|null, draftThinking?: 'on'|'off',
|
|
425
430
|
* onPhase?: (phase: string, data?: any) => void,
|
|
426
431
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
|
|
427
|
-
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
432
|
+
* seedFn?: Function, scoutFn?: Function, listingFn?: Function, lookFn?: Function|null,
|
|
428
433
|
* authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
|
|
429
434
|
* signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
|
|
430
435
|
* ask?: ((step: {kind: string, [k: string]: any}) => Promise<string|null>)|null,
|
|
@@ -465,6 +470,9 @@ export async function authorCloseForJob({
|
|
|
465
470
|
// callback back, and no decision in this file or below it depends on one.
|
|
466
471
|
onPhase = () => {}, onCall = () => {},
|
|
467
472
|
seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
|
|
473
|
+
// PART 1c - the look at a rubric job's inputs (src/authorscout.js `runInputLook`). `null` is an explicit "no look" (a test seam, like
|
|
474
|
+
// a caller-supplied `scout`); the shipped default is the real, paid, read-only look.
|
|
475
|
+
lookFn = runInputLook,
|
|
468
476
|
authorFn = authorClose, authorOpts = {},
|
|
469
477
|
// SOFTGREEN modules 4+5. `signerFix` is the D5 seam: a function (or a value)
|
|
470
478
|
// carrying the SIGNER's corrected artifacts back. Absent means the proposal is
|
|
@@ -504,7 +512,10 @@ export async function authorCloseForJob({
|
|
|
504
512
|
confirmed: null,
|
|
505
513
|
};
|
|
506
514
|
|
|
507
|
-
|
|
515
|
+
// PART 1a: a rubric job on local files has ONE question — the numbered job lines (the judge-example answer is required only
|
|
516
|
+
// of a job that holds a judged part, and this build admits none). The prompt shows just that question.
|
|
517
|
+
if (lang === RUBRIC_LANG) questions = { 1: questionsFor('soft-green')[1] };
|
|
518
|
+
const interview = runInterview({ answers, verdictType, repoPath, questions, ...(lang === RUBRIC_LANG ? { required: [1] } : {}) });
|
|
508
519
|
base.interview = interview;
|
|
509
520
|
if (!interview.ok) {
|
|
510
521
|
return { ...base, refusal: interview.refusal, reds: interview.reds, stop: interview.refusal ? 'refused' : 'interview-incomplete' };
|
|
@@ -530,7 +541,7 @@ export async function authorCloseForJob({
|
|
|
530
541
|
}
|
|
531
542
|
}
|
|
532
543
|
|
|
533
|
-
if (!
|
|
544
|
+
if (!DECLARABLE_LANGUAGES.includes(lang)) {
|
|
534
545
|
// THE GENRE REFUSAL, at the composer. This used to be a plain wiring red;
|
|
535
546
|
// under v1.57 §2 the interview no longer asks the user to confirm a genre, so
|
|
536
547
|
// this is where "we can't run this kind of job yet" actually lands — and it
|
|
@@ -588,6 +599,34 @@ export async function authorCloseForJob({
|
|
|
588
599
|
// arrived at by accident.
|
|
589
600
|
/** @type {any} */
|
|
590
601
|
let survey = scout;
|
|
602
|
+
// PART 1a: a rubric job on local files has no repository to survey. PART 1c: its survey is the LOOK - a read-only, bounded, paid
|
|
603
|
+
// look at the person's frozen input text that reports which facts can be checked and with which label (picked from a code-made
|
|
604
|
+
// list). Without a look (`lookFn` null, or no model boundary) the survey is just the numbered inputs, known to code at $0.
|
|
605
|
+
if (survey == null && lang === RUBRIC_LANG) {
|
|
606
|
+
const inputList = (Array.isArray(inputs) ? inputs : []).map((i) => ({ n: i.n, label: i.label }));
|
|
607
|
+
const frozen = lookFn && provider && Array.isArray(jobLines) && jobLines.length > 0 && inputList.length > 0
|
|
608
|
+
? await readFrozenInputs(workdir, inputList.map((i) => i.label)) : null;
|
|
609
|
+
if (frozen !== null && !frozen.ok) {
|
|
610
|
+
survey = { state: 'ABSENT', reason: `the look at your input could not start: ${frozen.why}`, cause: 'call-failed', budgetStop: null, calls: [], raws: [], facts: null };
|
|
611
|
+
} else if (frozen !== null && lookFn && !Object.values(frozen.texts).some((t) => pageContentChars(t) > 0)) {
|
|
612
|
+
// L2: an input with nothing in it (empty, or only a site's header and menus) is never looked at and never asked about - there is
|
|
613
|
+
// nothing to check a rule against. Plain words, before any model call.
|
|
614
|
+
survey = { state: 'ABSENT', reason: 'your input holds nothing to read (it is empty, or only the site\'s own header and menus) - nothing was asked and nothing was spent', cause: 'call-failed', budgetStop: null, calls: [], raws: [], facts: null };
|
|
615
|
+
} else if (frozen !== null && lookFn) {
|
|
616
|
+
onPhase('look', {});
|
|
617
|
+
const lookCallOptions = thinkingCallOptions(provider, draftThinking);
|
|
618
|
+
survey = await lookFn({
|
|
619
|
+
texts: frozen.texts, inputs: inputList, jobLines, provider, ceilingUsd, onCall, prove: proverOf(frozen.texts), ...(rates ? { rates } : {}),
|
|
620
|
+
...(Object.keys(lookCallOptions).length ? { callOptions: lookCallOptions } : {}),
|
|
621
|
+
});
|
|
622
|
+
onPhase('look-done', { state: survey?.state ?? null, facts: survey?.facts?.look?.facts?.length ?? 0 });
|
|
623
|
+
} else {
|
|
624
|
+
survey = {
|
|
625
|
+
state: 'PRESENT', reason: null, cause: null, budgetStop: null, calls: [], raws: [],
|
|
626
|
+
facts: { rubricFiles: true, inputs: inputList },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
591
630
|
// `== null` and not `!survey`: this is the `??` these two lines replaced, kept
|
|
592
631
|
// exactly — a caller-supplied survey is used whatever it is, and only an
|
|
593
632
|
// ABSENT one is paid for.
|
|
@@ -606,6 +645,22 @@ export async function authorCloseForJob({
|
|
|
606
645
|
onPhase('scout-done', { state: survey?.state ?? null, facts: Object.keys(survey?.facts ?? {}).length });
|
|
607
646
|
}
|
|
608
647
|
|
|
648
|
+
// PART 1c, I1 (hamr 2026-10-13): the look found that the person's intent on a particular `~` rule was NOT captured (its words or
|
|
649
|
+
// meaning are not in the source) - ask about THAT rule at once, before any plan or draft is paid for. The question gate is the
|
|
650
|
+
// existing one (`answerQuestions`: answer required, no skip, the answer becomes a `~` rule on its line; `normalizeQuestions` keeps
|
|
651
|
+
// at most MAX_QUESTIONS, each on a line the look named). Everything else the look could not find asks nothing piecemeal.
|
|
652
|
+
if (answerQuestions && lang === RUBRIC_LANG && survey?.state === 'PRESENT' && Array.isArray(survey.facts?.look?.notFound) && Array.isArray(jobLines)) {
|
|
653
|
+
const candidates = ruleQuestions(survey.facts.look.notFound, jobLines);
|
|
654
|
+
const kept = normalizeQuestions(candidates, [...new Set(candidates.map((q) => q.line))]);
|
|
655
|
+
if (kept.kept.length > 0) {
|
|
656
|
+
onPhase('look-ask', { questions: kept.kept.length });
|
|
657
|
+
const updated = await answerQuestions({ questions: kept.kept, dropped: kept.dropped, confirmed: null });
|
|
658
|
+
if (updated === null) return { ...base, interview, verdictType: picked, seedRef: seed, reds: [], stop: 'confirm-abandoned', cost: null };
|
|
659
|
+
jobLines = updated.jobLines;
|
|
660
|
+
interview.answers = updated.answers;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
609
664
|
// ── THE CONFIRM TURN'S PAID HALF (PRD item 33 M3 piece 4) ──────────────────
|
|
610
665
|
// Slotted between the scout and the listing: it reuses the scout's own
|
|
611
666
|
// survey (ruling 4 — never a second read of the source) and needs nothing
|
|
@@ -622,7 +677,9 @@ export async function authorCloseForJob({
|
|
|
622
677
|
let confirmPriorCalls = [];
|
|
623
678
|
/** @type {any[]} */
|
|
624
679
|
let confirmPriorRaws = [];
|
|
625
|
-
|
|
680
|
+
// PART 1a (hamr's ruling 1A): a rubric job on local files runs the SAME confirm turn a repo job does — the survey is the
|
|
681
|
+
// numbered inputs (synthetic, above), and the protections it shows come from code, never the model
|
|
682
|
+
if (ask && (isRepo || lang === RUBRIC_LANG) && survey?.state === 'PRESENT') {
|
|
626
683
|
if (typeof confirmGenerate !== 'function') {
|
|
627
684
|
throw new Error('[authorjob] authorCloseForJob was given `ask` for a repo job but no `confirmGenerate` — the '
|
|
628
685
|
+ 'confirm turn needs its OWN model boundary (bound to CONFIRM_SYSTEM), never the authoring `generate` '
|
|
@@ -645,7 +702,7 @@ export async function authorCloseForJob({
|
|
|
645
702
|
onPhase('confirm', {});
|
|
646
703
|
const confirm = await runConfirmTurn({
|
|
647
704
|
verdictType: picked, answers: interview.answers, questions: questions ?? questionsFor(picked),
|
|
648
|
-
facts: survey.facts, listing: null, writeScope, isRepo, lang, worseThanBefore, inputs,
|
|
705
|
+
facts: survey.facts, listing: null, writeScope, isRepo, lang, worseThanBefore, inputs, jobLines,
|
|
649
706
|
generate: confirmGenerate, book: confirmBook, ask, onPhase,
|
|
650
707
|
});
|
|
651
708
|
// DISTINCT from `runConfirmTurn`'s own per-round 'confirm-done' (fired once
|
|
@@ -696,7 +753,9 @@ export async function authorCloseForJob({
|
|
|
696
753
|
answers: stageAnswers, questions: questions ?? questionsFor(picked),
|
|
697
754
|
scout: survey, listing: seeds, generate, ceilingUsd, onPhase, onCall, writeScope,
|
|
698
755
|
priorCalls: confirmPriorCalls, priorRaws: confirmPriorRaws, confirmed, jobLines, inputs,
|
|
699
|
-
|
|
756
|
+
// PART 1c: a rubric job asks nothing piecemeal on a failed try - it stops, and the person reviews the whole job (the one early
|
|
757
|
+
// ask is about a `~` rule the look could not find, above)
|
|
758
|
+
...(answerQuestions && lang !== RUBRIC_LANG ? { offerQuestions: true } : {}),
|
|
700
759
|
...authorOpts,
|
|
701
760
|
});
|
|
702
761
|
// P7 item 5: the drafter, on the last try after two failed ones, asked. The person answers (required, no skip); each
|
|
@@ -752,7 +811,7 @@ export async function authorCloseForJob({
|
|
|
752
811
|
// `prepareSigning`, which refuses there.
|
|
753
812
|
/** @type {any} */
|
|
754
813
|
let closeDecl = {
|
|
755
|
-
genre:
|
|
814
|
+
genre: genreForLang(lang),
|
|
756
815
|
lang,
|
|
757
816
|
stages: authored.declaration.stages,
|
|
758
817
|
...(Object.keys(applied).length ? { genreEnv: { ...applied } } : {}),
|
|
@@ -770,7 +829,7 @@ export async function authorCloseForJob({
|
|
|
770
829
|
// P7: the job lines the drafter could not turn into a stage, each with its reason — the Job tab shows them. Free
|
|
771
830
|
// text the model wrote, so it rides the same redactor `notes` does.
|
|
772
831
|
...(authored.declaration.refused?.length
|
|
773
|
-
? { refused: authored.declaration.refused.map((/** @type {any} */ r) => ({ line: r.line, reason: redactSecrets(String(r.reason)) })) }
|
|
832
|
+
? { refused: authored.declaration.refused.map((/** @type {any} */ r) => ({ line: r.line, ...(Number.isInteger(r.rule) ? { rule: r.rule } : {}), reason: redactSecrets(String(r.reason)) })) }
|
|
774
833
|
: {}),
|
|
775
834
|
};
|
|
776
835
|
|
|
@@ -1301,7 +1360,7 @@ export async function prepareSigning({
|
|
|
1301
1360
|
}
|
|
1302
1361
|
// the class rides from the SPEC (PRD v1.57 §2) — gate 1a already refused a spec
|
|
1303
1362
|
// whose class is unknown or locked, so this is the class D5's battery hangs off
|
|
1304
|
-
const dv = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: spec.verdictType, writeScope: spec.writeScope, jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null });
|
|
1363
|
+
const dv = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: spec.verdictType, writeScope: spec.writeScope, jobLines: Array.isArray(spec.jobLines) ? spec.jobLines : null, inputLabels: inputLabelsOf(spec.inputs) });
|
|
1305
1364
|
// the grounded gate's reds quote BOTH untrusted sources — the declaration, and
|
|
1306
1365
|
// the seed listing itself (the listing rule names what really sits beside an
|
|
1307
1366
|
// invented path). The listing half is a channel gate 1a structurally cannot
|
|
@@ -1348,8 +1407,11 @@ export async function prepareSigning({
|
|
|
1348
1407
|
stops: stops.map((s) => ({ stage: s.stage, gap: s.gap })),
|
|
1349
1408
|
};
|
|
1350
1409
|
if (stops.length) {
|
|
1410
|
+
// M2: a rubric job's readout never uses the code-job wording ("install, build and test commands")
|
|
1411
|
+
const rubricJob = (Array.isArray(spec.closeDecl?.stages) ? spec.closeDecl.stages : []).some((/** @type {any} */ st) => st && (st.kind === 'rubric-step' || st.kind === 'judged-floor'));
|
|
1351
1412
|
const reds = stops.map((s) => ({
|
|
1352
1413
|
code: 'broken-close',
|
|
1414
|
+
...(rubricJob ? { rubricJob: true } : {}),
|
|
1353
1415
|
path: `closeDecl.stages.${s.stage}`,
|
|
1354
1416
|
detail: s.gap.join('\n') || `stage "${s.stage}" rendered no judgment`,
|
|
1355
1417
|
}));
|
package/src/authorreadout.js
CHANGED
|
@@ -313,9 +313,12 @@ export function phaseLine(phase, data = {}) {
|
|
|
313
313
|
case 'seed': return '· reading the seed commit…';
|
|
314
314
|
case 'scout': return `· scout running (up to ${data.attempts ?? '?'} attempt(s)) — a real survey over the repository…`;
|
|
315
315
|
case 'scout-done': return `· scout ${data.state ?? 'unknown'} — ${data.facts ?? 0} fact(s)`;
|
|
316
|
+
case 'look': return '· looking at your input — a read-only search for the facts the job asks for…';
|
|
317
|
+
case 'look-done': return `· look ${data.state ?? 'unknown'} — ${data.facts ?? 0} fact(s) can be checked`;
|
|
316
318
|
case 'listing': return '· listing the files the declaration may name…';
|
|
317
319
|
case 'listing-done': return `· listing ${data.stop ? `STOPPED (${data.stop})` : `${data.files ?? 'unknown'} file(s)`}`;
|
|
318
320
|
case 'author': return '· authoring the close declaration…';
|
|
321
|
+
case 'author-try-failed': return `· ${data.call} refused — ${tryRefusedText(data)}`;
|
|
319
322
|
case 'author-call': return `· ${data.call} (call ${(data.i ?? 0) + 1} of up to ${(data.of ?? 0) + 1})…`;
|
|
320
323
|
case 'seed-read': return `· measuring ${data.stages ?? '?'} stage(s) at the seed — each one runs a real toolchain…`;
|
|
321
324
|
case 'stage': return `· ${data.stage} [${data.kind}] ${data.verdict} — ${ms(data.durationMs)}`;
|
|
@@ -405,6 +408,14 @@ const PLAIN_MAX_BULLETS = 8;
|
|
|
405
408
|
const RECORD_MAX_REDS = 40;
|
|
406
409
|
const RECORD_STR_MAX = 600;
|
|
407
410
|
|
|
411
|
+
/** a code-written sentence that carries the person's own words (a rubric-step sorting red): scrubbed and one line, bounded wide enough to stay whole
|
|
412
|
+
* @param {unknown} s @returns {string} */
|
|
413
|
+
function fullText(s) {
|
|
414
|
+
// every caller renders a drafting refusal AFTER a model call was paid; a red text written for the $0 front doors ends "Nothing spent." — false here
|
|
415
|
+
const one = redactSecrets(String(s ?? '')).replace(/\s+/g, ' ').trim().replace(/\s*Nothing spent\.$/, '');
|
|
416
|
+
return one.length > 400 ? `${one.slice(0, 400)}…` : one;
|
|
417
|
+
}
|
|
418
|
+
|
|
408
419
|
/** model-derived text in a person-facing line: scrubbed, one line, bounded
|
|
409
420
|
* @param {unknown} s @returns {string} */
|
|
410
421
|
function safeText(s) {
|
|
@@ -417,6 +428,25 @@ function safeText(s) {
|
|
|
417
428
|
* @typedef {{kind: string, sentence: string, red: PlainRed}} PlainReason
|
|
418
429
|
*/
|
|
419
430
|
|
|
431
|
+
/** a shape red on a rubric-step stage's own parameters (live fix B3): invalid-value / missing-field at `stages[n].params.step|parts…` */
|
|
432
|
+
const isRubricShape = (/** @type {PlainRed} */ r) => (r.code === 'invalid-value' || r.code === 'missing-field') && /^stages\[\d+\]\.params\.(step|parts)\b/.test(r.path);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The plain sentence for a shape red on a rubric-step: WHERE (stage, part, fact, field — counted from 1, the way the plan card shows
|
|
436
|
+
* them) and WHAT is wrong, in the red's own detail. Code-worded, secret-scrubbed.
|
|
437
|
+
* @param {PlainRed} r @returns {string}
|
|
438
|
+
*/
|
|
439
|
+
function rubricShapeSentence(r) {
|
|
440
|
+
const m = /^stages\[(\d+)\]\.params\.(.*)$/.exec(r.path);
|
|
441
|
+
const where = [`stage ${Number(m?.[1] ?? 0) + 1}`];
|
|
442
|
+
for (const seg of (m?.[2] ?? '').split('.').filter(Boolean)) {
|
|
443
|
+
const idx = /^(parts|facts)\[(\d+)\]$/.exec(seg);
|
|
444
|
+
if (idx) where.push(`${idx[1] === 'parts' ? 'part' : 'fact'} ${Number(idx[2]) + 1}`);
|
|
445
|
+
else if (seg !== 'parts' && seg !== 'facts') where.push(`field "${safeText(seg)}"`);
|
|
446
|
+
}
|
|
447
|
+
return `The model's check for a job line is not shaped right (${where.join(', ')}): ${fullText(r.detail).replace(/\.$/, '')}.`;
|
|
448
|
+
}
|
|
449
|
+
|
|
420
450
|
/**
|
|
421
451
|
* The ONE table of plain kinds → sentence builders. A builder gets the red, the
|
|
422
452
|
* name of the case it lands on (when it lands on one) and the match of the
|
|
@@ -440,6 +470,9 @@ const PLAIN_TABLE = [
|
|
|
440
470
|
say: (r) => `Rubric line ${cardLineNo(r)} uses a rule that an earlier line already uses.` },
|
|
441
471
|
{ kind: 'card-line-no-text', when: (r) => r.code === 'invalid-value' && /^card item \d+: needs `text`/.test(r.detail),
|
|
442
472
|
say: (r) => `Rubric line ${cardLineNo(r)} has no wording.` },
|
|
473
|
+
// Part 1b live fix B3: the rubric-step reds are whole sentences already (src/rubricstep.js); a shape red on its parameters is worded here
|
|
474
|
+
{ kind: 'rubric-red', when: (r) => r.code.startsWith('rubric-'), say: (r) => fullText(r.detail) },
|
|
475
|
+
{ kind: 'rubric-shape', when: isRubricShape, say: (r) => rubricShapeSentence(r) },
|
|
443
476
|
{ kind: 'proposal-not-object', when: (r) => r.code === 'invalid-value' && r.path === 'proposal',
|
|
444
477
|
say: () => 'The model\'s answer was not a rubric plus practice cases.' },
|
|
445
478
|
// ── the practice-case set as a whole ──
|
|
@@ -544,6 +577,47 @@ function spendText(spend) {
|
|
|
544
577
|
return `${spend.spendComplete === false ? 'at least ' : ''}${moneyText(spend.knownUsd)}`;
|
|
545
578
|
}
|
|
546
579
|
|
|
580
|
+
/**
|
|
581
|
+
* WHY one drafting try was refused, in the code's own words: the unique `detail` of each red it raised (the same sentences
|
|
582
|
+
* `draftStopText` quotes at a stop), else the plain `reason` the loop gave (a seed-read failure raises no red). Scrubbed,
|
|
583
|
+
* one line, bounded; never empty.
|
|
584
|
+
* @param {{reds?: PlainRed[]|null, reason?: string|null}} o
|
|
585
|
+
* @returns {string}
|
|
586
|
+
*/
|
|
587
|
+
export function tryRefusedText({ reds = null, reason = null }) {
|
|
588
|
+
const said = [...new Set((Array.isArray(reds) ? reds : []).map((r) => fullText(r?.detail)).filter(Boolean))];
|
|
589
|
+
if (said.length === 0 && reason) said.push(fullText(reason));
|
|
590
|
+
if (said.length === 0) return 'no reason was recorded';
|
|
591
|
+
const text = said.slice(0, 3).join('; ') + (said.length > 3 ? `; …and ${said.length - 3} more` : '');
|
|
592
|
+
return text.length > 600 ? `${text.slice(0, 600)}…` : text;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** the most characters of a refused declaration the drafting log keeps (a bigger one is kept as cut text, said so) */
|
|
596
|
+
const DECLARATION_RECORD_MAX = 60000;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* A refused declaration as the drafting log keeps it (live fix A3), so a failed draft can be read and replayed at $0: the declaration
|
|
600
|
+
* itself, every secret masked as everywhere else (the JSON text goes through `redactSecrets`, then parses back), and bounded.
|
|
601
|
+
* @param {unknown} declaration
|
|
602
|
+
* @returns {{declaration: unknown}|{declarationText: string, declarationCut: true}|{}} nothing when there is no declaration
|
|
603
|
+
*/
|
|
604
|
+
export function declarationRecord(declaration) {
|
|
605
|
+
if (declaration === undefined || declaration === null) return {};
|
|
606
|
+
let text;
|
|
607
|
+
try { text = redactSecrets(JSON.stringify(declaration)); } catch { return {}; }
|
|
608
|
+
if (typeof text !== 'string') return {};
|
|
609
|
+
if (text.length > DECLARATION_RECORD_MAX) return { declarationText: text.slice(0, DECLARATION_RECORD_MAX), declarationCut: true };
|
|
610
|
+
try { return { declaration: JSON.parse(text) }; } catch { return { declarationText: text, declarationCut: true }; }
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** one red as a line of a stop message: a rubric red is a whole sentence already, a rubric shape red is worded by code, any other is bounded
|
|
614
|
+
* @param {any} r @returns {string} */
|
|
615
|
+
function said1(r) {
|
|
616
|
+
const red = /** @type {PlainRed} */ ({ code: String(r?.code ?? ''), path: String(r?.path ?? ''), detail: String(r?.detail ?? '') });
|
|
617
|
+
if (red.code.startsWith('rubric-')) return fullText(red.detail);
|
|
618
|
+
return isRubricShape(red) ? rubricShapeSentence(red) : safeText(red.detail);
|
|
619
|
+
}
|
|
620
|
+
|
|
547
621
|
/** the stops this module explains in plain words */
|
|
548
622
|
export const PLAIN_PROPOSAL_STOPS = Object.freeze(['proposal-invalid', 'rubric-invalid']);
|
|
549
623
|
|
|
@@ -582,10 +656,11 @@ const SCOUT_REASON_RE = /^the survey is [^(]*\((.*)\) — an absent facts/s;
|
|
|
582
656
|
|
|
583
657
|
/**
|
|
584
658
|
* @param {{stop: string, reds?: PlainRed[]|null, refusal?: {detail?: string}|null,
|
|
585
|
-
* spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null}} o
|
|
659
|
+
* spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null, wholeJob?: boolean}} o `wholeJob` - a rubric job on the person's
|
|
660
|
+
* own inputs (PART 1c, I1): a failed draft asks nothing piecemeal; the person reviews the whole job and submits it again
|
|
586
661
|
* @returns {string}
|
|
587
662
|
*/
|
|
588
|
-
export function draftStopText({ stop, reds = null, refusal = null, spend = null }) {
|
|
663
|
+
export function draftStopText({ stop, reds = null, refusal = null, spend = null, wholeJob = false }) {
|
|
589
664
|
const spent = spendText(spend);
|
|
590
665
|
/** @type {Record<string, string>} */
|
|
591
666
|
const heads = {
|
|
@@ -596,7 +671,7 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null
|
|
|
596
671
|
'max-revisions': 'The AI used all its tries without a plan that passed the checks.',
|
|
597
672
|
precheck: 'Drafting stopped before any call: something it needs was missing.',
|
|
598
673
|
unchanged: 'Drafting stopped: the AI sent the same plan twice.',
|
|
599
|
-
'scout-absent': 'Drafting stopped: the first look at your repo did not complete.',
|
|
674
|
+
'scout-absent': 'Drafting stopped: the first look at your repo or input did not complete.',
|
|
600
675
|
};
|
|
601
676
|
const redList = Array.isArray(reds) ? reds : [];
|
|
602
677
|
// F217: the first look at the repo failing (a dropped connection, a timeout) arrives as a `precheck` stop with a
|
|
@@ -608,7 +683,7 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null
|
|
|
608
683
|
const said = scoutRed
|
|
609
684
|
? [SCOUT_REASON_RE.exec(String(scoutRed.detail ?? ''))?.[1]
|
|
610
685
|
?.replace(/\s+/g, ' ').trim().slice(0, 240) ?? safeText(scoutRed.detail)].map((x) => redactSecrets(x)).filter(Boolean)
|
|
611
|
-
: [...new Set(redList.map((r) =>
|
|
686
|
+
: [...new Set(redList.map((r) => said1(r)).filter(Boolean))];
|
|
612
687
|
if (said.length === 0 && refusal?.detail) said.push(safeText(refusal.detail));
|
|
613
688
|
const lines = [`${head} Spent so far: ${spent ?? 'unknown'}.`];
|
|
614
689
|
if (said.length) {
|
|
@@ -616,7 +691,9 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null
|
|
|
616
691
|
for (const x of said.slice(0, PLAIN_MAX_BULLETS)) lines.push(` • ${x}`);
|
|
617
692
|
if (said.length > PLAIN_MAX_BULLETS) lines.push(` • …and ${said.length - PLAIN_MAX_BULLETS} more.`);
|
|
618
693
|
}
|
|
619
|
-
lines.push(stop === 'cap-halt' || stop === 'pricing-red'
|
|
694
|
+
lines.push(stop === 'cap-halt' || stop === 'pricing-red'
|
|
695
|
+
? 'What to do: raise the cap or fix the price in Settings, then draft again.'
|
|
696
|
+
: (wholeJob ? 'What to do: review the whole job - every line and its ~ rules, and the inputs - and submit it again.' : 'What to do: draft again.'));
|
|
620
697
|
return lines.join('\n');
|
|
621
698
|
}
|
|
622
699
|
|
|
@@ -716,6 +793,10 @@ const SIGNING_TABLE = [
|
|
|
716
793
|
say: () => ['The judge model could not be reached or was cut off while the practice cases were graded. Nothing says the rubric is wrong.'], todo: 'try again in a few minutes' },
|
|
717
794
|
{ kind: 'artifact-red', when: (r) => r.code === 'artifact-red',
|
|
718
795
|
say: () => ['The judge model answered with something bareloop could not read while the practice cases were graded. Nothing says the rubric is wrong.'], todo: 'try again' },
|
|
796
|
+
// M2: a rubric job's check has no install/build/test commands to blame — its own wording, never the code-job one
|
|
797
|
+
{ kind: 'broken-close-rubric', when: (r) => r.code === 'broken-close' && r.rubricJob === true,
|
|
798
|
+
say: (r) => [`A check of the close (${safeText(r.path.split('.').pop() ?? '')}) could not run, so nothing it reported could be trusted.`],
|
|
799
|
+
todo: 'draft again; if it fails the same way, report the check named above' },
|
|
719
800
|
{ kind: 'broken-close', when: (r) => r.code === 'broken-close',
|
|
720
801
|
say: (r) => [`A check of the close (${safeText(r.path.split('.').pop() ?? '')}) cannot run on this repository, so nothing it reported could be trusted.`],
|
|
721
802
|
todo: 'check that the project\'s tools (its install, build and test commands) run, then try again' },
|
|
@@ -726,6 +807,9 @@ const SIGNING_TABLE = [
|
|
|
726
807
|
{ kind: 'judged-outside-fence', when: (r) => r.code === 'judged-outside-write-scope',
|
|
727
808
|
say: (r) => [`The rubric check looks at ${listText(r.outside)}, but the job may only edit ${listText(r.fence)} — so the run could never fix what it finds.`],
|
|
728
809
|
todo: 'draft again' },
|
|
810
|
+
// Part 1a: the rubric-step sorting reds are already whole sentences in the person's words (src/rubricstep.js `checkRubricParts`)
|
|
811
|
+
{ kind: 'rubric-part', when: (r) => r.code.startsWith('rubric-'), say: (r) => [fullText(r.detail)], todo: 'draft again, or reword that line' },
|
|
812
|
+
{ kind: 'rubric-shape', when: isRubricShape, say: (r) => [rubricShapeSentence(r)], todo: 'draft again, or reword that line' },
|
|
729
813
|
{ kind: 'declaration', when: (r) => DECLARATION_CODES.includes(r.code) && !(r.code === 'invalid-value' && r.path === 'card'),
|
|
730
814
|
say: (r) => [`The close the model wrote was refused before anything ran ("${safeText(r.code)}" at ${safeText(r.path)}).`], todo: 'draft again' },
|
|
731
815
|
];
|