backpass 0.1.13 → 0.1.14
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/README.md +4 -1
- package/package.json +1 -1
- package/src/fold.js +4 -0
- package/src/proposal.js +7 -0
- package/templates/apply.html +290 -22
package/README.md
CHANGED
|
@@ -313,7 +313,10 @@ not duplicate content.
|
|
|
313
313
|
|
|
314
314
|
`backpass apply` is the only command that writes. It serves a review surface through
|
|
315
315
|
[`lavish-axi`](https://github.com/kunchenguid/lavish-axi): one card per edit with the diff,
|
|
316
|
-
the evidence quotes and their sources, a live budget gauge, and ACCEPT / REJECT.
|
|
316
|
+
the evidence quotes and their sources, a live budget gauge, and ACCEPT / REJECT. A compact
|
|
317
|
+
gap funnel shows how accumulated sightings narrowed through domain filtering, clustering,
|
|
318
|
+
and the corroboration floor to the gaps eligible for a proposal; older proposals without
|
|
319
|
+
recorded funnel counts omit it.
|
|
317
320
|
|
|
318
321
|
The surface is a static template shipped in the package - the CLI injects one JSON payload,
|
|
319
322
|
so it is instant, deterministic, and identical every run. Nothing there is model-generated.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpass",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"description": "Gradient descent for your agent memory - analyzes past agent session transcripts and proposes evidence-backed edits to AGENTS.md / CLAUDE.md",
|
|
6
6
|
"type": "module",
|
package/src/fold.js
CHANGED
|
@@ -143,6 +143,10 @@ export function foldEvidence(evidenceRecords, { minGapEvidence = 2, memoryFile =
|
|
|
143
143
|
totals: {
|
|
144
144
|
positive: positiveCount,
|
|
145
145
|
negative: negativeCount,
|
|
146
|
+
// The gap funnel's top: every sighting this fold clustered over (the pruned ledger
|
|
147
|
+
// when one is passed, this run's records otherwise). Orchestration sightings are the
|
|
148
|
+
// slice excluded before clustering; project-domain is the difference.
|
|
149
|
+
gapSightings: allObservations.length,
|
|
146
150
|
gapClusters: gaps.length,
|
|
147
151
|
droppedGapSingletons,
|
|
148
152
|
orchestrationGapSightings,
|
package/src/proposal.js
CHANGED
|
@@ -489,6 +489,13 @@ export function buildProposal(rawResult, context) {
|
|
|
489
489
|
positive: summary?.totals?.positive ?? 0,
|
|
490
490
|
negative: summary?.totals?.negative ?? 0,
|
|
491
491
|
gapClusters: summary?.totals?.gapClusters ?? 0,
|
|
492
|
+
// The gap funnel, for the apply surface: sightings -> project-domain (sightings
|
|
493
|
+
// minus orchestration) -> distinct gaps (clusters + dropped) -> eligible
|
|
494
|
+
// (clusters). `null`, never 0, when the summary predates a count - the surface
|
|
495
|
+
// hides what was not recorded rather than showing an invented zero.
|
|
496
|
+
gapSightings: summary?.totals?.gapSightings ?? null,
|
|
497
|
+
orchestrationGapSightings: summary?.totals?.orchestrationGapSightings ?? null,
|
|
498
|
+
droppedGapSingletons: summary?.totals?.droppedGapSingletons ?? null,
|
|
492
499
|
skillExtractions: accepted.reduce((n, e) => n + editSkills(e).length, 0),
|
|
493
500
|
},
|
|
494
501
|
edits: accepted,
|
package/templates/apply.html
CHANGED
|
@@ -126,6 +126,124 @@
|
|
|
126
126
|
margin-top: 3px;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/* ---------- gap funnel ---------- */
|
|
130
|
+
/* The template's own display rules (.statrow's grid) would beat the UA
|
|
131
|
+
stylesheet's [hidden]; this keeps the hidden attribute authoritative. */
|
|
132
|
+
[hidden] {
|
|
133
|
+
display: none !important;
|
|
134
|
+
}
|
|
135
|
+
.run-context {
|
|
136
|
+
border: 1px solid var(--line);
|
|
137
|
+
background: var(--line);
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr);
|
|
140
|
+
gap: 1px;
|
|
141
|
+
margin-bottom: 36px;
|
|
142
|
+
}
|
|
143
|
+
.ctx-funnel {
|
|
144
|
+
background: var(--panel);
|
|
145
|
+
padding: 13px 18px 14px;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
}
|
|
148
|
+
.ctx-stats {
|
|
149
|
+
display: grid;
|
|
150
|
+
grid-template-columns: 1fr 1fr;
|
|
151
|
+
grid-auto-rows: 1fr;
|
|
152
|
+
gap: 1px;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
}
|
|
155
|
+
.funnel-head {
|
|
156
|
+
display: flex;
|
|
157
|
+
justify-content: space-between;
|
|
158
|
+
align-items: baseline;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
gap: 8px;
|
|
161
|
+
margin-bottom: 10px;
|
|
162
|
+
}
|
|
163
|
+
.funnel-head .t {
|
|
164
|
+
font-size: 13px;
|
|
165
|
+
color: var(--dim);
|
|
166
|
+
}
|
|
167
|
+
.funnel-head .facts {
|
|
168
|
+
font-family: var(--mono);
|
|
169
|
+
font-size: 11px;
|
|
170
|
+
color: var(--faint);
|
|
171
|
+
}
|
|
172
|
+
.funnel-head .facts b {
|
|
173
|
+
color: var(--text);
|
|
174
|
+
font-weight: 500;
|
|
175
|
+
}
|
|
176
|
+
.frow {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 12px;
|
|
180
|
+
margin: 4px 0;
|
|
181
|
+
}
|
|
182
|
+
.frow .fl {
|
|
183
|
+
width: 148px;
|
|
184
|
+
flex: none;
|
|
185
|
+
font-family: var(--mono);
|
|
186
|
+
font-size: 10px;
|
|
187
|
+
letter-spacing: 0.11em;
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
color: var(--faint);
|
|
190
|
+
white-space: nowrap;
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
text-overflow: ellipsis;
|
|
193
|
+
}
|
|
194
|
+
.frow .fb {
|
|
195
|
+
flex: 1;
|
|
196
|
+
min-width: 0;
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
height: 16px;
|
|
200
|
+
background: var(--panel-2);
|
|
201
|
+
border: 1px solid var(--line-soft);
|
|
202
|
+
}
|
|
203
|
+
.frow .fb i {
|
|
204
|
+
display: block;
|
|
205
|
+
height: 100%;
|
|
206
|
+
background: rgba(110, 168, 255, 0.38);
|
|
207
|
+
border-right: 1px solid rgba(110, 168, 255, 0.75);
|
|
208
|
+
}
|
|
209
|
+
.frow.lit .fb i {
|
|
210
|
+
background: rgba(79, 227, 193, 0.4);
|
|
211
|
+
border-right-color: var(--accent);
|
|
212
|
+
}
|
|
213
|
+
.frow.zero .fb i {
|
|
214
|
+
border-right: none;
|
|
215
|
+
}
|
|
216
|
+
.frow .fv {
|
|
217
|
+
flex: none;
|
|
218
|
+
width: 34px;
|
|
219
|
+
font-family: var(--mono);
|
|
220
|
+
font-size: 12.5px;
|
|
221
|
+
color: var(--text);
|
|
222
|
+
}
|
|
223
|
+
.frow.zero .fv {
|
|
224
|
+
color: var(--faint);
|
|
225
|
+
}
|
|
226
|
+
.frow.lit .fv {
|
|
227
|
+
color: var(--accent);
|
|
228
|
+
}
|
|
229
|
+
.fdrop {
|
|
230
|
+
margin: 1px 0 6px 160px;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
color: var(--dim);
|
|
233
|
+
}
|
|
234
|
+
.fdrop b {
|
|
235
|
+
font-family: var(--mono);
|
|
236
|
+
font-weight: 500;
|
|
237
|
+
color: var(--faint);
|
|
238
|
+
}
|
|
239
|
+
.funnel-note {
|
|
240
|
+
margin-top: 10px;
|
|
241
|
+
padding-top: 9px;
|
|
242
|
+
border-top: 1px dashed var(--line-soft);
|
|
243
|
+
font-size: 12.5px;
|
|
244
|
+
color: var(--dim);
|
|
245
|
+
}
|
|
246
|
+
|
|
129
247
|
/* ---------- budget gauge ---------- */
|
|
130
248
|
.gauge-block {
|
|
131
249
|
border: 1px solid var(--line);
|
|
@@ -564,7 +682,7 @@
|
|
|
564
682
|
cursor: not-allowed;
|
|
565
683
|
}
|
|
566
684
|
|
|
567
|
-
@media (max-width:
|
|
685
|
+
@media (max-width: 760px) {
|
|
568
686
|
.wrap {
|
|
569
687
|
padding: 28px 16px 170px;
|
|
570
688
|
}
|
|
@@ -572,6 +690,15 @@
|
|
|
572
690
|
padding: 12px 16px;
|
|
573
691
|
gap: 12px;
|
|
574
692
|
}
|
|
693
|
+
.run-context {
|
|
694
|
+
grid-template-columns: minmax(0, 1fr);
|
|
695
|
+
}
|
|
696
|
+
.frow .fl {
|
|
697
|
+
width: 92px;
|
|
698
|
+
}
|
|
699
|
+
.fdrop {
|
|
700
|
+
margin-left: 0;
|
|
701
|
+
}
|
|
575
702
|
}
|
|
576
703
|
</style>
|
|
577
704
|
</head>
|
|
@@ -584,7 +711,19 @@
|
|
|
584
711
|
</div>
|
|
585
712
|
<div class="runline" id="runline"></div>
|
|
586
713
|
|
|
587
|
-
<div class="statrow" id="statrow"></div>
|
|
714
|
+
<div class="statrow" id="statrow" hidden></div>
|
|
715
|
+
|
|
716
|
+
<div class="run-context" id="ctx" hidden>
|
|
717
|
+
<div class="ctx-funnel">
|
|
718
|
+
<div class="funnel-head">
|
|
719
|
+
<span class="t">Gap funnel · counted across runs</span>
|
|
720
|
+
<span class="facts num" id="funnel-floor"></span>
|
|
721
|
+
</div>
|
|
722
|
+
<div id="funnel-bars"></div>
|
|
723
|
+
<div class="funnel-note" id="funnel-note" hidden></div>
|
|
724
|
+
</div>
|
|
725
|
+
<div class="ctx-stats" id="ctx-stats"></div>
|
|
726
|
+
</div>
|
|
588
727
|
|
|
589
728
|
<div class="gauge-block">
|
|
590
729
|
<div class="gauge-head">
|
|
@@ -668,26 +807,142 @@
|
|
|
668
807
|
document.createTextNode(" transcripts" + (harnessBits.length ? " (" + harnessBits.join(" · ") + ")" : "")),
|
|
669
808
|
);
|
|
670
809
|
|
|
671
|
-
// ---- stats ----
|
|
672
|
-
|
|
810
|
+
// ---- run context: one frame, the gap funnel beside the run stats ----
|
|
811
|
+
// A single bordered band. The left pane tells the gap story as proportional
|
|
812
|
+
// bars - sightings recorded by analysis -> project-domain (orchestration
|
|
813
|
+
// excluded) -> distinct gaps after clustering -> eligible to propose - with
|
|
814
|
+
// each drop-off explained in plain words. The right pane holds the run facts
|
|
815
|
+
// (edits, evidence, skills) as stat cells; a separate stat row would duplicate
|
|
816
|
+
// the funnel's last stage. Every count is recorded by the fold (`totals` in
|
|
817
|
+
// evidence-summary.json); a proposal saved before the counts existed falls
|
|
818
|
+
// back to the classic stat row rather than showing zeros nobody measured.
|
|
819
|
+
function funnelCounts() {
|
|
820
|
+
if (typeof (P.stats || {}).gapSightings !== "number") return null;
|
|
821
|
+
var eligible = P.stats.gapClusters || 0;
|
|
822
|
+
var dropped = P.stats.droppedGapSingletons || 0;
|
|
823
|
+
var orch = P.stats.orchestrationGapSightings || 0;
|
|
824
|
+
var project = Math.max(0, P.stats.gapSightings - orch);
|
|
825
|
+
return {
|
|
826
|
+
floor: Number((P.config || {}).minGapEvidence) || 2,
|
|
827
|
+
sighted: P.stats.gapSightings,
|
|
828
|
+
orch: orch,
|
|
829
|
+
project: project,
|
|
830
|
+
distinct: eligible + dropped,
|
|
831
|
+
merged: Math.max(0, project - (eligible + dropped)),
|
|
832
|
+
dropped: dropped,
|
|
833
|
+
eligible: eligible,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function funnelBar(host, value, label, max, lit) {
|
|
837
|
+
var row = el("div", "frow" + (value === 0 ? " zero" : lit ? " lit" : ""));
|
|
838
|
+
row.appendChild(el("span", "fl", label));
|
|
839
|
+
var track = el("div", "fb");
|
|
840
|
+
var fill = el("i");
|
|
841
|
+
fill.style.width = max > 0 ? Math.max((value / max) * 100, value > 0 ? 2 : 0) + "%" : "0%";
|
|
842
|
+
track.appendChild(fill);
|
|
843
|
+
row.appendChild(track);
|
|
844
|
+
row.appendChild(el("span", "fv num", fmt(value)));
|
|
845
|
+
host.appendChild(row);
|
|
846
|
+
}
|
|
847
|
+
function funnelDrop(host, n, text) {
|
|
848
|
+
if (!n) return;
|
|
849
|
+
var line = el("div", "fdrop");
|
|
850
|
+
line.appendChild(el("b", null, "− " + fmt(n) + " "));
|
|
851
|
+
line.appendChild(document.createTextNode(text));
|
|
852
|
+
host.appendChild(line);
|
|
853
|
+
}
|
|
854
|
+
var funnelNote = null;
|
|
855
|
+
var funnel = funnelCounts();
|
|
856
|
+
if (funnel) {
|
|
857
|
+
var bars = document.getElementById("funnel-bars");
|
|
858
|
+
funnelBar(bars, funnel.sighted, "sightings", funnel.sighted);
|
|
859
|
+
funnelDrop(
|
|
860
|
+
bars,
|
|
861
|
+
funnel.orch,
|
|
862
|
+
"orchestration: mistakes in how the task was run, not in this project - excluded from proposals by design",
|
|
863
|
+
);
|
|
864
|
+
funnelBar(bars, funnel.project, "project-domain", funnel.sighted);
|
|
865
|
+
funnelDrop(
|
|
866
|
+
bars,
|
|
867
|
+
funnel.merged,
|
|
868
|
+
"merged: repeat sightings of a gap already counted - they corroborate it rather than add a new one",
|
|
869
|
+
);
|
|
870
|
+
funnelBar(bars, funnel.distinct, "distinct gaps", funnel.sighted);
|
|
871
|
+
funnelDrop(
|
|
872
|
+
bars,
|
|
873
|
+
funnel.dropped,
|
|
874
|
+
"below the corroboration floor: seen in fewer than " +
|
|
875
|
+
funnel.floor +
|
|
876
|
+
" sessions so far - more transcripts can corroborate them",
|
|
877
|
+
);
|
|
878
|
+
funnelBar(bars, funnel.eligible, "eligible to propose", funnel.sighted, true);
|
|
879
|
+
|
|
880
|
+
document.getElementById("funnel-floor").textContent = "corroboration floor · " + funnel.floor + " sessions";
|
|
881
|
+
var statHost = document.getElementById("ctx-stats");
|
|
673
882
|
[
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
883
|
+
[
|
|
884
|
+
String(edits.length) + "/" + String((P.config || {}).maxEditsPerRun || edits.length),
|
|
885
|
+
"edits proposed / cap",
|
|
886
|
+
],
|
|
887
|
+
[fmt(P.stats.positive), "positive evidence"],
|
|
888
|
+
[fmt(P.stats.negative), "negative evidence"],
|
|
889
|
+
[fmt(P.stats.skillExtractions), "skill extractions"],
|
|
890
|
+
].forEach(function (s) {
|
|
891
|
+
var box = el("div", "stat");
|
|
892
|
+
box.appendChild(el("div", "v num", s[0]));
|
|
893
|
+
box.appendChild(el("div", "k", s[1]));
|
|
894
|
+
statHost.appendChild(box);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// The drop lines carry the takeaway; the visible note only speaks when there
|
|
898
|
+
// are no bars to read. funnelNote itself still feeds the zero-edit empty state.
|
|
899
|
+
if (!funnel.sighted) {
|
|
900
|
+
funnelNote = "Analysis reported no uncovered gaps.";
|
|
901
|
+
} else if (!funnel.eligible && funnel.dropped) {
|
|
902
|
+
funnelNote =
|
|
903
|
+
funnel.dropped +
|
|
904
|
+
" distinct gap" +
|
|
905
|
+
(funnel.dropped === 1 ? " is" : "s are") +
|
|
906
|
+
" still below the " +
|
|
907
|
+
funnel.floor +
|
|
908
|
+
"-session corroboration floor - more transcripts may corroborate " +
|
|
909
|
+
(funnel.dropped === 1 ? "it." : "them.");
|
|
910
|
+
} else if (!funnel.eligible && funnel.orch >= funnel.sighted) {
|
|
911
|
+
funnelNote =
|
|
912
|
+
"All " +
|
|
913
|
+
funnel.sighted +
|
|
914
|
+
" sighting" +
|
|
915
|
+
(funnel.sighted === 1 ? " was" : "s were") +
|
|
916
|
+
" orchestration-domain (caused by the orchestrating harness, not this repository) and " +
|
|
917
|
+
(funnel.sighted === 1 ? "is" : "are") +
|
|
918
|
+
" excluded by design.";
|
|
919
|
+
}
|
|
920
|
+
if (funnelNote && !funnel.sighted) {
|
|
921
|
+
var noteEl = document.getElementById("funnel-note");
|
|
922
|
+
noteEl.textContent = funnelNote;
|
|
923
|
+
noteEl.hidden = false;
|
|
924
|
+
}
|
|
925
|
+
document.getElementById("ctx").hidden = false;
|
|
926
|
+
} else {
|
|
927
|
+
// A proposal from before the funnel counts existed: the classic stat row.
|
|
928
|
+
var statrow = document.getElementById("statrow");
|
|
929
|
+
statrow.hidden = false;
|
|
930
|
+
[
|
|
931
|
+
[
|
|
932
|
+
String(edits.length) + "/" + String((P.config || {}).maxEditsPerRun || edits.length),
|
|
933
|
+
"edits proposed / cap",
|
|
934
|
+
],
|
|
935
|
+
[fmt(P.stats.positive), "positive evidence"],
|
|
936
|
+
[fmt(P.stats.negative), "negative evidence"],
|
|
937
|
+
[fmt(P.stats.gapClusters), "eligible gaps"],
|
|
938
|
+
[fmt(P.stats.skillExtractions), "skill extractions"],
|
|
939
|
+
].forEach(function (s) {
|
|
940
|
+
var box = el("div", "stat");
|
|
941
|
+
box.appendChild(el("div", "v num", s[0]));
|
|
942
|
+
box.appendChild(el("div", "k", s[1]));
|
|
943
|
+
statrow.appendChild(box);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
691
946
|
|
|
692
947
|
// ---- budget gauge ----
|
|
693
948
|
document.getElementById("gauge-title").textContent = "Always-loaded budget · " + (P.memoryFile.path || "");
|
|
@@ -700,8 +955,21 @@
|
|
|
700
955
|
edits.length + " of " + ((P.config || {}).maxEditsPerRun || edits.length) + " (learning-rate cap)";
|
|
701
956
|
|
|
702
957
|
if (!edits.length) {
|
|
958
|
+
var eligibleWithoutEdits = Number((P.stats || {}).gapClusters) || 0;
|
|
959
|
+
var emptyReason = funnelNote;
|
|
960
|
+
if (!emptyReason && eligibleWithoutEdits > 0) {
|
|
961
|
+
emptyReason =
|
|
962
|
+
fmt(eligibleWithoutEdits) +
|
|
963
|
+
" eligible gap" +
|
|
964
|
+
(eligibleWithoutEdits === 1 ? "" : "s") +
|
|
965
|
+
" reached synthesis, but no edit was proposed this run.";
|
|
966
|
+
}
|
|
703
967
|
host.appendChild(
|
|
704
|
-
el(
|
|
968
|
+
el(
|
|
969
|
+
"div",
|
|
970
|
+
"empty",
|
|
971
|
+
"No edits proposed. " + (emptyReason || "The evidence did not clear the thresholds this run."),
|
|
972
|
+
),
|
|
705
973
|
);
|
|
706
974
|
}
|
|
707
975
|
|