mirror of https://github.com/garrytan/gstack.git
fix(developer-profile): exclude mode:resources entries from SESSION_COUNT, TIER, and NUDGE_ELIGIBLE
The /office-hours Phase 6 closing auto-appends one or more mode:resources bookkeeping entries every run (to dedupe which founder-resource links the user has been shown). The do_read path counted these as real sessions: - SESSION_COUNT (and therefore TIER) used sessions.length with no filter - builderSessions used `e.mode !== 'startup'`, which both counted resources entries and excluded legitimate startup-mode sessions Result: normal repeated use silently inflated the tier and could arm the builder->founder nudge from bookkeeping alone (e.g. one real session plus a few closings reporting as TIER: regular with NUDGE_ELIGIBLE: true). Fix reuses the existing `realSessions` filter (already used for LAST_*/ CROSS_PROJECT, see line ~238) for the count, and scopes builderSessions to mode === 'builder' to match the nudge's intent (returning *builders*, not startup diagnostics). Adds 4 Tier-1 regression tests. Closes the count/tier/nudge half of the resources-entry hazard that the LAST_* fix (PR #1676, #1671) addressed for project/assignment fields.
This commit is contained in:
parent
a3259400a3
commit
87e6ce00d5
|
|
@ -225,17 +225,19 @@ do_read() {
|
||||||
cat "$PROFILE_FILE" | bun -e "
|
cat "$PROFILE_FILE" | bun -e "
|
||||||
const p = JSON.parse(await Bun.stdin.text());
|
const p = JSON.parse(await Bun.stdin.text());
|
||||||
const sessions = p.sessions || [];
|
const sessions = p.sessions || [];
|
||||||
const count = sessions.length;
|
|
||||||
|
// SESSION_COUNT / TIER / CROSS_PROJECT / NUDGE must reflect real sessions, not
|
||||||
|
// resource-tracking events (the Phase 6 auto-append). Without this filter, a
|
||||||
|
// session's resources entry written immediately after the real session inflates
|
||||||
|
// the count (bumping TIER), clobbers LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE,
|
||||||
|
// and pushes NUDGE_ELIGIBLE over its threshold from bookkeeping alone.
|
||||||
|
const realSessions = sessions.filter(e => e.mode !== 'resources');
|
||||||
|
|
||||||
|
const count = realSessions.length;
|
||||||
let tier = 'introduction';
|
let tier = 'introduction';
|
||||||
if (count >= 8) tier = 'inner_circle';
|
if (count >= 8) tier = 'inner_circle';
|
||||||
else if (count >= 4) tier = 'regular';
|
else if (count >= 4) tier = 'regular';
|
||||||
else if (count >= 1) tier = 'welcome_back';
|
else if (count >= 1) tier = 'welcome_back';
|
||||||
|
|
||||||
// LAST_* / CROSS_PROJECT must reflect real sessions, not resource-tracking
|
|
||||||
// events (the Phase 6 auto-append). Without this filter, a session's
|
|
||||||
// resources entry written immediately after the real session would clobber
|
|
||||||
// LAST_PROJECT/LAST_ASSIGNMENT/LAST_DESIGN_TITLE.
|
|
||||||
const realSessions = sessions.filter(e => e.mode !== 'resources');
|
|
||||||
const last = realSessions[realSessions.length - 1] || {};
|
const last = realSessions[realSessions.length - 1] || {};
|
||||||
const prev = realSessions[realSessions.length - 2] || {};
|
const prev = realSessions[realSessions.length - 2] || {};
|
||||||
const crossProject = prev.project_slug && last.project_slug
|
const crossProject = prev.project_slug && last.project_slug
|
||||||
|
|
@ -252,7 +254,11 @@ do_read() {
|
||||||
for (const v of Object.values(signalCounts)) totalSignals += v;
|
for (const v of Object.values(signalCounts)) totalSignals += v;
|
||||||
const signalStr = Object.entries(signalCounts).map(([k,v]) => k + ':' + v).join(',');
|
const signalStr = Object.entries(signalCounts).map(([k,v]) => k + ':' + v).join(',');
|
||||||
|
|
||||||
const builderSessions = sessions.filter(e => e.mode !== 'startup').length;
|
// Builder-mode design sessions only — the builder->founder nudge is about
|
||||||
|
// someone who keeps returning to *build*, not startup-mode diagnostics and not
|
||||||
|
// resources bookkeeping. (Was \`e.mode !== 'startup'\`, which counted resources
|
||||||
|
// entries and excluded real startup sessions.)
|
||||||
|
const builderSessions = realSessions.filter(e => e.mode === 'builder').length;
|
||||||
const nudgeEligible = builderSessions >= 3 && totalSignals >= 5;
|
const nudgeEligible = builderSessions >= 3 && totalSignals >= 5;
|
||||||
|
|
||||||
const resources = p.resources_shown || [];
|
const resources = p.resources_shown || [];
|
||||||
|
|
|
||||||
|
|
@ -556,3 +556,77 @@ describe('gstack-developer-profile --log-session (#1671 fix)', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// SESSION_COUNT / TIER / NUDGE_ELIGIBLE must ignore mode:resources entries.
|
||||||
|
//
|
||||||
|
// Phase 6 of /office-hours auto-appends one (or more) mode:resources bookkeeping
|
||||||
|
// entries every run, to dedupe which founder-resource links the user has seen.
|
||||||
|
// Those are not sessions. Counting them inflated SESSION_COUNT (and therefore
|
||||||
|
// TIER) and pushed NUDGE_ELIGIBLE over its threshold from bookkeeping alone —
|
||||||
|
// e.g. a single real session plus three closings reported as tier `regular`
|
||||||
|
// with the builder->founder nudge armed.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('gstack-developer-profile resources entries do not inflate count/tier/nudge', () => {
|
||||||
|
function logStartup(extra: Record<string, unknown> = {}) {
|
||||||
|
return runDev('--log-session', JSON.stringify({
|
||||||
|
date: '2026-05-20T00:00:00Z', mode: 'startup', project_slug: 'p',
|
||||||
|
signal_count: 5, signals: ['a', 'b', 'c', 'd', 'e'], ...extra,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function logResources(i: number) {
|
||||||
|
return runDev('--log-session', JSON.stringify({
|
||||||
|
date: '2026-05-20T01:00:00Z', mode: 'resources', project_slug: 'p',
|
||||||
|
resources_shown: [`url${i}`],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('SESSION_COUNT counts only real sessions, not resources entries', () => {
|
||||||
|
logStartup();
|
||||||
|
logResources(1);
|
||||||
|
logResources(2);
|
||||||
|
logResources(3);
|
||||||
|
const r = runDev('--read');
|
||||||
|
expect(r.stdout).toContain('SESSION_COUNT: 1');
|
||||||
|
expect(r.stdout).toContain('TIER: welcome_back');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TIER is not bumped to regular by resources bookkeeping', () => {
|
||||||
|
// 3 real sessions = welcome_back; adding resources entries must not reach the
|
||||||
|
// 4-session `regular` threshold.
|
||||||
|
logStartup();
|
||||||
|
logStartup();
|
||||||
|
logStartup();
|
||||||
|
for (let i = 0; i < 4; i++) logResources(i);
|
||||||
|
const r = runDev('--read');
|
||||||
|
expect(r.stdout).toContain('SESSION_COUNT: 3');
|
||||||
|
expect(r.stdout).toContain('TIER: welcome_back');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('NUDGE_ELIGIBLE stays false when builder-session bar is unmet despite resources noise', () => {
|
||||||
|
// One startup session carrying 5 signals, plus resources entries. builderSessions
|
||||||
|
// (mode === "builder") is 0, so the nudge must not arm regardless of signal count.
|
||||||
|
logStartup();
|
||||||
|
logResources(1);
|
||||||
|
logResources(2);
|
||||||
|
logResources(3);
|
||||||
|
const r = runDev('--read');
|
||||||
|
expect(r.stdout).toContain('NUDGE_ELIGIBLE: false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('NUDGE_ELIGIBLE arms on 3 real builder sessions with enough signals', () => {
|
||||||
|
runDev('--log-session', JSON.stringify({
|
||||||
|
date: '2026-05-20T00:00:00Z', mode: 'builder', project_slug: 'p', signals: ['a', 'b'],
|
||||||
|
}));
|
||||||
|
runDev('--log-session', JSON.stringify({
|
||||||
|
date: '2026-05-21T00:00:00Z', mode: 'builder', project_slug: 'p', signals: ['c', 'd'],
|
||||||
|
}));
|
||||||
|
runDev('--log-session', JSON.stringify({
|
||||||
|
date: '2026-05-22T00:00:00Z', mode: 'builder', project_slug: 'p', signals: ['e'],
|
||||||
|
}));
|
||||||
|
logResources(1); // bookkeeping must not change the verdict either way
|
||||||
|
const r = runDev('--read');
|
||||||
|
expect(r.stdout).toContain('NUDGE_ELIGIBLE: true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue