Skip to content

Commit 0006389

Browse files
neo-opus-adatobiu
andauthored
fix(ai): IssueIngestor resolves internal authors from registry (#12130) (#12144)
* fix(ai): IssueIngestor resolves internal authors from registry (#12130) The community-multiplier hardcoded the operator login 'tobiu' as the internal-author proxy, so in any fork / external / tenant deployment (no 'tobiu') every ticket read as community-authored and got the +0.5 boost, inverting the discriminator the multiplier exists to apply. Derive the internal-author set from the canonical identity registry (ai/graph/identityRoots.mjs) as bare logins, mirroring the established MemoryService.identityTrustTiers pattern. Add a static isCommunityAuthor() seam whose size>0 guard degrades safely (multiplier off) for an empty registry instead of boosting every ticket. The single hardcoded logic site is removed; out-of-scope JSDoc @neo-* mentions are left to the archaeology-cleanup epic. Adds unit coverage: registry-derived bare-login set, internal-author not boosted, external-author boosted, missing-author no-fire, and an empty-set negative-mutation asserting the safe degrade. Co-Authored-By: neo-opus-4-7 <neo-opus-4-7@neomjs.com> * test(ai): drop transient ticket anchors from IssueIngestor spec (#12130) Per PR #12144 review (@neo-gpt): remove the #12130 anchors from the community-multiplier test's comment block and describe title; behavior-only wording. No logic change. Co-Authored-By: neo-opus-4-7 <neo-opus-4-7@neomjs.com> --------- Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent 00c3014 commit 0006389

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

ai/services/ingestion/IssueIngestor.mjs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Base from '../../../src/core/Base.mjs';
77
import { Memory_StorageRouter as StorageRouter } from '../../services.mjs';
88
import { Memory_GraphService as GraphService } from '../../services.mjs';
99
import logger from '../../mcp/server/memory-core/logger.mjs';
10+
import {IDENTITIES} from '../../graph/identityRoots.mjs';
1011

1112
const __filename = fileURLToPath(import.meta.url);
1213
const __dirname = path.dirname(__filename);
@@ -52,6 +53,32 @@ class IssueIngestor extends Base {
5253
singleton: true
5354
}
5455

56+
/**
57+
* @summary Bare GitHub logins of registered internal authors (human owner + swarm maintainers),
58+
* derived from the canonical identity registry. The `@` prefix is stripped to match GitHub
59+
* issue-author logins. An empty set disables the community multiplier (safe degrade) instead of
60+
* boosting every ticket.
61+
* @member {Set<String>} internalAuthorLogins
62+
* @static
63+
*/
64+
static internalAuthorLogins = new Set(
65+
IDENTITIES.map(identity => identity.properties?.githubLogin).filter(Boolean).map(login => login.replace(/^@/, ''))
66+
)
67+
68+
/**
69+
* @summary Whether a ticket author qualifies for the community-multiplier boost: true when the
70+
* author exists, the internal-author registry is non-empty, and the author is not a registered
71+
* maintainer. The non-empty guard makes an unconfigured deployment degrade safely (multiplier
72+
* off) rather than treating every ticket as community-authored.
73+
* @param {String} author Ticket GitHub author login (bare form).
74+
* @param {Set<String>} [internalAuthorLogins=IssueIngestor.internalAuthorLogins] Injectable internal-author set (test seam).
75+
* @returns {Boolean}
76+
* @static
77+
*/
78+
static isCommunityAuthor(author, internalAuthorLogins = IssueIngestor.internalAuthorLogins) {
79+
return Boolean(author) && internalAuthorLogins.size > 0 && !internalAuthorLogins.has(author)
80+
}
81+
5582
/**
5683
* @summary Parses the local file system for markdown files and explicitly syncs their state
5784
* into the Native Graph database. Re-asserts edge weights for OPEN issues, heavily discounting
@@ -190,8 +217,8 @@ class IssueIngestor extends Base {
190217
baseWeight = 0.05;
191218
logger.debug(`[IssueIngestor] Discounting topological weight for ${issueId} because it is BLOCKED_BY an OPEN issue.`);
192219
} else {
193-
// Community Multiplier: Boost if ticket is external and has been triaged
194-
if (meta.author && meta.author !== 'tobiu') {
220+
// Community Multiplier: boost externally-authored (non-maintainer) tickets that have been triaged.
221+
if (IssueIngestor.isCommunityAuthor(meta.author)) {
195222
if (Array.isArray(meta.labels) && meta.labels.length > 0) {
196223
baseWeight += 0.5;
197224
}

test/playwright/unit/ai/services/ingestion/IssueIngestor.spec.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,54 @@ test.describe('Neo.ai.daemons.services.IssueIngestor', () => {
208208
});
209209
});
210210
});
211+
212+
/**
213+
* Community-multiplier internal-author resolution. Replaces the prior hardcoded
214+
* `meta.author !== 'tobiu'` check with a set derived from the canonical identity registry
215+
* (`ai/graph/identityRoots.mjs`). The empty-registry case must degrade safely (multiplier OFF),
216+
* not invert into boosting every ticket.
217+
*/
218+
test.describe('Neo.ai.daemons.services.IssueIngestor — community-multiplier internal-author set', () => {
219+
let IssueIngestorClass;
220+
221+
test.beforeAll(async () => {
222+
// The default export is the singleton instance; the static set + decision live on its class.
223+
IssueIngestorClass = (await import('../../../../../../ai/services/ingestion/IssueIngestor.mjs')).default.constructor;
224+
});
225+
226+
test('internalAuthorLogins is derived from the identity registry as bare logins (no @, no nulls)', () => {
227+
const logins = IssueIngestorClass.internalAuthorLogins;
228+
229+
expect(logins).toBeInstanceOf(Set);
230+
expect(logins.size).toBeGreaterThan(0);
231+
// Owner + a known agent maintainer, normalized to bare form (matches GitHub issue authors).
232+
expect(logins.has('tobiu')).toBe(true);
233+
expect(logins.has('neo-opus-4-7')).toBe(true);
234+
// The leading-@ registry form and null/empty entries must NOT leak through.
235+
expect(logins.has('@tobiu')).toBe(false);
236+
expect(logins.has('')).toBe(false);
237+
expect([...logins].every(Boolean)).toBe(true);
238+
});
239+
240+
test('isCommunityAuthor: a registered maintainer is NOT a community author (no boost)', () => {
241+
expect(IssueIngestorClass.isCommunityAuthor('tobiu')).toBe(false);
242+
expect(IssueIngestorClass.isCommunityAuthor('neo-opus-4-7')).toBe(false);
243+
});
244+
245+
test('isCommunityAuthor: an external author IS a community author (boost eligible)', () => {
246+
expect(IssueIngestorClass.isCommunityAuthor('some-external-contributor')).toBe(true);
247+
});
248+
249+
test('isCommunityAuthor: a missing author is never a community author', () => {
250+
expect(IssueIngestorClass.isCommunityAuthor('')).toBe(false);
251+
expect(IssueIngestorClass.isCommunityAuthor(undefined)).toBe(false);
252+
expect(IssueIngestorClass.isCommunityAuthor(null)).toBe(false);
253+
});
254+
255+
test('isCommunityAuthor: an empty internal-author registry disables the multiplier (safe degrade, not boost-all)', () => {
256+
// Negative-mutation guard: with an empty set, even an "external" author must NOT be boosted —
257+
// the `size > 0` clause is what prevents the inverted every-ticket-boosted failure mode.
258+
expect(IssueIngestorClass.isCommunityAuthor('some-external-contributor', new Set())).toBe(false);
259+
expect(IssueIngestorClass.isCommunityAuthor('tobiu', new Set())).toBe(false);
260+
});
261+
});

0 commit comments

Comments
 (0)