fix(learnings): cross-project trust gate is an allowlist, not a denylist (#1745)

gstack-learnings-search --cross-project is documented as an allowlist — foreign
learnings load only when user-stated/trusted, to stop one project's AI-generated
learnings from injecting into another project's reviews. It was implemented as a
denylist: `if (isCrossProject && e.trusted === false) continue`. Any row where
`trusted` is missing/undefined (legacy rows from before the field existed,
hand-edited rows, rows from other tools) passed `undefined === false` → false →
admitted. Those rows leaked across projects.

Flip to `e.trusted !== true`. Test: a foreign row with no `trusted` field is now
excluded (true still included, false still excluded).

Reported by @jbetala7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-07 22:49:36 -07:00
parent be3d4c7171
commit 549f32a8f9
2 changed files with 15 additions and 2 deletions
+10
View File
@@ -33,6 +33,9 @@ beforeAll(() => {
const otherEntries = [
{ ts: '2026-05-04T00:00:00Z', skill: 'test', type: 'pattern', key: 'foreign-observed', insight: 'A foreign observed insight', confidence: 8, source: 'observed', trusted: false, files: [] },
{ ts: '2026-05-05T00:00:00Z', skill: 'test', type: 'pattern', key: 'foreign-user', insight: 'A foreign user-stated insight', confidence: 8, source: 'user-stated', trusted: true, files: [] },
// #1745: legacy row with NO `trusted` field at all (written before the field
// existed). The old `=== false` denylist admitted these; the allowlist must exclude.
{ ts: '2026-05-06T00:00:00Z', skill: 'test', type: 'pattern', key: 'foreign-legacy', insight: 'A foreign legacy insight with no trusted field', confidence: 8, source: 'observed', files: [] },
];
fs.writeFileSync(path.join(projDir, 'learnings.jsonl'), entries.map(e => JSON.stringify(e)).join('\n') + '\n');
fs.writeFileSync(path.join(otherProjDir, 'learnings.jsonl'), otherEntries.map(e => JSON.stringify(e)).join('\n') + '\n');
@@ -79,4 +82,11 @@ describe('gstack-learnings-search cross-project trust gating', () => {
expect(out).toContain('[cross-project]');
expect(out).not.toContain('foreign-observed');
});
// #1745: the gate is an allowlist, not a denylist. A cross-project row with no
// `trusted` field (legacy / hand-edited / other-tool) must NOT be imported.
test('cross-project mode excludes foreign rows missing the trusted field (#1745)', () => {
const out = run(['--cross-project', '--query', 'foreign']);
expect(out).not.toContain('foreign-legacy');
});
});