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/pagerules.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// IDEA A CORE (docs/product/ITEM33-BUILD.md "Addendum 2026-10-18 — Idea A core: build spec", section 4.2): the plain code rules that
|
|
2
|
+
// keep or drop each page word the AI's map named. The AI never grades: it names words, code keeps only what the saved page proves.
|
|
3
|
+
// Pure - no I/O, no clock, no model. The rule bodies are PORTED from the frozen POC (.claude/scratch/idea-a-v2/rules.mjs and
|
|
4
|
+
// rules-v3-candidate.mjs; sha256 recorded in commit 0bcc5fe), with the OUT branches (R2, R2a, R2b, R2c, R3, R3U, V4, pageClaims) deleted.
|
|
5
|
+
|
|
6
|
+
import { lineRules } from './rubricstep.js';
|
|
7
|
+
import { MAX_CURRENCIES, MAX_UNITS, MAX_UNIT_CHARS, FACT_OPS, boundOk } from './facts.js';
|
|
8
|
+
import { countExact, countPhrase, squash } from './factwords.js';
|
|
9
|
+
import { normLabel } from './labelscan.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{op: string, value?: string|number, lo?: string|number, hi?: string|number}} Reading what a `~` rule means as a limit (PRD v1.107 Part 5b): the same shape as a signed fact rule
|
|
13
|
+
* @typedef {{name: string, from?: string, shape: string, words?: string[], currency?: string[], unit?: string[], reading?: Reading}} MapFact the AI's map, one fact
|
|
14
|
+
* @typedef {{facts: MapFact[]}} PageMap
|
|
15
|
+
* @typedef {{line: number, name: string, shape: string, from: string, words: string[], unit?: string[], currency?: string[], forRule?: number, reading?: Reading}} KeptFact
|
|
16
|
+
* `words` = what the page proves: the labels of a money/number/time fact, or the alternative phrases of a phrase fact (a place is a phrase)
|
|
17
|
+
* @typedef {{line: number, rule?: number, fact: string, word?: string, reason: string, other?: string}} Dropped `other` = what the drop is compared to: the first label (R1), the primary form (R1L), the fact it would have repeated (S1)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @param {unknown} v @returns {v is Record<string, any>} */
|
|
21
|
+
const isRec = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Which job line, and which `~` rule under it, a map fact carries out. The map writes `from` as "line N", "line N rule M", "rule M" or
|
|
25
|
+
* "job line"; anything else is the first job line.
|
|
26
|
+
* @param {unknown} from @param {{n: number, text?: string, rule?: string, rules?: string[]}[]} jobLines
|
|
27
|
+
* @returns {{line: number, rule?: number}}
|
|
28
|
+
*/
|
|
29
|
+
export function tieOf(from, jobLines) {
|
|
30
|
+
const s = String(from ?? '');
|
|
31
|
+
const lineM = /line\s*(\d+)/i.exec(s);
|
|
32
|
+
const ruleM = /rule\s*(\d+)/i.exec(s);
|
|
33
|
+
const first = jobLines[0]?.n ?? 1;
|
|
34
|
+
let line = lineM && jobLines.some((l) => l.n === Number(lineM[1])) ? Number(lineM[1]) : null;
|
|
35
|
+
const rule = ruleM ? Number(ruleM[1]) : undefined;
|
|
36
|
+
if (line === null && rule !== undefined) line = jobLines.find((l) => lineRules(l).length >= rule)?.n ?? null;
|
|
37
|
+
const out = { line: line ?? first };
|
|
38
|
+
if (rule !== undefined && lineRules(jobLines.find((l) => l.n === out.line)).length >= rule) return { ...out, rule };
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Is this a legal `reading` for a fact of this shape? Only a time or money fact carries one, with the op set and the bound types of a signed
|
|
44
|
+
* fact rule (src/facts.js): no other key, no missing bound. A malformed one makes the whole map answer malformed (no repair).
|
|
45
|
+
* @param {unknown} shape @param {unknown} r @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function validReading(shape, r) {
|
|
48
|
+
if (shape !== 'time' && shape !== 'money') return false;
|
|
49
|
+
if (!isRec(r) || !FACT_OPS.includes(r.op)) return false;
|
|
50
|
+
const keys = Object.keys(r).sort().join();
|
|
51
|
+
return r.op === 'between' ? keys === 'hi,lo,op' && boundOk(shape, r.lo) && boundOk(shape, r.hi) : keys === 'op,value' && boundOk(shape, r.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} text @returns {string[]} the page's lines, white space squashed (the unit every per-line rule counts in) */
|
|
55
|
+
export const linesOf = (text) => text.split('\n').map(squash);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* How many times a label occurs inside ONE page line, at most (plain indexOf on squashed lines, non-overlapping): the N2 measure.
|
|
59
|
+
* @param {string[]} lines @param {string} phrase @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
function maxPerLine(lines, phrase) {
|
|
62
|
+
const p = squash(phrase).trim();
|
|
63
|
+
if (!p) return 0;
|
|
64
|
+
let max = 0;
|
|
65
|
+
for (const ln of lines) {
|
|
66
|
+
let n = 0;
|
|
67
|
+
for (let i = ln.indexOf(p); i !== -1; i = ln.indexOf(p, i + p.length)) n += 1;
|
|
68
|
+
if (n > max) max = n;
|
|
69
|
+
}
|
|
70
|
+
return max;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** the value each shape reads right after a label: a money or number is digits (a sign may come first), a time is h:mm and AM/PM */
|
|
74
|
+
const READ = {
|
|
75
|
+
money: /^\s*[$€£]?\s*(\d[\d,]*(?:\.\d+)?)/,
|
|
76
|
+
time: /^\s*(\d{1,2}:\d{2}[\s\u202f\u00a0]?[AaPp][Mm])/,
|
|
77
|
+
number: /^\s*(\d[\d,]*(?:\.\d+)?)/,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* R1 reader: the set of values of a fact's shape written right after each occurrence of a label on the page.
|
|
82
|
+
* @param {string[]} lines @param {string} label @param {'money'|'time'|'number'} shape @returns {Set<string>}
|
|
83
|
+
*/
|
|
84
|
+
function valueSet(lines, label, shape) {
|
|
85
|
+
/** @type {Set<string>} */
|
|
86
|
+
const s = new Set();
|
|
87
|
+
const p = squash(label).trim();
|
|
88
|
+
if (!p) return s;
|
|
89
|
+
for (const ln of lines) {
|
|
90
|
+
for (let i = ln.indexOf(p); i !== -1; i = ln.indexOf(p, i + p.length)) {
|
|
91
|
+
const m = READ[shape].exec(ln.slice(i + p.length));
|
|
92
|
+
if (m) s.add(m[1]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return s;
|
|
96
|
+
}
|
|
97
|
+
/** @param {Set<string>} a @param {Set<string>} b @returns {boolean} */
|
|
98
|
+
const sameSet = (a, b) => a.size === b.size && [...a].every((x) => b.has(x));
|
|
99
|
+
|
|
100
|
+
/** a number as the page writes it: digits, thousands commas, optional decimals (a currency sign next to it is not part of it) */
|
|
101
|
+
const NUM = '\\d[\\d,]*(?:\\.\\d+)?';
|
|
102
|
+
const BEFORE = new RegExp(`(${NUM})\\s*$`);
|
|
103
|
+
const AFTER = new RegExp(`^\\s*(?:[$€£]\\s*)?(${NUM})`);
|
|
104
|
+
const AFTER_CUR = new RegExp(`^\\s*(${NUM})`);
|
|
105
|
+
const stripCommas = (/** @type {string} */ s) => s.replace(/,/g, '');
|
|
106
|
+
|
|
107
|
+
/** @param {string[]} lines @param {string} w @returns {[string, number][]} every occurrence of w as [its line, where it starts] */
|
|
108
|
+
function occs(lines, w) {
|
|
109
|
+
/** @type {[string, number][]} */
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const l of lines) for (let i = l.indexOf(w); i !== -1; i = l.indexOf(w, i + w.length)) out.push([l, i]);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* U1: how many times a label has a number right BEFORE it (whitespace only between) and right AFTER it (whitespace and one currency sign
|
|
117
|
+
* between). A marker the page writes after the number ("490 total") has B > 0 and A = 0.
|
|
118
|
+
* @param {string[]} lines @param {string} label @returns {{B: number, A: number}}
|
|
119
|
+
*/
|
|
120
|
+
function beforeAfter(lines, label) {
|
|
121
|
+
let B = 0;
|
|
122
|
+
let A = 0;
|
|
123
|
+
for (const [l, i] of occs(lines, label)) {
|
|
124
|
+
if (BEFORE.test(l.slice(0, i))) B += 1;
|
|
125
|
+
if (AFTER.test(l.slice(i + label.length))) A += 1;
|
|
126
|
+
}
|
|
127
|
+
return { B, A };
|
|
128
|
+
}
|
|
129
|
+
/** R1L: the numbers a UNIT form reads - the one right before each occurrence @param {string[]} lines @param {string} u @returns {Set<string>} */
|
|
130
|
+
const unitSet = (lines, u) => new Set(occs(lines, u).map(([l, i]) => BEFORE.exec(l.slice(0, i))?.[1]).filter((v) => typeof v === 'string').map((v) => stripCommas(/** @type {string} */ (v))));
|
|
131
|
+
/** R1L: the numbers a CURRENCY form reads - the one right after each occurrence @param {string[]} lines @param {string} c @returns {Set<string>} */
|
|
132
|
+
const curSet = (lines, c) => new Set(occs(lines, c).map(([l, i]) => AFTER_CUR.exec(l.slice(i + c.length))?.[1]).filter((v) => typeof v === 'string').map((v) => stripCommas(/** @type {string} */ (v))));
|
|
133
|
+
|
|
134
|
+
/** S1: a fact name that counts results ("the second flight") cannot be checked per result - one result is one record, ask for N results instead */
|
|
135
|
+
const ORDINALS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth', 'latest', 'earliest', 'newest', 'oldest', 'last', 'next', 'previous', 'final', 'nth'];
|
|
136
|
+
|
|
137
|
+
/** the symbols a currency may be (src/facts.js); a code like "EUR" is a word read after the number, so it is a unit */
|
|
138
|
+
const CURRENCY_SYMBOLS = ['$', '€', '£'];
|
|
139
|
+
/** the shapes that read a value after a label */
|
|
140
|
+
const VALUE_SHAPES = ['money', 'time', 'number'];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply the rules to the AI's map over one saved page. Returns the kept facts and, for everything a rule removed, one `dropped` entry.
|
|
144
|
+
* The order of the rules is the POC's (apply-v3.mjs): found-on-page, then the rules that read the page. A place is read as a phrase.
|
|
145
|
+
* @param {string} text the frozen page text @param {PageMap} mapping @param {{n: number, text?: string, rule?: string, rules?: string[]}[]} jobLines
|
|
146
|
+
* @returns {{facts: KeptFact[], dropped: Dropped[]}}
|
|
147
|
+
*/
|
|
148
|
+
export function applyPageRules(text, mapping, jobLines) {
|
|
149
|
+
/** @type {Dropped[]} */
|
|
150
|
+
const dropped = [];
|
|
151
|
+
const all = Array.isArray(mapping?.facts) ? mapping.facts.filter(isRec) : [];
|
|
152
|
+
/** @type {Map<MapFact, {line: number, rule?: number}>} */
|
|
153
|
+
const ties = new Map(all.map((f) => [f, tieOf(f.from, jobLines)]));
|
|
154
|
+
const drop = (/** @type {MapFact} */ f, /** @type {string|undefined} */ word, /** @type {string} */ reason, /** @type {string|undefined} */ other = undefined) => {
|
|
155
|
+
const t = /** @type {{line: number, rule?: number}} */ (ties.get(f));
|
|
156
|
+
dropped.push({ line: t.line, ...(t.rule === undefined ? {} : { rule: t.rule }), fact: String(f.name ?? ''), ...(word === undefined ? {} : { word }), reason, ...(other === undefined ? {} : { other }) });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---- S1, name part: a fact whose name counts results is refused before anything else reads it ----
|
|
160
|
+
const list = all.filter((f) => {
|
|
161
|
+
const hit = String(f.name ?? '').toLowerCase().split(/[^a-z]+/).find((w) => ORDINALS.includes(w));
|
|
162
|
+
if (hit === undefined) return true;
|
|
163
|
+
drop(f, undefined, 's1-name');
|
|
164
|
+
return false;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---- found-on-page, then U1 and R1L for the facts that read an amount ----
|
|
168
|
+
const lines = linesOf(text);
|
|
169
|
+
/** @type {{f: MapFact, kept: string[], cur: string[], uni: string[], first?: string|null, anchorless?: boolean}[]} */
|
|
170
|
+
const pending = [];
|
|
171
|
+
for (const f of list) {
|
|
172
|
+
const words = (Array.isArray(f.words) ? f.words : []).map((w) => String(w)).filter((w) => w.trim() !== '');
|
|
173
|
+
if (f.shape === 'place' || f.shape === 'phrase') {
|
|
174
|
+
const kept = [];
|
|
175
|
+
for (const w of words) {
|
|
176
|
+
const n = f.shape === 'phrase' ? countPhrase(text, w) : countExact(text, w);
|
|
177
|
+
if (n >= 1) kept.push(w); else drop(f, w, 'not-on-page');
|
|
178
|
+
}
|
|
179
|
+
pending.push({ f, kept, cur: [], uni: [] });
|
|
180
|
+
} else if (VALUE_SHAPES.includes(String(f.shape))) {
|
|
181
|
+
const amount = f.shape === 'money' || f.shape === 'number';
|
|
182
|
+
/** labels the page holds (and, for an amount, that have a number right after them) */
|
|
183
|
+
const labels = [];
|
|
184
|
+
/** the AI's labels as U1 leaves them, a word the page does not hold included: the first of these is R1's reference */
|
|
185
|
+
const asListed = [];
|
|
186
|
+
/** U1: labels the page writes after the number */
|
|
187
|
+
const moved = [];
|
|
188
|
+
for (const w0 of words) {
|
|
189
|
+
const w = normLabel(w0);
|
|
190
|
+
if (!w) continue;
|
|
191
|
+
if (countExact(text, w) < 1) { asListed.push(w0); drop(f, w0, 'not-on-page'); continue; }
|
|
192
|
+
if (amount) {
|
|
193
|
+
const { B, A } = beforeAfter(lines, w);
|
|
194
|
+
if (A === 0 && B > 0) { moved.push(w); continue; }
|
|
195
|
+
if (A === 0 && B === 0) { drop(f, w, 'no-value'); continue; }
|
|
196
|
+
}
|
|
197
|
+
asListed.push(w0);
|
|
198
|
+
labels.push(w);
|
|
199
|
+
}
|
|
200
|
+
/** @type {string[]} */ let uni = [];
|
|
201
|
+
/** @type {string[]} */ let cur = [];
|
|
202
|
+
if (f.shape === 'money') {
|
|
203
|
+
// the forms: units first (the U1-moved words, then the AI's listed units), then currency signs
|
|
204
|
+
const listedCurrencies = (Array.isArray(f.currency) ? f.currency : []).map(String);
|
|
205
|
+
// a currency SIGN listed in BOTH unit and currency is said once: the unit copy goes (fwd F71 drafts)
|
|
206
|
+
const listedUnits = (Array.isArray(f.unit) ? f.unit : []).map(String).filter((u) => {
|
|
207
|
+
if (!(CURRENCY_SYMBOLS.includes(u) && listedCurrencies.includes(u))) return true;
|
|
208
|
+
drop(f, u, 'sign-twice');
|
|
209
|
+
return false;
|
|
210
|
+
});
|
|
211
|
+
const units = [...new Set([...moved, ...listedUnits])].filter((u) => {
|
|
212
|
+
if (countPhrase(text, u) >= 1) return true;
|
|
213
|
+
drop(f, u, 'not-on-page');
|
|
214
|
+
return false;
|
|
215
|
+
});
|
|
216
|
+
const curs = listedCurrencies.filter((c) => {
|
|
217
|
+
if (countExact(text, c) >= 1) return true;
|
|
218
|
+
drop(f, c, 'not-on-page');
|
|
219
|
+
return false;
|
|
220
|
+
});
|
|
221
|
+
/** @type {{kind: 'unit'|'currency', w: string}[]} */
|
|
222
|
+
const forms = [...units.map((u) => /** @type {const} */ ({ kind: 'unit', w: u })), ...curs.map((c) => /** @type {const} */ ({ kind: 'currency', w: c }))];
|
|
223
|
+
if (forms.length) {
|
|
224
|
+
// R1L: every form reads the same set of numbers as the primary (the first form); one that reads more or fewer is dropped
|
|
225
|
+
const read = (/** @type {{kind: string, w: string}} */ x) => (x.kind === 'unit' ? unitSet(lines, x.w) : curSet(lines, x.w));
|
|
226
|
+
const ps = read(forms[0]);
|
|
227
|
+
const keep = [forms[0]];
|
|
228
|
+
// the same word listed as both a unit and a currency is ONE form: it is never compared with itself, only said (the unit, listed first, stays)
|
|
229
|
+
for (const x of forms.slice(1)) { if (x.w !== forms[0].w && sameSet(read(x), ps)) keep.push(x); else drop(f, x.w, 'r1l', forms[0].w); }
|
|
230
|
+
uni = keep.filter((x) => x.kind === 'unit').map((x) => x.w);
|
|
231
|
+
cur = keep.filter((x) => x.kind === 'currency').map((x) => x.w);
|
|
232
|
+
}
|
|
233
|
+
// a currency that is a word ("US dollars") is read after the number, so it is a unit, never a currency sign
|
|
234
|
+
cur = cur.filter((c) => {
|
|
235
|
+
if (CURRENCY_SYMBOLS.includes(c)) return true;
|
|
236
|
+
drop(f, c, 'currency-not-symbol');
|
|
237
|
+
return false;
|
|
238
|
+
});
|
|
239
|
+
// the guard's list limits (src/facts.js), applied here so a longer list is cut by code and SAID, never refused later: the first forms stay
|
|
240
|
+
uni = uni.filter((u, i) => {
|
|
241
|
+
if (u.length > MAX_UNIT_CHARS) { drop(f, u, 'unit-too-long'); return false; }
|
|
242
|
+
if (i >= MAX_UNITS) { drop(f, u, 'limit-unit'); return false; }
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
cur = cur.filter((c, i) => {
|
|
246
|
+
if (i < MAX_CURRENCIES) return true;
|
|
247
|
+
drop(f, c, 'limit-currency');
|
|
248
|
+
return false;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
pending.push({ f, kept: labels, cur, uni, first: asListed.length ? normLabel(asListed[0]) : null, anchorless: amount && asListed.length === 0 });
|
|
252
|
+
} else drop(f, undefined, 'bad-shape');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- R1, same-value: the labels after the AI's FIRST listed one are kept only if they read the same set of values as it does ----
|
|
256
|
+
for (const p of pending) {
|
|
257
|
+
if (!VALUE_SHAPES.includes(String(p.f.shape))) continue;
|
|
258
|
+
const shape = /** @type {'money'|'time'|'number'} */ (p.f.shape);
|
|
259
|
+
const ref = p.first ? valueSet(lines, p.first, shape) : new Set();
|
|
260
|
+
p.kept = p.kept.filter((w) => {
|
|
261
|
+
if (w === p.first) return true;
|
|
262
|
+
if (sameSet(valueSet(lines, w, shape), ref)) return true;
|
|
263
|
+
drop(p.f, w, 'r1', p.first ?? undefined);
|
|
264
|
+
return false;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---- N2, per line: a label written 2+ times inside one page line cannot say which value is meant, so it is unusable. (R2 grow-left is OUT.) ----
|
|
269
|
+
for (const p of pending) {
|
|
270
|
+
if (!VALUE_SHAPES.includes(String(p.f.shape))) continue;
|
|
271
|
+
p.kept = p.kept.filter((w) => {
|
|
272
|
+
if (maxPerLine(lines, w) <= 1) return true;
|
|
273
|
+
drop(p.f, w, 'n2');
|
|
274
|
+
return false;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- the kept facts, in the POC's order: places, phrases, then values ----
|
|
279
|
+
/** @type {KeptFact[]} */
|
|
280
|
+
const signed = [];
|
|
281
|
+
const make = (/** @type {{f: MapFact, kept: string[], cur: string[], uni: string[]}} */ p) => {
|
|
282
|
+
const t = /** @type {{line: number, rule?: number}} */ (ties.get(p.f));
|
|
283
|
+
/** @type {KeptFact} */
|
|
284
|
+
const out = { line: t.line, name: String(p.f.name), shape: p.f.shape === 'place' ? 'phrase' : String(p.f.shape), from: String(p.f.from ?? ''), words: [...p.kept] };
|
|
285
|
+
if (p.f.shape === 'money') {
|
|
286
|
+
if (p.uni.length) out.unit = [...p.uni];
|
|
287
|
+
if (p.cur.length) out.currency = [...p.cur];
|
|
288
|
+
}
|
|
289
|
+
if (t.rule !== undefined) out.forRule = t.rule;
|
|
290
|
+
if (validReading(out.shape, p.f.reading)) out.reading = /** @type {Reading} */ ({ ...p.f.reading });
|
|
291
|
+
return out;
|
|
292
|
+
};
|
|
293
|
+
for (const kind of ['place', 'phrase', 'value']) {
|
|
294
|
+
for (const p of pending) {
|
|
295
|
+
const isValue = VALUE_SHAPES.includes(String(p.f.shape));
|
|
296
|
+
if (kind === 'value' ? !isValue : p.f.shape !== kind) continue;
|
|
297
|
+
// a fact left with no label is dropped, unless U1 moved every label to a unit/currency form of a money fact (it then reads that form
|
|
298
|
+
// alone); a money fact also needs a currency or a unit the page holds
|
|
299
|
+
const forms = p.cur.length + p.uni.length;
|
|
300
|
+
const ok = p.f.shape === 'money' ? forms > 0 && (p.kept.length > 0 || p.anchorless === true) : p.kept.length > 0;
|
|
301
|
+
if (!ok) { drop(p.f, undefined, 'no-label'); continue; }
|
|
302
|
+
signed.push(make(p));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ---- S1, shape and label part: two facts of one result may not share a shape AND a label (they would read the same value and can never differ).
|
|
306
|
+
// The first one stays; a fact with no label is keyed by its first unit or currency, so two different amounts are never confused. ----
|
|
307
|
+
/** @type {Map<string, string>} */
|
|
308
|
+
const seen = new Map();
|
|
309
|
+
const kept = signed.filter((k) => {
|
|
310
|
+
const key = `${k.shape}\u0000${k.words[0] ?? k.unit?.[0] ?? k.currency?.[0] ?? ''}`;
|
|
311
|
+
const first = seen.get(key);
|
|
312
|
+
if (first === undefined) { seen.set(key, k.name); return true; }
|
|
313
|
+
const f = /** @type {MapFact} */ (all.find((x) => String(x.name) === k.name));
|
|
314
|
+
drop(f, undefined, 's1', first);
|
|
315
|
+
return false;
|
|
316
|
+
});
|
|
317
|
+
const order = list.map((f) => String(f.name));
|
|
318
|
+
return { facts: kept.sort((a, b) => order.findIndex((n) => a.name.startsWith(n)) - order.findIndex((n) => b.name.startsWith(n))), dropped };
|
|
319
|
+
}
|