feat(mcp): score-proportional byte allocation for explore, with a relative cliff (CG-12, #1500)
The explore envelope used to follow FILE SIZE, not relevance. Every admitted
file was capped at the same flat `maxCharsPerFile`, while the whole-file rule
handed anything under `maxCharsPerFile * 3` its entire contents — a 3x swing
decided by how big a file happened to be:
- self-query: `memory-budget.ts` (score 18) shipped whole and took 51.2% of
the response; `src/mcp/tools.ts` (score 41, 4x the graph mass, 3x the term
hits — it holds the allocator itself) was clipped at 3,800 and got 32.9%.
- #1500 Go fixture: two generated CRUD files shipped whole at ~4.5K each AND
consumed two of the tier's four file slots, so `BuildPayslip` — the
hand-written "calculate" half of the question — ranked #6 and never
rendered at all.
`allocateExploreBudget` now reserves each ranked file a share of the envelope
before anything renders, so the render loop spends a reservation instead of
racing for whatever the files above it left:
- weight = score x worth x (spine ? 2 : 1), where `worth` is `rankPenalty`
applied a SECOND time — ranking answers "is this file about the query",
allocation answers "will these bytes teach the agent anything", and
generated CRUD can legitimately rank while its bytes stay boilerplate;
- a relative cliff at 15% of the top weight (capped at SCORE_FLOOR_MAX, so a
god-file can't silence peers the score floor just admitted) gives a file
ZERO source — path, symbols and line numbers only — and crucially frees its
`maxFiles` slot for a file that earns its bytes;
- every admitted file gets MIN_CHARS, then the remainder splits by weight:
the floor keeps a diffuse survey question returning a spread, the remainder
concentrates a precise one;
- the flat per-file cap is retired as the primary guard, leaving a 70%-of-
envelope safety valve.
Two changes were needed to make the reservation bite: an oversize cluster now
shrinks by whole MEMBER symbol ranges (a single-cluster god-file previously
took ~40% more than allotted, and the file below it was dropped for lack of
room), and the arrival-order budget stops are gone — they cut files by the
order they were reached rather than by merit.
Measured: payroll-go answer group 25.6% -> 78.7%, generated 57.4% -> 0%, and
`func (s *Service) BuildPayslip` now delivered; self-query `tools.ts` 18.5% ->
60.6%, past the epic's >50% bar. Controls hold: cobra/gin diffuse survey
queries keep their file spread (3->3, 3->4), express's middleware query is
byte-identical, and gin's flow query moves its top file from the thin `ginS`
singleton wrapper to `routergroup.go`.
One documented exception to "no previously-unclipped file becomes clipped":
`memory-budget.ts` was unclipped-whole at 5,672 and now clusters within its
3.1K reservation. That is the epic's own diagnosis of the bug — it scored 18
against 58 and was taking the larger slice purely for being small.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a3898cdc70
commit
5f7f5f59df
@@ -218,11 +218,13 @@ describe('#1500 — generated Go CRUD beside a hand-written payroll workflow', (
|
||||
* `cycle.go` now delivers 38.9% and the generated layer 23.5%. Four of the
|
||||
* five gates below are green and are now live regressions.
|
||||
*
|
||||
* STILL OPEN, for CG-12 (proportional byte allocation): the render loop
|
||||
* still allocates by FILE SIZE within the ranked set, and `maxFiles` is 4 at
|
||||
* this tier — so `payslip_builder.go` ranks #6 and never renders, and
|
||||
* `func (s *Service) BuildPayslip` is absent. Its `it.fails` passes ONLY
|
||||
* while that is still true. ⚠ When it goes red, remove `.fails`, keep it.
|
||||
* AFTER CG-12 (score-proportional allocation): every file's share of the
|
||||
* envelope is reserved before anything renders, and a file under 15% of the
|
||||
* top weight gets no source at all — so the two generated files cliff to
|
||||
* pointers, hand their `maxFiles` slots to the hand-written store and
|
||||
* builder, and the answer group takes ~79% with the generated layer at 0%.
|
||||
* `func (s *Service) BuildPayslip` — the "calculate" half of the question —
|
||||
* finally reaches the agent. All gates below are live regressions now.
|
||||
*/
|
||||
it('CG-10 GATE: concentrates the envelope on the hand-written workflow', () => {
|
||||
expect(answerShare()).toBeGreaterThanOrEqual(0.55);
|
||||
@@ -249,14 +251,22 @@ describe('#1500 — generated Go CRUD beside a hand-written payroll workflow', (
|
||||
expect(response).toContain('s.store.Upsert(ctx, slip)');
|
||||
});
|
||||
|
||||
it.fails('CG-12 GATE: delivers the calculation the question asks about', () => {
|
||||
// `payslip_builder.go` ranks #6; the tier's maxFiles is 4 and the render
|
||||
// loop spends by file size, so it never gets bytes. Score-proportional
|
||||
// allocation (CG-12) is what closes this.
|
||||
it('CG-12 GATE: delivers the calculation the question asks about', () => {
|
||||
// `payslip_builder.go` ranks #6 and the tier's maxFiles is 4 — it reaches
|
||||
// the response only because the two generated files cliff to pointers
|
||||
// WITHOUT consuming a slot. That slot hand-off is the CG-12 mechanism.
|
||||
expect(bytes.get('internal/usecase/payroll/payslip_builder.go') ?? 0).toBeGreaterThan(0);
|
||||
expect(response).toContain('func (s *Service) BuildPayslip');
|
||||
});
|
||||
|
||||
it('CG-12 GATE: withholds the generated CRUD bytes but still names it', () => {
|
||||
// A cliffed file costs ~100 chars instead of ~4,500, and stays one
|
||||
// follow-up explore away — withholding is only cheap if it stays nameable.
|
||||
expect(bytes.get('internal/gen/fkit/payroll/payslip.go') ?? 0).toBe(0);
|
||||
expect(response).toContain('**Not shown above — explore these names for their source**');
|
||||
expect(response).toMatch(/internal\/gen\/fkit\/payroll\/payslip\.go: \w+:\d+/);
|
||||
});
|
||||
|
||||
it('records the shape of the allocation so a regression is legible', () => {
|
||||
// Not a gate — a snapshot of the split, so a future change that shifts the
|
||||
// numbers shows up in the diff rather than silently flipping a gate.
|
||||
@@ -269,7 +279,7 @@ describe('#1500 — generated Go CRUD beside a hand-written payroll workflow', (
|
||||
}).toEqual({
|
||||
generatedWinsEnvelope: false,
|
||||
workflowFileDelivers: true,
|
||||
builderFileDelivers: false,
|
||||
builderFileDelivers: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user