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/authorflow.js
CHANGED
|
@@ -81,12 +81,16 @@ import { seedRead as runSeedReadStages, makeSeedTrees, AGGREGATES, SIGNS, MAX_TE
|
|
|
81
81
|
import { JUDGE_RULE_IDS } from './judged.js';
|
|
82
82
|
import {
|
|
83
83
|
KIND_CATALOGUE, MAX_STAGES, DIRECTIONS, BASELINES,
|
|
84
|
-
GENRE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
85
|
-
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES, MENU_CLASSES,
|
|
84
|
+
GENRE_LANGUAGES, RUBRIC_LANG, DECLARABLE_LANGUAGES, TYPES_GENRE_TEMPLATE, classGuards, genreEnv, genreOwnedEnvNames, genreInstruments,
|
|
85
|
+
validateDeclaration, envCapableKind, classMenu, VERDICT_CLASSES, LIVE_CLASSES, MENU_CLASSES, catalogueForLang,
|
|
86
86
|
} from './authoring.js';
|
|
87
87
|
import { buildSeedListing, cleanEntry, SURVEY_CAUSES, AUTHOR_CALL_TIMEOUT_MS } from './authorscout.js';
|
|
88
88
|
import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop, callCasualty } from './text.js';
|
|
89
89
|
import { redactSecrets } from './validate.js';
|
|
90
|
+
import { stepFileContract, inputLabelsOf, stepFileRel, readFrozenInputs, withFactBindings, settlePersonsWords } from './rubricstep.js';
|
|
91
|
+
import { factShapeGuide } from './facts.js';
|
|
92
|
+
import { renderLookReport, labelListsOf } from './inputlook.js';
|
|
93
|
+
import { checkDeclarationWords } from './factwords.js';
|
|
90
94
|
|
|
91
95
|
const require = createRequire(import.meta.url);
|
|
92
96
|
const { Loop } = require('bare-agent');
|
|
@@ -632,10 +636,10 @@ export const GUARD_DESCRIPTIONS = Object.freeze({
|
|
|
632
636
|
* close — one source, never a second hand-typed list — rendered through the
|
|
633
637
|
* frozen line in {@link GUARD_DESCRIPTIONS}, plus the write fence (a real,
|
|
634
638
|
* signed fence — {@link writeScopeBlock}'s own subject) when one is set.
|
|
635
|
-
* @param {{verdictType: string, lang: string, writeScope?: string[]|null}} o
|
|
639
|
+
* @param {{verdictType: string, lang: string, writeScope?: string[]|null, jobLines?: {n: number}[]|null}} o
|
|
636
640
|
* @returns {string[]}
|
|
637
641
|
*/
|
|
638
|
-
export function confirmProtections({ verdictType, lang, writeScope = null }) {
|
|
642
|
+
export function confirmProtections({ verdictType, lang, writeScope = null, jobLines = null }) {
|
|
639
643
|
const guards = classGuards({ verdictType, lang });
|
|
640
644
|
const lines = guards.map((g) => {
|
|
641
645
|
const desc = GUARD_DESCRIPTIONS[g.name];
|
|
@@ -648,6 +652,11 @@ export function confirmProtections({ verdictType, lang, writeScope = null }) {
|
|
|
648
652
|
if (Array.isArray(writeScope) && writeScope.length > 0) {
|
|
649
653
|
lines.push(`write fence — the run may only change files matching: ${writeScope.join(', ')}`);
|
|
650
654
|
}
|
|
655
|
+
// PART 1a: a rubric job on local files always checks, for EVERY numbered line, that the line's file exists, is complete
|
|
656
|
+
// JSON and has text — a fact of the close's shape (`rubric-step`), not a plan of the model's, so it is listed from code
|
|
657
|
+
if (lang === RUBRIC_LANG && Array.isArray(jobLines)) {
|
|
658
|
+
for (const l of jobLines) lines.push(`line ${l.n} file — ${stepFileRel(l.n)} must exist, be complete JSON, and carry text`);
|
|
659
|
+
}
|
|
651
660
|
return lines;
|
|
652
661
|
}
|
|
653
662
|
|
|
@@ -801,6 +810,68 @@ export const PARAM_SCHEMAS = Object.freeze({
|
|
|
801
810
|
items: { type: 'string' },
|
|
802
811
|
description: 'the AUTHORITATIVE artifacts to judge, repository-relative, read off the listing. One paid call each.',
|
|
803
812
|
},
|
|
813
|
+
step: { type: 'integer', minimum: 1, description: 'the number of the job line this stage serves; its file is output/steps/<step>.json' },
|
|
814
|
+
parts: {
|
|
815
|
+
type: 'array',
|
|
816
|
+
description: 'every constraint the person put on that job line, in their own words, each SORTED: countable (a machine counts it, or a '
|
|
817
|
+
+ 'quote from a named input must back it), judged (only reading it could decide), guard (a rule about web sources), none (you cannot sort it)',
|
|
818
|
+
items: {
|
|
819
|
+
type: 'object',
|
|
820
|
+
properties: {
|
|
821
|
+
text: { type: 'string', minLength: 1, description: 'the constraint, in the person\'s own words' },
|
|
822
|
+
class: { enum: ['countable', 'fact', 'judged', 'guard', 'none'] },
|
|
823
|
+
check: { enum: ['maxWords', 'minWords', 'maxLines', 'sections', 'sectionWords', 'cited', 'minClaims', 'maxClaims'], description: 'countable only; minClaims / maxClaims count the claims listed under the input named in source' },
|
|
824
|
+
value: { type: 'integer', minimum: 1, description: 'maxWords / minWords / maxLines: the limit the person asked for' },
|
|
825
|
+
names: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 }, description: 'sections / sectionWords: the section names' },
|
|
826
|
+
wordsPerSection: { type: 'integer', minimum: 1 },
|
|
827
|
+
source: { type: 'string', minLength: 1, description: 'cited: the label of the input a quote must come from' },
|
|
828
|
+
domains: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 }, description: 'guard only: the web domains the rule is about' },
|
|
829
|
+
covers: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'integer', minimum: 1 }, description: 'the number(s) of the CONFIRMED check(s) this part carries out' },
|
|
830
|
+
rules: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'integer', minimum: 1 }, description: 'the number(s) of the job line\'s ~ rules this part carries out (1 = the first ~ rule under the line)' },
|
|
831
|
+
facts: {
|
|
832
|
+
type: 'array',
|
|
833
|
+
minItems: 1,
|
|
834
|
+
maxItems: 10,
|
|
835
|
+
description: 'fact only: the REQUIRED FACTS every result must hold, one per thing the person asked for (from the job line AND its ~ rules)',
|
|
836
|
+
items: {
|
|
837
|
+
type: 'object',
|
|
838
|
+
properties: {
|
|
839
|
+
name: { type: 'string', minLength: 1, description: 'what the fact is called: letters and spaces only, no digits' },
|
|
840
|
+
shape: { enum: ['money', 'number', 'date', 'time', 'phrase', 'text'], description: 'money / number / date / time may carry a rule; phrase = fixed words the quote must contain; text = words the quote holds, as it writes them' },
|
|
841
|
+
unit: { type: 'string', minLength: 1, description: 'money: the words the input writes after the amount, copied verbatim (or give currency instead)' },
|
|
842
|
+
currency: { enum: ['$', '€', '£'], description: 'money: the symbol the input writes before the amount (or give unit instead)' },
|
|
843
|
+
phrase: { type: 'string', minLength: 1, description: 'phrase only: the fixed words, copied verbatim from the input' },
|
|
844
|
+
anchor: { type: 'string', minLength: 1, description: 'a label copied VERBATIM from the input that sits right before the value (picks the right one when a result holds several)' },
|
|
845
|
+
until: { type: 'string', minLength: 1, description: 'text with an anchor: the label copied VERBATIM that ends the value' },
|
|
846
|
+
rule: {
|
|
847
|
+
type: 'object',
|
|
848
|
+
description: 'only when the person asked for a limit',
|
|
849
|
+
properties: {
|
|
850
|
+
op: { enum: ['lt', 'lte', 'gt', 'gte', 'between'] },
|
|
851
|
+
value: { type: ['number', 'string'], description: 'a number for number/money; a string for date (YYYY-MM-DD) / time (HH:MM)' },
|
|
852
|
+
lo: { type: ['number', 'string'] },
|
|
853
|
+
hi: { type: ['number', 'string'] },
|
|
854
|
+
},
|
|
855
|
+
required: ['op'],
|
|
856
|
+
additionalProperties: false,
|
|
857
|
+
},
|
|
858
|
+
aiProposal: { type: 'boolean', description: 'true when the rule\'s number is YOURS (made out of the person\'s words), not one the person wrote' },
|
|
859
|
+
},
|
|
860
|
+
required: ['name', 'shape'],
|
|
861
|
+
additionalProperties: false,
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
count: {
|
|
865
|
+
type: 'object',
|
|
866
|
+
description: 'fact only: how many results the person asked for',
|
|
867
|
+
properties: { min: { type: 'integer', minimum: 1 }, max: { type: 'integer', minimum: 1 } },
|
|
868
|
+
additionalProperties: false,
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
required: ['text', 'class'],
|
|
872
|
+
additionalProperties: false,
|
|
873
|
+
},
|
|
874
|
+
},
|
|
804
875
|
ask: {
|
|
805
876
|
type: 'string',
|
|
806
877
|
minLength: 1,
|
|
@@ -854,10 +925,12 @@ export function schemaCoverage(catalogue = KIND_CATALOGUE) {
|
|
|
854
925
|
* and failing loudly beats emitting a schema that quietly cannot say something
|
|
855
926
|
* the validator will then demand.
|
|
856
927
|
* `questions` (P7 item 5, last drafting try only) adds the optional `questions` property; absent, the schema is byte-identical.
|
|
928
|
+
* `kinds` (Part 1a) narrows the branches to the kinds a language may use (`catalogueForLang`) WITHOUT narrowing the coverage check,
|
|
929
|
+
* which stays over the whole catalogue; null (the default) = no narrowing.
|
|
857
930
|
* @param {Record<string, any>} [catalogue]
|
|
858
|
-
* @param {{verdictType?: string|null, questions?: boolean}} [o]
|
|
931
|
+
* @param {{verdictType?: string|null, questions?: boolean, kinds?: string[]|null}} [o]
|
|
859
932
|
*/
|
|
860
|
-
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null, questions = false } = {}) {
|
|
933
|
+
export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = null, questions = false, kinds = null } = {}) {
|
|
861
934
|
const cov = schemaCoverage(catalogue);
|
|
862
935
|
if (!cov.ok) {
|
|
863
936
|
throw new Error('[authorflow] the declaration schema does not cover the catalogue — '
|
|
@@ -865,7 +938,7 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
865
938
|
}
|
|
866
939
|
const menu = classMenu(verdictType, catalogue);
|
|
867
940
|
const branches = Object.entries(catalogue)
|
|
868
|
-
.filter(([kind, spec]) => !spec.locked && menu.includes(kind))
|
|
941
|
+
.filter(([kind, spec]) => !spec.locked && menu.includes(kind) && (kinds === null || kinds.includes(kind)))
|
|
869
942
|
.map(([kind, spec]) => {
|
|
870
943
|
/** @type {Record<string, any>} */
|
|
871
944
|
const properties = {};
|
|
@@ -910,12 +983,13 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
910
983
|
type: 'object',
|
|
911
984
|
properties: {
|
|
912
985
|
line: { type: 'integer', minimum: 1, description: 'the number of a job line no stage checks' },
|
|
986
|
+
rule: { type: 'integer', minimum: 1, description: 'optional: the number of ONE ~ rule of that line that nothing checks (the line itself still has a stage)' },
|
|
913
987
|
reason: { type: 'string', minLength: 1, description: 'why, in plain words the person can read' },
|
|
914
988
|
},
|
|
915
989
|
required: ['line', 'reason'],
|
|
916
990
|
additionalProperties: false,
|
|
917
991
|
},
|
|
918
|
-
description: 'job lines you could not turn into a
|
|
992
|
+
description: 'job lines (or single ~ rules of a line) you could not turn into a check, each with a reason in the person\'s words — a note does not cover a line or a rule',
|
|
919
993
|
},
|
|
920
994
|
...(questions ? {
|
|
921
995
|
questions: {
|
|
@@ -938,13 +1012,13 @@ export function declarationSchema(catalogue = KIND_CATALOGUE, { verdictType = nu
|
|
|
938
1012
|
* The output channel. It records and acknowledges; it takes no action, which is
|
|
939
1013
|
* why handing it to a toolless authoring call does not reopen D3.
|
|
940
1014
|
* @param {{calls: any[]}} box
|
|
941
|
-
* @param {{catalogue?: Record<string, any>, verdictType?: string|null, questions?: boolean}} [o]
|
|
1015
|
+
* @param {{catalogue?: Record<string, any>, verdictType?: string|null, questions?: boolean, kinds?: string[]|null}} [o]
|
|
942
1016
|
*/
|
|
943
|
-
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null, questions = false } = {}) {
|
|
1017
|
+
export function declarationTool(box, { catalogue = KIND_CATALOGUE, verdictType = null, questions = false, kinds = null } = {}) {
|
|
944
1018
|
return {
|
|
945
1019
|
name: DECLARATION_TOOL_NAME,
|
|
946
1020
|
description: 'Deliver the finished close. Call this exactly once, with the whole declaration as its arguments.',
|
|
947
|
-
parameters: declarationSchema(catalogue, { verdictType, questions }),
|
|
1021
|
+
parameters: declarationSchema(catalogue, { verdictType, questions, kinds }),
|
|
948
1022
|
execute: async (/** @type {any} */ args) => { box.calls.push(args); return DECLARATION_ACK; },
|
|
949
1023
|
};
|
|
950
1024
|
}
|
|
@@ -1121,8 +1195,9 @@ export function writeScopeBlock(writeScope) {
|
|
|
1121
1195
|
* never merge.
|
|
1122
1196
|
* @param {{checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[],
|
|
1123
1197
|
* answeredQuestions?: string[]}} confirmed
|
|
1198
|
+
* @param {{numbered?: boolean}} [o] `numbered` (a rubric job on files): each confirmed check carries its `#n`, which a part cites in `covers`
|
|
1124
1199
|
*/
|
|
1125
|
-
export function confirmedBlock(confirmed) {
|
|
1200
|
+
export function confirmedBlock(confirmed, { numbered = false } = {}) {
|
|
1126
1201
|
const checks = confirmed.checks ?? [];
|
|
1127
1202
|
const protections = confirmed.protections ?? [];
|
|
1128
1203
|
const openQuestions = confirmed.openQuestions ?? [];
|
|
@@ -1132,8 +1207,8 @@ export function confirmedBlock(confirmed) {
|
|
|
1132
1207
|
+ 'Compose stages for these checks and no other — a genre never adds a check the goal did not ask for, and '
|
|
1133
1208
|
+ 'neither do you:\n\n'
|
|
1134
1209
|
+ `${(confirmed.checkItems?.length
|
|
1135
|
-
? confirmed.checkItems.map((c) => ` - ${c.fromLine && c.fromLine.length ? `[line ${c.fromLine.join(', ')}] ` : ''}${c.text}`)
|
|
1136
|
-
: checks.map((c) => ` - ${c}`)).join('\n') || ' (none)'}\n\n`
|
|
1210
|
+
? confirmed.checkItems.map((c, i) => ` - ${numbered ? `#${i + 1} ` : ''}${c.fromLine && c.fromLine.length ? `[line ${c.fromLine.join(', ')}] ` : ''}${c.text}`)
|
|
1211
|
+
: checks.map((c, i) => ` - ${numbered ? `#${i + 1} ` : ''}${c}`)).join('\n') || ' (none)'}\n\n`
|
|
1137
1212
|
+ 'These protections are already always-on and unchanged by this plan — never compose one of these as a check:\n\n'
|
|
1138
1213
|
+ `${protections.map((p) => ` - ${p}`).join('\n') || ' (none)'}\n\n`
|
|
1139
1214
|
+ (notChecked.length
|
|
@@ -1183,14 +1258,16 @@ export function jobLineTagBlock(count) {
|
|
|
1183
1258
|
* @param {{answers: Record<string|number, string>, questions?: Record<string|number, string>,
|
|
1184
1259
|
* facts: any, listingBlock: string, lang: string, verdictType: string,
|
|
1185
1260
|
* guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
|
|
1186
|
-
* ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any
|
|
1261
|
+
* ownedEnvNames?: string[], mode?: 'tool'|'text', catalogue?: Record<string, any>|null,
|
|
1187
1262
|
* writeScope?: string[]|null,
|
|
1188
1263
|
* jobLines?: {n: number}[]|null, inputs?: {n: number, label: string, value: string}[]|null,
|
|
1189
|
-
* confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null
|
|
1264
|
+
* confirmed?: {checks: string[], checkItems?: {text: string, fromLine: number[]|null}[], protections: string[], openQuestions?: string[], notChecked?: string[], answeredQuestions?: string[]}|null,
|
|
1265
|
+
* labelLists?: string[]|null,
|
|
1266
|
+
* }} o
|
|
1190
1267
|
*/
|
|
1191
1268
|
export function authorPrompt({
|
|
1192
1269
|
answers, questions = GREEN_QUESTIONS, facts, listingBlock, lang, verdictType, guards,
|
|
1193
|
-
ownedEnvNames = [], mode = 'tool', catalogue =
|
|
1270
|
+
ownedEnvNames = [], mode = 'tool', catalogue: catalogueIn = null, writeScope = null,
|
|
1194
1271
|
// P7: the person's numbered job lines and the inputs they pointed at. ABSENT = byte-identical prompt (every caller
|
|
1195
1272
|
// that predates the job block). Present: the drafter is told to tag every stage it writes with the line it serves.
|
|
1196
1273
|
jobLines = null, inputs = null,
|
|
@@ -1198,7 +1275,16 @@ export function authorPrompt({
|
|
|
1198
1275
|
// turn (or ran without `ask`) — byte-identical prompt, nothing added. Present
|
|
1199
1276
|
// only when a confirm turn actually ran and the person accepted a plan.
|
|
1200
1277
|
confirmed = null,
|
|
1278
|
+
// PART 1c: the label lists code made from the frozen inputs (src/labelscan.js `renderLabelList`); a label is picked from them
|
|
1279
|
+
labelLists = null,
|
|
1201
1280
|
}) {
|
|
1281
|
+
// PART 1a: a rubric job on the person's own local files has no repository, no genre template and no tool to measure with —
|
|
1282
|
+
// it gets its own brief (`rubricAuthorPrompt`), which still ends in the same declaration tool and the same guards
|
|
1283
|
+
// an absent catalogue is the language's own menu (`catalogueForLang`), so a code job's prompt never lists a rubric-only kind
|
|
1284
|
+
const catalogue = catalogueIn ?? catalogueForLang(lang);
|
|
1285
|
+
if (lang === RUBRIC_LANG) {
|
|
1286
|
+
return rubricAuthorPrompt({ answers, questions, facts, listingBlock, verdictType, guards, mode, catalogue, writeScope, jobLines, inputs, confirmed, labelLists });
|
|
1287
|
+
}
|
|
1202
1288
|
const statement = CLASS_STATEMENTS[String(verdictType)] ?? null;
|
|
1203
1289
|
if (statement === null) {
|
|
1204
1290
|
throw new Error(`[authorflow] the authoring prompt needs the verdict class the user picked, and "${verdictType}" `
|
|
@@ -1303,6 +1389,170 @@ measure anything.`;
|
|
|
1303
1389
|
].join('\n\n---\n\n');
|
|
1304
1390
|
}
|
|
1305
1391
|
|
|
1392
|
+
/**
|
|
1393
|
+
* What a rubric job on local files means by DONE — the class statement of the text genre (Part 1a). It is stated as an order
|
|
1394
|
+
* about KINDS: every part of a job line is counted or checked against a quote, because nothing in this build judges.
|
|
1395
|
+
*/
|
|
1396
|
+
export const RUBRIC_FILES_STATEMENT = 'The person declared this job SOFT-GREEN and pointed it at their own text files: the work is writing '
|
|
1397
|
+
+ 'about those files. This build has no judge, so every part of a job line must be one of three things. COUNTED: a limit on words, '
|
|
1398
|
+
+ 'sections or lines of what is written. Or a REQUIRED FACT: something the line finds, lists or looks up, each answer quoted word for '
|
|
1399
|
+
+ 'word from a named input and checked by its shape (money, number, date, time, phrase, text). Or BACKED BY A QUOTE: a claim that '
|
|
1400
|
+
+ 'must carry words copied from a named input. '
|
|
1401
|
+
+ 'Compose ONE rubric-step stage per numbered job line, after the mandatory guard, and name only limits the person actually '
|
|
1402
|
+
+ 'asked for. A part you cannot express that way is not approximated by a stage that means something else.';
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* THE RUBRIC RULES, in one place (live fix B1): what the confirm step and the drafter are both told about turning the person's words
|
|
1406
|
+
* into checks. The confirm note ({@link RUBRIC_CONFIRM_NOTE}) and the composer brief ({@link REQUIRED_FACTS_BRIEF}) interpolate THIS
|
|
1407
|
+
* constant, so the two cannot drift (in smv6k6l8dxkzs the confirm step planned a limit the drafter then could not carry, and the
|
|
1408
|
+
* drafter added an airport code the person never wrote). Field-neutral: neither turn needs the other's field names.
|
|
1409
|
+
*/
|
|
1410
|
+
export const RUBRIC_RULES = 'THE RULES FOR TURNING THE PERSON\'S WORDS INTO CHECKS (the same for every turn of this job):\n - A limit the person WROTE ("under $450") is a rule that can be checked: plan it as a limit with its number.\n'
|
|
1411
|
+
+ ' - A number YOU make out of their words ("not a red-eye" becomes a departure time between 06:00 and 21:00) is your proposal, marked '
|
|
1412
|
+
+ 'as the AI\'s proposal for the person to check; a limit on a time that you propose is a RANGE (between), never one-sided. The input does '
|
|
1413
|
+
+ 'not have to hold the person\'s word (it holds no "red-eye"): the limit goes on the time the input DOES hold (e.g. the one after "Departure time:"), as a '
|
|
1414
|
+
+ 'range marked as the AI\'s proposal.\n'
|
|
1415
|
+
+ ' - Names, labels and units are the INPUT\'s own words, copied exactly as the input writes them. When the input does not use the '
|
|
1416
|
+
+ 'person\'s word (they wrote SFO, the input says San Francisco International Airport), use the words the input uses; never invent a '
|
|
1417
|
+
+ 'word the input does not hold, and never add a code (like SFO) that the person did not write and the input does not hold.\n'
|
|
1418
|
+
+ ' - A label for a line that asks for N results (a count of N) must sit in each result, so the label list must count it N or more times '
|
|
1419
|
+
+ '(the x-number beside it). A label counted fewer times is on the page once or twice (a sort tab, a switch) and is refused at $0 with its count.\n'
|
|
1420
|
+
+ ' - The same holds for the words a "must say" (phrase) fact makes each quote say: they must be in the input at least N times for N results. Words counted fewer times (page-header words like "from San Francisco" when each result says "Leaves San Francisco International Airport") are refused at $0 with their count; pick words every result says, or drop the check.';
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* REQUIRED FACTS, as the composer is told them (PART 1b, PRD item 33 RF1–RF17). The shape of the fact part is in the catalogue and the
|
|
1424
|
+
* schema; this is the judgement the schema cannot carry: what becomes a fact, where its words come from, and what is Not checked.
|
|
1425
|
+
*/
|
|
1426
|
+
export const REQUIRED_FACTS_BRIEF = 'REQUIRED FACTS — what a line that finds, lists or looks something up leaves behind\n\n'
|
|
1427
|
+
+ 'For such a line write ONE part of class "fact". The worker\'s file for that line holds only results, each with ONE quote copied '
|
|
1428
|
+
+ 'from the input and the value of every fact as the quote writes it — no free text. Code checks every value against its quote.\n'
|
|
1429
|
+
+ ' - The facts come from the job line itself AND from its ~ rules: "SFO to LAX" is a from fact and a to fact even with no ~ line. '
|
|
1430
|
+
+ 'One fact per thing the person asked for. Give every fact a shape. A rule (op, value, or between lo and hi) only where the person '
|
|
1431
|
+
+ 'asked for a limit.\n'
|
|
1432
|
+
+ ' - THE LOOK above is what a read of the inputs found: write the facts from it. Copy every label (anchor), unit and phrase EXACTLY '
|
|
1433
|
+
+ 'as the look gives it (the look\'s phrase facts become phrase facts with the same words), and say in "notes" when you used the input\'s '
|
|
1434
|
+
+ 'words instead of the person\'s. A label (anchor) is PICKED from the labels the look found in the input, never typed; one that is not '
|
|
1435
|
+
+ 'among them is refused at $0. What the look could not find is not checked: list it under "refused" in the person\'s words.\n'
|
|
1436
|
+
+ ' - When one result holds several values of one kind (leaves / arrives, subtotal / total) give the fact an anchor, the label that sits right before the value you mean.\n'
|
|
1437
|
+
+ ' - Set aiProposal true on a fact whose number you made out of their words.\n'
|
|
1438
|
+
+ RUBRIC_RULES + '\n'
|
|
1439
|
+
+ ' - How MANY results were asked for is a count ("3 releases": count {min: 3, max: 3}). WHICH ones ("the last", "the newest") and '
|
|
1440
|
+
+ 'anything else you cannot express as a shape and a rule ("the cheapest", "the best", "a good pick") is NOT CHECKED: list it in "refused" as {"line": N, "rule": the number of that ~ rule, "reason": the '
|
|
1441
|
+
+ 'person\'s own words}. EVERY ~ rule of a line must be carried out by a part (list its number in the part\'s "rules", 1 = the first ~ '
|
|
1442
|
+
+ 'rule under the line) or be listed in "refused"; a rule that is neither is refused at $0.\n'
|
|
1443
|
+
+ ' - A line that WRITES (a summary, a report) keeps word, section and line counts. Code ties its numbers to the facts of the lines '
|
|
1444
|
+
+ 'before it; you do not add that.\n'
|
|
1445
|
+
+ ' - A look fact that says "carries ~ rule N" is how the look carried that rule: put N in the fact part\'s "rules" and do not also list rule N in "refused".\n'
|
|
1446
|
+
+ ' - A ~ rule that you carry out as a fact (a limit on a time, a price, a date: "not a red-eye" is a time fact with a between rule) is carried by the '
|
|
1447
|
+
+ 'fact part: list its number in that part\'s "rules". It is NOT also a part of its own: on a line that lists facts, a part of class judged or none '
|
|
1448
|
+
+ 'is refused. A rule you cannot express as a shape and a rule goes in "refused", never into a part.\n'
|
|
1449
|
+
+ ' - "covers" and "rules" are fields of the PART, written once on it as a list of numbers. A fact never has a "covers" or a "rules" field.\n'
|
|
1450
|
+
+ ' - ONE stage per job line, holding ALL of that line\'s facts (and its other parts). Two stages for one line are refused: put a '
|
|
1451
|
+
+ '~ rule\'s fact (a "not a red-eye" time) on the same stage as the line\'s other facts.\n'
|
|
1452
|
+
+ ' - WHICH SHAPE TAKES WHICH FIELDS (a field a shape does not take is refused):\n'
|
|
1453
|
+
+ factShapeGuide();
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* The composer prompt for a rubric job on local files (Part 1a, docs/product/ITEM33-BUILD.md). There is no repository
|
|
1457
|
+
* and no survey: the facts are the numbered inputs and the listing is the frozen tree (input/ files, output/). It states
|
|
1458
|
+
* the step-file contract the worker is also told ({@link stepFileContract}), so the stage and the file cannot drift.
|
|
1459
|
+
* @param {{answers: Record<string|number, string>, questions?: Record<string|number, string>, facts: any, listingBlock: string,
|
|
1460
|
+
* verdictType: string, guards: {name: string, kind: string, params: Record<string, any>, fill: string[]}[],
|
|
1461
|
+
* mode?: 'tool'|'text', catalogue?: Record<string, any>, writeScope?: string[]|null,
|
|
1462
|
+
* jobLines?: {n: number}[]|null, inputs?: {n: number, label: string, value: string}[]|null, confirmed?: any,
|
|
1463
|
+
* labelLists?: string[]|null,
|
|
1464
|
+
* }} o
|
|
1465
|
+
* @returns {string}
|
|
1466
|
+
*/
|
|
1467
|
+
export function rubricAuthorPrompt({
|
|
1468
|
+
answers, questions = GREEN_QUESTIONS, facts, listingBlock, verdictType, guards,
|
|
1469
|
+
mode = 'tool', catalogue = KIND_CATALOGUE, writeScope = null, jobLines = null, inputs = null, confirmed = null, labelLists = null,
|
|
1470
|
+
}) {
|
|
1471
|
+
const interview = Object.entries(questions)
|
|
1472
|
+
.map(([n, q]) => `Q${n}. ${q}\nA${n}. ${answers?.[n] ?? '(no answer given)'}`)
|
|
1473
|
+
.join('\n\n');
|
|
1474
|
+
const role = `You are composing the close for one job.
|
|
1475
|
+
|
|
1476
|
+
The close is what decides whether a run of this job came back DONE. It runs after the work is
|
|
1477
|
+
finished, it is the only truth, and nothing else can overrule it. It is a list of STAGES. Each
|
|
1478
|
+
stage names a kind from the catalogue below and fills that kind's parameters. Stages run in the
|
|
1479
|
+
order you declare them.
|
|
1480
|
+
|
|
1481
|
+
Two sources fed you this brief and neither of them is you:
|
|
1482
|
+
- the person who wants the job done answered an interview, below, in their own words. They are
|
|
1483
|
+
not a software engineer and did not name commands, thresholds or patterns;
|
|
1484
|
+
- the person's own text files, frozen into the job. There is no code repository: the files sit
|
|
1485
|
+
under input/ and the run writes only under output/.`;
|
|
1486
|
+
const guardJson = guards.map((g) => ({
|
|
1487
|
+
name: g.name,
|
|
1488
|
+
kind: g.kind,
|
|
1489
|
+
params: { ...g.params, ...Object.fromEntries(g.fill.map((f) => [f, FILL_MARKER])) },
|
|
1490
|
+
}));
|
|
1491
|
+
const output = mode === 'tool'
|
|
1492
|
+
? `HOW TO ANSWER
|
|
1493
|
+
|
|
1494
|
+
Call the ${DECLARATION_TOOL_NAME} tool exactly once. Its arguments ARE the declaration: an ordered
|
|
1495
|
+
"stages" array, plus "notes" for anything you could not express, and "refused" for a job line no stage can check.
|
|
1496
|
+
Do not write the declaration as text.
|
|
1497
|
+
|
|
1498
|
+
Stage names are unique. Every parameter name comes from the catalogue and no other. No parameter is
|
|
1499
|
+
left as a placeholder. You never write a number the person did not ask for.`
|
|
1500
|
+
: `HOW TO ANSWER
|
|
1501
|
+
|
|
1502
|
+
A single JSON object and nothing else: an ordered "stages" array, plus "notes" for anything you
|
|
1503
|
+
could not express, and "refused" for a job line no stage can check.
|
|
1504
|
+
|
|
1505
|
+
Stage names are unique. Every parameter name comes from the catalogue and no other. No parameter is
|
|
1506
|
+
left as a placeholder. You never write a number the person did not ask for.`;
|
|
1507
|
+
return [
|
|
1508
|
+
role,
|
|
1509
|
+
`WHAT THE PERSON DECLARED "DONE" TO MEAN — their answer, not yours\n\n${RUBRIC_FILES_STATEMENT}`,
|
|
1510
|
+
`THE INTERVIEW — the person's own words\n\n${interview}`
|
|
1511
|
+
+ (inputs && inputs.length ? `\n\n${renderInputs(inputs)}` : ''),
|
|
1512
|
+
...(jobLines && jobLines.length ? [jobLineTagBlock(jobLines.length)] : []),
|
|
1513
|
+
...(writeScope && writeScope.length ? [writeScopeBlock(writeScope)] : []),
|
|
1514
|
+
...(confirmed ? [confirmedBlock(confirmed, { numbered: true }),
|
|
1515
|
+
'EVERY CONFIRMED CHECK IS COMPOSED OR MOVED, NEVER DROPPED\n\nThe person saw the numbered checks above. On each countable or fact part that carries one of them out, list its number in the PART\'s "covers" (once on the part, never on a fact inside it). '
|
|
1516
|
+
+ 'A confirmed check that no part covers is not silently left out: it is moved to Not checked before signing, and the person is told. '
|
|
1517
|
+
+ 'To count "N things listed", use minClaims / maxClaims (it counts the claims listed under an input); anything else you cannot count is not composed.'] : []),
|
|
1518
|
+
REQUIRED_FACTS_BRIEF,
|
|
1519
|
+
`THE STEP FILES — what the worker leaves behind for each job line\n\n${stepFileContract()}\n\n`
|
|
1520
|
+
+ 'The rubric-step stage for job line N reads exactly that file, output/steps/N.json. It names the inputs (by label) the '
|
|
1521
|
+
+ 'step must cite from, and the limits on its "text".',
|
|
1522
|
+
`THE FILES — the inputs the person pointed at, frozen into the job\n\n${JSON.stringify(withoutLook(facts), null, 2)}`
|
|
1523
|
+
+ (listingBlock ? `\n\n${listingBlock}` : ''),
|
|
1524
|
+
...(Array.isArray(labelLists) && labelLists.length ? [labelLists.join('\n\n')] : []),
|
|
1525
|
+
...(facts && facts.look ? [renderLookReport(facts.look)] : []),
|
|
1526
|
+
rubricCatalogueBlock(catalogue, { verdictType }),
|
|
1527
|
+
'MANDATORY GUARDS — already placed and already parameterised\n\n'
|
|
1528
|
+
+ 'This kind of job carries a stage the person did not ask for. It is fixed and you may not change, drop, rename, or '
|
|
1529
|
+
+ `re-kind it. The ONLY thing you fill in is each "${FILL_MARKER}" slot, from the interview:\n\n${JSON.stringify(guardJson, null, 2)}\n\n`
|
|
1530
|
+
+ 'Place it first: it is cheap, and the first red ends the judgement.',
|
|
1531
|
+
output,
|
|
1532
|
+
].join('\n\n---\n\n');
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/** the facts object without the look's report (that is its own block, written by code) @param {any} facts */
|
|
1536
|
+
function withoutLook(facts) {
|
|
1537
|
+
if (!facts || typeof facts !== 'object' || !('look' in facts)) return facts;
|
|
1538
|
+
const { look: _look, ...rest } = facts;
|
|
1539
|
+
return rest;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* The kind rows of the catalogue, without the command-output parser text a rubric job never needs.
|
|
1544
|
+
* @param {Record<string, any>} catalogue @param {{verdictType?: string|null}} [o]
|
|
1545
|
+
*/
|
|
1546
|
+
function rubricCatalogueBlock(catalogue, { verdictType = null } = {}) {
|
|
1547
|
+
const menu = classMenu(verdictType, catalogue);
|
|
1548
|
+
const rows = Object.entries(catalogue).filter(([name, spec]) => menu.includes(name) && !spec.locked).map(([name, spec]) => `- ${name}\n ${spec.shape}\n required: ${spec.required.join(', ')}`
|
|
1549
|
+
+ `${spec.optional.length ? ` optional: ${spec.optional.join(', ')}` : ''}\n asserts: ${spec.asserts}`);
|
|
1550
|
+
return `THE KIND CATALOGUE — these are the only kinds that exist.
|
|
1551
|
+
You parameterise kinds. You never write a script, a shell fragment, or a new kind.
|
|
1552
|
+
|
|
1553
|
+
${rows.join('\n\n')}`;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1306
1556
|
// ── 3. THE MEASURED BLOCKS ───────────────────────────────────────────────────
|
|
1307
1557
|
|
|
1308
1558
|
/** F6, at the rendering boundary: an absent number is unknown, never 0. */
|
|
@@ -1784,12 +2034,12 @@ export async function askStructured({ messages, generate, mode, retries, label,
|
|
|
1784
2034
|
/** THE DECLARATION CHANNEL — the one this loop has always used. Its schema is
|
|
1785
2035
|
* CLASS-SCOPED, like the prompt's catalogue block: the two must offer one set of
|
|
1786
2036
|
* kinds, or the composer reads about a kind it cannot call.
|
|
1787
|
-
* @param {{catalogue: Record<string, any>, verdictType: string, questions?: boolean}} o */
|
|
1788
|
-
const declarationChannel = ({ catalogue, verdictType, questions = false }) => ({
|
|
2037
|
+
* @param {{catalogue: Record<string, any>, verdictType: string, questions?: boolean, kinds?: string[]|null}} o */
|
|
2038
|
+
const declarationChannel = ({ catalogue, verdictType, questions = false, kinds = null }) => ({
|
|
1789
2039
|
name: DECLARATION_TOOL_NAME,
|
|
1790
2040
|
instruction: STRUCTURE_INSTRUCTION_TOOL,
|
|
1791
2041
|
textPath: 'declaration',
|
|
1792
|
-
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType, questions }),
|
|
2042
|
+
tool: (/** @type {{calls: any[]}} */ box) => declarationTool(box, { catalogue, verdictType, questions, kinds }),
|
|
1793
2043
|
});
|
|
1794
2044
|
|
|
1795
2045
|
/** The declaration ask, over the shared ladder. `declaration` is `artifact`
|
|
@@ -1797,11 +2047,11 @@ const declarationChannel = ({ catalogue, verdictType, questions = false }) => ({
|
|
|
1797
2047
|
* anonymous artifact, and one word for two things is how a reader loses which.
|
|
1798
2048
|
* @param {{messages: any[], generate: Function, mode: 'tool'|'text', retries: number,
|
|
1799
2049
|
* label: string, book: ReturnType<typeof makeCostBook>, catalogue: Record<string, any>,
|
|
1800
|
-
* verdictType: string, questions?: boolean}} o
|
|
2050
|
+
* verdictType: string, questions?: boolean, kinds?: string[]|null}} o
|
|
1801
2051
|
*/
|
|
1802
|
-
async function askDeclaration({ messages, generate, mode, retries, label, book, catalogue, verdictType, questions = false }) {
|
|
2052
|
+
async function askDeclaration({ messages, generate, mode, retries, label, book, catalogue, verdictType, questions = false, kinds = null }) {
|
|
1803
2053
|
const r = await askStructured({
|
|
1804
|
-
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType, questions }),
|
|
2054
|
+
messages, generate, mode, retries, label, book, channel: declarationChannel({ catalogue, verdictType, questions, kinds }),
|
|
1805
2055
|
});
|
|
1806
2056
|
return { ...r, declaration: r.artifact };
|
|
1807
2057
|
}
|
|
@@ -1923,6 +2173,23 @@ const confirmChannel = () => ({
|
|
|
1923
2173
|
* an ORDER to the model itself, the same register {@link AUTHOR_SYSTEM} and
|
|
1924
2174
|
* {@link CLASS_STATEMENTS} already use, rather than a code matcher over prose.
|
|
1925
2175
|
*/
|
|
2176
|
+
/**
|
|
2177
|
+
* What the confirm turn tells the model about a rubric job on local text files (model-facing, registered). There is no
|
|
2178
|
+
* repository and no judge: a check is a limit a machine counts (words, lines, sections), or a claim a quote from a named
|
|
2179
|
+
* input must back. Everything else the person asked for is named in `notChecked`, in their words.
|
|
2180
|
+
*/
|
|
2181
|
+
export const RUBRIC_CONFIRM_NOTE = 'THIS JOB HAS NO REPOSITORY. Its inputs are the person\'s own text files, and each numbered job line leaves ONE '
|
|
2182
|
+
+ 'file the run writes (output/steps/<line>.json: a "text" and, per input, quoted claims — or, for a line that finds, lists or '
|
|
2183
|
+
+ 'looks something up, only filled facts, each quoted from the input). In this build only three kinds of check exist: a COUNTED '
|
|
2184
|
+
+ 'limit on that text (words, lines, sections, words per section); a claim that must be backed by a quote copied from a named input; '
|
|
2185
|
+
+ 'and a REQUIRED FACT, something a line finds (a price, a date, a time, a name, a phrase) with a limit only if the person gave one '
|
|
2186
|
+
+ '— plan one fact check for each thing the line asks to find, including those named in the line itself ("SFO to LAX"). Plan a check '
|
|
2187
|
+
+ 'only for what the person asked for in those terms; '
|
|
2188
|
+
+ 'anything that needs reading the text to judge it (tone, quality, accuracy), any superlative ("the cheapest", "the best") and any '
|
|
2189
|
+
+ 'choice of WHICH ones ("the last", "the newest") goes in `notChecked`, in the person\'s own words; how MANY results were asked for is '
|
|
2190
|
+
+ 'a count and can be checked.\n'
|
|
2191
|
+
+ RUBRIC_RULES;
|
|
2192
|
+
|
|
1926
2193
|
export const CONFIRM_SYSTEM = 'You read what a NON-ENGINEER answered, plus a read-only survey or listing of their '
|
|
1927
2194
|
+ 'own repository, and draft a PLAN for a job\'s definition of done: the checks you plan to compose and one '
|
|
1928
2195
|
+ 'signed goal sentence. You never propose a check the numbered job lines (and the ~ rules written under them) did not ask for '
|
|
@@ -1974,6 +2241,9 @@ export function confirmPrompt({ answers, questions, labels = {}, facts = null, l
|
|
|
1974
2241
|
lines.push('', `WORSE THAN BEFORE (a constraint the person named, repo jobs only): ${worseThanBefore}`);
|
|
1975
2242
|
}
|
|
1976
2243
|
lines.push('', `LANGUAGE: ${lang}`);
|
|
2244
|
+
// PART 1a: a rubric job on the person's own text files. The model is told what a check CAN be here, so that anything
|
|
2245
|
+
// needing judgement lands in `notChecked` instead of being promised as a check
|
|
2246
|
+
if (lang === RUBRIC_LANG) lines.push('', RUBRIC_CONFIRM_NOTE);
|
|
1977
2247
|
if (writeScope) lines.push(`WRITE SCOPE (the fence — the run may write only here): ${writeScope.join(', ')}`);
|
|
1978
2248
|
if (protections.length > 0) {
|
|
1979
2249
|
lines.push(
|
|
@@ -2026,7 +2296,8 @@ async function askConfirmPlan({ convo, generate, mode, book, label }) {
|
|
|
2026
2296
|
* generate: Function, book: ReturnType<typeof makeCostBook>,
|
|
2027
2297
|
* ask: (step: {kind: string, [k: string]: any}) => Promise<string|null>,
|
|
2028
2298
|
* onPhase?: (phase: string, data?: any) => void, mode?: 'tool'|'text',
|
|
2029
|
-
* worseThanBefore?: string, inputs?: {n: number, label: string, value: string}[]|null
|
|
2299
|
+
* worseThanBefore?: string, inputs?: {n: number, label: string, value: string}[]|null,
|
|
2300
|
+
* jobLines?: {n: number}[]|null}} o retired field (hamr's ruling 2026-09-28):
|
|
2030
2301
|
* `worseThanBefore` is never asked any more and defaults to `''` — a
|
|
2031
2302
|
* caller may still pass a string through (kept for a future, non-ask
|
|
2032
2303
|
* source of the same constraint), it just never comes from an ask() step.
|
|
@@ -2048,6 +2319,8 @@ export async function runConfirmTurn({
|
|
|
2048
2319
|
worseThanBefore: presetWorseThanBefore = '',
|
|
2049
2320
|
// P7: the inputs the person pointed at; absent = the prompt is byte-identical
|
|
2050
2321
|
inputs = null,
|
|
2322
|
+
// PART 1a: the numbered job lines, so a rubric job's per-line file protections are listed from code
|
|
2323
|
+
jobLines = null,
|
|
2051
2324
|
}) {
|
|
2052
2325
|
/** @typedef {null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart'} ConfirmStop */
|
|
2053
2326
|
/** @returns {{ok: boolean, stop: ConfirmStop, rounds: number, accepted: null, reds: Red[], cost: any}} */
|
|
@@ -2073,7 +2346,7 @@ export async function runConfirmTurn({
|
|
|
2073
2346
|
// the prompt below so the model can check `notChecked` against it, and
|
|
2074
2347
|
// reused (never recomputed) after the call so the shown, prompted, and
|
|
2075
2348
|
// recorded protections are always the exact same list.
|
|
2076
|
-
const protections = confirmProtections({ verdictType, lang: resolvedLang, writeScope });
|
|
2349
|
+
const protections = confirmProtections({ verdictType, lang: resolvedLang, writeScope, jobLines });
|
|
2077
2350
|
|
|
2078
2351
|
// ── up to 2 paid rounds (ruling 5) ─────────────────────────────────────────
|
|
2079
2352
|
/** @type {string|null} */
|
|
@@ -2308,12 +2581,15 @@ export async function authorClose({
|
|
|
2308
2581
|
maxRevisions = MAX_REVISIONS,
|
|
2309
2582
|
structureRetries = MAX_STRUCTURE_RETRIES,
|
|
2310
2583
|
structuredMode = 'tool',
|
|
2311
|
-
catalogue = KIND_CATALOGUE,
|
|
2584
|
+
catalogue: catalogueIn = KIND_CATALOGUE,
|
|
2312
2585
|
// P7 item 5 (fwdloop am25): OFF unless the caller can really ask the person (the panel). When on, ONLY the last of the
|
|
2313
2586
|
// full drafting ladder (the author call plus both revisions), and only after the two before it both failed validation,
|
|
2314
2587
|
// is offered the optional `questions` property — see `askOpen` below. Absent = byte-identical to today.
|
|
2315
2588
|
offerQuestions = false,
|
|
2316
2589
|
}) {
|
|
2590
|
+
// PART 1a: the menu this close may draw from — a rubric job on local files sees its own two kinds, a code job never sees
|
|
2591
|
+
// the rubric-only ones (`catalogueForLang`). Everything below (prompt, schema, validator, env) reads THIS one catalogue.
|
|
2592
|
+
const catalogue = catalogueForLang(lang, catalogueIn);
|
|
2317
2593
|
// TIGHTEN-ONLY: a caller may lower the ceiling, never raise it — unlimited
|
|
2318
2594
|
// revising launders thrash as adaptation (addendum 5). The floor is zero: one
|
|
2319
2595
|
// authoring call with no revise round is a legal ask, and a garbage value is
|
|
@@ -2431,7 +2707,7 @@ export async function authorClose({
|
|
|
2431
2707
|
}], 'precheck');
|
|
2432
2708
|
}
|
|
2433
2709
|
|
|
2434
|
-
if (!
|
|
2710
|
+
if (!DECLARABLE_LANGUAGES.includes(lang)) {
|
|
2435
2711
|
return refuse([{
|
|
2436
2712
|
code: 'genre-unsupported',
|
|
2437
2713
|
path: 'lang',
|
|
@@ -2473,9 +2749,20 @@ export async function authorClose({
|
|
|
2473
2749
|
if (envProbe.reds.length) return refuse(envProbe.reds, 'precheck');
|
|
2474
2750
|
|
|
2475
2751
|
// ── the grounded loop ─────────────────────────────────────────────────────
|
|
2752
|
+
// PART 1c: a rubric job on files - the frozen input text the found-once check proves every word against, and the label lists code
|
|
2753
|
+
// made from it (a label is PICKED from them, never typed). null = not a files job, or the freeze could not be read here.
|
|
2754
|
+
/** @type {Record<string, string>|null} */
|
|
2755
|
+
let inputTexts = null;
|
|
2756
|
+
/** @type {string[]|null} */
|
|
2757
|
+
let labelLists = null;
|
|
2758
|
+
if (lang === RUBRIC_LANG && Array.isArray(inputs) && inputs.length > 0) {
|
|
2759
|
+
const frozen = await readFrozenInputs(workdir, inputs.map((i) => i.label));
|
|
2760
|
+
if (frozen.ok) { inputTexts = frozen.texts; labelLists = Object.values(labelListsOf(frozen.texts).lists); }
|
|
2761
|
+
}
|
|
2476
2762
|
const prompt = authorPrompt({
|
|
2477
2763
|
answers, questions, facts, listingBlock: /** @type {string} */ (seeds.block),
|
|
2478
2764
|
lang, verdictType, guards, ownedEnvNames, mode: structuredMode, catalogue, writeScope, confirmed, jobLines, inputs,
|
|
2765
|
+
labelLists,
|
|
2479
2766
|
});
|
|
2480
2767
|
/** @type {any[]} */
|
|
2481
2768
|
let messages = [{ role: 'user', content: prompt }];
|
|
@@ -2562,7 +2849,7 @@ export async function authorClose({
|
|
|
2562
2849
|
// P7 item 5: a mechanism, never a wish. Only the last try of the full ladder carries the property, and only when the
|
|
2563
2850
|
// two before it both failed (validation or the seed read; `questionLines` is set at the end of the second one, below).
|
|
2564
2851
|
const askOpen = questionLines !== null && i === revisionCap;
|
|
2565
|
-
const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue, verdictType, questions: askOpen });
|
|
2852
|
+
const ask = await askDeclaration({ messages, generate, mode: structuredMode, retries: retryCap, label, book, catalogue: catalogueIn, kinds: Object.keys(catalogue), verdictType, questions: askOpen });
|
|
2566
2853
|
messages = ask.convo;
|
|
2567
2854
|
/** @type {{reason: string, raw: any}[]} */
|
|
2568
2855
|
let droppedQuestions = [];
|
|
@@ -2624,6 +2911,7 @@ export async function authorClose({
|
|
|
2624
2911
|
iter.cutOff = true;
|
|
2625
2912
|
if (nextTryMayAsk(i)) {
|
|
2626
2913
|
questionLines = allLines.length ? allLines : null;
|
|
2914
|
+
onPhase('author-try-failed', { call: label, i, of: revisionCap, reds: [scrubRed(/** @type {Red} */ (ask.red))] });
|
|
2627
2915
|
const block = renderRejectBlock({ kind: 'artifact', reds: [/** @type {Red} */ (ask.red).detail] });
|
|
2628
2916
|
messages = [...ask.convo, { role: 'assistant', content: '(the reply carried no text)' },
|
|
2629
2917
|
{ role: 'user', content: `${block}\n\n${structuredMode === 'tool' ? STRUCTURE_INSTRUCTION_TOOL : STRUCTURE_INSTRUCTION_TEXT}` }];
|
|
@@ -2652,7 +2940,15 @@ export async function authorClose({
|
|
|
2652
2940
|
}
|
|
2653
2941
|
previous = ask.declaration;
|
|
2654
2942
|
|
|
2655
|
-
|
|
2943
|
+
// PART 1b: code puts the number tie on every prose step that follows a fact step, before anything is validated or signed
|
|
2944
|
+
let v = validateDeclaration(withFactBindings(settlePersonsWords(ask.declaration, jobLines)), { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope, jobLines, inputLabels: inputLabelsOf(inputs) });
|
|
2945
|
+
// PART 1c: code proves, at $0, that every label is PICKED from the input's own list and every word a fact depends on is really
|
|
2946
|
+
// in the source (white-space blind, once is enough). A word that is not found is a red naming the word and the input; the
|
|
2947
|
+
// drafter revises like it does for any validation red.
|
|
2948
|
+
if (v.ok && lang === RUBRIC_LANG) {
|
|
2949
|
+
const words = checkDeclarationWords(v.declaration, inputTexts);
|
|
2950
|
+
if (words.length > 0) v = { ...v, ok: false, reds: [...v.reds, ...words], declaration: null };
|
|
2951
|
+
}
|
|
2656
2952
|
// SCRUBBED HERE, once, where the reds enter this module's records — the same
|
|
2657
2953
|
// boundary rule `renderRejectBlock` already states, applied to the OTHER
|
|
2658
2954
|
// channel they travel down. A validation red quotes what the model declared
|
|
@@ -2673,6 +2969,8 @@ export async function authorClose({
|
|
|
2673
2969
|
// same channel and counts as a revision (addendum 5)
|
|
2674
2970
|
measured = renderRejectBlock({ kind: 'validation', reds: vReds });
|
|
2675
2971
|
tryFails[i] = { lines: linesNamedByReds(vReds, ask.declaration, (jobLines ?? []).map((l) => l.n)) };
|
|
2972
|
+
// WHY this try was refused, for the drafting log and the progress line (the reds are already scrubbed)
|
|
2973
|
+
onPhase('author-try-failed', { call: label, i, of: revisionCap, reds: vReds, declaration: ask.declaration });
|
|
2676
2974
|
} else {
|
|
2677
2975
|
// what gets MEASURED is what will RUN: the resolved declaration with the
|
|
2678
2976
|
// genre's environment already injected
|
|
@@ -2703,6 +3001,16 @@ export async function authorClose({
|
|
|
2703
3001
|
measuredIterations.push({ label, declaration: injected.declaration, seedRead: rows, droppedEnv: injected.dropped });
|
|
2704
3002
|
const sf = seedReadFailure(rows, injected.declaration, guards.map((/** @type {any} */ g) => g.name), (jobLines ?? []).map((l) => l.n));
|
|
2705
3003
|
tryFails[i] = sf.failed ? { lines: sf.lines } : null;
|
|
3004
|
+
if (sf.failed) {
|
|
3005
|
+
const guardNames = guards.map((/** @type {any} */ g) => g.name);
|
|
3006
|
+
const stoppedNames = [...new Set(rows.filter((/** @type {any} */ r) => !guardNames.includes(r?.stage) && r?.verdict === 'instrument-stop').map((/** @type {any} */ r) => String(r.stage)))];
|
|
3007
|
+
onPhase('author-try-failed', {
|
|
3008
|
+
call: label, i, of: revisionCap, declaration: ask.declaration,
|
|
3009
|
+
reason: stoppedNames.length
|
|
3010
|
+
? `a check could not run at the starting point (${stoppedNames.join(', ')})`
|
|
3011
|
+
: 'no check was failing at the starting point, so none of them could tell a finished job from an unfinished one',
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
2706
3014
|
}
|
|
2707
3015
|
|
|
2708
3016
|
if (i === revisionCap) { stop = 'max-revisions'; break; }
|