bareloop 0.45.0 → 0.46.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/bareloop.context.md +34 -7
  3. package/package.json +1 -1
  4. package/src/attemptdiff.js +116 -0
  5. package/src/authorreadout.js +55 -14
  6. package/src/authorscout.js +1 -1
  7. package/src/bundle.js +90 -6
  8. package/src/close-integrity.js +39 -5
  9. package/src/config.js +5 -2
  10. package/src/inputlook.js +1 -47
  11. package/src/judged.js +3 -3
  12. package/src/labelscan.js +13 -4
  13. package/src/ladder.js +2 -2
  14. package/src/learn.js +943 -0
  15. package/src/learnstore.js +154 -0
  16. package/src/modellist.js +1 -1
  17. package/src/pagedry.js +2 -3
  18. package/src/pagefetch.js +2 -1
  19. package/src/pagerules.js +27 -4
  20. package/src/panel/authorroutes.js +4 -3
  21. package/src/panel/authorsession.js +5 -7
  22. package/src/panel/index.html +108 -0
  23. package/src/panel/schedulestart.js +5 -0
  24. package/src/panel/server.js +16 -134
  25. package/src/panel/status.js +11 -0
  26. package/src/planrun.js +40 -6
  27. package/src/providerrows.js +3 -3
  28. package/src/run.js +4 -2
  29. package/src/schedulewords.js +1 -1
  30. package/src/source.js +1 -1
  31. package/src/stagewords.js +145 -0
  32. package/src/text.js +4 -2
  33. package/src/userrun.js +28 -0
  34. package/src/validate.js +10 -5
  35. package/src/worktree.js +8 -2
  36. package/types/attemptdiff.d.ts +38 -0
  37. package/types/authorreadout.d.ts +7 -0
  38. package/types/config.d.ts +1 -1
  39. package/types/inputlook.d.ts +0 -23
  40. package/types/labelscan.d.ts +6 -0
  41. package/types/learn.d.ts +575 -0
  42. package/types/learnstore.d.ts +107 -0
  43. package/types/pagerules.d.ts +7 -1
  44. package/types/panel/status.d.ts +8 -0
  45. package/types/planrun.d.ts +13 -3
  46. package/types/reuse.d.ts +1 -1
  47. package/types/run.d.ts +8 -2
  48. package/types/stagewords.d.ts +91 -0
package/src/config.js CHANGED
@@ -9,6 +9,7 @@
9
9
  //
10
10
  // Shape (unknown fields are kept, never dropped):
11
11
  // { "monthlyLimitUsd": <number > 0>, // absent = no limit, no check
12
+ // "learningHints": true|false, // absent = false: the planner is handed cross-run learnings (src/learn.js); the tally is kept either way
12
13
  // "keys": { "<ENV NAME>": { "shape": "<shape id>", "baseUrl": "http(s)://…", "legacyMaxTokens": true|false } },
13
14
  // "models": { "<ENV NAME>": [ { "model": "<id>", "use": true|false, "inPerM": n, "outPerM": n } ] } }
14
15
  // An older file's `keys.<ENV>.{name,priceInPerM,priceOutPerM}` is tolerated and read as ONE model
@@ -91,7 +92,7 @@ export function providerFieldProblem(field, v) {
91
92
  }
92
93
 
93
94
  /** A Base URL as stored: trailing slashes dropped. @param {string} v @returns {string} */
94
- export const cleanBaseUrl = (v) => v.replace(/\/+$/, '');
95
+ export const cleanBaseUrl = (v) => v.replace(/(?<!\/)\/+$/, '');
95
96
 
96
97
  /**
97
98
  * THE one rule for a row's model lines, used by config validation and the Settings routes. A price
@@ -128,6 +129,7 @@ export function modelsProblem(list) {
128
129
  */
