Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 16 additions & 10 deletions src/lib/search/searchShareRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,33 @@ const isEntityGroups = (value: any): value is Record<string, EntityGroup> =>
// Keyed by every kind the wire names, so a kind Search adds fails the type
// check until this list takes it.
const ASSUMPTION_KINDS: Record<AssumptionKind, true> = {
name_corrected: true,
declared_default: true,
open_role: true,
place_scope: true,
};

// Search's own bounds on an echoed `Assumption`: a text of 1 to 200 code
// points, at most one ref of at most 1024 characters, a binding ref of at
// most 1036 characters, and at most 67 items in a part's list. A share
// storing one past them cannot open.
// Search's own bounds on an echoed `Assumption`: a text, and an `asked`, of
// 1 to 200 code points, at most one ref of at most 1024 characters, a binding
// ref of at most 1036 characters, and at most 67 items in a part's list. A
// share storing one past them cannot open.
const ASSUMPTION_TEXT_MAX_LENGTH = 200;
const ASSUMPTION_MAX_REFS = 1;
const ASSUMPTION_REF_MAX_LENGTH = 1024;
const ASSUMPTION_BINDING_REF_MAX_LENGTH = 1036;
const ASSUMPTIONS_MAX = 67;

// Words within the bound span at most two UTF-16 units per code point.
const isAssumptionWords = (value: unknown): boolean =>
typeof value === 'string' &&
value.length > 0 &&
value.length <= ASSUMPTION_TEXT_MAX_LENGTH * 2 &&
[...value].length <= ASSUMPTION_TEXT_MAX_LENGTH;

const isAssumption = (value: any): value is Assumption =>
isPlainObject(value) &&
Object.keys(value).every((member) =>
['kind', 'binding_ref', 'refs', 'text'].includes(member),
['kind', 'binding_ref', 'refs', 'text', 'asked'].includes(member),
) &&
typeof value.kind === 'string' &&
hasKey(ASSUMPTION_KINDS, value.kind) &&
Expand All @@ -123,11 +131,9 @@ const isAssumption = (value: any): value is Assumption =>
(ref: unknown) =>
typeof ref === 'string' && ref.length <= ASSUMPTION_REF_MAX_LENGTH,
))) &&
typeof value.text === 'string' &&
value.text.length > 0 &&
// A text within the bound spans at most two UTF-16 units per code point.
value.text.length <= ASSUMPTION_TEXT_MAX_LENGTH * 2 &&
[...value.text].length <= ASSUMPTION_TEXT_MAX_LENGTH;
isAssumptionWords(value.text) &&
// `asked` is set on a corrected name only, and absent or null on the rest.
(value.asked == null || isAssumptionWords(value.asked));

/** A part's echoed list as Search takes it: within the item cap, every item
* an `Assumption` within its bounds. */
Expand Down
11 changes: 11 additions & 0 deletions src/lib/search/test/searchShareRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,17 @@ test('a part reads with its assumptions, [] and null included, and a malformed o
const scope = { kind: 'place_scope', binding_ref: 'where:', refs: [], text: 'every country' };
// The wire's `refs` is optional, so an item without it reads.
const donors = { kind: 'open_role', binding_ref: 'origin:', text: 'every donor' };
const germany = {
kind: 'name_corrected',
binding_ref: 'origin:',
refs: ['country/DEU'],
text: '"Germny" read as Germany',
asked: 'Germny',
};

assert.deepEqual(reads([oda, scope])?.assumptions, [oda, scope]);
assert.deepEqual(reads([donors])?.assumptions, [donors]);
assert.deepEqual(reads([germany])?.assumptions, [germany]);
assert.deepEqual(reads([])?.assumptions, []);
assert.equal(reads(null)?.assumptions, null);
assert.ok(reads(undefined));
Expand All @@ -121,6 +129,9 @@ test('a part reads with its assumptions, [] and null included, and a malformed o
[{ ...oda, refs: null }],
[{ ...oda, text: null }],
[{ ...oda, note: 'extra' }],
[{ ...germany, asked: '' }],
[{ ...germany, asked: 'a'.repeat(201) }],
[{ ...germany, asked: 7 }],
[{ ...oda, text: '' }],
[{ ...oda, text: 'a'.repeat(201) }],
[{ ...oda, refs: ['ODA', 'OOF'] }],
Expand Down
8 changes: 8 additions & 0 deletions src/lib/search/test/why.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ test('many variables: rows name the role scope and the list reads the members do
{ label: 'Recipients', value: 'All 191 with data' },
]);
// No time_window_defaulted on the wire: the headline's year_named says it.
assert.equal(
scopeAssumptions(
oda,
[{ ...openRecipients, kind: 'name_corrected', text: '"Germny" read as Germany' }],
[],
)[0],
'"Germny" read as Germany.',
);
assert.deepEqual(scopeAssumptions(oda, [openRecipients], odaWarnings), [
'The question left this role open, so the answer covers every recipient from Germany.',
'No year was named, so the counts are for 2024, the latest year with data.',
Expand Down
3 changes: 2 additions & 1 deletion src/lib/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export type DeclinedReason = wire.DeclinedAnswer['reason'];
/** Which candidate universe the options door enumerates for a place role. */
export type PlaceOptionsScope = Present<Known<wire.OptionsFor['scope']>>;

/** What an assumption fills: a declared default, an open flow role, or the place scope. */
/** What an assumption fills: a corrected name, a declared default, an open flow role, or the place
* scope. */
export type AssumptionKind = Known<wire.Assumption['kind']>;

/** The block of the answer a sentence token belongs to. */
Expand Down
5 changes: 4 additions & 1 deletion src/lib/search/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,9 @@ const lowerFirst = (text: string): string =>
const assumptionSentence = (spec: SpecReading, assumption: Assumption): string => {
const { kind, binding_ref: ref, text } = assumption;

// The text states the correction whole: '"Germny" read as Germany'.
if (kind === 'name_corrected') return `${text}.`;

if (kind === 'place_scope') return `No place was named, so the answer covers ${text}.`;

if (kind === 'open_role') {
Expand All @@ -1279,7 +1282,7 @@ const assumptionSentence = (spec: SpecReading, assumption: Assumption): string =
};

/** What a definite part's answer assumed: one sentence per item of the
* part's `assumptions` (a declared default, an open role, the place scope),
* part's `assumptions` (a corrected name, a declared default, an open role, the place scope),
* in the API's order, then the year. "About this answer" reads this list
* (through resultNotes's aboutAnswerNotes). The year sentence keys on
* `headline.year_named` as well as `time_window_defaulted`, since a
Expand Down
Binary file modified vendor/developmentfinance-client-0.1.0.tgz
Binary file not shown.
Loading