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 <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz committed 2026-10-04 14:32:13 +02:00
1 parent a1059d9dba
commit 9d6e69fd97
11 files changed
+84 -49

No files matched your search

@@ -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');
@@ -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.']);
});
});
@@ -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');
@@ -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.');
});
});
@@ -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}` : ''; };
+5 -3
View File
@@ -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 };
@@ -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.',
]);
});
@@ -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', () => {
+19 -16
View File
@@ -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}.`,
+1 -1
View File
@@ -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]);
+1 -1
View File
@@ -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');