bareloop 0.42.0 → 0.44.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 +88 -0
- package/bareloop.context.md +103 -41
- package/package.json +2 -2
- package/src/authorflow.js +26 -11
- package/src/authoring.js +1 -1
- package/src/authorjob.js +33 -15
- package/src/authorreadout.js +7 -6
- package/src/declaredclose.js +6 -3
- package/src/draftspend.js +20 -2
- package/src/facts.js +54 -9
- package/src/factwords.js +58 -6
- package/src/inputlook.js +38 -3
- package/src/interviewrun.js +0 -1
- package/src/job.js +39 -12
- package/src/kinds.js +5 -0
- package/src/pagedropwords.js +64 -0
- package/src/pagemap.js +264 -0
- package/src/pagerules.js +319 -0
- package/src/panel/authorroutes.js +138 -60
- package/src/panel/authorsession.js +88 -32
- package/src/panel/index.html +1251 -522
- package/src/panel/jobplan.js +4 -2
- package/src/panel/runroutes.js +188 -6
- package/src/panel/scheduler.js +156 -0
- package/src/panel/schedules.js +667 -0
- package/src/panel/schedulestart.js +243 -0
- package/src/panel/server.js +1234 -68
- package/src/panel/status.js +33 -0
- package/src/panelmoney.js +29 -0
- package/src/planrun.js +92 -11
- package/src/promptregisters.js +4 -0
- package/src/providerrows.js +3 -3
- package/src/replay.js +118 -7
- package/src/reviewdoor.js +37 -0
- package/src/rubricstep.js +123 -11
- package/src/runlist.js +37 -1
- package/src/schedulewords.js +299 -0
- package/src/source.js +18 -0
- package/src/thinking.js +63 -2
- package/src/trigger.js +48 -0
- package/src/triggerclock.js +127 -0
- package/src/triggerfiles.js +215 -0
- package/src/triggerledger.js +79 -0
- package/src/userrun.js +34 -21
- package/types/authorflow.d.ts +42 -6
- package/types/authorjob.d.ts +10 -2
- package/types/declaredclose.d.ts +3 -1
- package/types/draftspend.d.ts +27 -1
- package/types/facts.d.ts +15 -0
- package/types/factwords.d.ts +26 -3
- package/types/inputlook.d.ts +15 -0
- package/types/job.d.ts +8 -1
- package/types/kinds.d.ts +5 -0
- package/types/pagedropwords.d.ts +26 -0
- package/types/pagemap.d.ts +154 -0
- package/types/pagerules.d.ts +86 -0
- package/types/panel/authorroutes.d.ts +50 -14
- package/types/panel/authorsession.d.ts +37 -12
- package/types/panel/runroutes.d.ts +32 -1
- package/types/panel/scheduler.d.ts +17 -0
- package/types/panel/schedules.d.ts +412 -0
- package/types/panel/schedulestart.d.ts +106 -0
- package/types/panel/server.d.ts +297 -15
- package/types/panel/status.d.ts +51 -0
- package/types/panelmoney.d.ts +5 -0
- package/types/planrun.d.ts +2 -0
- package/types/providerrows.d.ts +2 -2
- package/types/replay.d.ts +21 -1
- package/types/reviewdoor.d.ts +27 -0
- package/types/rubricstep.d.ts +57 -1
- package/types/runlist.d.ts +24 -1
- package/types/schedulewords.d.ts +137 -0
- package/types/source.d.ts +11 -0
- package/types/thinking.d.ts +39 -0
- package/types/trigger.d.ts +37 -0
- package/types/triggerclock.d.ts +52 -0
- package/types/triggerfiles.d.ts +93 -0
- package/types/triggerledger.d.ts +35 -0
- package/types/userrun.d.ts +4 -0
package/src/authorflow.js
CHANGED
|
@@ -87,7 +87,8 @@ import {
|
|
|
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';
|
|
90
|
+
import { stepFileContract, inputLabelsOf, stepFileRel, readFrozenInputs, withFactBindings, settlePersonsWords, withDropNotes, lineRules } from './rubricstep.js';
|
|
91
|
+
import { dropRows, dropLine } from './pagedropwords.js';
|
|
91
92
|
import { factShapeGuide } from './facts.js';
|
|
92
93
|
import { renderLookReport, labelListsOf } from './inputlook.js';
|
|
93
94
|
import { checkDeclarationWords } from './factwords.js';
|
|
@@ -116,8 +117,8 @@ export const AUTHOR_MAX_TOKENS = 32000;
|
|
|
116
117
|
/** the one slot the model fills on an injected guard */
|
|
117
118
|
export const FILL_MARKER = '<FILL IN>';
|
|
118
119
|
|
|
119
|
-
/** P7 item 5 (fwdloop am24/25): the most questions the MACHINE keeps from the try-3 drafter. The drafter is never told this number. */
|
|
120
|
-
export const MAX_QUESTIONS =
|
|
120
|
+
/** P7 item 5 (fwdloop am24/25): the most questions the MACHINE keeps from the try-3 drafter. The drafter is never told this number. PRD v1.107 Part 5b: three (hamr's ruling), shared with the code-forced early asks. */
|
|
121
|
+
export const MAX_QUESTIONS = 3;
|
|
121
122
|
|
|
122
123
|
/**
|
|
123
124
|
* The model-visible words of the optional `questions` property — offered ONLY on the last drafting try and only after the
|
|
@@ -158,10 +159,10 @@ export function linesNamedByReds(reds, declaration, lineNums) {
|
|
|
158
159
|
* The machine's keep-or-drop of the try-3 questions. Kept only when its line is one the previous try's reds named and its
|
|
159
160
|
* text is non-blank, at most {@link MAX_QUESTIONS}; every drop comes back with its reason, for the draft's log.
|
|
160
161
|
* @param {any} raw @param {number[]} failedLines
|
|
161
|
-
* @returns {{kept: {line: number, question: string}[], dropped: {reason: string, raw: any}[]}}
|
|
162
|
+
* @returns {{kept: {line: number, question: string, rule?: number, source?: string, ask?: 'reading'|'cant-check', reading?: string}[], dropped: {reason: string, raw: any}[]}}
|
|
162
163
|
*/
|
|
163
164
|
export function normalizeQuestions(raw, failedLines) {
|
|
164
|
-
/** @type {{line: number, question: string}[]} */
|
|
165
|
+
/** @type {{line: number, question: string, rule?: number, source?: string, ask?: 'reading'|'cant-check', reading?: string}[]} */
|
|
165
166
|
const kept = [];
|
|
166
167
|
/** @type {{reason: string, raw: any}[]} */
|
|
167
168
|
const dropped = [];
|
|
@@ -172,7 +173,13 @@ export function normalizeQuestions(raw, failedLines) {
|
|
|
172
173
|
if (!failedLines.includes(q.line)) { dropped.push({ reason: `line ${q.line} is not a line the previous try's checks failed on`, raw: q }); continue; }
|
|
173
174
|
if (typeof q.question !== 'string' || q.question.trim() === '') { dropped.push({ reason: 'blank question', raw: q }); continue; }
|
|
174
175
|
if (kept.length >= MAX_QUESTIONS) { dropped.push({ reason: 'beyond the machine\'s limit', raw: q }); continue; }
|
|
175
|
-
kept.push({
|
|
176
|
+
kept.push({
|
|
177
|
+
line: q.line, question: redactSecrets(q.question.trim()),
|
|
178
|
+
// PRD v1.106 Part 5 Q3: a code-forced question (the early look ask) names the `~` rule it is about and says so; a model's question has neither
|
|
179
|
+
...(Number.isInteger(q.rule) ? { rule: q.rule } : {}), ...(typeof q.source === 'string' ? { source: q.source } : {}),
|
|
180
|
+
// PRD v1.107 Part 5b: the kind of a code-forced ask, and (reading asks) the plain words `agree` writes as the `~` rule
|
|
181
|
+
...(q.ask === 'reading' || q.ask === 'cant-check' ? { ask: q.ask } : {}), ...(q.ask === 'reading' && typeof q.reading === 'string' ? { reading: redactSecrets(q.reading) } : {}),
|
|
182
|
+
});
|
|
176
183
|
}
|
|
177
184
|
return { kept, dropped };
|
|
178
185
|
}
|
|
@@ -838,8 +845,8 @@ export const PARAM_SCHEMAS = Object.freeze({
|
|
|
838
845
|
properties: {
|
|
839
846
|
name: { type: 'string', minLength: 1, description: 'what the fact is called: letters and spaces only, no digits' },
|
|
840
847
|
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
|
|
842
|
-
currency: { enum: ['$', '€', '£'], description: 'money: the symbol the input writes before the amount
|
|
848
|
+
unit: { anyOf: [{ type: 'string', minLength: 1 }, { type: 'array', minItems: 1, maxItems: 10, uniqueItems: true, items: { type: 'string', minLength: 1 } }], description: 'money: the words the input writes after the amount, copied verbatim - one, or a list of up to 10 (give unit, currency, or both)' },
|
|
849
|
+
currency: { anyOf: [{ enum: ['$', '€', '£'] }, { type: 'array', minItems: 1, maxItems: 3, uniqueItems: true, items: { enum: ['$', '€', '£'] } }], description: 'money: the symbol the input writes before the amount - one, or a list of up to 3 (give unit, currency, or both)' },
|
|
843
850
|
phrase: { type: 'string', minLength: 1, description: 'phrase only: the fixed words, copied verbatim from the input' },
|
|
844
851
|
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
852
|
until: { type: 'string', minLength: 1, description: 'text with an anchor: the label copied VERBATIM that ends the value' },
|
|
@@ -1433,6 +1440,11 @@ export const REQUIRED_FACTS_BRIEF = 'REQUIRED FACTS — what a line that finds,
|
|
|
1433
1440
|
+ '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
1441
|
+ '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
1442
|
+ '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'
|
|
1443
|
+
+ ' - A money fact names at least one of "unit" (the words the input writes after the amount) or "currency" (a sign: $, € or £), and may name both. Each is one value or a list: '
|
|
1444
|
+
+ 'at most 3 currencies and 10 units, none repeated, a unit at most 100 characters. A code like EUR is a unit (it is read after the number), never a currency. '
|
|
1445
|
+
+ 'Copy the forms the look gives for the fact, in its order (the first is the main one); a form the look does not give is refused at $0.\n'
|
|
1446
|
+
+ ' - A word the look lists under NOT CHECKED or LEFT OUT is never used in a fact - not as a label, a closing label, a unit or a currency - and is refused at $0 with the reason if you do. '
|
|
1447
|
+
+ 'A fact or ~ rule listed under NOT CHECKED is not turned into a check: code lists it under Not checked for the person, so you do not write it into "refused" yourself.\n'
|
|
1436
1448
|
+ ' - 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
1449
|
+ ' - Set aiProposal true on a fact whose number you made out of their words.\n'
|
|
1438
1450
|
+ RUBRIC_RULES + '\n'
|
|
@@ -2378,7 +2390,7 @@ export async function runConfirmTurn({
|
|
|
2378
2390
|
for (let i = 0; i < qs.length; i += 1) {
|
|
2379
2391
|
const question = qs[i];
|
|
2380
2392
|
for (;;) {
|
|
2381
|
-
const raw = await ask({ kind: 'answer', field: ANSWER_QUESTION_FIELD, question, index: i + 1, total: qs.length });
|
|
2393
|
+
const raw = await ask({ kind: 'answer', source: 'confirm', field: ANSWER_QUESTION_FIELD, question, index: i + 1, total: qs.length });
|
|
2382
2394
|
if (raw === null) return null;
|
|
2383
2395
|
const trimmed = redactSecrets(String(raw).trim());
|
|
2384
2396
|
if (trimmed === '') continue; // blank is not an answer — re-ask the same question
|
|
@@ -2426,7 +2438,10 @@ export async function runConfirmTurn({
|
|
|
2426
2438
|
// the list the model was prompted with is byte-identical to the list
|
|
2427
2439
|
// shown at confirm-done and recorded on acceptance. `notChecked` is the
|
|
2428
2440
|
// model's own honest gap list, carried through unchanged.
|
|
2441
|
+
// NO SILENT DROP (idea A core, spec 4.5): what the page rules removed is said HERE too, in the same words as the signed plan, before
|
|
2442
|
+
// drafting is paid for. The words come from the one table (src/pagedropwords.js).
|
|
2429
2443
|
const notChecked = [...(r.plan.notChecked ?? [])];
|
|
2444
|
+
for (const row of dropRows(facts?.look?.dropped)) { const text = dropLine(row, jobLines, lineRules); if (!notChecked.includes(text)) notChecked.push(text); }
|
|
2430
2445
|
const checkItems = normalizeChecks(r.plan.checks);
|
|
2431
2446
|
r.plan = { ...r.plan, checks: checkItems.map((c) => c.text), checkItems };
|
|
2432
2447
|
const plan = { ...r.plan, protections, notChecked };
|
|
@@ -2941,12 +2956,12 @@ export async function authorClose({
|
|
|
2941
2956
|
previous = ask.declaration;
|
|
2942
2957
|
|
|
2943
2958
|
// 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) });
|
|
2959
|
+
let v = validateDeclaration(withFactBindings(withDropNotes(settlePersonsWords(ask.declaration, jobLines), facts?.look?.dropped, jobLines)), { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope, jobLines, inputLabels: inputLabelsOf(inputs) });
|
|
2945
2960
|
// 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
2961
|
// 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
2962
|
// drafter revises like it does for any validation red.
|
|
2948
2963
|
if (v.ok && lang === RUBRIC_LANG) {
|
|
2949
|
-
const words = checkDeclarationWords(v.declaration, inputTexts);
|
|
2964
|
+
const words = checkDeclarationWords(v.declaration, inputTexts, facts?.look ?? null);
|
|
2950
2965
|
if (words.length > 0) v = { ...v, ok: false, reds: [...v.reds, ...words], declaration: null };
|
|
2951
2966
|
}
|
|
2952
2967
|
// SCRUBBED HERE, once, where the reds enter this module's records — the same
|
package/src/authoring.js
CHANGED
|
@@ -355,7 +355,7 @@ export const KIND_CATALOGUE = Object.freeze({
|
|
|
355
355
|
shape: 'step: integer (the job line number), parts: [{text: string, class: "countable" | "fact" | "judged" | "guard" | "none", '
|
|
356
356
|
+ 'check?: "maxWords" | "minWords" | "maxLines" | "sections" | "sectionWords" | "cited" | "minClaims" | "maxClaims", value?: integer, names?: string[], '
|
|
357
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
|
|
358
|
+
+ 'fact parts: facts?: [{name, shape: "money"|"number"|"date"|"time"|"phrase"|"text", unit?|currency? (each one value or a list; money names at least one), phrase?, anchor?, until?, '
|
|
359
359
|
+ 'rule?: {op, value | lo+hi}, aiProposal?: boolean}], count?: {min?, max?}}]',
|
|
360
360
|
asserts: 'the file output/<step>.json under output/steps/ exists, is a JSON object, and its "text" is present and within the '
|
|
361
361
|
+ 'limits the person asked for. Write ONE rubric-step stage per numbered job line (step = that line number, fromLine = [that line]). '
|
package/src/authorjob.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
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';
|
|
103
|
+
import { inputLabelsOf, readFrozenInputs, lineRules } from './rubricstep.js';
|
|
104
104
|
import { proverOf } from './factwords.js';
|
|
105
105
|
// SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
|
|
106
106
|
// and the gate that decides whether the ruler they describe is a ruler.
|
|
@@ -113,12 +113,12 @@ import {
|
|
|
113
113
|
} from './authoring.js';
|
|
114
114
|
import {
|
|
115
115
|
QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook,
|
|
116
|
-
runConfirmTurn, LANGUAGE_PICK_FIELD, normalizeQuestions,
|
|
116
|
+
runConfirmTurn, LANGUAGE_PICK_FIELD, normalizeQuestions, MAX_QUESTIONS,
|
|
117
117
|
} from './authorflow.js';
|
|
118
|
-
import { ruleQuestions } from './inputlook.js';
|
|
119
118
|
import { thinkingCallOptions, assertThinkingMode } from './thinking.js';
|
|
120
119
|
import { pageContentChars } from './pagefetch.js';
|
|
121
|
-
import { runAuthorScout,
|
|
120
|
+
import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
|
|
121
|
+
import { runPageMap, askableFromLook, tooManyAsksText } from './pagemap.js';
|
|
122
122
|
import {
|
|
123
123
|
DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
|
|
124
124
|
} from './declaredclose.js';
|
|
@@ -433,8 +433,8 @@ function composerRefusal(reds) {
|
|
|
433
433
|
* authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
|
|
434
434
|
* signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
|
|
435
435
|
* ask?: ((step: {kind: string, [k: string]: any}) => Promise<string|null>)|null,
|
|
436
|
-
* answerQuestions?: ((o: {questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
|
|
437
|
-
* Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any
|
|
436
|
+
* answerQuestions?: ((o: {questions: {line: number, question: string, rule?: number, source?: string, ask?: string, reading?: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
|
|
437
|
+
* Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any>, left?: {line: number, rule: number}[]}|null>)|null,
|
|
438
438
|
* confirmGenerate?: Function|null, isRepo?: boolean,
|
|
439
439
|
* langResult?: {kind: string, candidates?: string[], [k: string]: any}|null}} o
|
|
440
440
|
* @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
|
|
@@ -470,9 +470,9 @@ export async function authorCloseForJob({
|
|
|
470
470
|
// callback back, and no decision in this file or below it depends on one.
|
|
471
471
|
onPhase = () => {}, onCall = () => {},
|
|
472
472
|
seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
|
|
473
|
-
// PART 1c - the look at a rubric job's inputs
|
|
474
|
-
//
|
|
475
|
-
lookFn =
|
|
473
|
+
// PART 1c / IDEA A CORE - the look at a rubric job's inputs: the default is the page-word map (src/pagemap.js `runPageMap`, one paid AI
|
|
474
|
+
// call per page input, its words kept by code). `null` is an explicit "no look" (a test seam, like a caller-supplied `scout`).
|
|
475
|
+
lookFn = runPageMap,
|
|
476
476
|
authorFn = authorClose, authorOpts = {},
|
|
477
477
|
// SOFTGREEN modules 4+5. `signerFix` is the D5 seam: a function (or a value)
|
|
478
478
|
// carrying the SIGNER's corrected artifacts back. Absent means the proposal is
|
|
@@ -645,12 +645,18 @@ export async function authorCloseForJob({
|
|
|
645
645
|
onPhase('scout-done', { state: survey?.state ?? null, facts: Object.keys(survey?.facts ?? {}).length });
|
|
646
646
|
}
|
|
647
647
|
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
648
|
+
// PRD v1.106 Part 5 Q3 (T2) + v1.107 Part 5b: CODE forces the early asks, from the look's own result, before any plan or draft is paid for
|
|
649
|
+
// (src/pagemap.js `askableFromLook`; the model never decides whether to ask). Two kinds: a READING the code made of a `~` rule whose limit the
|
|
650
|
+
// person did not write (agree / type their own), and a rule the page cannot check (drop it / type how). More than MAX_QUESTIONS asks is a job to
|
|
651
|
+
// rewrite, not a questionnaire: nothing is asked, nothing more is spent, and the stop lists every ask with its reading. The question gate is the
|
|
652
|
+
// existing one (`answerQuestions`: answer required, no skip, the answer becomes a `~` rule on its line; `normalizeQuestions` keeps at most
|
|
653
|
+
// MAX_QUESTIONS). The answer "drop it" adds no rule: the rule is put on the look's own dropped list, so it reads Not checked on the
|
|
654
|
+
// confirm card, in the signed plan and in the drafter's block, and can never be written back as a check.
|
|
655
|
+
if (answerQuestions && lang === RUBRIC_LANG && survey?.state === 'PRESENT' && survey.facts?.look && typeof survey.facts.look === 'object' && Array.isArray(jobLines)) {
|
|
656
|
+
const candidates = askableFromLook(survey.facts.look, jobLines);
|
|
657
|
+
if (candidates.length > MAX_QUESTIONS) {
|
|
658
|
+
return { ...base, interview, verdictType: picked, seedRef: seed, reds: [{ code: 'look-asks-too-many', path: 'jobLines', detail: tooManyAsksText(candidates) }], stop: 'look-asks-too-many', cost: null };
|
|
659
|
+
}
|
|
654
660
|
const kept = normalizeQuestions(candidates, [...new Set(candidates.map((q) => q.line))]);
|
|
655
661
|
if (kept.kept.length > 0) {
|
|
656
662
|
onPhase('look-ask', { questions: kept.kept.length });
|
|
@@ -658,6 +664,12 @@ export async function authorCloseForJob({
|
|
|
658
664
|
if (updated === null) return { ...base, interview, verdictType: picked, seedRef: seed, reds: [], stop: 'confirm-abandoned', cost: null };
|
|
659
665
|
jobLines = updated.jobLines;
|
|
660
666
|
interview.answers = updated.answers;
|
|
667
|
+
for (const lf of Array.isArray(updated.left) ? updated.left : []) {
|
|
668
|
+
const ruleText = lineRules(jobLines.find((l) => l.n === lf.line) ?? {})[lf.rule - 1];
|
|
669
|
+
if (ruleText === undefined) continue;
|
|
670
|
+
const look = survey.facts.look;
|
|
671
|
+
look.dropped = [...(Array.isArray(look.dropped) ? look.dropped : []), { line: lf.line, rule: lf.rule, fact: ruleText, reason: 'person-left', source: 'person' }];
|
|
672
|
+
}
|
|
661
673
|
}
|
|
662
674
|
}
|
|
663
675
|
|
|
@@ -1026,6 +1038,12 @@ export function assembleSpec(specDraft, { closeDecl, verdictType }) {
|
|
|
1026
1038
|
+ 'this fold writes it. Merging would drop or overwrite what the draft says, and a signed spec that does not '
|
|
1027
1039
|
+ 'contain what its author typed is the one failure nobody can see. Remove the field(s) from the draft');
|
|
1028
1040
|
}
|
|
1041
|
+
// T (PRD v1.99, the agent never sets a trigger): a schedule is the person's own two-click act in the panel, never a
|
|
1042
|
+
// draft field. Refused by name, like the authored half.
|
|
1043
|
+
if (draft.trigger !== undefined) {
|
|
1044
|
+
throw new Error('the spec draft carries trigger — a schedule is set only by the person, in the panel\'s two-click schedule '
|
|
1045
|
+
+ 'door, never by a draft. Remove the field from the draft');
|
|
1046
|
+
}
|
|
1029
1047
|
return { ...draft, verdictType, closeDecl };
|
|
1030
1048
|
}
|
|
1031
1049
|
|
package/src/authorreadout.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// not because they are readouts: the runner is a script, and a rule no test can
|
|
25
25
|
// reach is a rule nothing checks.
|
|
26
26
|
import { scrubRaw } from './text.js';
|
|
27
|
+
import { panelMoney2 } from './panelmoney.js';
|
|
27
28
|
import { redactSecrets } from './validate.js';
|
|
28
29
|
import { judgedStages } from './kinds.js';
|
|
29
30
|
import { CALIBRATION_SIZE } from './judged.js';
|
|
@@ -560,11 +561,6 @@ export function plainReasons(reds, { cases = null } = {}) {
|
|
|
560
561
|
});
|
|
561
562
|
}
|
|
562
563
|
|
|
563
|
-
/** @param {number} n @returns {string} the panel's money spelling: 2 decimals, "<$0.01" under a cent */
|
|
564
|
-
function moneyText(n) {
|
|
565
|
-
return n > 0 && n < 0.005 ? '<$0.01' : `$${n.toFixed(2)}`;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
564
|
/**
|
|
569
565
|
* The money clause: the measured figure, "at least" when a call was unpriced,
|
|
570
566
|
* and `null` when there is no honest figure (the caller then says unknown or
|
|
@@ -574,7 +570,7 @@ function moneyText(n) {
|
|
|
574
570
|
*/
|
|
575
571
|
function spendText(spend) {
|
|
576
572
|
if (!spend || typeof spend.knownUsd !== 'number' || !Number.isFinite(spend.knownUsd)) return null;
|
|
577
|
-
return `${spend.spendComplete === false ? 'at least ' : ''}${
|
|
573
|
+
return `${spend.spendComplete === false ? 'at least ' : ''}${panelMoney2(spend.knownUsd)}`;
|
|
578
574
|
}
|
|
579
575
|
|
|
580
576
|
/**
|
|
@@ -662,6 +658,11 @@ const SCOUT_REASON_RE = /^the survey is [^(]*\((.*)\) — an absent facts/s;
|
|
|
662
658
|
*/
|
|
663
659
|
export function draftStopText({ stop, reds = null, refusal = null, spend = null, wholeJob = false }) {
|
|
664
660
|
const spent = spendText(spend);
|
|
661
|
+
// PRD v1.107 Part 5b: too many early asks - the code-owned list (every ask and its reading) is the whole message, quoted in full
|
|
662
|
+
if (stop === 'look-asks-too-many') {
|
|
663
|
+
const detail = (Array.isArray(reds) ? reds : []).find((r) => r?.code === 'look-asks-too-many')?.detail;
|
|
664
|
+
if (detail) return `${redactSecrets(String(detail))}\nSpent so far: ${spent ?? 'unknown'}.`;
|
|
665
|
+
}
|
|
665
666
|
/** @type {Record<string, string>} */
|
|
666
667
|
const heads = {
|
|
667
668
|
'provider-red': 'The AI provider failed before a usable plan came back — a connection or service fault, not a problem with your job.',
|
package/src/declaredclose.js
CHANGED
|
@@ -107,7 +107,7 @@ export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'note
|
|
|
107
107
|
/** what a stored calibration set carries, ENUMERATED: the frozen cases and the judge
|
|
108
108
|
* tier that graded them. Both are inside the spec, so `jobSpecHash` covers both — which
|
|
109
109
|
* is the whole mechanism behind "a judge-model bump forces a full recalibration"
|
|
110
|
-
* (src/judged.js, `
|
|
110
|
+
* (src/judged.js, `resolveJudge`): the field moves, the hash moves, the signature dies,
|
|
111
111
|
* and the re-sign re-runs the gate. Anything else in there is a field nobody reads. */
|
|
112
112
|
export const CALIBRATION_FIELDS = Object.freeze(['cases', 'judgeModel']);
|
|
113
113
|
|
|
@@ -447,7 +447,7 @@ export function validateCloseDecl(closeDecl, opts = {}) {
|
|
|
447
447
|
// WHICH TIER GRADED THIS SET, and it is REQUIRED rather than optional. A
|
|
448
448
|
// stored set is only worth the judge that graded it (§4.2's safety argument
|
|
449
449
|
// is BA-20's haiku-4.5 injection evidence and nothing else), so a set with
|
|
450
|
-
// no model beside it is a set nobody can attribute — and the `
|
|
450
|
+
// no model beside it is a set nobody can attribute — and the `CALIBRATION_FIELDS`
|
|
451
451
|
// docstring's promise that a bump forces recalibration has nothing to fire
|
|
452
452
|
// on. Required HERE and not at the stage, for the same reason the SET's own
|
|
453
453
|
// presence is the signing gate's call and not this validator's: this gate
|
|
@@ -576,13 +576,14 @@ function translate(r, redact) {
|
|
|
576
576
|
* judgeLoop?: ((o: {system: string}) => any)|null,
|
|
577
577
|
* judgeModel?: string|null,
|
|
578
578
|
* onJudgeCost?: ((c: any) => void)|null,
|
|
579
|
+
* onRubricEvidence?: ((e: any) => void)|null,
|
|
579
580
|
* callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
|
|
580
581
|
* @returns {Promise<any>}
|
|
581
582
|
*/
|
|
582
583
|
export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
583
584
|
const {
|
|
584
585
|
timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
|
|
585
|
-
judgeLoop = null, judgeModel = null, onJudgeCost = null, callBounds = null,
|
|
586
|
+
judgeLoop = null, judgeModel = null, onJudgeCost = null, callBounds = null, onRubricEvidence = null,
|
|
586
587
|
} = opts;
|
|
587
588
|
if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
|
|
588
589
|
// Refuse rather than guess. A declared close measures against a SEED; a
|
|
@@ -634,6 +635,8 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
|
|
|
634
635
|
// the honest absence (an adopter/test that wires no clock through), never a
|
|
635
636
|
// defaulted number invented here.
|
|
636
637
|
callBounds,
|
|
638
|
+
// REPORT-ONLY seam: what a rubric-step stage's cited checks read (bareguard `full.evidence`); the runner records it, nothing here reads it
|
|
639
|
+
onRubricEvidence,
|
|
637
640
|
seedTrees,
|
|
638
641
|
};
|
|
639
642
|
try {
|
package/src/draftspend.js
CHANGED
|
@@ -25,6 +25,8 @@ export const DRAFT_LOG_FILE = 'draft-log.jsonl';
|
|
|
25
25
|
* - `step-start` {no, id, label, at}
|
|
26
26
|
* - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
|
|
27
27
|
* - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway" / "can't change it" / "not confirmed")
|
|
28
|
+
* - `human` {no, source: 'confirm'|'try-3'|'look', question, answer, line?, at}: one question the person was asked and what they answered (the answer is secret-masked
|
|
29
|
+
* before it is written; the row is written only when BOTH exist, so a question nobody answered leaves no row)
|
|
28
30
|
* - `call` {no, step, label, model, thinking, thinkingSupport, thinkingStatus?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
|
|
29
31
|
* (`thinking` = the tick's mode: off | on; `thinkingSupport` = bare-agent's lookup for the model: true | false | 'unverified';
|
|
30
32
|
* `thinkingStatus` = bare-agent's own verdict for the call's run: applied | unsupported | ignored | unknown, present only when thinkingMode was sent)
|
|
@@ -126,12 +128,26 @@ export function sessionDirForRow(row, opts = {}) {
|
|
|
126
128
|
return null;
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
/**
|
|
132
|
+
* PRD v1.106 Part 5 Q1: one answered question as the two lines the Audit's drafting table shows under its step. ONE place owns the words.
|
|
133
|
+
* @param {{question: string, answer: string, source?: string, line?: number, at?: string}} h
|
|
134
|
+
* @returns {{question: string, answer: string, source: string, line?: number, at?: string, askedText: string, saidText: string}}
|
|
135
|
+
*/
|
|
136
|
+
export function humanRowWords(h) {
|
|
137
|
+
return {
|
|
138
|
+
question: h.question, answer: h.answer, source: typeof h.source === 'string' ? h.source : 'confirm',
|
|
139
|
+
...(Number.isInteger(h.line) ? { line: h.line } : {}), ...(h.at ? { at: h.at } : {}),
|
|
140
|
+
askedText: `you were asked: ${h.question}`, saidText: `you said: ${h.answer}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
/**
|
|
130
145
|
* Fold `<dir>/draft-log.jsonl` into one record per drafting step, each with its calls. A line that cannot be parsed is
|
|
131
146
|
* skipped (a torn last line must not hide the rest).
|
|
132
147
|
* @param {string} dir
|
|
133
148
|
* @returns {{no: number, id: string, label: string, status: 'running'|'done'|'failed', startedAt: string, endedAt: string|null, wallMs: number|null,
|
|
134
|
-
* calls: {label: string, model: string|null, costUsd: number|null, unpricedRounds: number, at: string}[]
|
|
149
|
+
* calls: {label: string, model: string|null, costUsd: number|null, unpricedRounds: number, at: string}[],
|
|
150
|
+
* humans: ReturnType<typeof humanRowWords>[]}[]}
|
|
135
151
|
*/
|
|
136
152
|
export function readDraftSteps(dir) {
|
|
137
153
|
/** @type {string} */
|
|
@@ -145,7 +161,7 @@ export function readDraftSteps(dir) {
|
|
|
145
161
|
try { ev = JSON.parse(line); } catch { continue; }
|
|
146
162
|
if (!ev || typeof ev !== 'object' || !Number.isInteger(ev.no)) continue;
|
|
147
163
|
if (ev.kind === 'step-start') {
|
|
148
|
-
byNo.set(ev.no, { no: ev.no, id: String(ev.id), label: String(ev.label), status: 'running', startedAt: ev.at, endedAt: null, wallMs: null, calls: [] });
|
|
164
|
+
byNo.set(ev.no, { no: ev.no, id: String(ev.id), label: String(ev.label), status: 'running', startedAt: ev.at, endedAt: null, wallMs: null, calls: [], humans: [] });
|
|
149
165
|
} else if (ev.kind === 'step-end' && byNo.has(ev.no)) {
|
|
150
166
|
const x = byNo.get(ev.no);
|
|
151
167
|
x.status = ev.status === 'failed' ? 'failed' : 'done';
|
|
@@ -157,6 +173,8 @@ export function readDraftSteps(dir) {
|
|
|
157
173
|
const x = byNo.get(ev.no);
|
|
158
174
|
x.status = 'running';
|
|
159
175
|
x.endedAt = null;
|
|
176
|
+
} else if (ev.kind === 'human' && byNo.has(ev.no) && typeof ev.question === 'string' && typeof ev.answer === 'string') {
|
|
177
|
+
byNo.get(ev.no).humans.push(humanRowWords({ question: ev.question, answer: ev.answer, source: ev.source, ...(Number.isInteger(ev.line) ? { line: ev.line } : {}), at: ev.at }));
|
|
160
178
|
} else if (ev.kind === 'call') {
|
|
161
179
|
const c = { label: String(ev.label), model: typeof ev.model === 'string' ? ev.model : null, costUsd: typeof ev.costUsd === 'number' ? ev.costUsd : null, unpricedRounds: Number(ev.unpricedRounds) || 0, at: String(ev.at) };
|
|
162
180
|
byNo.get(ev.no)?.calls.push(c);
|
package/src/facts.js
CHANGED
|
@@ -24,6 +24,22 @@ export const FACT_PART_FIELDS = Object.freeze(['source', 'facts', 'count']);
|
|
|
24
24
|
/** a fact's name appears in the claim text bareguard scans for numbers, so it carries letters only (no digit can red the claim) */
|
|
25
25
|
const NAME_RE = /^[A-Za-z][A-Za-z _-]{0,63}$/;
|
|
26
26
|
|
|
27
|
+
/** the limits bareguard 0.24 puts on a money fact's lists (ruling #65), enforced here first so the red is ours and plain */
|
|
28
|
+
export const MAX_CURRENCIES = 3;
|
|
29
|
+
export const MAX_UNITS = 10;
|
|
30
|
+
export const MAX_UNIT_CHARS = 100;
|
|
31
|
+
const CURRENCY_SIGNS = ['$', '€', '£'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A money fact's `unit` / `currency` is ONE string (every spec signed before lists) or a non-empty list of them; a string is read exactly as a
|
|
35
|
+
* one-element list. Nothing is rewritten, so an old spec's bytes and hash stay as they were.
|
|
36
|
+
* @param {unknown} v @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
export const asList = (v) => (Array.isArray(v) ? v.map(String) : (typeof v === 'string' ? [v] : []));
|
|
39
|
+
|
|
40
|
+
/** @param {any} f a money fact @returns {string[]} every form the page writes the amount in: the units first (as R1L reads them), then the currency signs */
|
|
41
|
+
export const moneyForms = (f) => [...asList(f.unit), ...asList(f.currency)];
|
|
42
|
+
|
|
27
43
|
/** @param {unknown} v @returns {v is Record<string, any>} */
|
|
28
44
|
const isRec = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
29
45
|
/** @param {unknown} v @returns {v is string} */
|
|
@@ -36,7 +52,7 @@ export function factPartOf(params) {
|
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
/** @param {any} v @returns {boolean} a rule bound has the type its shape needs: a number for number/money, a string for date/time */
|
|
39
|
-
const boundOk = (/** @type {string} */ shape, /** @type {unknown} */ v) => (shape === 'number' || shape === 'money' ? typeof v === 'number' && Number.isFinite(v) : typeof v === 'string' && v !== '');
|
|
55
|
+
export const boundOk = (/** @type {string} */ shape, /** @type {unknown} */ v) => (shape === 'number' || shape === 'money' ? typeof v === 'number' && Number.isFinite(v) : typeof v === 'string' && v !== '');
|
|
40
56
|
|
|
41
57
|
/**
|
|
42
58
|
* The SHAPE of a `fact` part (what it may carry), pushed as reds in the same form `rubricStepShapeReds` uses. Whether the
|
|
@@ -72,10 +88,27 @@ export function factShapeReds(part, w, red) {
|
|
|
72
88
|
if (Object.hasOwn(f, 'aiProposal') && typeof f.aiProposal !== 'boolean') red('invalid-value', `${fw}.aiProposal`, 'true when the AI turned the person\'s words into this number');
|
|
73
89
|
const has = (/** @type {string} */ k) => Object.hasOwn(f, k);
|
|
74
90
|
if (f.shape === 'money') {
|
|
75
|
-
if (has('unit')
|
|
91
|
+
if (!has('unit') && !has('currency')) red('invalid-value', `${fw}.unit`, 'money names at least one of unit (words the input writes after the amount, e.g. "US dollars") or currency ("$", "€" or "£"); each may be one value or a list');
|
|
76
92
|
} else if (has('unit') || has('currency')) red('invalid-value', `${fw}.${has('unit') ? 'unit' : 'currency'}`, 'only money has a unit or a currency');
|
|
77
|
-
if (has('unit')
|
|
78
|
-
|
|
93
|
+
if (has('unit')) {
|
|
94
|
+
const us = f.unit;
|
|
95
|
+
if (Array.isArray(us) ? us.length === 0 || us.some((/** @type {unknown} */ u) => !nonEmpty(u)) : !nonEmpty(us)) red('invalid-value', `${fw}.unit`, 'a non-empty string copied from the input, or a non-empty list of them');
|
|
96
|
+
else {
|
|
97
|
+
const list = asList(us);
|
|
98
|
+
if (list.length > MAX_UNITS) red('invalid-value', `${fw}.unit`, `at most ${MAX_UNITS} units (${list.length} given)`);
|
|
99
|
+
else if (list.some((u) => u.length > MAX_UNIT_CHARS)) red('invalid-value', `${fw}.unit`, `a unit is at most ${MAX_UNIT_CHARS} characters`);
|
|
100
|
+
else if (new Set(list).size !== list.length) red('invalid-value', `${fw}.unit`, 'the units are not repeated');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (has('currency')) {
|
|
104
|
+
const cs = f.currency;
|
|
105
|
+
if (Array.isArray(cs) ? cs.length === 0 || cs.some((/** @type {unknown} */ c) => !CURRENCY_SIGNS.includes(/** @type {string} */ (c))) : !CURRENCY_SIGNS.includes(cs)) red('invalid-value', `${fw}.currency`, 'one of $ | € | £, or a list of them');
|
|
106
|
+
else {
|
|
107
|
+
const list = asList(cs);
|
|
108
|
+
if (list.length > MAX_CURRENCIES) red('invalid-value', `${fw}.currency`, `at most ${MAX_CURRENCIES} currencies (${list.length} given)`);
|
|
109
|
+
else if (new Set(list).size !== list.length) red('invalid-value', `${fw}.currency`, 'the currencies are not repeated');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
79
112
|
if (f.shape === 'phrase') {
|
|
80
113
|
if (!nonEmpty(f.phrase)) red('invalid-value', `${fw}.phrase`, 'the fixed words the quote must contain, copied from the input');
|
|
81
114
|
for (const k of ['anchor', 'until', 'rule']) if (has(k)) red('invalid-value', `${fw}.${k}`, 'a phrase is fixed words the quote must contain; it has no anchor, until or rule');
|
|
@@ -112,7 +145,7 @@ export function factDecides(part) {
|
|
|
112
145
|
if (f.shape === 'phrase') return { phrases: [f.phrase] };
|
|
113
146
|
/** @type {Record<string, any>} */
|
|
114
147
|
const d = { valueKind: f.shape };
|
|
115
|
-
if (f.shape === 'money') { if (f.unit !== undefined) d.unit = f.unit;
|
|
148
|
+
if (f.shape === 'money') { if (f.unit !== undefined) d.unit = f.unit; if (f.currency !== undefined) d.currency = f.currency; }
|
|
116
149
|
if (f.anchor !== undefined) d.anchor = f.anchor;
|
|
117
150
|
if (f.until !== undefined) d.until = f.until;
|
|
118
151
|
if (f.rule) {
|
|
@@ -151,12 +184,24 @@ export function factClaims(part, results) {
|
|
|
151
184
|
|
|
152
185
|
/** @param {unknown} n @param {string} shape @param {any} f @returns {string} one bound, as the person would say it */
|
|
153
186
|
function sayBound(n, shape, f) {
|
|
154
|
-
if (shape === 'money') return f
|
|
187
|
+
if (shape === 'money') return moneyAs(f, /** @type {string|number} */ (n), false);
|
|
155
188
|
return String(n);
|
|
156
189
|
}
|
|
157
190
|
|
|
191
|
+
/**
|
|
192
|
+
* One amount of a money fact in the fact's OWN signed forms, never a hardcoded sign: `symbolFirst` puts the currency sign before the amount
|
|
193
|
+
* ("$144") when the fact has one, else the unit after it ("144 US dollars"); not `symbolFirst` (a rule's bound) prefers the unit.
|
|
194
|
+
* @param {any} f a money fact @param {string|number} n @param {boolean} symbolFirst @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
export function moneyAs(f, n, symbolFirst) {
|
|
197
|
+
const cur = asList(f.currency)[0];
|
|
198
|
+
const uni = asList(f.unit)[0];
|
|
199
|
+
if (symbolFirst) return cur !== undefined ? `${cur}${n}` : `${n} ${uni}`;
|
|
200
|
+
return uni !== undefined ? `${n} ${uni}` : `${cur}${n}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
158
203
|
/** @param {any} f a fact with a rule @returns {string} the rule in words, e.g. "at most 450 US dollars" */
|
|
159
|
-
function ruleWords(f) {
|
|
204
|
+
export function ruleWords(f) {
|
|
160
205
|
const r = f.rule;
|
|
161
206
|
return r.op === 'between' ? `between ${sayBound(r.lo, f.shape, f)} and ${sayBound(r.hi, f.shape, f)}`
|
|
162
207
|
: `${{ lt: 'under', lte: 'at most', gt: 'over', gte: 'at least' }[/** @type {'lt'|'lte'|'gt'|'gte'} */ (r.op)]} ${sayBound(r.value, f.shape, f)}`;
|
|
@@ -173,7 +218,7 @@ export function factSentence(f, source) {
|
|
|
173
218
|
const bits = [];
|
|
174
219
|
if (f.shape === 'phrase') bits.push('phrase', `must say "${f.phrase}"`);
|
|
175
220
|
else {
|
|
176
|
-
bits.push(f.shape === 'money' ? `money in ${f.
|
|
221
|
+
bits.push(f.shape === 'money' ? `money in ${moneyForms(f).join(' or ')}` : f.shape);
|
|
177
222
|
if (f.rule) bits.push(ruleWords(f));
|
|
178
223
|
else if (f.anchor === undefined) bits.push('as the quote writes it');
|
|
179
224
|
if (f.anchor !== undefined) bits.push(f.until !== undefined ? `the words after "${f.anchor}" up to "${f.until}"` : `after "${f.anchor}"`);
|
|
@@ -190,7 +235,7 @@ export function factSentence(f, source) {
|
|
|
190
235
|
export function factBrief(f) {
|
|
191
236
|
if (f.shape === 'phrase') return `${f.name} — no value to write; the quote you copy must say "${f.phrase}"`;
|
|
192
237
|
/** @type {string[]} */
|
|
193
|
-
const bits = [f.shape === 'money' ? `an amount in ${f.
|
|
238
|
+
const bits = [f.shape === 'money' ? `an amount in ${moneyForms(f).join(' or ')}` : `a ${f.shape}`];
|
|
194
239
|
if (f.shape === 'text') bits[0] = 'words';
|
|
195
240
|
if (f.anchor !== undefined) bits.push(f.until !== undefined ? `the words right after "${f.anchor}" up to "${f.until}"` : `right after "${f.anchor}"`);
|
|
196
241
|
if (f.rule) bits.push(`must be ${ruleWords(f)}`);
|
package/src/factwords.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// page) is one space on both sides.
|
|
8
8
|
|
|
9
9
|
import { labelOnList, normLabel, scanLabels } from './labelscan.js';
|
|
10
|
+
import { asList } from './facts.js';
|
|
11
|
+
import { dropSentence } from './pagedropwords.js';
|
|
10
12
|
|
|
11
13
|
const WS = /[\s ]+/g;
|
|
12
14
|
const escapeRe = (/** @type {string} */ s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -46,19 +48,63 @@ export function countPhrase(source, words) {
|
|
|
46
48
|
/** @param {string} shape @returns {'money'|'number'|'date'|'time'|'text'} the label list a fact's anchor is picked from */
|
|
47
49
|
const listOf = (shape) => /** @type {any} */ (['money', 'number', 'date', 'time'].includes(shape) ? shape : 'text');
|
|
48
50
|
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What the page-word map (src/pagemap.js) KEPT and DROPPED for one input: the sets the second proof reads. A look from the old search-and-read
|
|
54
|
+
* path has no `dropped` list, so it gives null and the label is proved against the code-made list as before.
|
|
55
|
+
* @param {{facts?: any[], dropped?: (import('./pagerules.js').Dropped & {source?: string})[]}|null|undefined} look @param {string} source
|
|
56
|
+
* @returns {{labels: Set<string>, units: Set<string>, currencies: Set<string>, words: Set<string>, dropped: Map<string, import('./pagerules.js').Dropped>}|null}
|
|
57
|
+
*/
|
|
58
|
+
export function keptSetOf(look, source) {
|
|
59
|
+
if (!look || !Array.isArray(look.dropped)) return null;
|
|
60
|
+
const facts = (Array.isArray(look.facts) ? look.facts : []).filter((f) => f && f.source === source && Array.isArray(f.words));
|
|
61
|
+
const set = (/** @type {string[]} */ a) => new Set(a.map((w) => normLabel(String(w))));
|
|
62
|
+
/** @type {Map<string, import('./pagerules.js').Dropped>} */
|
|
63
|
+
const dropped = new Map();
|
|
64
|
+
for (const d of look.dropped) if (d.source === source && typeof d.word === 'string' && !dropped.has(normLabel(d.word))) dropped.set(normLabel(d.word), d);
|
|
65
|
+
return {
|
|
66
|
+
labels: set(facts.filter((f) => f.shape !== 'phrase').flatMap((f) => f.words)),
|
|
67
|
+
units: set(facts.flatMap((f) => f.unit ?? [])),
|
|
68
|
+
currencies: set(facts.flatMap((f) => f.currency ?? [])),
|
|
69
|
+
words: set(facts.flatMap((f) => [...f.words, ...(f.unit ?? []), ...(f.currency ?? [])])),
|
|
70
|
+
dropped,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
/**
|
|
50
75
|
* The reds for ONE fact part against its frozen source text.
|
|
51
|
-
* @param {{part: any, text: string, line: number|string, scan?: import('./labelscan.js').LabelScan}} o
|
|
76
|
+
* @param {{part: any, text: string, line: number|string, scan?: import('./labelscan.js').LabelScan, kept?: ReturnType<typeof keptSetOf>}} o
|
|
52
77
|
* @returns {{code: string, detail: string}[]}
|
|
53
78
|
*/
|
|
54
|
-
export function checkFactWords({ part, text, line, scan = scanLabels(text) }) {
|
|
79
|
+
export function checkFactWords({ part, text, line, scan = scanLabels(text), kept = null }) {
|
|
55
80
|
/** @type {{code: string, detail: string}[]} */
|
|
56
81
|
const reds = [];
|
|
57
82
|
const src = part.source;
|
|
58
83
|
const red = (/** @type {string} */ code, /** @type {string} */ msg) => reds.push({ code, detail: `line ${line}: ${msg} Nothing was signed.` });
|
|
59
84
|
for (const f of Array.isArray(part.facts) ? part.facts : []) {
|
|
60
85
|
const who = `fact "${f.name}"`;
|
|
61
|
-
|
|
86
|
+
// a word the page rules DROPPED (and no other fact kept) can never come back through the drafter (idea A core, spec 4.3)
|
|
87
|
+
const dropRed = (/** @type {string} */ what, /** @type {unknown} */ w) => {
|
|
88
|
+
const n = normLabel(String(w));
|
|
89
|
+
const d = kept?.dropped.get(n);
|
|
90
|
+
if (!kept || !d || kept.words.has(n)) return false;
|
|
91
|
+
red('rubric-fact-word-dropped', `${who}: the ${what} ${JSON.stringify(n)} cannot be used. ${dropSentence(d)}`);
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
94
|
+
if (f.anchor !== undefined && f.shape !== 'phrase' && kept) {
|
|
95
|
+
// a page input read by the page-word map: the label must be one the map KEPT (the code-made list cannot hold a marker after a number)
|
|
96
|
+
const label = normLabel(String(f.anchor));
|
|
97
|
+
if (dropRed('label', f.anchor)) { /* said above */ } else if (!kept.labels.has(label)) {
|
|
98
|
+
const some = [...kept.labels].slice(0, 12).map((l) => JSON.stringify(l)).join(', ');
|
|
99
|
+
red('rubric-fact-label-off-list', `${who}: the label ${JSON.stringify(label)} is not one of the labels the page words for "${src}" kept. A label is picked from those, never typed${some ? `: ${some}` : ' (there are none - leave the label out)'}.`);
|
|
100
|
+
} else if (countExact(text, label) < 1) {
|
|
101
|
+
red('rubric-fact-word-missing', `${who}: the label ${JSON.stringify(label)} is not in the input "${src}".`);
|
|
102
|
+
} else if (Number.isInteger(part.count?.min) && countExact(text, label) < part.count.min) {
|
|
103
|
+
const n = part.count.min;
|
|
104
|
+
const seen = countExact(text, label);
|
|
105
|
+
red('rubric-fact-label-rare', `${who}: the label ${JSON.stringify(label)} is in the input "${src}" only ${seen} time${seen === 1 ? '' : 's'}, but this line asks for ${n} results, so a label that sits in each result must be there at least ${n} times. Pick a label that is counted ${n} or more times.`);
|
|
106
|
+
}
|
|
107
|
+
} else if (f.anchor !== undefined && f.shape !== 'phrase') {
|
|
62
108
|
const list = listOf(f.shape);
|
|
63
109
|
const row = labelOnList(scan, list, f.anchor);
|
|
64
110
|
if (!row) {
|
|
@@ -73,6 +119,7 @@ export function checkFactWords({ part, text, line, scan = scanLabels(text) }) {
|
|
|
73
119
|
red('rubric-fact-label-rare', `${who}: the label ${JSON.stringify(row.label)} is in the input "${src}" only ${row.count} time${row.count === 1 ? '' : 's'}, but this line asks for ${n} results, so a label that sits in each result must be there at least ${n} times. Pick a label from the list that is counted ${n} or more times.`);
|
|
74
120
|
}
|
|
75
121
|
}
|
|
122
|
+
if (f.until !== undefined) dropRed('closing label', f.until);
|
|
76
123
|
if (f.until !== undefined && countExact(text, f.until) < 1) red('rubric-fact-word-missing', `${who}: ${JSON.stringify(f.until)} is not in the input "${src}". Copy the closing words from the input, or leave them out.`);
|
|
77
124
|
if (f.shape === 'phrase' && typeof f.phrase === 'string') {
|
|
78
125
|
const seen = countPhrase(text, f.phrase);
|
|
@@ -84,7 +131,11 @@ export function checkFactWords({ part, text, line, scan = scanLabels(text) }) {
|
|
|
84
131
|
red('rubric-fact-phrase-rare', `${who}: the words ${JSON.stringify(f.phrase)} are in the input "${src}" only ${seen} time${seen === 1 ? '' : 's'}, but this line asks for ${n} results - each result's own quote must say them. Pick words every result says, or drop this check.`);
|
|
85
132
|
}
|
|
86
133
|
}
|
|
87
|
-
|
|
134
|
+
// a unit (one string, or every one of a list) must be found as whole words; a currency sign (each of a list) must be found as written
|
|
135
|
+
for (const u of asList(f.unit)) dropRed('unit', u);
|
|
136
|
+
for (const c of asList(f.currency)) dropRed('currency', c);
|
|
137
|
+
for (const u of asList(f.unit)) if (countPhrase(text, u) < 1) red('rubric-fact-word-missing', `${who}: the unit ${JSON.stringify(u)} is not in the input "${src}". Copy the words the input really uses.`);
|
|
138
|
+
for (const c of asList(f.currency)) if (countExact(text, c) < 1) red('rubric-fact-word-missing', `${who}: the currency ${JSON.stringify(c)} is not in the input "${src}". Copy the sign the input really uses.`);
|
|
88
139
|
}
|
|
89
140
|
return reds;
|
|
90
141
|
}
|
|
@@ -93,9 +144,10 @@ export function checkFactWords({ part, text, line, scan = scanLabels(text) }) {
|
|
|
93
144
|
* The reds for every fact part of a declaration. A fact part whose input text is not at hand is a red: a check whose words were never
|
|
94
145
|
* proved against the source is never signed.
|
|
95
146
|
* @param {any} declaration @param {Record<string, string>|null} texts label -> frozen text
|
|
147
|
+
* @param {{facts?: any[], dropped?: any[]}|null} [look] the page-word map's report: what it kept and dropped (null = no map, the code-made list is the proof)
|
|
96
148
|
* @returns {{code: string, path: string, detail: string}[]}
|
|
97
149
|
*/
|
|
98
|
-
export function checkDeclarationWords(declaration, texts) {
|
|
150
|
+
export function checkDeclarationWords(declaration, texts, look = null) {
|
|
99
151
|
/** @type {{code: string, path: string, detail: string}[]} */
|
|
100
152
|
const reds = [];
|
|
101
153
|
if (!declaration || !Array.isArray(declaration.stages)) return reds;
|
|
@@ -114,7 +166,7 @@ export function checkDeclarationWords(declaration, texts) {
|
|
|
114
166
|
return;
|
|
115
167
|
}
|
|
116
168
|
if (!scans.has(p.source)) scans.set(p.source, scanLabels(text));
|
|
117
|
-
for (const r of checkFactWords({ part: p, text, line: st.params.step, scan: scans.get(p.source) })) reds.push({ code: r.code, path: at, detail: r.detail });
|
|
169
|
+
for (const r of checkFactWords({ part: p, text, line: st.params.step, scan: scans.get(p.source), kept: keptSetOf(look, p.source) })) reds.push({ code: r.code, path: at, detail: r.detail });
|
|
118
170
|
});
|
|
119
171
|
});
|
|
120
172
|
return reds;
|