diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16f4500..b3837c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -948,7 +948,7 @@ packages: postcss-selector-parser: ^7.1.1 '@developmentfinance/client@file:vendor/developmentfinance-client-0.1.0.tgz': - resolution: {integrity: sha512-AKzXailDdFBtL4Pp88ZGY+wYbcs7zueMN3f7TDE3bK3kyLsMwWkKWSXZmqy7WdSw1KDv2Rks+B1BGAMwykvcyQ==, tarball: file:vendor/developmentfinance-client-0.1.0.tgz} + resolution: {integrity: sha512-BWxtOIB6AwYMn26yxtRykRyyEetVMNsRpBWhRl3anPkhSBq+ygoQ+JY7BC8kXdRZUo3RBivoGSWP2M1A8+zPMw==, tarball: file:vendor/developmentfinance-client-0.1.0.tgz} version: 0.1.0 engines: {node: '>=22.18'} peerDependencies: @@ -1873,6 +1873,7 @@ packages: '@opentelemetry/sdk-trace-base@2.11.0': resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use @opentelemetry/sdk-trace package peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -4105,6 +4106,7 @@ packages: eslint@9.39.5: resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -10170,7 +10172,7 @@ snapshots: eslint: 9.39.5 eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5) eslint-plugin-react: 7.37.5(eslint@9.39.5) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5) @@ -10203,7 +10205,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5) transitivePeerDependencies: - supports-color @@ -10218,7 +10220,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 diff --git a/src/lib/search/searchShareRequest.ts b/src/lib/search/searchShareRequest.ts index 7b2c38c..428475b 100644 --- a/src/lib/search/searchShareRequest.ts +++ b/src/lib/search/searchShareRequest.ts @@ -91,25 +91,33 @@ const isEntityGroups = (value: any): value is Record => // 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 = { + 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) && @@ -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. */ diff --git a/src/lib/search/test/searchShareRequest.test.ts b/src/lib/search/test/searchShareRequest.test.ts index d6fb0b3..15c8f6f 100644 --- a/src/lib/search/test/searchShareRequest.test.ts +++ b/src/lib/search/test/searchShareRequest.test.ts @@ -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)); @@ -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'] }], diff --git a/src/lib/search/test/why.test.ts b/src/lib/search/test/why.test.ts index b15d053..5871797 100644 --- a/src/lib/search/test/why.test.ts +++ b/src/lib/search/test/why.test.ts @@ -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.', diff --git a/src/lib/search/types.ts b/src/lib/search/types.ts index 03ea585..75c0117 100644 --- a/src/lib/search/types.ts +++ b/src/lib/search/types.ts @@ -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>; -/** 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; /** The block of the answer a sentence token belongs to. */ diff --git a/src/lib/search/view.ts b/src/lib/search/view.ts index 2b9c2d2..fbc1537 100644 --- a/src/lib/search/view.ts +++ b/src/lib/search/view.ts @@ -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') { @@ -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 diff --git a/vendor/developmentfinance-client-0.1.0.tgz b/vendor/developmentfinance-client-0.1.0.tgz index 5abf089..525ed4e 100644 Binary files a/vendor/developmentfinance-client-0.1.0.tgz and b/vendor/developmentfinance-client-0.1.0.tgz differ