From d5564369f410ddd5c852f78003689a010a4a4a7f Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 24 Oct 2026 23:48:37 +0000 Subject: [PATCH] feat(plan-tune): per-dimension contributing_events in the inferred profile Port of the provenance part of #2625 by @v639dragoon: --derive records, per dimension, how many unique log rows moved it, so "no evidence" and "balanced evidence" no longer both print a bare 0.5. Also ports the test harness fix: logQuestion sets GSTACK_QUESTION_LOG_NO_DERIVE=1 so the background derive cannot race the test's own --derive. The registry-independent autonomy signal from #2625 stays parked, as the Oct 8 review decided. Co-authored-by: v639dragoon Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-developer-profile | 16 ++++++++++++--- test/gstack-developer-profile.test.ts | 29 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/bin/gstack-developer-profile b/bin/gstack-developer-profile index f5bfd5391..b4890a3f0 100755 --- a/bin/gstack-developer-profile +++ b/bin/gstack-developer-profile @@ -437,6 +437,11 @@ do_derive() { const skills = new Set(); const qids = new Set(); const days = new Set(); + // Provenance: UNIQUE ROWS contributing to each dimension, not signal + // applications. One row can apply several deltas, and conflating them + // would overstate the evidence (#2625). + const contributors = {}; + for (const d of Object.keys(totals)) contributors[d] = new Set(); let count = 0; let signalEvents = 0; for (const line of lines) { @@ -448,10 +453,12 @@ do_derive() { qids.add(e.question_id); if (e.ts) days.add(String(e.ts).slice(0,10)); const def = QUESTIONS[e.question_id]; - if (def && def.signal_key && applySignal(totals, def.signal_key, e.user_choice).length > 0) { - signalEvents++; - } + const applied = def && def.signal_key ? applySignal(totals, def.signal_key, e.user_choice) : []; + if (applied.length > 0) signalEvents++; + for (const d of applied) contributors[d.dim]?.add(count); } + const contributingEvents = {}; + for (const d of Object.keys(totals)) contributingEvents[d] = contributors[d].size; const values = {}; for (const [dim, total] of Object.entries(totals)) { @@ -464,6 +471,9 @@ do_derive() { // Events that actually moved a dimension (B10): sample_size counts every // logged answer, most of which match no signal and leave the 0.5 seed. signal_events: signalEvents, + // Distinguishes 'no evidence' from 'balanced evidence': both print + // 0.5, which is how five 0.5s sat beside sample_size 1286 (#2623). + contributing_events: contributingEvents, diversity: { skills_covered: skills.size, question_ids_covered: qids.size, diff --git a/test/gstack-developer-profile.test.ts b/test/gstack-developer-profile.test.ts index 0550dfc31..65577ce80 100644 --- a/test/gstack-developer-profile.test.ts +++ b/test/gstack-developer-profile.test.ts @@ -47,7 +47,9 @@ function runDev(...args: string[]): { stdout: string; stderr: string; status: nu function logQuestion(payload: Record): number { const res = spawnSync(BIN_LOG, [JSON.stringify(payload)], { - env: { ...process.env, GSTACK_HOME: tmpHome }, + // gstack-question-log fires `--derive` in the background after every + // append, which races the explicit runDev('--derive') a test then makes. + env: { ...process.env, GSTACK_HOME: tmpHome, GSTACK_QUESTION_LOG_NO_DERIVE: '1' }, encoding: 'utf-8', cwd: ROOT, timeout: 30_000, @@ -308,6 +310,31 @@ describe('gstack-developer-profile --derive', () => { expect(p.inferred.sample_size).toBe(1); expect(p.inferred.values.scope_appetite).toBeCloseTo(0.5, 2); }); + + test('contributing_events counts unique rows per dimension (#2625)', () => { + // plan-eng-review-arch-finding → fix-now writes architecture_care AND + // risk_tolerance from one row; an ad-hoc row writes nothing. + for (let i = 0; i < 2; i++) { + logQuestion({ skill: 'plan-eng-review', question_id: 'plan-eng-review-arch-finding', question_summary: 'arch?', user_choice: 'fix-now', session_id: `s${i}` }); + } + logQuestion({ skill: 'plan-eng-review', question_id: 'adhoc-unregistered-question', question_summary: 'x', user_choice: 'fix-now', session_id: 's9' }); + runDev('--derive'); + const p = readProfile() as { inferred: { contributing_events: Record; signal_events: number } }; + expect(p.inferred.contributing_events).toEqual({ + scope_appetite: 0, + risk_tolerance: 2, + detail_preference: 0, + autonomy: 0, + architecture_care: 2, + }); + expect(p.inferred.signal_events).toBe(2); + }); + + test('contributing_events is all zero when no answer matches a signal', () => { + runDev('--derive'); + const p = readProfile() as { inferred: { contributing_events: Record } }; + expect(Object.values(p.inferred.contributing_events)).toEqual([0, 0, 0, 0, 0]); + }); }); // -----------------------------------------------------------------------