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 <matthew@dohma.ai>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
committed 2026-10-24 23:48:37 +00:00
1 parent 00100f1598
commit d5564369f4
2 files changed
+41 -4

No files matched your search

+13 -3
View File
@@ -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,
+28 -1
View File
@@ -47,7 +47,9 @@ function runDev(...args: string[]): { stdout: string; stderr: string; status: nu
function logQuestion(payload: Record<string, unknown>): 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<string, number>; 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<string, number> } };
expect(Object.values(p.inferred.contributing_events)).toEqual([0, 0, 0, 0, 0]);
});
});
// -----------------------------------------------------------------------