129
130
  function docProblem(doc) {
130
131
  // monthlyLimitUsd is NOT judged here: `monthlyLimitOf` (src/monthly.js) owns its read-time sentence
132
+ if (doc.learningHints !== undefined && typeof doc.learningHints !== 'boolean') return 'learningHints must be true or false';
131
133
  if (doc.keys !== undefined) {
132
134
  if (doc.keys === null || typeof doc.keys !== 'object' || Array.isArray(doc.keys)) return 'keys must be an object of key variable names';
133
135
  for (const [env, row] of Object.entries(doc.keys)) {
@@ -156,7 +158,7 @@ function docProblem(doc) {
156
158
  * `keys` merges per key variable name, then per field; `models.<ENV>` is REPLACED WHOLE (`null`
157
159
  * clears it). Validated before anything is written; nothing is written on a refusal.
158
160
  *
159
- * Patch fields understood: `monthlyLimitUsd` (finite > 0), `keys.<ENV>.{shape,baseUrl,legacyMaxTokens}`,
161
+ * Patch fields understood: `monthlyLimitUsd` (finite > 0), `learningHints` (true | false), `keys.<ENV>.{shape,baseUrl,legacyMaxTokens}`,
160
162
  * `models.<ENV>` (a list judged by {@link modelsProblem}). The first write of a row's `models` list
161
163
  * removes that row's older `name` / `priceInPerM` / `priceOutPerM` in the SAME write (never two
162
164
  * sources); a retired top-level `anthropicBalanceNote` is dropped on any write. Any other field is
@@ -176,6 +178,7 @@ export function updateConfig(patch, opts = {}) {
176
178
  if (k === 'monthlyLimitUsd' && !(typeof v === 'number' && Number.isFinite(v) && v > 0)) {
177
179
  throw new ConfigError('monthlyLimitUsd must be a number above 0 (remove the limit to have none)');
178
180
  }
181
+ if (k === 'learningHints' && typeof v !== 'boolean') throw new ConfigError('learningHints must be true or false');
179
182
  next[k] = v;
180
183
  }
181
184
  delete next.anthropicBalanceNote;
package/src/inputlook.js CHANGED
@@ -8,9 +8,8 @@
8
8
  // not on the list is NOT carried into the report - it is moved to "could not check", named. Code never decides two words mean the
9
9
  // same: the AI proposes, the list and the source prove.
10
10
 
11
- import { scanLabels, labelOnList, renderLabelList, normLabel, LABEL_SHAPES } from './labelscan.js';
11
+ import { scanLabels, labelOnList, renderLabelList, normLabel, LABEL_SHAPES, TAG } from './labelscan.js';
12
12
  import { FACT_SHAPES, FACT_OPS } from './facts.js';
13
- import { lineRules } from './rubricstep.js';
14
13
  import { dropRows, dropLine } from './pagedropwords.js';
15
14
 
16
15
  /** the name of the answer tool - the one place the AI's report enters */
@@ -20,7 +19,6 @@ export const LOOK_MAX_HITS = 40;
20
19
  const SNIPPET = 220;
21
20
  const READ_LINE = 320;
22
21
  const READ_LINES = 40;
23
- const TAG = /\[[A-Za-z]+=[^\]\s]*\]/g;
24
22
  const WS = /[\s   ]+/g;
25
23
 
26
24
  /**
@@ -290,47 +288,3 @@ export function labelListsOf(texts) {
290
288
  }
291
289
 
292
290
  export { LABEL_SHAPES };
293
-
294
- /**
295
- * L3: a reason the AI gave, cut only at a sentence end (never mid-sentence): whole sentences while they fit in `max` characters,
296
- * and nothing at all if the first one alone does not fit. A trailing full stop is dropped (the caller closes the clause).
297
- * @param {string} text @param {number} max @returns {string}
298
- */
299
- export function wholeSentences(text, max) {
300
- const sentences = text.replace(/\s+/g, ' ').trim().split(/(?<=[.!?])\s+/).filter(Boolean);
301
- let out = '';
302
- for (const s of sentences) {
303
- const next = out === '' ? s : `${out} ${s}`;
304
- if (next.length > max) break;
305
- out = next;
306
- }
307
- return out.replace(/[.!?]+$/, '');
308
- }
309
-
310
- /**
311
- * I1 (hamr, 2026-10-13, "Ruled by hamr" 2): when the look shows the person's intent on one `~` rule was NOT captured - its words or
312
- * meaning are not in the source - the person is asked about THAT rule at once, named by its line and rule. Only a look entry that names
313
- * a rule number counts; anything else the look could not find is not asked piecemeal (it is Not checked, or the whole job is reviewed).
314
- * The wording is the code's own, never the model's. The keep-or-drop and the cap are `normalizeQuestions` (src/authorflow.js).
315
- * @param {LookNotFound[]} notFound @param {{n: number, text?: string, rule?: string, rules?: string[]}[]} jobLines
316
- * @returns {{line: number, question: string}[]} every candidate question (the caller applies the machine's caps)
317
- */
318
- export function ruleQuestions(notFound, jobLines) {
319
- /** @type {{line: number, question: string}[]} */
320
- const out = [];
321
- /** @type {Set<string>} */
322
- const seen = new Set();
323
- for (const n of Array.isArray(notFound) ? notFound : []) {
324
- if (!Number.isInteger(n.rule)) continue;
325
- const line = jobLines.find((l) => l.n === n.line);
326
- const rule = line ? lineRules(line)[/** @type {number} */ (n.rule) - 1] : undefined;
327
- if (!line || rule === undefined) continue;
328
- const key = `${n.line}:${n.rule}`;
329
- if (seen.has(key)) continue;
330
- seen.add(key);
331
- const words = n.words ? `${JSON.stringify(n.words)} (from your words)` : 'what this rule asks for';
332
- const why = wholeSentences(String(n.why), 200);
333
- out.push({ line: n.line, question: `Line ${n.line}, rule ${n.rule} - "${rule}": I could not find ${words} in your input${why ? ` (${why})` : ''}. What do you mean by it? Your answer is added as a new ~ rule on line ${n.line}.` });
334
- }
335
- return out;
336
- }
package/src/judged.js CHANGED
@@ -323,7 +323,7 @@ export function docQuoteInBlock(text, declQuote, docQuote) {
323
323
  */
324
324
  function hasDocBlockDirectlyAbove(text, fn) {
325
325
  const n = fn.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
326
- const re = new RegExp(`^(?:export\\s+)?(?:default\\s+)?(?:(?:async\\s+)?function\\s*\\*?\\s*${n}\\b`
326
+ const re = new RegExp(`^(?:export\\s+)?(?:default\\s+)?(?:(?:async\\s+)?function\\s*(?:\\*\\s*)?${n}\\b`
327
327
  + `|(?:const|let|var)\\s+${n}\\s*=|(?:static\\s+)?(?:async\\s+)?${n}\\s*\\()`);
328
328
  const hits = text.split('\n').map((l) => l.trim()).filter((l) => re.test(l));
329
329
  if (hits.length !== 1) return false;
@@ -401,7 +401,7 @@ function sharesPrefix(a, b) {
401
401
  * @param {string} text @returns {string[]}
402
402
  */
403
403
  function patternFields(text) {
404
- const m = /^\s*\{([^{}[\]()]*)\}\s*(?:=\s*\{\s*\})?\s*$/.exec(text);
404
+ const m = /^\s*\{([^{}[\]()]*)\}(?:\s*=\s*\{\s*\})?\s*$/.exec(text);
405
405
  if (!m || m[1].includes('...')) return [];
406
406
  /** @type {string[]} */
407
407
  const out = [];
@@ -417,7 +417,7 @@ function patternFields(text) {
417
417
  /** a doc-block line with its comment decoration (opener, leading star, closer) removed
418
418
  * @param {string} line @returns {string} */
419
419
  function undecorated(line) {
420
- return line.trim().replace(/^\/\*+/, '').replace(/\*+\/$/, '').replace(/^\*+/, '').trim();
420
+ return line.trim().replace(/^\/\*+/, '').replace(/(?<!\*)\*+\/$/, '').replace(/^\*+/, '').trim();
421
421
  }
422
422
 
423
423
  /**
package/src/labelscan.js CHANGED
@@ -28,22 +28,31 @@ const MAX_GAP_LABEL = 24;
28
28
  const MIN_PHRASE = 3;
29
29
  /** how many repeated phrases are looked behind */
30
30
  const MAX_PHRASES = 20;
31
- /** a line is scanned up to this many characters, and for this many values (a minified page is one huge line; the bound keeps the scan linear) */
31
+ /** a line is scanned up to this many characters, and for this many values (a minified page is one huge line; the bound caps the work per line, each finder's own regex is kept linear below) */
32
32
  const MAX_LINE = 20000;
33
33
  const MAX_VALUES_PER_LINE = 400;
34
34
  /** only this much of a source is scanned (a source over it is scanned up to a line end) */
35
35
  export const SCAN_MAX_CHARS = 2_000_000;
36
36
 
37
37
  const WS = /[\s   ]+/g;
38
- const TAG = /\[[A-Za-z]+=[^\]\s]*\]/g;
38
+ /**
39
+ * The page reader's bracketed tag (`[ref=549]`), shared by the three places that strip it. The lookahead and lookbehind add no match and drop no
40
+ * match: a `[` that sits inside the run of an earlier `[letters=` (no `]` or white space between) is skipped, because that earlier start reads the same
41
+ * run to the same end - it matches (and consumes this one) or fails (and so does this one). Without them "[a=[a=[a=..." with no `]` is quadratic.
42
+ */
43
+ export const TAG = /\[(?=[A-Za-z]+=)(?<!\[[A-Za-z]+=[^\]\s]*?)[A-Za-z]+=[^\]\s]*\]/g;
39
44
  const MONTH = '(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)';
40
45
  /** value finders in priority order: an earlier shape owns a span a later one would also match */
41
46
  const FINDERS = /** @type {[string, RegExp][]} */ ([
42
47
  ['date', new RegExp(`\\b\\d{4}-\\d{2}-\\d{2}\\b|\\b\\d{1,2}/\\d{1,2}/\\d{2,4}\\b|\\b${MONTH}\\.? \\d{1,2}(?:, \\d{4})?\\b|\\b\\d{1,2} ${MONTH}\\.? \\d{4}\\b`, 'g')],
43
48
  ['time', /\b\d{1,2}:\d{2}(?: ?[AaPp]\.?[Mm]\.?)?(?![\w:])/g],
44
- ['money', /[$€£] ?\d[\d,]*(?:\.\d+)?|\b\d[\d,]*(?:\.\d+)? ?(?:US dollars?|dollars?|USD|euros?|EUR|pounds?|GBP)\b/g],
49
+ // the 2nd shape's leading lookbehind skips a start inside a digit/comma run that an earlier start of the run already covers (that start reads the same
50
+ // run to the same end, so it matches first or fails like this one); without it "1,1,1,...x" is quadratic. The covering start is never one right after
51
+ // a "." - the digits of a decimal can already belong to an earlier match ("$1.5,2 dollars" reads "2 dollars" as its own)
52
+ ['money', /[$€£] ?\d[\d,]*(?:\.\d+)?|(?<!(?<!\.)\b\d[\d,]*?)\b\d[\d,]*(?:\.\d+)? ?(?:US dollars?|dollars?|USD|euros?|EUR|pounds?|GBP)\b/g],
45
53
  ['number', /(?<![\w.,])\d[\d,]*(?:\.\d+)?(?![\w]|\.\d)/g],
46
- ['text', /[A-Za-z]*\d[\w.\-]*/g],
54
+ // (?<![A-Za-z]): a start inside a run of letters is covered by the run's first letter (same match, earlier), so a long run of letters with no digit is linear
55
+ ['text', /(?<![A-Za-z])[A-Za-z]*\d[\w.\-]*/g],
47
56
  ]);
48
57
  const PHRASE = /\b[A-Z][a-z]+(?: [A-Z][a-z]+)+\b/g;
49
58
 
package/src/ladder.js CHANGED
@@ -58,11 +58,11 @@ export const STRIKE_LIMIT = 2;
58
58
  */
59
59
  const VOLATILE = [
60
60
  // TAP's own timing line, dropped WHOLE: its value is the only thing on it
61
- [/^[ \t]*#?[ \t]*duration_ms[ \t]*[:=].*$/gm, ''],
61
+ [/^[ \t]*(?:#[ \t]*)?duration_ms[ \t]*[:=].*$/gm, ''],
62
62
  // ISO-8601 stamps, wherever they appear in a close's output
63
63
  [/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/g, '<ts>'],
64
64
  // any millisecond figure (`(1.738ms)`, `took 12 ms`)
65
- [/\d+(?:\.\d+)?[ \t]*ms\b/g, '<ms>'],
65
+ [/(?<!\d)\d+(?:\.\d+)?[ \t]*ms\b/g, '<ms>'],
66
66
  ];
67
67
 
68
68
  /**