diff --git a/graph-ui/src/browser-ai/BrowserChatDock.test.tsx b/graph-ui/src/browser-ai/BrowserChatDock.test.tsx index aab9a3c8d..bf926021d 100644 --- a/graph-ui/src/browser-ai/BrowserChatDock.test.tsx +++ b/graph-ui/src/browser-ai/BrowserChatDock.test.tsx @@ -930,7 +930,7 @@ describe('graph answers and answer limits', () => { const last = () => [...container.querySelectorAll('.cbm-chat-turn')].at(-1)!; await type('wer ruf jsonbagg auf'); await click('Send ↑'); expect(runtime.chat).not.toHaveBeenCalled(); - expect(last().querySelector('.cbm-chat-answer-text')?.textContent).toContain('Aufrufer von JSONBAgg im geladenen Graphen'); + expect(last().querySelector('.cbm-chat-answer-text')?.textContent).toContain('11 Aufrufer (CALLS) von JSONBAgg im geladenen Graphen.'); expect([...last().querySelectorAll('button.cbm-chat-retry')].map(item => item.textContent)).toEqual(['Modell fragen']); await type('jsonbagg aufrufe?'); await click('Send ↑'); expect(runtime.chat).not.toHaveBeenCalled(); @@ -939,7 +939,7 @@ describe('graph answers and answer limits', () => { expect([...last().querySelectorAll('button.cbm-chat-retry')].map(item => item.textContent)).toEqual(['Liste anzeigen', 'Modell fragen']); await click('Liste anzeigen'); expect(runtime.chat).not.toHaveBeenCalled(); - expect(last().querySelector('.cbm-chat-answer-text')?.textContent).toContain('Aufrufer von JSONBAgg im geladenen Graphen'); + expect(last().querySelector('.cbm-chat-answer-text')?.textContent).toContain('11 Aufrufer (CALLS) von JSONBAgg im geladenen Graphen.'); for (const name of JSONB_AGG_CALLERS) expect(last().textContent).toContain(name); await type('jsonbagg calls'); await click('Send ↑'); expect(last().querySelector('.cbm-chat-answer-text')?.textContent).toContain('Did you mean: what JSONBAgg calls?'); diff --git a/graph-ui/src/browser-ai/chat-wording.test.ts b/graph-ui/src/browser-ai/chat-wording.test.ts index 34ce36204..24356eb5a 100644 --- a/graph-ui/src/browser-ai/chat-wording.test.ts +++ b/graph-ui/src/browser-ai/chat-wording.test.ts @@ -82,3 +82,43 @@ describe('relationship counts that read as counts, not as a score (W2)', () => { expect(selectionSummary(lonely, 'de').slice(1, 3)).toEqual(['Eingehend: keine Beziehungen in diesem Ausschnitt.', 'Ausgehend: keine Beziehungen in diesem Ausschnitt.']); }); }); + +describe('the heading of a listed caller or callee answer (W3)', () => { + const answer = (prompt: string, context = jsonbAggEvidence()) => relationshipAnswer(prompt, [context])!.markdown; + // Two callers of JSONBAgg with CALLS only, and three calls from it. + const calling = () => jsonbAggEvidence({ edges: [{ source: 100, target: 32360, type: 'CALLS' }, { source: 101, target: 32360, type: 'CALLS' }, + { source: 32360, target: 7, type: 'CALLS' }, { source: 32360, target: 8, type: 'CALLS' }, { source: 32360, target: 9, type: 'CALLS' }, { source: 32360, target: 7, type: 'INHERITS' }] }); + + it('counts the callers it lists and gives all incoming relationships as a sentence of their own', () => { + expect(answer('Who calls JSONBAgg?').split('\n')[0]).toBe('11 callers (CALLS) of `JSONBAgg` in the loaded graph. All incoming: 23 relationships from 12 symbols.'); + expect(answer('Wer ruft JSONBAgg auf?').split('\n')[0]).toBe('11 Aufrufer (CALLS) von `JSONBAgg` im geladenen Graphen. Alle eingehenden: 23 Beziehungen von 12 Symbolen.'); + // With CALLS as the only type there is nothing more to count. + expect(answer('Who calls JSONBAgg?', calling()).split('\n')[0]).toBe('2 callers (CALLS) of `JSONBAgg` in the loaded graph.'); + }); + + it('says what the selection calls as a full sentence, in English and German', () => { + expect(answer('What does JSONBAgg call?', calling()).split('\n')[0]) + .toBe('`JSONBAgg` calls 3 symbols (CALLS) in the loaded graph. All outgoing: 4 relationships to 3 symbols.'); + expect(answer('Was ruft JSONBAgg auf?', calling()).split('\n')[0]) + .toBe('`JSONBAgg` ruft im geladenen Graphen 3 Symbole auf (CALLS). Alle ausgehenden: 4 Beziehungen zu 3 Symbolen.'); + expect(answer('Was ruft JSONBAgg auf?').split('\n')[0]) + .toBe('`JSONBAgg` hat in diesem Ausschnitt keine ausgehende CALLS-Kante. Ausgehend: 2 Beziehungen zu 2 Symbolen.'); + expect(answer('What does JSONBAgg call?').split('\n')[0]) + .toBe('`JSONBAgg` has no outgoing CALLS edge in this scope. Outgoing: 2 relationships to 2 symbols.'); + expect(answer('Was ruft JSONBAgg auf?')).not.toContain('Was `JSONBAgg` im geladenen Graphen aufruft.'); + }); + + it('heads the other edge types "Andere" in German, also right after "keine ausgehende CALLS-Kante"', () => { + expect(answer('Was ruft JSONBAgg auf?')).toContain('\n\nAndere ausgehende Beziehungen:\n- **INHERITS (2):**'); + expect(answer('Wer ruft JSONBAgg auf?')).toContain('\n\nAndere eingehende Beziehungen:\n- **TESTS (11):**'); + expect(answer('What does JSONBAgg call?')).toContain('\n\nOther outgoing relationships:\n- **INHERITS (2):**'); + expect(answer('Wer ruft JSONBAgg auf?') + answer('Was ruft JSONBAgg auf?')).not.toContain('Weitere'); + }); + + it('says in a full sentence why nothing is listed', () => { + expect(answer('Who calls JSONBAgg?', jsonbAggEvidence({ direction: 'outbound' })).split('\n')[0]).toMatch(/^The callers of `JSONBAgg` cannot be listed from this scope\. /); + expect(answer('Was ruft JSONBAgg auf?', jsonbAggEvidence({ direction: 'inbound' })).split('\n')[0]).toMatch(/^Was `JSONBAgg` aufruft, lässt sich aus diesem Ausschnitt nicht auflisten\. /); + expect(answer('Who calls JSONBAgg?', jsonbAggEvidence({ edges: [] })).split('\n')[0]).toBe('`JSONBAgg` has no callers and no other incoming relationships in this scope.'); + expect(answer('Was ruft JSONBAgg auf?', jsonbAggEvidence({ edges: [] })).split('\n')[0]).toBe('`JSONBAgg` ruft in diesem Ausschnitt nichts auf und hat keine anderen ausgehenden Beziehungen.'); + }); +}); diff --git a/graph-ui/src/browser-ai/relationship-answer.test.ts b/graph-ui/src/browser-ai/relationship-answer.test.ts index 26679cb84..22aeeef75 100644 --- a/graph-ui/src/browser-ai/relationship-answer.test.ts +++ b/graph-ui/src/browser-ai/relationship-answer.test.ts @@ -4,6 +4,10 @@ import { JSONB_AGG_CALLERS, jsonbAggEvidence, jsonbAggRenderLimited, largeFolder import { correctRelationWords, relationshipAnswer, relationshipQuestion, relationshipSuggestion } from './relationship-answer'; const line = (markdown: string, type: string) => markdown.split('\n').find(item => item.startsWith(`- **${type} (`)) ?? ''; +/** How a listed answer about JSONBAgg begins: its callers, or that it calls nothing (it only inherits). */ +const heading = (side: 'incoming' | 'outgoing', language: 'en' | 'de') => side === 'incoming' + ? language === 'de' ? '11 Aufrufer (CALLS) von `JSONBAgg` im geladenen Graphen.' : '11 callers (CALLS) of `JSONBAgg` in the loaded graph.' + : language === 'de' ? '`JSONBAgg` hat in diesem Ausschnitt keine ausgehende CALLS-Kante.' : '`JSONBAgg` has no outgoing CALLS edge in this scope.'; describe('caller and callee questions', () => { it.each([ @@ -49,7 +53,7 @@ describe('caller and callee questions', () => { describe('listed relationship answers', () => { it('lists the callers first with their count and every other incoming relationship under its own heading (C2)', () => { const answer = relationshipAnswer('Who calls JSONBAgg? List every caller and the edge type.', [jsonbAggEvidence()])!; - expect(answer.markdown).toContain('Callers of `JSONBAgg` in the loaded graph. 23 incoming relationships from 12 symbols.'); + expect(answer.markdown).toContain('11 callers (CALLS) of `JSONBAgg` in the loaded graph. All incoming: 23 relationships from 12 symbols.'); const calls = line(answer.markdown, 'CALLS'), tests = line(answer.markdown, 'TESTS'); // TESTS and DEFINES are no callers: they follow under their own heading. expect(answer.markdown.indexOf(calls)).toBeLessThan(answer.markdown.indexOf('Other incoming relationships:')); @@ -70,7 +74,7 @@ describe('listed relationship answers', () => { it('answers callee questions with the outgoing side only and says when no CALLS edge exists', () => { const answer = relationshipAnswer('What does JSONBAgg call?', [jsonbAggEvidence()])!; - expect(answer.markdown).toContain('What `JSONBAgg` calls in the loaded graph. 2 outgoing relationships to 2 symbols.'); + expect(answer.markdown).toContain('`JSONBAgg` has no outgoing CALLS edge in this scope. Outgoing: 2 relationships to 2 symbols.'); expect(answer.markdown).toContain('`JSONBAgg` has no outgoing CALLS edge in this scope.'); expect(answer.markdown).toContain('Other outgoing relationships:'); expect(line(answer.markdown, 'INHERITS')).toMatch(/^- \*\*INHERITS \(2\):\*\* `OrderableAggMixin`.*`Aggregate`/); @@ -79,15 +83,15 @@ describe('listed relationship answers', () => { it('answers in German for a German question', () => { const answer = relationshipAnswer('Wer ruft JSONBAgg auf?', [jsonbAggEvidence()])!; - expect(answer.markdown).toContain('Aufrufer von `JSONBAgg` im geladenen Graphen. 23 eingehende Beziehungen aus 12 Symbolen.'); + expect(answer.markdown).toContain('11 Aufrufer (CALLS) von `JSONBAgg` im geladenen Graphen. Alle eingehenden: 23 Beziehungen von 12 Symbolen.'); expect(line(answer.markdown, 'CALLS')).toMatch(/^- \*\*CALLS \(11\):\*\* `test_default_argument`/); - expect(answer.markdown.indexOf('Weitere eingehende Beziehungen:')).toBeGreaterThan(answer.markdown.indexOf(line(answer.markdown, 'CALLS'))); + expect(answer.markdown.indexOf('Andere eingehende Beziehungen:')).toBeGreaterThan(answer.markdown.indexOf(line(answer.markdown, 'CALLS'))); expect(line(answer.markdown, 'TESTS')).toMatch(/^- \*\*TESTS \(11\):\*\* /); expect(line(answer.markdown, 'DEFINES')).toBe('- **DEFINES (1):** `general.py`, die Datei, die `JSONBAgg` definiert'); - expect(answer.markdown).not.toMatch(/\bvon 1[12]\b/); + expect(answer.markdown).not.toMatch(/\b(?:CALLS|TESTS|DEFINES) von \d/); const callees = relationshipAnswer('Was ruft JSONBAgg auf?', [jsonbAggEvidence()])!.markdown; - expect(callees).toContain('Was `JSONBAgg` im geladenen Graphen aufruft. 2 ausgehende Beziehungen zu 2 Symbolen.'); - expect(callees).toContain('Weitere ausgehende Beziehungen:'); + expect(callees).toContain('`JSONBAgg` hat in diesem Ausschnitt keine ausgehende CALLS-Kante. Ausgehend: 2 Beziehungen zu 2 Symbolen.'); + expect(callees).toContain('Andere ausgehende Beziehungen:'); expect(answer.markdown).toContain('Ausschnitt: 1 Schritt in beide Richtungen'); }); @@ -118,7 +122,7 @@ describe('listed relationship answers', () => { it('answers for a large documented scope with complete counts and every edge type', () => { const markdown = relationshipAnswer('What does postgres call?', [selectionEvidenceContext(largeFolderScope())])!.markdown; - expect(markdown).toContain('150 outgoing relationships to 150 symbols.'); + expect(markdown).toContain('`postgres` calls 30 symbols (CALLS) in the loaded graph. All outgoing: 150 relationships to 150 symbols.'); for (const type of ['CALLS', 'TESTS', 'USAGE', 'IMPORTS', 'DEFINES_METHOD']) expect(line(markdown, type)).toMatch(/^- \*\*\w+ \(30\):\*\* .*; \+\d+ more$/); expect(markdown).toContain('fully loaded.'); }); @@ -129,7 +133,7 @@ describe('listed relationship answers', () => { const markdown = relationshipAnswer('What does postgres call?', [cut])!.markdown; expect(markdown).not.toMatch(/to 0 symbols/); expect(markdown).toContain('the snapshot left part of its relationships out, so counts and names can be incomplete'); - expect(markdown).toMatch(/Outgoing: relationships left out of this snapshot\.|\d+ outgoing relationships?\./); + expect(markdown).toMatch(/What `postgres` calls cannot be listed from this scope\. The snapshot left the outgoing relationships out\.|All outgoing: \d+ relationships? to \d+ symbols?\./); }); it('leaves questions about another symbol, attached non-Galaxy evidence and other questions to the model', () => { @@ -175,7 +179,7 @@ describe('tolerant caller and callee questions (K16)', () => { expect(relationshipQuestion(prompt)).toEqual({ sides, language, subject }); const markdown = relationshipAnswer(prompt, [jsonbAggEvidence()])?.markdown ?? ''; expect(markdown).toContain('`JSONBAgg`'); - expect(markdown).toContain(language === 'de' ? 'im geladenen Graphen' : 'in the loaded graph'); + expect(markdown).toContain(heading(sides[0], language)); }); it.each(['falls jsonbagg fehlt, was passiert?', 'Erkläre jsonbagg', 'who calls jsonbagg and why', 'auf jsonbagg', 'cells of jsonbagg', @@ -197,7 +201,7 @@ describe('tolerant caller and callee questions (K16)', () => { expect(suggestion.markdown).toContain(language === 'de' ? `Meintest du: ${side === 'incoming' ? 'Aufrufer von' : 'von'} \`JSONBAgg\`` : `Did you mean: ${side === 'incoming' ? 'callers of' : 'what'} \`JSONBAgg\``); expect(relationshipAnswer(suggestion.question, [jsonbAggEvidence()])?.markdown) - .toContain(side === 'incoming' ? (language === 'de' ? 'Aufrufer von `JSONBAgg`' : 'Callers of `JSONBAgg`') : (language === 'de' ? 'Was `JSONBAgg` im geladenen Graphen aufruft' : 'What `JSONBAgg` calls in the loaded graph')); + .toContain(heading(side, language)); }); it.each([ @@ -271,7 +275,7 @@ describe('caller questions with a misspelled selection or a free word order (K16 expect(question?.sides).toEqual(['incoming']); expect(question?.language).toBe(language); expect(question?.subject?.toLowerCase()).not.toMatch(/^(?:wo|woher|irgendwo|überall|where|whom)$/); - expect(listedAnswer(prompt)).toContain(language === 'de' ? 'Aufrufer von `JSONBAgg` im geladenen Graphen' : 'Callers of `JSONBAgg` in the loaded graph'); + expect(listedAnswer(prompt)).toContain(heading('incoming', language)); }); // A typo in the selected name, up to two edits and in any case: never straight to the model. @@ -301,7 +305,7 @@ describe('caller questions with a misspelled selection or a free word order (K16 const listed = relationshipAnswer(prompt, evidence()); const suggestion = relationshipSuggestion(prompt, evidence()); if (expected === 'listed') { - expect(listed?.markdown).toContain(language === 'de' ? 'Aufrufer von `JSONBAgg`' : 'Callers of `JSONBAgg`'); + expect(listed?.markdown).toContain(heading('incoming', language)); return; } expect(listed).toBeUndefined(); @@ -312,9 +316,7 @@ describe('caller questions with a misspelled selection or a free word order (K16 // The uncertain part is the name, and the reply says so. if (relationshipQuestion(prompt)) expect(suggestion?.markdown).toContain(language === 'de' ? 'ist nicht genau der Name der Auswahl' : 'is not exactly the name of the selection'); // The suggested question lists the selection when chosen. - expect(relationshipAnswer(suggestion!.question, evidence())?.markdown).toContain(expected === 'incoming' - ? language === 'de' ? 'Aufrufer von `JSONBAgg` im geladenen Graphen' : 'Callers of `JSONBAgg` in the loaded graph' - : language === 'de' ? 'Was `JSONBAgg` im geladenen Graphen aufruft' : 'What `JSONBAgg` calls in the loaded graph'); + expect(relationshipAnswer(suggestion!.question, evidence())?.markdown).toContain(heading(expected, language)); }); it.each([ diff --git a/graph-ui/src/browser-ai/relationship-answer.ts b/graph-ui/src/browser-ai/relationship-answer.ts index 62ff14408..1bf92862a 100644 --- a/graph-ui/src/browser-ai/relationship-answer.ts +++ b/graph-ui/src/browser-ai/relationship-answer.ts @@ -182,16 +182,18 @@ function listed(question: RelationshipQuestion, evidence: GalaxyEvidence): strin const name = quote(evidence.label); const sections: string[] = []; for (const side of question.sides) { - const heading = side === 'incoming' ? words.callersOf(name) : words.calleesOf(name); - if (evidence.depth === 0) { sections.push(`${heading}. ${words.notExpanded}`); continue; } - if (!sideLoaded(evidence, side)) { sections.push(`${heading}. ${words.notLoaded(side)}`); continue; } + const unlisted = words.unlisted(side, name); + if (evidence.depth === 0) { sections.push(`${unlisted} ${words.notExpanded}`); continue; } + if (!sideLoaded(evidence, side)) { sections.push(`${unlisted} ${words.notLoaded(side)}`); continue; } const groups = evidence.relationships[side]; - if (!groups.length) { sections.push(`${heading}. ${evidence.truncated ? words.cut(side) : words.noRelationships}`); continue; } + if (!groups.length) { sections.push(evidence.truncated ? `${unlisted} ${words.cutListed(side)}` : words.nothing(side, name)); continue; } const symbols = side === 'incoming' ? evidence.relationships.incomingSymbols : evidence.relationships.outgoingSymbols; const total = groups.reduce((sum, group) => sum + group.count, 0); const calls = groups.find(group => group.type === 'CALLS'); const others = groups.filter(group => group !== calls); - sections.push(`${heading}. ${words.total(side, total, symbols)}${calls ? '' : ` ${words.noCalls(name, side)}`}`); + // The heading counts the callers it lists; all relationships of the side follow apart (W3). + sections.push(calls ? `${side === 'incoming' ? words.callers(name, calls.count) : words.callees(name, calls.count)}${others.length ? ` ${words.all(side, total, symbols)}` : ''}` + : `${words.noCalls(name, side)} ${side === 'incoming' ? words.incoming(total, symbols) : words.outgoing(total, symbols)}`); if (calls) sections.push(listedLine(calls, side, words, name)); if (others.length) sections.push(`${words.otherRelationships(side)}\n${others.map(group => listedLine(group, side, words, name)).join('\n')}`); } diff --git a/graph-ui/src/browser-ai/strings.ts b/graph-ui/src/browser-ai/strings.ts index 98f352172..f0cc8e1e3 100644 --- a/graph-ui/src/browser-ai/strings.ts +++ b/graph-ui/src/browser-ai/strings.ts @@ -74,11 +74,18 @@ const englishRelationshipWords = { notInScope: (label: string, kind: string) => `Selected: ${label} (${kind}); its symbols are not in the loaded scope.`, selectedGroup: (kind: string, label: string, count: number, listed: string, omitted: number) => `Selected ${kind}: ${label} with ${en(count)} symbols: ${listed}${omitted > 0 ? `; +${en(omitted)} more` : ''}.`, - callersOf: (name: string) => `Callers of ${name} in the loaded graph`, - calleesOf: (name: string) => `What ${name} calls in the loaded graph`, - /** The total of a listed answer: "23 incoming relationships from 12 symbols", never "23 from 12" (C2). */ - total: (side: 'incoming' | 'outgoing', total: number, symbols?: number) => `${en(total)} ${side} ${total === 1 ? 'relationship' : 'relationships'}` + /** The heading of a listed answer counts what it lists, the CALLS edges; every other relationship + * of that side is counted apart, so 23 relationships never head a list of 11 callers (W3). */ + callers: (name: string, count: number) => `${en(count)} ${count === 1 ? 'caller' : 'callers'} (CALLS) of ${name} in the loaded graph.`, + callees: (name: string, count: number) => `${name} calls ${en(count)} ${count === 1 ? 'symbol' : 'symbols'} (CALLS) in the loaded graph.`, + all: (side: 'incoming' | 'outgoing', total: number, symbols?: number) => `All ${side}: ${en(total)} ${total === 1 ? 'relationship' : 'relationships'}` + `${symbols === undefined ? '' : ` ${side === 'incoming' ? 'from' : 'to'} ${en(symbols)} ${symbols === 1 ? 'symbol' : 'symbols'}`}.`, + /** Why a side cannot be listed; the reason follows. */ + unlisted: (side: 'incoming' | 'outgoing', name: string) => side === 'incoming' ? `The callers of ${name} cannot be listed from this scope.` + : `What ${name} calls cannot be listed from this scope.`, + nothing: (side: 'incoming' | 'outgoing', name: string) => side === 'incoming' ? `${name} has no callers and no other incoming relationships in this scope.` + : `${name} calls nothing and has no other outgoing relationships in this scope.`, + cutListed: (side: 'incoming' | 'outgoing') => `The snapshot left the ${side} relationships out.`, /** One edge type of a listed answer with the number of its symbols: "TESTS (11)". */ typeCount: (type: string, count: number) => `${type} (${en(count)})`, /** What an incoming DEFINES edge from a file, module or class says about the selection. */ @@ -86,7 +93,6 @@ const englishRelationshipWords = { noCalls: (name: string, side: 'incoming' | 'outgoing') => side === 'incoming' ? `No CALLS edge reaches ${name} in this scope.` : `${name} has no outgoing CALLS edge in this scope.`, otherRelationships: (side: 'incoming' | 'outgoing') => `Other ${side} relationships:`, - noRelationships: 'None in this scope.', notLoaded: (side: 'incoming' | 'outgoing'): string => side === 'incoming' ? 'The current scope does not follow incoming relationships. Trace incoming or both directions, then ask again.' : 'The current scope does not follow outgoing relationships. Trace outgoing or both directions, then ask again.', @@ -141,10 +147,15 @@ export const relationshipWords: { en: RelationshipWords; de: RelationshipWords } notInScope: (label: string, kind: string) => `Ausgewählt: ${label} (${kind}); seine Symbole sind nicht im geladenen Ausschnitt.`, selectedGroup: (kind: string, label: string, count: number, listed: string, omitted: number) => `Ausgewählt (${kind}): ${label} mit ${de(count)} Symbolen: ${listed}${omitted > 0 ? `; +${de(omitted)} weitere` : ''}.`, - callersOf: (name: string) => `Aufrufer von ${name} im geladenen Graphen`, - calleesOf: (name: string) => `Was ${name} im geladenen Graphen aufruft`, - total: (side: 'incoming' | 'outgoing', total: number, symbols?: number) => `${de(total)} ${side === 'incoming' ? 'eingehende' : 'ausgehende'} ${total === 1 ? 'Beziehung' : 'Beziehungen'}` - + `${symbols === undefined ? '' : ` ${side === 'incoming' ? 'aus' : 'zu'} ${de(symbols)} ${symbols === 1 ? 'Symbol' : 'Symbolen'}`}.`, + callers: (name: string, count: number) => `${de(count)} Aufrufer (CALLS) von ${name} im geladenen Graphen.`, + callees: (name: string, count: number) => `${name} ruft im geladenen Graphen ${de(count)} ${count === 1 ? 'Symbol' : 'Symbole'} auf (CALLS).`, + all: (side: 'incoming' | 'outgoing', total: number, symbols?: number) => `Alle ${side === 'incoming' ? 'eingehenden' : 'ausgehenden'}: ${de(total)} ${total === 1 ? 'Beziehung' : 'Beziehungen'}` + + `${symbols === undefined ? '' : ` ${side === 'incoming' ? 'von' : 'zu'} ${de(symbols)} ${symbols === 1 ? 'Symbol' : 'Symbolen'}`}.`, + unlisted: (side: 'incoming' | 'outgoing', name: string) => side === 'incoming' ? `Die Aufrufer von ${name} lassen sich aus diesem Ausschnitt nicht auflisten.` + : `Was ${name} aufruft, lässt sich aus diesem Ausschnitt nicht auflisten.`, + nothing: (side: 'incoming' | 'outgoing', name: string) => side === 'incoming' ? `${name} hat in diesem Ausschnitt keine Aufrufer und keine anderen eingehenden Beziehungen.` + : `${name} ruft in diesem Ausschnitt nichts auf und hat keine anderen ausgehenden Beziehungen.`, + cutListed: (side: 'incoming' | 'outgoing') => `Der Schnappschuss hat die ${side === 'incoming' ? 'eingehenden' : 'ausgehenden'} Beziehungen ausgelassen.`, typeCount: (type: string, count: number) => `${type} (${de(count)})`, definer: (kind: string, count: number, name: string) => { const noun = ({ file: ['die Datei', 'die Dateien'], module: ['das Modul', 'die Module'], class: ['die Klasse', 'die Klassen'] } as Record)[kind.toLowerCase()] @@ -153,8 +164,8 @@ export const relationshipWords: { en: RelationshipWords; de: RelationshipWords } }, noCalls: (name: string, side: 'incoming' | 'outgoing') => side === 'incoming' ? `Keine CALLS-Kante führt in diesem Ausschnitt zu ${name}.` : `${name} hat in diesem Ausschnitt keine ausgehende CALLS-Kante.`, - otherRelationships: (side: 'incoming' | 'outgoing') => `Weitere ${side === 'incoming' ? 'eingehende' : 'ausgehende'} Beziehungen:`, - noRelationships: 'Keine in diesem Ausschnitt.', + /** "Andere": the types other than CALLS, also where no CALLS edge exists (W3). */ + otherRelationships: (side: 'incoming' | 'outgoing') => `Andere ${side === 'incoming' ? 'eingehende' : 'ausgehende'} Beziehungen:`, notLoaded: (side: 'incoming' | 'outgoing') => side === 'incoming' ? 'Der aktuelle Ausschnitt folgt keinen eingehenden Beziehungen. Verfolge eingehend oder beide Richtungen und frage noch einmal.' : 'Der aktuelle Ausschnitt folgt keinen ausgehenden Beziehungen. Verfolge ausgehend oder beide Richtungen und frage noch einmal.', diff --git a/graph-ui/tools/handtest-fixes-chat.mjs b/graph-ui/tools/handtest-fixes-chat.mjs index 9ea01c7cf..bb6eecdd7 100644 --- a/graph-ui/tools/handtest-fixes-chat.mjs +++ b/graph-ui/tools/handtest-fixes-chat.mjs @@ -301,9 +301,9 @@ async function k16(page) { const luftCalls = await sentSince(page, luftMark); await shot(page, 'K16', 'luft-to-model', '"Hat diese Klasse Luft?" is not read as a call question: the model answers it.'); const callers = ['test_default_argument', 'test_empty_result_set', 'test_jsonb_agg', 'test_values_list']; - check('K16', 'Typo caller questions are listed; uncertain ones are suggested in the right direction and language; no model call', /Aufrufer von JSONBAgg im geladenen Graphen/.test(typo) - && callers.every((name) => typo.includes(name)) && /Aufrufer von JSONBAgg im geladenen Graphen/.test(shortTypo) && /Meintest du: Aufrufer von JSONBAgg\?/.test(suggestion) - && JSON.stringify(germanButtons) === JSON.stringify(['Liste anzeigen', 'Modell fragen']) && /Aufrufer von JSONBAgg im geladenen Graphen/.test(listed) + check('K16', 'Typo caller questions are listed; uncertain ones are suggested in the right direction and language; no model call', /Aufrufer \(CALLS\) von JSONBAgg im geladenen Graphen/.test(typo) + && callers.every((name) => typo.includes(name)) && /Aufrufer \(CALLS\) von JSONBAgg im geladenen Graphen/.test(shortTypo) && /Meintest du: Aufrufer von JSONBAgg\?/.test(suggestion) + && JSON.stringify(germanButtons) === JSON.stringify(['Liste anzeigen', 'Modell fragen']) && /Aufrufer \(CALLS\) von JSONBAgg im geladenen Graphen/.test(listed) && JSON.stringify(listedButtons) === JSON.stringify(['Modell fragen']) && /Did you mean: what JSONBAgg calls\?/.test(direction) && JSON.stringify(englishButtons) === JSON.stringify(['Show the list', 'Ask the model']) && modelCalls.length === 0 && luftCalls.length === 1 && !/Meintest du|Did you mean/.test(luft), { typoAnswerStart: typo.slice(0, 120), shortTypo: shortTypo.slice(0, 120), suggestion: suggestion.slice(0, 200), germanButtons, afterShowList: listed.slice(0, 120), listedButtons, @@ -324,10 +324,10 @@ async function k16(page) { await shot(page, 'K16', 'name-typo-listed', 'After "Show the list": the complete callers of JSONBAgg.'); const typoModelCalls = (await sentSince(page, typoMark)).length; check('K16', 'A typo in the selected name (up to two letters, any case) is offered for the selection, a free word order with "wo" is listed; none of them reach the model', - orders.every((item) => /Aufrufer von JSONBAgg im geladenen Graphen/.test(item.answer) && callers.every((name) => item.answer.includes(name))) + orders.every((item) => /Aufrufer \(CALLS\) von JSONBAgg im geladenen Graphen/.test(item.answer) && callers.every((name) => item.answer.includes(name))) && typos.every((item) => item.language === 'de' ? /Meintest du: Aufrufer von JSONBAgg\?/.test(item.answer) && /ist nicht genau der Name der Auswahl/.test(item.answer) : /Did you mean: callers of JSONBAgg\?/.test(item.answer) && /is not exactly the name of the selection/.test(item.answer)) - && /Callers of JSONBAgg in the loaded graph/.test(typoListed) && typoModelCalls === 0, + && /callers \(CALLS\) of JSONBAgg in the loaded graph/.test(typoListed) && typoModelCalls === 0, { orders: orders.map((item) => `${item.question} => ${item.answer.replace(/\s+/g, ' ').slice(0, 140)}`), typos: typos.map((item) => `${item.question} => ${item.answer.replace(/\s+/g, ' ').slice(0, 160)}`), afterShowList: typoListed.replace(/\s+/g, ' ').slice(0, 140), modelCalls: typoModelCalls }); }