From 9d6e69fd97a2a56db7aa6050c50628e25bced2c6 Mon Sep 17 00:00:00 2001 From: Bernhard Jackiewicz Date: Sun, 4 Oct 2026 14:32:13 +0200 Subject: [PATCH] fix(ui): write relationship counts with their units "Incoming relationships: 23 from 12 symbols" and "2 zu 2 Symbolen" read like a score. The summary lines now say "Incoming: 23 relationships from 12 symbols" and "Eingehend: 23 Beziehungen von 12 Symbolen", and the empty, not loaded and cut sides use the same labels. The prompt and the Source block head each edge type as "CALLS (11):" instead of "CALLS from 11:", which the model repeated as a sentence. The K7 hand test and the call feedback acceptance check expect the new wording. Signed-off-by: Bernhard Jackiewicz --- .../src/browser-ai/BrowserChatDock.test.tsx | 14 ++++---- graph-ui/src/browser-ai/chat-wording.test.ts | 30 ++++++++++++++++ .../browser-ai/explanation-context.test.ts | 26 +++++++------- .../browser-ai/explanation-response.test.ts | 2 +- .../src/browser-ai/explanation-response.ts | 2 +- graph-ui/src/browser-ai/galaxy-evidence.ts | 8 +++-- .../browser-ai/grounded-explanation.test.ts | 10 +++--- .../browser-ai/relationship-answer.test.ts | 2 +- graph-ui/src/browser-ai/strings.ts | 35 ++++++++++--------- graph-ui/tools/call-feedback-acceptance.mjs | 2 +- graph-ui/tools/handtest-fixes-chat.mjs | 2 +- 11 files changed, 84 insertions(+), 49 deletions(-) diff --git a/graph-ui/src/browser-ai/BrowserChatDock.test.tsx b/graph-ui/src/browser-ai/BrowserChatDock.test.tsx index a87cee2e7..aab9a3c8d 100644 --- a/graph-ui/src/browser-ai/BrowserChatDock.test.tsx +++ b/graph-ui/src/browser-ai/BrowserChatDock.test.tsx @@ -914,7 +914,7 @@ describe('graph answers and answer limits', () => { expect(answer).toContain('TESTS (11):'); expect(answer).toContain('DEFINES (1): general.py, the file that defines JSONBAgg'); expect(answer).toContain('not generated by the model'); - expect(container.querySelector('.cbm-chat-answer .cbm-chat-source-content')?.textContent).toContain('Incoming relationships: 23 from 12 symbols.'); + expect(container.querySelector('.cbm-chat-answer .cbm-chat-source-content')?.textContent).toContain('Incoming: 23 relationships from 12 symbols.'); expect([...container.querySelectorAll('button')].some(item => item.textContent === 'Retry')).toBe(false); expect(button('Ask the model')).toBeDefined(); expect(container.querySelector('textarea')?.value).toBe(''); @@ -1050,7 +1050,7 @@ describe('graph answers and answer limits', () => { const request = runtime.chat.mock.calls[0][0]; expect(request.map(message => message.role)).toEqual(['system', 'user']); expect(request[1].content).toContain('Who calls JSONBAgg?'); - expect(request[1].content).toContain('Incoming relationships: 23 from 12 symbols.'); + expect(request[1].content).toContain('Incoming: 23 relationships from 12 symbols.'); expect(container.querySelectorAll('.cbm-chat-turn')).toHaveLength(1); expect(container.querySelector('.cbm-chat-answer-text')?.textContent).toBe('Adds the two values.'); expect(button('Retry')).toBeDefined(); @@ -1227,7 +1227,7 @@ describe('per-selection explanation cache', () => { await settleSelection(); expect(runtime.chat).toHaveBeenCalledOnce(); // The complete scope's facts are listed in the card; the model reads the symbol's source (K7, K14). - expect(card()).toContain('Incoming relationships: 23 from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).'); + expect(card()).toContain('Incoming: 23 relationships from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).'); expect(runtime.chat.mock.calls[0][0].at(-1)!.content).toContain('class JSONBAgg(OrderableAggMixin, Aggregate):'); await render({ ...props, proactive: true, proactiveSelection: jsonbAggEvidence({ state: 'partial' }) }); await settleSelection(); expect(runtime.chat).toHaveBeenCalledOnce(); @@ -1358,7 +1358,7 @@ describe('grounded automatic explanations (K14, K7)', () => { expect(prompt).toContain('```\nclass JSONBAgg(OrderableAggMixin, Aggregate):\n function = "JSONB_AGG"'); expect(prompt).toContain('Describe this class in one short sentence that starts with `JSONBAgg`.'); expect(prompt).not.toContain('Source unavailable'); - expect(card().textContent).toContain('Incoming relationships: 23 from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).'); + expect(card().textContent).toContain('Incoming: 23 relationships from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).'); expect(card().textContent).toContain('JSONBAgg sets function to "JSONB_AGG" and allows distinct values.'); expect(card().textContent).not.toContain('list of integers'); expect(card().textContent).toContain('Facts listed from the indexed graph; the last sentence is generated by the model.'); @@ -1439,7 +1439,7 @@ describe('grounded automatic explanations (K14, K7)', () => { file_path: `tests/postgres_tests/callers/test_module_number_${index}.py`, x: 0, y: 0, z: 0, size: 1, color: '#999' })); const evidence = jsonbAggEvidence({ edges: callers.map(caller => ({ source: caller.id, target: 32360, type: 'CALLS' })), nodes: callers }); await render({ ...props, proactive: true, proactiveSelection: evidence, readSource: vi.fn(async () => snippet) }); await click('Download & load'); await settle(); - expect(card().textContent).toContain('Incoming relationships: 40 from 40 symbols (CALLS 40).'); + expect(card().textContent).toContain('Incoming: 40 relationships from 40 symbols (CALLS 40).'); expect(card().textContent).not.toContain('too large for the local'); }); @@ -1450,7 +1450,7 @@ describe('grounded automatic explanations (K14, K7)', () => { expect(readSource).toHaveBeenCalledOnce(); expect(runtime.countTokens).not.toHaveBeenCalled(); expect(runtime.chat).not.toHaveBeenCalled(); - expect(card().textContent).toContain('Incoming relationships: 23 from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).'); + expect(card().textContent).toContain('Incoming: 23 relationships from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).'); expect(card().textContent).toContain('Listed from the indexed graph; not generated by the model.'); }); @@ -1541,7 +1541,7 @@ describe('general questions, prompts without a question and configuration files expect(request.at(-1)!.content).toContain('Describe this class in one short sentence that starts with `JSONBAgg`. Answer in German.'); expect(request.at(-1)!.content).toContain('function = "JSONB_AGG"'); expect(answer()).toContain('Ausgewählt: JSONBAgg (Class) in django/contrib/postgres/aggregates/general.py:50-54.'); - expect(answer()).toContain('Eingehende Beziehungen: 23 aus 12 Symbolen (CALLS 11, TESTS 11, DEFINES 1).'); + expect(answer()).toContain('Eingehend: 23 Beziehungen von 12 Symbolen (CALLS 11, TESTS 11, DEFINES 1).'); expect(answer()).toContain('Ausschnitt: 1 Schritt in beide Richtungen'); expect(answer()).toContain('JSONBAgg setzt function auf "JSONB_AGG".'); expect(answer()).not.toContain('Liste von'); diff --git a/graph-ui/src/browser-ai/chat-wording.test.ts b/graph-ui/src/browser-ai/chat-wording.test.ts index 56d0e3372..34ce36204 100644 --- a/graph-ui/src/browser-ai/chat-wording.test.ts +++ b/graph-ui/src/browser-ai/chat-wording.test.ts @@ -52,3 +52,33 @@ describe('a scope that finished loading (W4)', () => { expect(listed).toContain('vollständig geladen.'); }); }); + +describe('relationship counts that read as counts, not as a score (W2)', () => { + it('names the unit beside each number in the summary, in English and German', () => { + expect(selectionSummary(jsonbAggEvidence()).slice(1, 3)).toEqual([ + 'Incoming: 23 relationships from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).', + 'Outgoing: 2 relationships to 2 symbols (INHERITS 2).', + ]); + expect(selectionSummary(jsonbAggEvidence(), 'de').slice(1, 3)).toEqual([ + 'Eingehend: 23 Beziehungen von 12 Symbolen (CALLS 11, TESTS 11, DEFINES 1).', + 'Ausgehend: 2 Beziehungen zu 2 Symbolen (INHERITS 2).', + ]); + }); + + it('heads each edge type of the prompt with its count in parentheses, so the model has no "CALLS from 11" to repeat', () => { + const prompt = formatExplanationEvidence(prepareExplanationContext(undefined, jsonbAggEvidence(), 3200)); + expect(prompt).toContain('Incoming: 23 relationships from 12 symbols.'); + expect(prompt).toContain('Outgoing: 2 relationships to 2 symbols.'); + expect(prompt).toMatch(/^- CALLS \(11\): test_default_argument/m); + expect(prompt).toMatch(/^- INHERITS \(2\): OrderableAggMixin/m); + expect(prompt).not.toMatch(/\b(?:CALLS|TESTS|DEFINES|INHERITS) (?:from|to) \d/); + }); + + it('uses the same words where a side has nothing, was not loaded or was cut', () => { + expect(selectionSummary(jsonbAggEvidence({ direction: 'outbound' }))[1]).toBe('Incoming: not loaded; the scope does not follow incoming edges.'); + expect(selectionSummary(jsonbAggEvidence({ direction: 'outbound' }), 'de')[1]).toBe('Eingehend: nicht geladen; der Ausschnitt folgt keinen eingehenden Kanten.'); + const lonely = jsonbAggEvidence({ edges: [] }); + expect(selectionSummary(lonely).slice(1, 3)).toEqual(['Incoming: no relationships in this scope.', 'Outgoing: no relationships in this scope.']); + expect(selectionSummary(lonely, 'de').slice(1, 3)).toEqual(['Eingehend: keine Beziehungen in diesem Ausschnitt.', 'Ausgehend: keine Beziehungen in diesem Ausschnitt.']); + }); +}); diff --git a/graph-ui/src/browser-ai/explanation-context.test.ts b/graph-ui/src/browser-ai/explanation-context.test.ts index 409015a2c..08c3dadc5 100644 --- a/graph-ui/src/browser-ai/explanation-context.test.ts +++ b/graph-ui/src/browser-ai/explanation-context.test.ts @@ -146,18 +146,18 @@ describe('bounded explanation evidence', () => { const text = outputText(prepared); expect(text).toContain('Selected: JSONBAgg (Class) in django/contrib/postgres/aggregates/general.py:50-54.'); expect(text).toContain('Scope: 1 hop in both directions, all relationship types; 15 symbols and 25 relationships; fully loaded.'); - expect(text).toContain('Incoming relationships: 23 from 12 symbols.'); + expect(text).toContain('Incoming: 23 relationships from 12 symbols.'); const line = (type: string) => text.split('\n').find(item => item.startsWith(`- ${type} `)) ?? ''; for (const type of ['CALLS', 'TESTS']) { - expect(line(type)).toMatch(new RegExp(`^- ${type} from 11: `)); + expect(line(type)).toMatch(new RegExp(`^- ${type} \\(11\\): `)); for (const name of JSONB_AGG_CALLERS) expect(line(type)).toContain(name); expect(line(type)).toContain('(Method, tests/postgres_tests/test_aggregates.py)'); } - expect(line('DEFINES')).toBe('- DEFINES from 1: general.py (File, django/contrib/postgres/aggregates/general.py)'); - expect(text).toContain('Outgoing relationships: 2 to 2 symbols.'); - expect(line('INHERITS')).toMatch(/^- INHERITS to 2: OrderableAggMixin .*Aggregate/); + expect(line('DEFINES')).toBe('- DEFINES (1): general.py (File, django/contrib/postgres/aggregates/general.py)'); + expect(text).toContain('Outgoing: 2 relationships to 2 symbols.'); + expect(line('INHERITS')).toMatch(/^- INHERITS \(2\): OrderableAggMixin .*Aggregate/); // Callers never appear under the outgoing side. - expect(text.slice(text.indexOf('Outgoing relationships'))).not.toContain('test_jsonb_agg'); + expect(text.slice(text.indexOf('Outgoing:'))).not.toContain('test_jsonb_agg'); expect(prepared.capacity).toBeUndefined(); }); @@ -170,8 +170,8 @@ describe('bounded explanation evidence', () => { it('lists names until the budget ends, then says how many more there are and reports the capacity', () => { const prepared = prepareExplanationContext(undefined, jsonbAggEvidence(), 1900); const text = outputText(prepared); - expect(text).toContain('Incoming relationships: 23 from 12 symbols.'); - const calls = text.split('\n').find(item => item.startsWith('- CALLS from 11: '))!; + expect(text).toContain('Incoming: 23 relationships from 12 symbols.'); + const calls = text.split('\n').find(item => item.startsWith('- CALLS (11): '))!; const listed = JSONB_AGG_CALLERS.filter(name => calls.includes(`${name},`) || calls.includes(`${name} (`)).length; expect(listed).toBeGreaterThan(0); expect(calls).toMatch(new RegExp(`; \\+${11 - listed} more$`)); @@ -186,14 +186,14 @@ describe('bounded explanation evidence', () => { const context = jsonbAggEvidence({ edges: [...scope.edges, ...many] }); const text = outputText(prepareExplanationContext(undefined, context, 6000)); // The extra callers are not in the node list, so they are named by id; the count stays exact. - expect(text).toContain('- CALLS from 71: '); - expect(text).toMatch(/- CALLS from 71: .*; \+47 more/); + expect(text).toContain('- CALLS (71): '); + expect(text).toMatch(/- CALLS \(71\): .*; \+47 more/); }); it('keeps a large documented scope readable and says when its snapshot cut relationships', () => { const whole = outputText(prepareExplanationContext(undefined, selectionEvidenceContext(largeFolderScope()), 6000)); - expect(whole).toContain('Incoming relationships: 150 from 150 symbols.'); - expect(whole).toContain('Outgoing relationships: 150 to 150 symbols.'); + expect(whole).toContain('Incoming: 150 relationships from 150 symbols.'); + expect(whole).toContain('Outgoing: 150 relationships to 150 symbols.'); expect(whole).not.toMatch(/Snapshot\.|Selected\.|Relationships\.|incompl/); const evidence = largeFolderScope(); const cut = selectionEvidenceContext({ ...evidence, selected: { ...evidence.selected as object, notes: Array.from({ length: 13 }, () => 'n'.repeat(1200)) } }); @@ -204,7 +204,7 @@ describe('bounded explanation evidence', () => { it('says when a side was not loaded instead of reporting no callers', () => { const text = outputText(prepareExplanationContext(undefined, jsonbAggEvidence({ direction: 'outbound' }))); - expect(text).toContain('Incoming relationships: not loaded; the scope does not follow incoming edges.'); + expect(text).toContain('Incoming: not loaded; the scope does not follow incoming edges.'); expect(text).toContain('outgoing only'); expect(outputText(prepareExplanationContext(undefined, jsonbAggEvidence({ state: 'loading-partial-preview' })))) .toContain('still loading, so this is a partial preview'); diff --git a/graph-ui/src/browser-ai/explanation-response.test.ts b/graph-ui/src/browser-ai/explanation-response.test.ts index b89e23e34..5ef5a7a11 100644 --- a/graph-ui/src/browser-ai/explanation-response.test.ts +++ b/graph-ui/src/browser-ai/explanation-response.test.ts @@ -119,6 +119,6 @@ describe('evidence sections in the prompt', () => { expect(prompt).not.toMatch(/\[(?:graph|source)-\d+\]/); expect(prompt).not.toMatch(/\b(?:graph|source)[- ]\d+\b/i); expect(formatExplanationEvidence(packet)).toMatch(/^Source django\/contrib\/postgres\/aggregates\/general\.py:50-51, a Python source file:$/m); - expect(formatExplanationEvidence(packet)).toContain('Incoming relationships: 23 from 12 symbols.'); + expect(formatExplanationEvidence(packet)).toContain('Incoming: 23 relationships from 12 symbols.'); }); }); diff --git a/graph-ui/src/browser-ai/explanation-response.ts b/graph-ui/src/browser-ai/explanation-response.ts index a6a630659..e2e304126 100644 --- a/graph-ui/src/browser-ai/explanation-response.ts +++ b/graph-ui/src/browser-ai/explanation-response.ts @@ -9,7 +9,7 @@ export const AUTO_OUTPUT_TOKENS = 128; export const CHAT_INPUT_TOKENS = 2048; /** Code sections are headed by where they come from; graph facts already begin with - * their own words ("Selected:", "Incoming relationships:"). Numbered ids such as + * their own words ("Selected:", "Incoming:"). Numbered ids such as * "[graph-1]" stay out: a small model repeats them as "Graph 1" in its answer. */ export function formatExplanationEvidence(packet: PreparedExplanationContext): string { const kind = (path: string) => { const name = fileKind(path); return name ? `, a ${name}` : ''; }; diff --git a/graph-ui/src/browser-ai/galaxy-evidence.ts b/graph-ui/src/browser-ai/galaxy-evidence.ts index ad9cd8cb1..055bc2ee1 100644 --- a/graph-ui/src/browser-ai/galaxy-evidence.ts +++ b/graph-ui/src/browser-ai/galaxy-evidence.ts @@ -104,11 +104,13 @@ export function fairShares(natural: readonly number[], budget: number): number[] } /** One edge type: complete count, then names until the budget, then an explicit "+N more". - * A listed answer heads it "**TESTS (11):**", the prompt "TESTS from 11:" (C2). */ -export function relationshipLine(group: RelationshipGroup, side: 'incoming' | 'outgoing', budget: number, + * A listed answer heads it "**TESTS (11):**", the prompt "TESTS (11):"; "CALLS from 11" was + * repeated by the model as if it were a sentence (C2, W2). Both sides now head a line alike; + * the side stays in the signature for its callers. */ +export function relationshipLine(group: RelationshipGroup, _side: 'incoming' | 'outgoing', budget: number, words: RelationshipWords, markdown = false): { text: string; listed: number } { const quote = (value: string) => markdown ? `\`${value.replace(/`/g, "'")}\`` : value; - const head = markdown ? `- **${words.typeCount(group.type, group.count)}:** ` : `- ${group.type} ${side === 'incoming' ? words.from : words.to} ${group.count}: `; + const head = markdown ? `- **${words.typeCount(group.type, group.count)}:** ` : `- ${words.typeCount(group.type, group.count)}: `; let body = '', listed = 0; const more = (shown: number) => group.count > shown ? `${body ? '; ' : ''}${words.more(group.count - shown)}` : ''; if (head.length + more(0).length > budget) return { text: '', listed: 0 }; diff --git a/graph-ui/src/browser-ai/grounded-explanation.test.ts b/graph-ui/src/browser-ai/grounded-explanation.test.ts index 6900b08f5..2eec8830c 100644 --- a/graph-ui/src/browser-ai/grounded-explanation.test.ts +++ b/graph-ui/src/browser-ai/grounded-explanation.test.ts @@ -63,7 +63,7 @@ describe('the selected symbol source in a Galaxy explanation (K14)', () => { const packet = prepareExplanationContext(undefined, jsonbAggEvidence(), 3200, source); const prompt = formatExplanationEvidence(packet); expect(prompt).toContain('Source django/contrib/postgres/aggregates/general.py:50-52, a Python source file:\nclass JSONBAgg(OrderableAggMixin, Aggregate):'); - expect(prompt).toContain('Incoming relationships: 23 from 12 symbols.'); + expect(prompt).toContain('Incoming: 23 relationships from 12 symbols.'); expect(packet.limitations.join('\n')).not.toContain('Source unavailable'); expect(prepareExplanationContext(undefined, jsonbAggEvidence(), 3200).limitations.join('\n')).toContain('Source unavailable'); }); @@ -107,8 +107,8 @@ describe('the selected symbol source in a Galaxy explanation (K14)', () => { it('summarizes the selection from the graph: what it is and its relationships by direction and type', () => { expect(selectionSummary(jsonbAggEvidence())).toEqual([ 'Selected: JSONBAgg (Class) in django/contrib/postgres/aggregates/general.py:50-54.', - 'Incoming relationships: 23 from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).', - 'Outgoing relationships: 2 to 2 symbols (INHERITS 2).', + 'Incoming: 23 relationships from 12 symbols (CALLS 11, TESTS 11, DEFINES 1).', + 'Outgoing: 2 relationships to 2 symbols (INHERITS 2).', 'Scope: 1 hop in both directions, all relationship types; 15 symbols and 25 relationships; fully loaded.', ]); expect(selectionSummary(djangoAreaEvidence())[0]).toBe('Selected source area: `django` (2310 files · 15299 indexed nodes).'); @@ -138,8 +138,8 @@ describe('the selected symbol source in a Galaxy explanation (K14)', () => { it('summarizes the selection in German for a German question (C5)', () => { expect(selectionSummary(jsonbAggEvidence(), 'de')).toEqual([ 'Ausgewählt: JSONBAgg (Class) in django/contrib/postgres/aggregates/general.py:50-54.', - 'Eingehende Beziehungen: 23 aus 12 Symbolen (CALLS 11, TESTS 11, DEFINES 1).', - 'Ausgehende Beziehungen: 2 zu 2 Symbolen (INHERITS 2).', + 'Eingehend: 23 Beziehungen von 12 Symbolen (CALLS 11, TESTS 11, DEFINES 1).', + 'Ausgehend: 2 Beziehungen zu 2 Symbolen (INHERITS 2).', 'Ausschnitt: 1 Schritt in beide Richtungen, alle Beziehungstypen; 15 Symbole und 25 Beziehungen; vollständig geladen.', ]); }); diff --git a/graph-ui/src/browser-ai/relationship-answer.test.ts b/graph-ui/src/browser-ai/relationship-answer.test.ts index 89c604625..26679cb84 100644 --- a/graph-ui/src/browser-ai/relationship-answer.test.ts +++ b/graph-ui/src/browser-ai/relationship-answer.test.ts @@ -129,7 +129,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(/Outgoing: relationships left out of this snapshot\.|\d+ outgoing relationships?\./); }); it('leaves questions about another symbol, attached non-Galaxy evidence and other questions to the model', () => { diff --git a/graph-ui/src/browser-ai/strings.ts b/graph-ui/src/browser-ai/strings.ts index 96b175983..98f352172 100644 --- a/graph-ui/src/browser-ai/strings.ts +++ b/graph-ui/src/browser-ai/strings.ts @@ -37,7 +37,6 @@ export const browserAiText = { const en = (value: number) => value.toLocaleString('en-US'); const de = (value: number) => value.toLocaleString('de-DE'); const englishRelationshipWords = { - from: 'from', to: 'to', more: (count: number) => `+${en(count)} more`, hops: (depth: number) => depth === 0 ? 'the selection only' : depth === 1 ? '1 hop' : `${depth} hops`, both: 'in both directions', inbound: 'incoming only', outbound: 'outgoing only', @@ -56,13 +55,16 @@ const englishRelationshipWords = { + (layer > 1 ? 'so counts and names further out can be incomplete' : 'so counts and names can be incomplete, the direct relationships included'), exhausted: 'nothing further beyond this depth', scope: (shape: string, size: string, state: string) => `Scope: ${shape}; ${size}; ${state}.`, - incoming: (total: number, symbols?: number) => `Incoming relationships: ${en(total)}${symbols === undefined ? '' : ` from ${en(symbols)} ${symbols === 1 ? 'symbol' : 'symbols'}`}.`, - outgoing: (total: number, symbols?: number) => `Outgoing relationships: ${en(total)}${symbols === undefined ? '' : ` to ${en(symbols)} ${symbols === 1 ? 'symbol' : 'symbols'}`}.`, - noIncoming: 'Incoming relationships: none in this scope.', - noOutgoing: 'Outgoing relationships: none in this scope.', - incomingNotLoaded: 'Incoming relationships: not loaded; the scope does not follow incoming edges.', - outgoingNotLoaded: 'Outgoing relationships: not loaded; the scope does not follow outgoing edges.', - cut: (side: 'incoming' | 'outgoing') => `${side === 'incoming' ? 'Incoming' : 'Outgoing'} relationships: left out of this snapshot.`, + /** Each number with its unit: "Incoming: 23 relationships from 12 symbols", never "23 from 12", which reads like a score (W2). */ + incoming: (total: number, symbols?: number) => `Incoming: ${en(total)} ${total === 1 ? 'relationship' : 'relationships'}` + + `${symbols === undefined ? '' : ` from ${en(symbols)} ${symbols === 1 ? 'symbol' : 'symbols'}`}.`, + outgoing: (total: number, symbols?: number) => `Outgoing: ${en(total)} ${total === 1 ? 'relationship' : 'relationships'}` + + `${symbols === undefined ? '' : ` to ${en(symbols)} ${symbols === 1 ? 'symbol' : 'symbols'}`}.`, + noIncoming: 'Incoming: no relationships in this scope.', + noOutgoing: 'Outgoing: no relationships in this scope.', + incomingNotLoaded: 'Incoming: not loaded; the scope does not follow incoming edges.', + outgoingNotLoaded: 'Outgoing: not loaded; the scope does not follow outgoing edges.', + cut: (side: 'incoming' | 'outgoing') => `${side === 'incoming' ? 'Incoming' : 'Outgoing'}: relationships left out of this snapshot.`, truncated: 'the snapshot left part of its relationships out, so counts and names can be incomplete', moreTypes: (count: number) => `+${en(count)} more relationship ${count === 1 ? 'type' : 'types'}`, internal: (summary: string) => `Between the selected symbols: ${summary}.`, @@ -104,7 +106,6 @@ export type RelationshipWords = typeof englishRelationshipWords; export const relationshipWords: { en: RelationshipWords; de: RelationshipWords } = { en: englishRelationshipWords, de: { - from: 'von', to: 'zu', more: (count: number) => `+${de(count)} weitere`, hops: (depth: number) => depth === 0 ? 'nur die Auswahl' : depth === 1 ? '1 Schritt' : `${depth} Schritte`, both: 'in beide Richtungen', inbound: 'nur eingehend', outbound: 'nur ausgehend', @@ -122,13 +123,15 @@ export const relationshipWords: { en: RelationshipWords; de: RelationshipWords } }, exhausted: 'dahinter folgt nichts mehr', scope: (shape: string, size: string, state: string) => `Ausschnitt: ${shape}; ${size}; ${state}.`, - incoming: (total: number, symbols?: number) => `Eingehende Beziehungen: ${de(total)}${symbols === undefined ? '' : ` aus ${de(symbols)} ${symbols === 1 ? 'Symbol' : 'Symbolen'}`}.`, - outgoing: (total: number, symbols?: number) => `Ausgehende Beziehungen: ${de(total)}${symbols === undefined ? '' : ` zu ${de(symbols)} ${symbols === 1 ? 'Symbol' : 'Symbolen'}`}.`, - noIncoming: 'Eingehende Beziehungen: keine in diesem Ausschnitt.', - noOutgoing: 'Ausgehende Beziehungen: keine in diesem Ausschnitt.', - incomingNotLoaded: 'Eingehende Beziehungen: nicht geladen; der Ausschnitt folgt keinen eingehenden Kanten.', - outgoingNotLoaded: 'Ausgehende Beziehungen: nicht geladen; der Ausschnitt folgt keinen ausgehenden Kanten.', - cut: (side: 'incoming' | 'outgoing') => `${side === 'incoming' ? 'Eingehende' : 'Ausgehende'} Beziehungen: in diesem Schnappschuss ausgelassen.`, + incoming: (total: number, symbols?: number) => `Eingehend: ${de(total)} ${total === 1 ? 'Beziehung' : 'Beziehungen'}` + + `${symbols === undefined ? '' : ` von ${de(symbols)} ${symbols === 1 ? 'Symbol' : 'Symbolen'}`}.`, + outgoing: (total: number, symbols?: number) => `Ausgehend: ${de(total)} ${total === 1 ? 'Beziehung' : 'Beziehungen'}` + + `${symbols === undefined ? '' : ` zu ${de(symbols)} ${symbols === 1 ? 'Symbol' : 'Symbolen'}`}.`, + noIncoming: 'Eingehend: keine Beziehungen in diesem Ausschnitt.', + noOutgoing: 'Ausgehend: keine Beziehungen in diesem Ausschnitt.', + incomingNotLoaded: 'Eingehend: nicht geladen; der Ausschnitt folgt keinen eingehenden Kanten.', + outgoingNotLoaded: 'Ausgehend: nicht geladen; der Ausschnitt folgt keinen ausgehenden Kanten.', + cut: (side: 'incoming' | 'outgoing') => `${side === 'incoming' ? 'Eingehend' : 'Ausgehend'}: Beziehungen in diesem Schnappschuss ausgelassen.`, truncated: 'der Schnappschuss hat einen Teil der Beziehungen ausgelassen, Anzahlen und Namen können unvollständig sein', moreTypes: (count: number) => `+${de(count)} weitere ${count === 1 ? 'Beziehungstyp' : 'Beziehungstypen'}`, internal: (summary: string) => `Zwischen den ausgewählten Symbolen: ${summary}.`, diff --git a/graph-ui/tools/call-feedback-acceptance.mjs b/graph-ui/tools/call-feedback-acceptance.mjs index f62b70c53..f278914da 100644 --- a/graph-ui/tools/call-feedback-acceptance.mjs +++ b/graph-ui/tools/call-feedback-acceptance.mjs @@ -457,7 +457,7 @@ async function chatChecks(page) { const callers = [...new Set(truth.filter((row) => row.type === 'CALLS').map((row) => row.source))]; const byType = truth.reduce((acc, row) => { acc[row.type] = (acc[row.type] ?? 0) + 1; return acc; }, {}); const card = await page.locator('.cbm-chat-explanation').innerText().catch(() => ''); - const factsInCard = Object.entries(byType).every(([type, count]) => card.includes(`${type} ${count}`)) && card.includes(`Incoming relationships: ${truth.length}`); + const factsInCard = Object.entries(byType).every(([type, count]) => card.includes(`${type} ${count}`)) && card.includes(`Incoming: ${truth.length} relationship`); const sourceInPrompt = autoPrompt.includes('class JSONBAgg('); check('C1', 'Erklaerung: Fakten aus dem Index fest in der Karte, Prompt mit Quelltext und ohne interne Feldnamen', autoPrompt.length > 0 && forbidden.length === 0 && sourceInPrompt && factsInCard, { forbiddenFound: forbidden, sourceInPrompt, factsInCard, truthByType: byType, callers: callers.length, cardStart: card.slice(0, 400), promptStart: autoPrompt.slice(0, 300) }, [explanationImage]); diff --git a/graph-ui/tools/handtest-fixes-chat.mjs b/graph-ui/tools/handtest-fixes-chat.mjs index 5a9cb46ac..9ea01c7cf 100644 --- a/graph-ui/tools/handtest-fixes-chat.mjs +++ b/graph-ui/tools/handtest-fixes-chat.mjs @@ -487,7 +487,7 @@ async function k7(page) { let card = await explanationText(page); outputs.push({ selection: 'JSONBAgg incoming CALLS', card, model: (await answersSince(page, from))[0]?.output, prompt: promptText(request) }); await shot(page, 'K7', 'galaxy-incoming-calls', 'JSONBAgg, incoming, CALLS only: listed facts, at most one model sentence, no "list of integers / list of strings".'); - const galaxyOk = Boolean(request) && /Incoming relationships: 11 from 11 symbols \(CALLS 11\)/.test(card) && !/list of (?:integers|strings)|output is/i.test(card) + const galaxyOk = Boolean(request) && /Incoming: 11 relationships from 11 symbols \(CALLS 11\)/.test(card) && !/list of (?:integers|strings)|output is/i.test(card) && /Never state types, parameters, inputs, outputs/.test(promptText(request)) && /class JSONBAgg\(OrderableAggMixin, Aggregate\)/.test(promptText(request)); // Architecture, Overview: the django area (hand test 17:53). await tab(page, 'architecture');