bareloop 0.9.0 → 0.10.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 +204 -0
- package/README.md +101 -117
- package/bareloop.context.md +139 -12
- package/package.json +1 -1
- package/src/authorflow.js +189 -22
- package/src/authoring.js +190 -0
- package/src/authorjob.js +11 -3
- package/src/authorscout.js +70 -9
- package/src/bridges.js +14 -1
- package/src/index.js +3 -0
- package/src/kinds.js +102 -11
- package/src/ledger.js +5 -1
- package/src/planrun.js +210 -25
- package/src/ralph.js +5 -0
- package/src/reuse.js +52 -2
- package/src/run.js +2 -1
- package/src/text.js +79 -0
- package/src/tools.js +11 -0
- package/src/validate.js +168 -11
- package/types/authorflow.d.ts +52 -8
- package/types/authoring.d.ts +112 -0
- package/types/authorjob.d.ts +3 -2
- package/types/authorscout.d.ts +16 -3
- package/types/index.d.ts +1 -1
- package/types/kinds.d.ts +58 -6
- package/types/planrun.d.ts +43 -0
- package/types/reuse.d.ts +1 -1
- package/types/text.d.ts +67 -0
- package/types/validate.d.ts +74 -9
package/src/authoring.js
CHANGED
|
@@ -417,6 +417,86 @@ export const TYPES_GENRE = Object.freeze({
|
|
|
417
417
|
/** no import-resolution hazard on this runner — an EMPTY list, declared,
|
|
418
418
|
* never an absent field that would read as "not looked into" (F59) */
|
|
419
419
|
env: Object.freeze([]),
|
|
420
|
+
instruments: Object.freeze([
|
|
421
|
+
Object.freeze({
|
|
422
|
+
id: 'tsc-error-line',
|
|
423
|
+
what: 'one type error, as the TypeScript checker prints it',
|
|
424
|
+
lineMatch: 'error TS\\d+',
|
|
425
|
+
capture: null,
|
|
426
|
+
// REAL, from the captured `npm run typecheck -- --strict` of the pulselog
|
|
427
|
+
// patient (2026-08-09): 67 error lines, every one in this shape.
|
|
428
|
+
example: 'src/backup.js(21,19): error TS7006: Parameter \'now\' implicitly has an \'any\' type.',
|
|
429
|
+
why: 'tsc prints a DIFFERENT shape on a terminal (`src/backup.js:21:19 - error TS7006:`) than through a '
|
|
430
|
+
+ 'pipe, and a close only ever reads the piped one. Run msmbpjk6 composed the terminal shape: it matched '
|
|
431
|
+
+ '0 of 67 real error lines while the tool exited 2, so the stage read a crashed instrument. This pattern '
|
|
432
|
+
+ 'is anchored on the CODE, not on the path spelling, so it reads both shapes and cannot lose that flip.',
|
|
433
|
+
}),
|
|
434
|
+
// ── node --test, the SAME pipe-vs-terminal trap, MEASURED ──────────────
|
|
435
|
+
//
|
|
436
|
+
// PROVENANCE, two independent halves.
|
|
437
|
+
//
|
|
438
|
+
// (1) REAL CAPTURED OUTPUT. The mailproof job-#2 battery's own spines carry
|
|
439
|
+
// whole `npm test` → `node --test` captures inside their `close-precheck`
|
|
440
|
+
// gap: `bareloop-patients/mailproof-job2-bareloop/battery-P1-mrm7gk25.jsonl`
|
|
441
|
+
// (`# tests 317` / `# pass 312` / `# fail 5`, five `not ok` lines) and
|
|
442
|
+
// `battery-P2-mrm7gk25.jsonl` (`# pass 316` / `# fail 1`). Nothing there was
|
|
443
|
+
// written to match a pattern — it is what the runner printed down the close's
|
|
444
|
+
// pipe, months before this table existed.
|
|
445
|
+
//
|
|
446
|
+
// (2) A SHIPPED, LIVE-FIRED SPELLING. `jobs/pulselog-author-types.json`
|
|
447
|
+
// already grades `^# pass (\d+)$` and `^# fail (\d+)$` (stages `tests-kept`
|
|
448
|
+
// and `test-suite-zero-failures`), and run msoc6t8v graded green through it.
|
|
449
|
+
// The table is not minting a new ruler; it is HANDING OVER the one the tree
|
|
450
|
+
// already grades with, the way the tsc term does.
|
|
451
|
+
//
|
|
452
|
+
// WHY IT IS THE TSC TRAP AGAIN, measured on this machine (node v22.22.2,
|
|
453
|
+
// one test file, identical args, 2026-08-13): through a PIPE the runner
|
|
454
|
+
// prints TAP — `TAP version 13`, `# pass 1`, `not ok 2 - b`; on a real pty it
|
|
455
|
+
// prints the spec reporter — `ℹ pass 1`, no `#`, no TAP at all. A close only
|
|
456
|
+
// ever reads the piped half, and a pattern composed against the pretty half
|
|
457
|
+
// matches nothing while the suite runs fine. Exactly what cost run msmbpjk6
|
|
458
|
+
// its authoring pass on tsc, waiting behind a second tool.
|
|
459
|
+
Object.freeze({
|
|
460
|
+
id: 'node-test-total-count',
|
|
461
|
+
what: 'how many tests the runner EXECUTED, off the TAP summary',
|
|
462
|
+
lineMatch: '^# tests (\\d+)$',
|
|
463
|
+
capture: 1,
|
|
464
|
+
example: '# tests 317',
|
|
465
|
+
why: 'the EXECUTED count is the honest denominator: a passed-count floor is satisfiable by a suite that '
|
|
466
|
+
+ 'stopped running, so a stage asking "is the suite still whole?" counts this, not passes. Anchored '
|
|
467
|
+
+ 'both ends because the runner prefixes ordinary chatter with the same `# ` (`# Subtest: <name>`), and '
|
|
468
|
+
+ 'an unanchored `# tests` would count test NAMES.',
|
|
469
|
+
}),
|
|
470
|
+
Object.freeze({
|
|
471
|
+
id: 'node-test-pass-count',
|
|
472
|
+
what: 'how many tests passed, off the TAP summary',
|
|
473
|
+
lineMatch: '^# pass (\\d+)$',
|
|
474
|
+
capture: 1,
|
|
475
|
+
example: '# pass 316',
|
|
476
|
+
why: 'the spelling `jobs/pulselog-author-types.json` already grades with, green through run msoc6t8v. '
|
|
477
|
+
+ 'The runner prints this line on a green run too, so a zero here is a REPORTED zero.',
|
|
478
|
+
}),
|
|
479
|
+
Object.freeze({
|
|
480
|
+
id: 'node-test-fail-count',
|
|
481
|
+
what: 'how many tests failed, off the TAP summary',
|
|
482
|
+
lineMatch: '^# fail (\\d+)$',
|
|
483
|
+
capture: 1,
|
|
484
|
+
example: '# fail 1',
|
|
485
|
+
why: 'this runner prints every counter on every run — a fully green suite still prints `# fail 0` '
|
|
486
|
+
+ '(measured), so reading the figure cannot confuse "no failures" with "never reported". Its pytest '
|
|
487
|
+
+ 'counterpart has no such guarantee, which is why python has no fail-COUNT term.',
|
|
488
|
+
}),
|
|
489
|
+
Object.freeze({
|
|
490
|
+
id: 'node-test-failed-line',
|
|
491
|
+
what: 'one failing test, as the TAP reporter names it',
|
|
492
|
+
lineMatch: '^not ok \\d+ ',
|
|
493
|
+
capture: null,
|
|
494
|
+
example: 'not ok 7 - edit-renotify: a non-participant edit, and edits on a pending event, ping no one',
|
|
495
|
+
why: 'one line per failing test, so it TALLIES and no match is a genuine counted zero. The trailing '
|
|
496
|
+
+ 'space and the `^` both carry weight: every passing test prints `ok 7 - …`, and without the anchor '
|
|
497
|
+
+ 'each of those would count as a failure.',
|
|
498
|
+
}),
|
|
499
|
+
]),
|
|
420
500
|
}),
|
|
421
501
|
python: Object.freeze({
|
|
422
502
|
suppressions: Object.freeze([
|
|
@@ -437,6 +517,58 @@ export const TYPES_GENRE = Object.freeze({
|
|
|
437
517
|
+ 'found by probe). It moves no seed number, so no seed-verdict read can see its absence.',
|
|
438
518
|
}),
|
|
439
519
|
]),
|
|
520
|
+
instruments: Object.freeze([
|
|
521
|
+
Object.freeze({
|
|
522
|
+
id: 'mypy-error-line',
|
|
523
|
+
what: 'one type error, as mypy prints it',
|
|
524
|
+
lineMatch: ' error: ',
|
|
525
|
+
capture: null,
|
|
526
|
+
// REAL, from a live aurora run's own spine
|
|
527
|
+
// (bareloop-patients/aurora-u-bareloop/u-ms2c0ls7.jsonl).
|
|
528
|
+
example: 'packages/spawner/src/aurora_spawner/timeout_policy.py:89: error: Statement is unreachable [unreachable]',
|
|
529
|
+
why: 'the operator\'s own hand-written close reads mypy this way (scripts/u-spawner-close.mjs:133), minted '
|
|
530
|
+
+ 'by paid live runs. It is anchored on the SEVERITY word, not on the path spelling, so a relative or '
|
|
531
|
+
+ 'absolute path, a package prefix or a config-driven root all read the same.',
|
|
532
|
+
}),
|
|
533
|
+
// ── pytest, from the aurora battery's own captured runs ────────────────
|
|
534
|
+
//
|
|
535
|
+
// PROVENANCE: six `close-precheck` gaps in
|
|
536
|
+
// `bareloop-patients/aurora-soar-bareloop/battery-A{1..4}-*.jsonl` carry the
|
|
537
|
+
// whole captured pytest run of the aurora patient (pytest 9.1.1, python
|
|
538
|
+
// 3.14.6, the patient's own `pytest.ini` addopts, which include `-ra`) —
|
|
539
|
+
// 2,690-odd tests, the `short test summary info` section and the final counts
|
|
540
|
+
// line. Real, uncrafted, paid for.
|
|
541
|
+
//
|
|
542
|
+
// WHAT IS NOT ANCHORED, and why. Every archived counts line arrives padded
|
|
543
|
+
// with `=` on both sides, and the padding WIDTH is terminal-derived: the
|
|
544
|
+
// archived long line gets exactly one `=` per side, a short local run gets
|
|
545
|
+
// twenty-five (measured, pytest 9.0.2, 2026-08-13). Under `-q` the padding is
|
|
546
|
+
// absent entirely. So the term anchors on the WORD and its number, never on
|
|
547
|
+
// the decoration — the same discipline as anchoring tsc on the code rather
|
|
548
|
+
// than the path.
|
|
549
|
+
Object.freeze({
|
|
550
|
+
id: 'pytest-pass-count',
|
|
551
|
+
what: 'how many tests passed, off pytest\'s final counts line',
|
|
552
|
+
lineMatch: '(\\d+) passed',
|
|
553
|
+
capture: 1,
|
|
554
|
+
example: '= 1 failed, 2690 passed, 3 skipped, 18 deselected, 1775 warnings in 326.12s (0:05:26) =',
|
|
555
|
+
why: 'the ONE segment of this line pytest prints whether the run was green or red, so it is the only '
|
|
556
|
+
+ 'count on it that can be read as a figure without risking "never reported". The other segments '
|
|
557
|
+
+ '(failed, skipped, deselected) are omitted when their count is zero — a green run prints just '
|
|
558
|
+
+ '`1 passed in 0.04s` (measured) — so a term reading THOSE would go silent exactly when the news is '
|
|
559
|
+
+ 'good, and a silent figure term is a broken instrument, not a zero.',
|
|
560
|
+
}),
|
|
561
|
+
Object.freeze({
|
|
562
|
+
id: 'pytest-failed-line',
|
|
563
|
+
what: 'one failing test, as pytest\'s short summary names it',
|
|
564
|
+
lineMatch: '^FAILED ',
|
|
565
|
+
capture: null,
|
|
566
|
+
example: 'FAILED packages/soar/tests/test_agent_registry_deprecation.py::TestAgentRegistryDeprecation::test_registry_and_discovery_equivalent_results',
|
|
567
|
+
why: 'this is how you count pytest FAILURES: exactly one line per failing test (measured under both `-ra` '
|
|
568
|
+
+ 'and `-q`), so it TALLIES and no match at all is a legitimate counted zero rather than an unreported '
|
|
569
|
+
+ 'number. It needs pytest\'s short summary section, which the patient\'s own config must not suppress.',
|
|
570
|
+
}),
|
|
571
|
+
]),
|
|
440
572
|
}),
|
|
441
573
|
}),
|
|
442
574
|
});
|
|
@@ -599,6 +731,64 @@ export function genreOwnedEnvNames(lang) {
|
|
|
599
731
|
return language(lang).env.map((/** @type {any} */ e) => e.name);
|
|
600
732
|
}
|
|
601
733
|
|
|
734
|
+
/**
|
|
735
|
+
* HOW THE GENRE'S OWN TOOLS PRINT — genre property, like the suppression battery
|
|
736
|
+
* and like `MYPYPATH`, and for the same reason: it is a FACT ABOUT AN INSTRUMENT
|
|
737
|
+
* WE ALREADY OWN, not a composition choice. Seven hand-written closes read tsc
|
|
738
|
+
* with the identical `/error TS\d+/` (`scripts/u-pulselog-close.mjs:122`,
|
|
739
|
+
* `u-bareguard-close.mjs:123`, `u-bareagent-close.mjs:123`,
|
|
740
|
+
* `u-baremobile-close.mjs:122`, `u-litectx-close.mjs:140`, `types-close.mjs:190`,
|
|
741
|
+
* `types-check.mjs:60`) and one reads mypy with `' error: '`
|
|
742
|
+
* (`u-spawner-close.mjs:133`) — the same provenance the SUPPRESSIONS table has:
|
|
743
|
+
* operator knowledge minted by paying for live runs.
|
|
744
|
+
*
|
|
745
|
+
* WHY IT BECAME GENRE DATA (run msmbpjk6, 2026-08-09, $0.43). Left to the model,
|
|
746
|
+
* the parser for a known tool is a DRAFT-TIME LOTTERY on a fact nobody needed to
|
|
747
|
+
* guess: that run composed tsc's TTY-only pretty shape
|
|
748
|
+
* (`^\S+\.js:\d+:\d+ - error TS\d+:`), which matched 0 of the 67 real error lines
|
|
749
|
+
* the piped run actually printed, while the tool exited 2. The gates held — the
|
|
750
|
+
* stage read as a crashed instrument and signing was refused, correctly — but the
|
|
751
|
+
* refusal cost a whole authoring run to learn something the tree already knew. An
|
|
752
|
+
* earlier run (mslwbkz7) happened to roll the right shape. A coin-flip on an
|
|
753
|
+
* owned fact is not a capability question.
|
|
754
|
+
*
|
|
755
|
+
* WHAT THIS IS NOT: it does not close the parser. A stage counting something we
|
|
756
|
+
* have NOT measured still writes its own pattern, and that remains a real gap —
|
|
757
|
+
* named, not smoothed. This table is where a measured instrument fact lands, one
|
|
758
|
+
* at a time, with its provenance.
|
|
759
|
+
*
|
|
760
|
+
* THE TEST-COUNT FACTS (2026-08-13) landed under exactly that rule, and closed
|
|
761
|
+
* two of the three gaps the paragraph above used to name by hand: the executed
|
|
762
|
+
* count and the failure line, for both runners, are now measured facts rather
|
|
763
|
+
* than draft-time lotteries. Provenance rides on each entry.
|
|
764
|
+
*
|
|
765
|
+
* WHAT STAYED OUT, deliberately, because no evidence covers it:
|
|
766
|
+
* - a pytest FAIL-COUNT figure. A green pytest run omits the segment entirely
|
|
767
|
+
* (`1 passed in 0.04s`), so a figure term over it reports "never reported" on
|
|
768
|
+
* precisely the runs that went well. Failures are counted with the `^FAILED `
|
|
769
|
+
* tally instead, where no match IS zero.
|
|
770
|
+
* - a pytest EXECUTED count. `collected N items` is printed under `-ra` and NOT
|
|
771
|
+
* under `-q` (measured both ways), so no single spelling survives the modes a
|
|
772
|
+
* patient may configure; a stage needing it still composes its own and says so.
|
|
773
|
+
*
|
|
774
|
+
* Returns a fresh copy per call, for the reason `classGuards` does: a frozen
|
|
775
|
+
* constant handed to a caller that edits it is one shared table away from a
|
|
776
|
+
* silently changed ruler.
|
|
777
|
+
* @param {string} lang
|
|
778
|
+
* @returns {{id: string, what: string, lineMatch: string, capture: number|null, example: string, why: string}[]}
|
|
779
|
+
*/
|
|
780
|
+
export function genreInstruments(lang) {
|
|
781
|
+
const instruments = language(lang).instruments ?? [];
|
|
782
|
+
// F59's shape at a table that feeds a RULER: an empty list would read as "this
|
|
783
|
+
// language's tools need no stated format", which is exactly the sentence that
|
|
784
|
+
// put the pretty-format regex into a signed-close candidate.
|
|
785
|
+
if (!instruments.length) {
|
|
786
|
+
throw new Error(`no known-instrument formats for language "${lang}" — a language the genre admits states how its `
|
|
787
|
+
+ 'own checker prints, or the model is back to guessing a fact we already own');
|
|
788
|
+
}
|
|
789
|
+
return instruments.map((/** @type {any} */ i) => ({ ...i }));
|
|
790
|
+
}
|
|
791
|
+
|
|
602
792
|
/**
|
|
603
793
|
* GROUNDING for a RECORDED genre environment — the provenance half of the
|
|
604
794
|
* owned-env rule, and the reason the by-value comparison is worth anything.
|
package/src/authorjob.js
CHANGED
|
@@ -365,7 +365,7 @@ function composerRefusal(reds) {
|
|
|
365
365
|
* @param {{answers: Record<string|number, any>, repoPath?: string|null, lang: string,
|
|
366
366
|
* verdictType?: string|null,
|
|
367
367
|
* questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
|
|
368
|
-
* seedRef?: string|null, scout?: any, listing?: any,
|
|
368
|
+
* seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
|
|
369
369
|
* seedFn?: Function, scoutFn?: Function, listingFn?: Function,
|
|
370
370
|
* authorFn?: Function, authorOpts?: object}} o
|
|
371
371
|
* @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
|
|
@@ -375,6 +375,7 @@ function composerRefusal(reds) {
|
|
|
375
375
|
export async function authorCloseForJob({
|
|
376
376
|
answers, repoPath = null, lang, verdictType = null, questions = null,
|
|
377
377
|
generate, provider = null, seedRef = null, scout = null, listing = null,
|
|
378
|
+
ceilingUsd = null,
|
|
378
379
|
seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
|
|
379
380
|
authorFn = authorClose, authorOpts = {},
|
|
380
381
|
}) {
|
|
@@ -437,7 +438,14 @@ export async function authorCloseForJob({
|
|
|
437
438
|
// D11 — the scout LOOKS, the author WRITES, neither acts. An ABSENT survey is
|
|
438
439
|
// never "no special facts are needed" (F59), and `authorClose` refuses on it at
|
|
439
440
|
// $0, so nothing here has to second-guess the classification.
|
|
440
|
-
|
|
441
|
+
// ONE CEILING, BOTH PAID SEAMS. The survey and the declaration loop are two
|
|
442
|
+
// populations of paid call with two accumulators, and the operator set ONE
|
|
443
|
+
// number: handing it to both is what makes the advertised ceiling and the
|
|
444
|
+
// enforced ceiling the same ceiling (the standing hard line, in its money
|
|
445
|
+
// form). `null` travels EXPLICITLY rather than being left off — unbounded is a
|
|
446
|
+
// stated operator choice, and an absent field read as falsy is the same state
|
|
447
|
+
// arrived at by accident.
|
|
448
|
+
const survey = scout ?? await scoutFn({ workdir, provider, ceilingUsd });
|
|
441
449
|
const seeds = listing ?? await listingFn({
|
|
442
450
|
workdir, seedRef: seed, sourcePaths: survey?.facts?.sourcePaths, testPaths: survey?.facts?.testPaths,
|
|
443
451
|
});
|
|
@@ -445,7 +453,7 @@ export async function authorCloseForJob({
|
|
|
445
453
|
const authored = await authorFn({
|
|
446
454
|
workdir, seedRef: seed, lang, verdictType: picked,
|
|
447
455
|
answers: interview.answers, questions: questions ?? questionsFor(picked),
|
|
448
|
-
scout: survey, listing: seeds, generate, ...authorOpts,
|
|
456
|
+
scout: survey, listing: seeds, generate, ceilingUsd, ...authorOpts,
|
|
449
457
|
});
|
|
450
458
|
|
|
451
459
|
if (!authored.ok) {
|
package/src/authorscout.js
CHANGED
|
@@ -50,11 +50,11 @@ import { createRequire } from 'node:module';
|
|
|
50
50
|
import { join } from 'node:path';
|
|
51
51
|
import { Gate } from 'bareguard';
|
|
52
52
|
import { LiteCtx } from 'litectx';
|
|
53
|
-
import { seedListing } from './kinds.js';
|
|
53
|
+
import { seedListing, GATE_AUDIT_FILE, ARBITER_BOOK_STORES } from './kinds.js';
|
|
54
54
|
import { TOOL_MENU, WRITE_VERBS, STORE_VERBS } from './job.js';
|
|
55
55
|
import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, strategyFor } from './tools.js';
|
|
56
56
|
import { redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
57
|
-
import { extractArtifact, priceOf, scrubRaw, stampRaw } from './text.js';
|
|
57
|
+
import { extractArtifact, priceOf, scrubRaw, stampRaw, tallyCalls, capStop } from './text.js';
|
|
58
58
|
|
|
59
59
|
const require = createRequire(import.meta.url);
|
|
60
60
|
const { Loop, wireGate } = require('bare-agent');
|
|
@@ -115,6 +115,16 @@ export const SURVEY_CAUSES = Object.freeze({
|
|
|
115
115
|
NON_OBJECT: 'non-object',
|
|
116
116
|
/** valid JSON, no content — F59's whole point, and a SEMANTIC failure */
|
|
117
117
|
EMPTY_OBJECT: 'empty-object',
|
|
118
|
+
/**
|
|
119
|
+
* THE SURVEY WAS NEVER ASKED FOR: the operator's money ceiling was already
|
|
120
|
+
* spent when the first attempt came due, so no call was made. Named as its own
|
|
121
|
+
* cause for the reason this enum exists at all — a new route to ABSENT has to
|
|
122
|
+
* be named, and naming it is the moment somebody decides whether it retries.
|
|
123
|
+
* This one does not, and cannot: a retry is precisely what the cap forbade.
|
|
124
|
+
* It is deliberately ABSENT of the model's own doing, and the prose says so —
|
|
125
|
+
* "the scout did not complete" would blame a model that never spoke.
|
|
126
|
+
*/
|
|
127
|
+
NOT_FUNDED: 'not-funded',
|
|
118
128
|
});
|
|
119
129
|
|
|
120
130
|
/**
|
|
@@ -280,9 +290,9 @@ export function classifySurvey(blob, meta) {
|
|
|
280
290
|
* `runAuthorScout` where they can be proven.
|
|
281
291
|
* @param {{workdir: string, granted: readonly string[], auditPath?: string, ctx?: boolean}} o
|
|
282
292
|
*/
|
|
283
|
-
export async function defaultSurveyor({ workdir, granted, auditPath = join(workdir,
|
|
293
|
+
export async function defaultSurveyor({ workdir, granted, auditPath = join(workdir, GATE_AUDIT_FILE), ctx = true }) {
|
|
284
294
|
const gate = new Gate({
|
|
285
|
-
fs: { writeScope: [], readScope: [workdir], deny: [auditPath,
|
|
295
|
+
fs: { writeScope: [], readScope: [workdir], deny: [auditPath, ...ARBITER_BOOK_STORES.map((s) => join(workdir, s))] },
|
|
286
296
|
limits: { maxTurns: AUTHOR_SCOUT_ROUNDS + 2 },
|
|
287
297
|
audit: { path: auditPath },
|
|
288
298
|
secrets: { patterns: SECRET_PATTERNS },
|
|
@@ -338,11 +348,12 @@ const defaultLoop = (/** @type {any} */ { system, policy, onLlmResult, provider
|
|
|
338
348
|
*
|
|
339
349
|
* @param {{workdir: string, provider?: any,
|
|
340
350
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
341
|
-
* attempts?: number,
|
|
351
|
+
* attempts?: number, ceilingUsd?: number|null,
|
|
342
352
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
343
353
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
344
354
|
* @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
|
|
345
|
-
* calls: {label: string, costUsd: number|null, unpricedRounds: number}[]
|
|
355
|
+
* calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
|
|
356
|
+
* budgetStop: 'cap-halt'|'pricing-red'|null}>}
|
|
346
357
|
*/
|
|
347
358
|
export async function runAuthorScout({
|
|
348
359
|
workdir, provider = null,
|
|
@@ -352,6 +363,7 @@ export async function runAuthorScout({
|
|
|
352
363
|
maxTokens = AUTHOR_SCOUT_MAX_TOKENS,
|
|
353
364
|
ctx = true,
|
|
354
365
|
attempts = SCOUT_ATTEMPTS,
|
|
366
|
+
ceilingUsd = null,
|
|
355
367
|
createLoop = defaultLoop,
|
|
356
368
|
createSurveyor = defaultSurveyor,
|
|
357
369
|
}) {
|
|
@@ -395,8 +407,21 @@ export async function runAuthorScout({
|
|
|
395
407
|
* judged. An instrument that misaddresses its own reading is the blind
|
|
396
408
|
* -instrument class, one field wide. */
|
|
397
409
|
let verdictAt = -1;
|
|
410
|
+
/** the operator's money ceiling, when it is what ended the ladder. `null` on
|
|
411
|
+
* every other path INCLUDING the unbounded one, so a reader never has to
|
|
412
|
+
* infer a governance stop from an absence.
|
|
413
|
+
* @type {'cap-halt'|'pricing-red'|null} */
|
|
414
|
+
let budgetStop = null;
|
|
398
415
|
|
|
399
416
|
while (attempt < allowed) {
|
|
417
|
+
// THE CEILING, between attempts. The attempt cap bounds how many times the
|
|
418
|
+
// survey may ask; it says nothing about what those asks cost, and this is
|
|
419
|
+
// the pipeline's FIRST paid stage — a ladder that spends the whole wallet
|
|
420
|
+
// before the declaration is ever authored is the failure this closes.
|
|
421
|
+
// Checked before the counter moves, so `attempt` stays the number of
|
|
422
|
+
// attempts that actually happened.
|
|
423
|
+
const halt = capStop({ ceilingUsd, ...tallyCalls(calls) });
|
|
424
|
+
if (halt) { budgetStop = halt; break; }
|
|
400
425
|
attempt += 1;
|
|
401
426
|
const label = attempt === 1 ? 'author-scout' : `author-scout#${attempt}`;
|
|
402
427
|
|
|
@@ -432,7 +457,29 @@ export async function runAuthorScout({
|
|
|
432
457
|
// F59's reserved TOOLLESS final round. It fires on the ONE population it
|
|
433
458
|
// was sized for — a survey that was CUT OFF and came back short — never
|
|
434
459
|
// on a survey that simply finished with little to say.
|
|
435
|
-
|
|
460
|
+
// …and the reserved round is a PAID CALL like any other, so it sits behind
|
|
461
|
+
// the same ceiling. Left outside it, the one call the cap most needs to
|
|
462
|
+
// stop is the one that escapes: the recovery fires exactly when the first
|
|
463
|
+
// call was cut off, which is exactly when the wallet is thinnest.
|
|
464
|
+
//
|
|
465
|
+
// THE CEILING IS ASKED ABOUT A CALL THAT WOULD ACTUALLY BE MADE, and the
|
|
466
|
+
// ORDER is what makes that true: the recovery predicate is settled FIRST,
|
|
467
|
+
// and `capStop` is consulted only when a recovery was genuinely pending.
|
|
468
|
+
// Asked unconditionally it answers a question nobody put — the wallet is
|
|
469
|
+
// over, but nothing was refused — and `budgetStop` means the ceiling is
|
|
470
|
+
// WHAT ENDED THE LADDER, which is then a lie on every other attempt-1
|
|
471
|
+
// ending: a dead socket on an unbounded first call, or a survey that came
|
|
472
|
+
// back PRESENT and needed no second call at all. Both then render as
|
|
473
|
+
// `cap-halt` at `path: budgetUsd`, steering the operator to raise a number
|
|
474
|
+
// that cannot move the outcome. Nothing about the REFUSAL moves here — the
|
|
475
|
+
// same predicate, the same tally, the same call not made (the top-of-loop
|
|
476
|
+
// latch still names the ceiling whenever a retry was due). This is which
|
|
477
|
+
// ending gets the label, and nothing else.
|
|
478
|
+
const recoveryWanted = bounded && Buffer.byteLength(blob) < minBytes
|
|
479
|
+
&& Array.isArray(r?.msgs) && r.msgs.length > 0;
|
|
480
|
+
const recoveryHalt = recoveryWanted ? capStop({ ceilingUsd, ...tallyCalls(calls) }) : null;
|
|
481
|
+
if (recoveryHalt) budgetStop = recoveryHalt;
|
|
482
|
+
if (recoveryWanted && !recoveryHalt) {
|
|
436
483
|
const recovery = createLoop({ system, policy: surveyor.policy, onLlmResult: surveyor.onLlmResult, provider });
|
|
437
484
|
s2 = await recovery.run([...r.msgs, { role: 'user', content: SCOUT_RECOVERY_PROMPT }], [],
|
|
438
485
|
{ cacheMessages: true, maxTokens });
|
|
@@ -484,12 +531,26 @@ export async function runAuthorScout({
|
|
|
484
531
|
if (!SCOUT_RETRY_CAUSES.includes(/** @type {string} */ (survey.cause))) break;
|
|
485
532
|
}
|
|
486
533
|
|
|
487
|
-
|
|
534
|
+
// THE SURVEY THAT WAS NEVER ASKED FOR. The ceiling was already spent when the
|
|
535
|
+
// first attempt came due, so no call was made and there is no reply to
|
|
536
|
+
// classify. It is spelled out rather than run through `classifySurvey`, which
|
|
537
|
+
// would read the empty blob and report that the model came back EMPTY — a
|
|
538
|
+
// model that never spoke cannot have said nothing, and blaming it would send
|
|
539
|
+
// the operator to the scout when the fact is the wallet.
|
|
540
|
+
const final = /** @type {Survey} */ (survey ?? {
|
|
541
|
+
state: 'ABSENT',
|
|
542
|
+
facts: null,
|
|
543
|
+
cause: SURVEY_CAUSES.NOT_FUNDED,
|
|
544
|
+
reason: budgetStop === 'pricing-red'
|
|
545
|
+
? 'the survey was never asked for: spend could not be priced, so the ceiling could not be enforced (F6)'
|
|
546
|
+
: `the survey was never asked for: the $${ceilingUsd} ceiling was already spent`,
|
|
547
|
+
meta: { bytes: 0, rounds: 0, bounded: false, recovered: false, error: null, attempts: 0, attemptsAllowed: allowed },
|
|
548
|
+
});
|
|
488
549
|
// Exhaustion is still the same honest ABSENT — it now says how many times we
|
|
489
550
|
// paid to find out. One attempt says nothing about retries, because there
|
|
490
551
|
// were none.
|
|
491
552
|
const reason = final.reason !== null && attempt > 1 ? `${final.reason} — after ${attempt} attempts` : final.reason;
|
|
492
|
-
return { ...final, reason, raw: blob, raws, calls };
|
|
553
|
+
return { ...final, reason, raw: blob, raws, calls, budgetStop };
|
|
493
554
|
} finally {
|
|
494
555
|
// a leaked surveyor leaks a gate handle and a litectx index — always, even
|
|
495
556
|
// when the loop threw
|
package/src/bridges.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
36
36
|
import { join } from 'node:path';
|
|
37
|
-
import { isObj, isNonEmptyString } from './validate.js';
|
|
37
|
+
import { isObj, isNonEmptyString, sweepNestedQuantifiers } from './validate.js';
|
|
38
38
|
import { TOOL_MENU, LOCKED_TOOLS } from './job.js';
|
|
39
39
|
import { closeStagesOf } from './plan.js';
|
|
40
40
|
|
|
@@ -183,6 +183,19 @@ export function validateBridge(input) {
|
|
|
183
183
|
if (!Array.isArray(e.history)) red('invalid-value', 'history', 'array of per-run rows');
|
|
184
184
|
else e.history.forEach((/** @type {unknown} */ h, /** @type {number} */ i) => validateHistoryRow(h, `history.${i}.`, red));
|
|
185
185
|
|
|
186
|
+
// PRD v1.60 §1 — the operator-regex admissibility sweep, whose population was
|
|
187
|
+
// `jobs/*.json` and is now every signed spec wherever it lives. A bridge ships
|
|
188
|
+
// its plans VERBATIM, and a stored `artifact-written.pattern` reaches the same
|
|
189
|
+
// untimed exit evaluator on the same main event loop that F49 measured. The
|
|
190
|
+
// reject is UNCHANGED (`hasNestedQuantifier`, one inventory) — only the
|
|
191
|
+
// population is wider, so a clean bridge is byte-identically clean.
|
|
192
|
+
//
|
|
193
|
+
// Whole-entry rather than per-field, because a bridge's regexes sit inside a
|
|
194
|
+
// plan stored as opaque data (`isObj(o.plan)` is all this validator asserts
|
|
195
|
+
// about it): enumerating a path into a document this one deliberately does not
|
|
196
|
+
// parse is how a sweep goes blind the next time the plan schema moves.
|
|
197
|
+
sweepNestedQuantifiers(e, red);
|
|
198
|
+
|
|
186
199
|
// R1's coherence, checked only once everything it reads is itself valid (a
|
|
187
200
|
// second red on an already-broken field would bury the real defect): every green
|
|
188
201
|
// mints exactly one version, and a red mints none.
|
package/src/index.js
CHANGED
|
@@ -96,6 +96,9 @@ export {
|
|
|
96
96
|
export {
|
|
97
97
|
validateDeclaration, normalizeDeclaration, KIND_CATALOGUE, CATALOGUE_KINDS, CATALOGUE_LIVE_KINDS,
|
|
98
98
|
LOCKED_KINDS, TYPES_GENRE, TYPES_GENRE_TEMPLATE, GENRE_LANGUAGES, genreEnv, genreOwnedEnvNames,
|
|
99
|
+
// how the genre's own tools PRINT — genre property for the same reason the
|
|
100
|
+
// suppression battery is (2026-08-09, run msmbpjk6)
|
|
101
|
+
genreInstruments,
|
|
99
102
|
DIRECTIONS, BASELINES, MAX_STAGES,
|
|
100
103
|
// the verdict-class surface (PRD v1.57 §1–§2): the radio's own menu, the guard
|
|
101
104
|
// battery keyed off it, and the ceiling that makes the pick a promise
|
package/src/kinds.js
CHANGED
|
@@ -346,24 +346,55 @@ export async function seedListing(workdir, seedRef) {
|
|
|
346
346
|
// ── contract (c): the changed-set primitive ──────────────────────────────────
|
|
347
347
|
|
|
348
348
|
/**
|
|
349
|
-
* The
|
|
349
|
+
* The arbiter books, and the shape of each exclusion is the whole point.
|
|
350
350
|
*
|
|
351
|
-
* `.litectx/`
|
|
352
|
-
* arbiter. `gate-audit.jsonl` is an EXACT repo-relative path and
|
|
353
|
-
* basename or a pattern: the gate audit is written to
|
|
351
|
+
* `.litectx/` and `.smoke/` are PREFIXES — everything beneath either store
|
|
352
|
+
* belongs to the arbiter. `gate-audit.jsonl` is an EXACT repo-relative path and
|
|
353
|
+
* never a basename or a pattern: the gate audit is written to
|
|
354
354
|
* `join(workdir, 'gate-audit.jsonl')` and nowhere else, so a worker-authored
|
|
355
355
|
* `src/gate-audit.jsonl` is NOT an arbiter book and must still count as work.
|
|
356
356
|
* Matching by basename would swallow it — and swallowing a worker's real
|
|
357
|
-
* write is the failure direction that reads as a clean tree.
|
|
357
|
+
* write is the failure direction that reads as a clean tree. `src/smoke.js` and
|
|
358
|
+
* `tests/smoke.test.js` are that same decoy for the store below.
|
|
359
|
+
*
|
|
360
|
+
* `.smoke/` — F98's parked residual, closed here. The fence has always denied
|
|
361
|
+
* `join(workdir, '.smoke')` on every worker (src/planrun.js, src/authorscout.js)
|
|
362
|
+
* and the persona now states the rule; this reader, which decides what COUNTS AS
|
|
363
|
+
* WORK, was the one place that never knew the name. On patients whose .gitignore
|
|
364
|
+
* default-denies dot-directories the store never reaches `ls-files --others` and
|
|
365
|
+
* the gap is masked — by the PATIENT's config, not by anything here. Incidental
|
|
366
|
+
* masking is not an exclusion: on a patient without that line the arbiter's own
|
|
367
|
+
* store would enter the changed set as the worker's writing.
|
|
358
368
|
* @param {string} rel a repo-relative POSIX path
|
|
359
369
|
* @returns {string|null} the book it belongs to, or null
|
|
360
370
|
*/
|
|
361
371
|
export function isArbiterBook(rel) {
|
|
362
|
-
|
|
363
|
-
|
|
372
|
+
for (const store of ARBITER_BOOK_STORES) {
|
|
373
|
+
if (rel === store || rel.startsWith(`${store}/`)) return `${store}/`;
|
|
374
|
+
}
|
|
375
|
+
if (rel === GATE_AUDIT_FILE) return GATE_AUDIT_FILE;
|
|
364
376
|
return null;
|
|
365
377
|
}
|
|
366
378
|
|
|
379
|
+
/**
|
|
380
|
+
* The arbiter's book NAMES, one home. F98's mechanism was exactly this list
|
|
381
|
+
* living as independent literals: the fence deny arrays (src/planrun.js,
|
|
382
|
+
* src/authorscout.js) knew `.smoke` while `isArbiterBook` — the reader that
|
|
383
|
+
* decides what COUNTS AS WORK — did not, and the drift was masked by the
|
|
384
|
+
* patient's own .gitignore. Every consumer (the fence deny lists, this file's
|
|
385
|
+
* reader, the persona's stated law, run.js's smoke-store root) now spells the
|
|
386
|
+
* names from here, so adding or renaming a book is one edit that cannot leave
|
|
387
|
+
* a site behind.
|
|
388
|
+
*
|
|
389
|
+
* `ARBITER_BOOK_STORES` are directory PREFIXES; `GATE_AUDIT_FILE` is an EXACT
|
|
390
|
+
* repo-relative path (see `isArbiterBook`'s docstring for why never a
|
|
391
|
+
* basename). Order is the fence deny-list order and the persona's prose order.
|
|
392
|
+
*/
|
|
393
|
+
export const SMOKE_STORE = '.smoke';
|
|
394
|
+
export const LITECTX_STORE = '.litectx';
|
|
395
|
+
export const GATE_AUDIT_FILE = 'gate-audit.jsonl';
|
|
396
|
+
export const ARBITER_BOOK_STORES = Object.freeze([SMOKE_STORE, LITECTX_STORE]);
|
|
397
|
+
|
|
367
398
|
/**
|
|
368
399
|
* Everything this run changed against the seed: the tracked diff PLUS
|
|
369
400
|
* untracked files (an untracked file has no diff — the case a naive
|
|
@@ -699,13 +730,36 @@ export function normalizeParser(parser, at = 'parser') {
|
|
|
699
730
|
* that matched BEFORE the filter judged them — kept, unattributable and dropped
|
|
700
731
|
* alike — and it is the only evidence of liveness a caller has once the scope
|
|
701
732
|
* has removed the whole population. It is never the number being read.
|
|
733
|
+
*
|
|
734
|
+
* `matched` is the KEPT lines themselves, PER TERM and index-aligned with
|
|
735
|
+
* `breakdown`: entry i holds, in output order, exactly the lines term i's
|
|
736
|
+
* subtotal was computed FROM. Run u-msn227nq paid for the echo — the executor
|
|
737
|
+
* counted 8 real `error TS…` lines and told the worker "8 match(es)", discarding
|
|
738
|
+
* every address, and a worker with a number and no file to open went looking for
|
|
739
|
+
* the arbiter's own books instead.
|
|
740
|
+
*
|
|
741
|
+
* TWO EXCLUSIONS, both of them "this line is not what the count is made of":
|
|
742
|
+
* - a scope-DROPPED line is never in here. It was never counted, so aiming the
|
|
743
|
+
* worker at it would aim it outside its own population.
|
|
744
|
+
* - under `first`, only the line the aggregate actually READ. The later matches
|
|
745
|
+
* are real matches (`breakdown[i].matches` still counts them) but the value
|
|
746
|
+
* never contained them, and echoing them points the worker at a number that
|
|
747
|
+
* is not in the reading.
|
|
748
|
+
*
|
|
749
|
+
* And the GROUPING is the third: a term's sign lives in `breakdown[i]`, so lines
|
|
750
|
+
* pooled flat across terms lose the one fact that says what they DID to the
|
|
751
|
+
* number. A subtracted term's line rendered beside an added one reads as a wall
|
|
752
|
+
* to go fix, and fixing it moves the count the wrong way. Deduplication is
|
|
753
|
+
* therefore within a term only — one line matched by two terms plays two
|
|
754
|
+
* arithmetic roles and is named under each. The count is computed from `values`,
|
|
755
|
+
* never from this list.
|
|
702
756
|
* @param {string} output @param {any[]} terms normalised terms
|
|
703
757
|
* Every stop here is `crashed` in the arbiter's vocabulary — the command RAN,
|
|
704
758
|
* came back, and the number could not be read out of it. Nothing in this
|
|
705
759
|
* function can observe a spawn failure, a timeout or a signal, so it never
|
|
706
760
|
* claims one.
|
|
707
761
|
* @param {{scope?: any, workdir?: string}} [o]
|
|
708
|
-
* @returns {{stop: string, fault: string, notes?: string[]}|{stop: null, value: number, breakdown: any[], notes: string[], preScopeMatches: number}}
|
|
762
|
+
* @returns {{stop: string, fault: string, notes?: string[]}|{stop: null, value: number, breakdown: any[], notes: string[], preScopeMatches: number, matched: string[][]}}
|
|
709
763
|
*/
|
|
710
764
|
export function parseValue(output, terms, { scope = null, workdir = '' } = {}) {
|
|
711
765
|
let value = 0;
|
|
@@ -715,6 +769,10 @@ export function parseValue(output, terms, { scope = null, workdir = '' } = {}) {
|
|
|
715
769
|
const breakdown = [];
|
|
716
770
|
/** @type {string[]} */
|
|
717
771
|
const notes = [];
|
|
772
|
+
// the KEPT lines PER TERM, in output order and deduplicated within the term —
|
|
773
|
+
// see the note above
|
|
774
|
+
/** @type {string[][]} */
|
|
775
|
+
const matched = [];
|
|
718
776
|
const filtering = scopeFilters(scope);
|
|
719
777
|
// one resolver (and one realpath cache) per call, built only when a filter
|
|
720
778
|
// will actually read it
|
|
@@ -739,6 +797,9 @@ export function parseValue(output, terms, { scope = null, workdir = '' } = {}) {
|
|
|
739
797
|
try { re = new RegExp(t.lineMatch, 'g'); } catch (e) { return { stop: `INSTRUMENT: term ${i} lineMatch /${t.lineMatch}/ is not a valid regex: ${String(/** @type {any} */ (e)?.message ?? e)}`, fault: STOP_FAULTS.CRASHED }; }
|
|
740
798
|
/** @type {number[]} */
|
|
741
799
|
const values = [];
|
|
800
|
+
/** @type {string[]} */
|
|
801
|
+
const kept = [];
|
|
802
|
+
const seen = new Set();
|
|
742
803
|
let dropped = 0;
|
|
743
804
|
let unattributable = 0;
|
|
744
805
|
for (const line of region.split('\n')) {
|
|
@@ -750,6 +811,10 @@ export function parseValue(output, terms, { scope = null, workdir = '' } = {}) {
|
|
|
750
811
|
if (p === null) unattributable += 1;
|
|
751
812
|
else if (!inScope(p, scope, phys)) { dropped += 1; continue; }
|
|
752
813
|
}
|
|
814
|
+
// past the filter, so this line is a CANDIDATE for the harvest — kept here
|
|
815
|
+
// rather than at aggregation because this is the only place the raw line
|
|
816
|
+
// still exists, and narrowed to what the aggregate actually read below
|
|
817
|
+
if (!seen.has(line)) { seen.add(line); kept.push(line); }
|
|
753
818
|
for (const h of hits) {
|
|
754
819
|
if (t.capture === null) { values.push(1); continue; }
|
|
755
820
|
const rawCapture = h[t.capture];
|
|
@@ -789,10 +854,14 @@ export function parseValue(output, terms, { scope = null, workdir = '' } = {}) {
|
|
|
789
854
|
}
|
|
790
855
|
subtotal = values[0];
|
|
791
856
|
}
|
|
857
|
+
// `first` read ONE line — `kept[0]`, since `values[0]` is the first hit on
|
|
858
|
+
// the first kept line. The rest matched (and `matches` below still says so)
|
|
859
|
+
// but are not what this subtotal is made of.
|
|
860
|
+
matched.push(t.aggregate === 'first' ? kept.slice(0, 1) : kept);
|
|
792
861
|
breakdown.push({ term: i, lineMatch: t.lineMatch, aggregate: t.aggregate, sign: t.sign, matches: values.length, subtotal, contribution: t.sign * subtotal });
|
|
793
862
|
value += t.sign * subtotal;
|
|
794
863
|
}
|
|
795
|
-
return { stop: null, value, breakdown, notes, preScopeMatches };
|
|
864
|
+
return { stop: null, value, breakdown, notes, preScopeMatches, matched };
|
|
796
865
|
}
|
|
797
866
|
|
|
798
867
|
// ── D12: the baseline is MEASURED at this run's own seed, every run ──────────
|
|
@@ -1084,7 +1153,7 @@ async function runCountNotWorse(stage, ctx) {
|
|
|
1084
1153
|
notes: [],
|
|
1085
1154
|
};
|
|
1086
1155
|
}
|
|
1087
|
-
return { stop: null, value: v.value, breakdown: v.breakdown, notes: v.notes, exit: r.code, dropped: r.dropped, fault: STOP_FAULTS.FAILED };
|
|
1156
|
+
return { stop: null, value: v.value, breakdown: v.breakdown, notes: v.notes, matched: v.matched, exit: r.code, dropped: r.dropped, fault: STOP_FAULTS.FAILED };
|
|
1088
1157
|
};
|
|
1089
1158
|
|
|
1090
1159
|
const now = await measure(ctx.workdir, 'the current tree');
|
|
@@ -1126,8 +1195,30 @@ async function runCountNotWorse(stage, ctx) {
|
|
|
1126
1195
|
if (worse) {
|
|
1127
1196
|
gap.push(`${stage.name}: ${value} against a baseline of ${baseline} (${p.direction}) — worse`);
|
|
1128
1197
|
gap.push(` baseline ${baselineSource}`);
|
|
1129
|
-
|
|
1198
|
+
// …each term stating itself, and THEN the lines that term's subtotal is made
|
|
1199
|
+
// of, nested under it. A count with no addresses is the semantic genre F38
|
|
1200
|
+
// measured as inert: run u-msn227nq's worker was handed "8 match(es)", had
|
|
1201
|
+
// nowhere to open, and probed the arbiter's books until the deny guard ended
|
|
1202
|
+
// the run. The instrument NAMED these lines — echoing what it named is the
|
|
1203
|
+
// same licence `pattern-absent-in-diff` already runs under, not the barred
|
|
1204
|
+
// move of naming a culprit the instrument never reported.
|
|
1205
|
+
//
|
|
1206
|
+
// NESTED, not pooled, because a line's arithmetic role is the term's and only
|
|
1207
|
+
// the term's row carries it. `value` is a SIGNED sum: a term declared
|
|
1208
|
+
// `sign: -1` subtracts, so its lines are ones the count went DOWN for, and a
|
|
1209
|
+
// worker handed them flat beside the added ones reads every line as a wall
|
|
1210
|
+
// and "fixes" one that moves the number the wrong way. The row directly above
|
|
1211
|
+
// each block already says `contributes +n` / `-n`; sitting the lines under it
|
|
1212
|
+
// is what makes that statement reach them. Nothing is withheld — under
|
|
1213
|
+
// `higher-is-better` the subtracted term is exactly where the news usually is
|
|
1214
|
+
// — and nothing is relabelled: the lines are still verbatim, one indent in.
|
|
1215
|
+
//
|
|
1216
|
+
// They ride the Gap like every other line: the stage prefix (Layer R's
|
|
1217
|
+
// redKeep derives from it), the cap, and the trim marker on overflow.
|
|
1218
|
+
const perTerm = /** @type {string[][]} */ (now.matched ?? []);
|
|
1219
|
+
for (const [i, b] of /** @type {any[]} */ (now.breakdown).entries()) {
|
|
1130
1220
|
gap.push(` term ${b.term} /${b.lineMatch}/ ${b.aggregate}: ${b.matches} match(es), subtotal ${b.subtotal}, contributes ${b.contribution >= 0 ? '+' : ''}${b.contribution}`);
|
|
1221
|
+
for (const l of perTerm[i] ?? []) gap.push(` ${l}`);
|
|
1131
1222
|
}
|
|
1132
1223
|
}
|
|
1133
1224
|
return result(stage, {
|
package/src/ledger.js
CHANGED
|
@@ -75,7 +75,11 @@ const EXCLUDED_ESCALATIONS = new Set([
|
|
|
75
75
|
// real upstream ask would be sourced from anyway.
|
|
76
76
|
'step-stalled',
|
|
77
77
|
'step-variance', // A's replan trigger: a planning story, never a lib bug
|
|
78
|
-
|
|
78
|
+
// Governance working as intended — and no longer minted anywhere in this tree
|
|
79
|
+
// (a denial streak is a bounded attempt, not a terminal). Kept listed because
|
|
80
|
+
// this set is the EXECUTABLE excluded-set: dropping a name does not delete the
|
|
81
|
+
// category, it re-files any future emission of it as a counted capability gap.
|
|
82
|
+
'gate-red',
|
|
79
83
|
'smoke-red', // already counted via primitive-smoke
|
|
80
84
|
'hitl-close', // by design: a human is the close
|
|
81
85
|
'close-unsupported', // honest refusal, by design
|