@singi-labs/sifa-sdk 0.19.31 → 0.19.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/jev/index.cjs +3 -1
- package/dist/jev/index.cjs.map +1 -1
- package/dist/jev/index.d.cts +17 -3
- package/dist/jev/index.d.ts +17 -3
- package/dist/jev/index.js +3 -1
- package/dist/jev/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6696,7 +6696,7 @@ function describeSifaRecord(collection, value) {
|
|
|
6696
6696
|
}
|
|
6697
6697
|
|
|
6698
6698
|
// src/index.ts
|
|
6699
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
6699
|
+
var SIFA_SDK_VERSION = "0.19.32";
|
|
6700
6700
|
|
|
6701
6701
|
exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
|
|
6702
6702
|
exports.ACTIVITY_VERBS = ACTIVITY_VERBS;
|
package/dist/index.js
CHANGED
|
@@ -6694,7 +6694,7 @@ function describeSifaRecord(collection, value) {
|
|
|
6694
6694
|
}
|
|
6695
6695
|
|
|
6696
6696
|
// src/index.ts
|
|
6697
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
6697
|
+
var SIFA_SDK_VERSION = "0.19.32";
|
|
6698
6698
|
|
|
6699
6699
|
export { ACTIVITY_TIERS, ACTIVITY_VERBS, ACTIVITY_VISIBILITY_RULES, ADULT_CONTENT_LABELS, ALL_SECTIONS, APP_CATEGORIES, APP_CATEGORY_IDS, APP_CATEGORY_MAP, APP_URL_PATTERNS, ARTIFACT_LINK_KIND_LABELS, ARTIFACT_LINK_KIND_OPTIONS, ArtifactLinkSchema, AtmosphereFeedItemSchema, BlobRefSchema, CALENDAR_EVENT_MODE_LABELS, CALENDAR_EVENT_STATUS_LABELS, CATEGORY_LABELS, CATEGORY_ORDER, COLLECTION_TO_APP, COMPANY_OPTIONAL_EMPLOYMENT_TYPES, COMPANY_PAGE_MIN_FIRMOGRAPHIC_FIELDS, COMPLETENESS_MAX_SCORE, CONTINENTS, COUNTRIES, COURSE_ROLE_LABELS, COURSE_ROLE_OPTIONS, COURSE_ROLE_TAKEN, COURSE_ROLE_TAUGHT, COURSE_ROLE_TEACHING_ASSISTANT, DIMENSIONS_MAX_SCORE, EMPLOYMENT_TYPE_GROUPS, EMPLOYMENT_TYPE_LABELS, ENTITY_REF_ANCHORS, EndorsementConfirmationRecordSchema, EndorsementRecordSchema, EntityImportSearchResponseSchema, EntityMintDomainResponseSchema, EntityResolveDomainRequestSchema, EntityResolveDomainResponseSchema, EntitySearchResponseSchema, EntitySearchResultSchema, EntitySelectRequestSchema, EntitySelectResponseSchema, FEATURE_FLAGS, FeatureAllowlistEntrySchema, FeedActorSchema, FollowFeedItemSchema, FollowFeedPageSchema, FollowProfilePageSchema, FollowProfileSchema, GraphFollowRecordSchema, INDUSTRY_OPTIONS, INVESTMENT_ROLE_LABELS, INVESTMENT_ROLE_OPTIONS, INVESTMENT_STAGE_LABELS, INVESTMENT_STAGE_OPTIONS, INVESTMENT_STATUS_LABELS, INVESTMENT_STATUS_OPTIONS, INVOLVEMENT_HEADING_ORDER, INVOLVEMENT_KIND_HEADINGS, INVOLVEMENT_KIND_LABELS, INVOLVEMENT_KIND_OPTIONS, InvestmentAmountSchema, MIN_SKILLS, ON_BEHALF_OF_EMPLOYMENT_TYPES, OPEN_TO_OPTIONS, OPEN_TO_TOKENS, OPEN_TO_TOKEN_TO_VALUE, OPEN_TO_VALUE_TO_TOKEN, OrgEmploymentAttestationRecordSchema, OrgProfileRecordSchema, PLATFORM_LABELS, PLATFORM_OPTIONS, PRESENTATION_LINK_TYPE_LABELS, PRESENTATION_LINK_TYPE_OPTIONS, PRESENTATION_ROLE_LABELS, PRESENTATION_ROLE_OPTIONS, PROFILE_INVESTMENT_NSID, PROFILE_INVOLVEMENT_NSID, PROJECT_ROLES, PUBLICATION_TYPE_LABELS, PUBLICATION_TYPE_OPTIONS, PUBLISHERS, PresentationDurationSchema, PresentationLinkSchema, ProfileCertificationRecordSchema, ProfileCourseRecordSchema, ProfileEducationRecordSchema, ProfileExternalAccountRecordSchema, ProfileHonorRecordSchema, ProfileInvestmentRecordSchema, ProfileInvolvementRecordSchema, ProfileLanguageRecordSchema, ProfilePositionRecordSchema, ProfilePresentationDeliveryRecordSchema, ProfilePresentationRecordSchema, ProfileProjectRecordSchema, ProfilePublicationRecordSchema, ProfileSelfRecordSchema, ProfileSkillRecordSchema, ProfileVolunteeringRecordSchema, PublicationAuthorSchema, ROADMAP_ISSUES_REPO, ROADMAP_ITEM_META, SECTION_GROUPS, SECTION_LABELS, SELF_APP_ID, SIFA_REPO_GROUPS, SIFA_SDK_VERSION, SKILL_CATEGORIES, STANDARD_PUBLISHER_ID, STREAM_VERBS, SifaFeedItemSchema, VERIFICATION_PROVIDERS, WORKPLACE_TYPE_LABELS, WORKPLACE_TYPE_OPTIONS, actorShowsIdentity, agentRefSchema, atUriSchema, buildMetaDescription, buildProfileHighlights, buildTalkSlug, categoryForApp, certDateExtractor, cidSchema, classifyEntityRef, collapseContributors, completenessPercent, completenessScore, contrastRatio, countFilledDimensions, countRecentActivity, countryCodeToFlag, dateRangeExtractor, datetimeSchema, decodeFeedCursor, dedupeSkills, describeSifaRecord, detectPdsProvider, didSchema, dimensionsFromInputs, durationFromMinutes, encodeFeedCursor, entityDisambiguationLabel, entityResultKey, excludeSelfApp, externalRecordRefSchema, filterHidden, findIndustry, formatCompanyName, formatDateRange, formatDisplayUrl, formatDistanceToNow, formatEventDate, formatIsoTitle, formatLocation, formatPresentationDuration, formatRelationship, formatRelativeTime, formatSingleDate, formatSpanDate, formatTimelineDate, getActivityTaxonomyVersion, getActivityTier, getActivityVerbsVersion, getAppCategoryIcon, getAppIdForCollection, getArtifactLinkKindLabel, getCalendarEventModeLabel, getCalendarEventStatusLabel, getContinent, getCourseRoleLabel, getDisplayLabel, getEmploymentTypeLabel, getFaviconUrl, getFilledDimensionsMap, getHandleStem, getIndustryLabelKey, getInvestmentRoleLabel, getInvestmentStageLabel, getInvestmentStatusLabel, getInvolvementKindHeading, getInvolvementKindLabel, getLexiconEntry, getOpenToLabelKey, getPdsDisplayName, getPlatformLabel, getPresentationLinkTypeLabel, getPresentationRoleLabel, getPublicationTypeLabel, getPublisherByHost, getPublisherById, getPublisherFromSiteUrl, getTierMeta, getVerificationProvider, getVisibleSectionIds, getWorkplaceTypeLabel, groupInvolvementByHeading, groupSkillsByCategory, groupSkillsBySubCategory, groupSkillsForDisplay, hasAdultContent, hasPersonalProfileContent, hoistPrimary, isAppCategory, isCompanyPageIndexable, isCompanyRequired, isKnownAppId, isKnownPlatform, isKnownVerificationProvider, isLinked, isOnBehalfOfApplicable, isPseudoEmployer, isRegistrableDomainHandle, isRelationalActivity, isRoleLineRedundant, isSectionPopulated, isSelfAuthoredRich, isTeachingCourse, isValidRgbColor, isVisibleActivityItem, languageTagSchema, lexiconDateExtractor, limitCombiningMarks, locationSegments, looksLikeDomain, makeGraphFollowRecordSchema, maxGraphemes, meetsContrastAA, normalizeCompanyKey, normalizeDoi, normalizeLegalForm, normalizeOpenTo, normalizePlatformId, normalizePresentationMode, normalizePresentationRole, normalizeSkillCategory, normalizeWorkplaceTypes, openToTokenToValue, openToValueToToken, parseEndorsementComment, parseIntendedAudiences, parseLocationString, parsePresentationDuration, parseTalkRkey, partialDateSchema, pdsProviderFromApi, pickPrimaryFlagged, pickPrimaryPosition, presentationCsvRowToRecord, presentationDeliveryCsvRowToRecord, primaryVerification, profileToDimensionInputs, qualifiesAsOrg, relativeLuminance, renderAsLine, rendersCompanyProfile, rendersPersonalProfile, repoGroupForCollection, resolveAccountFacetMode, resolveAgentRef, resolveCardHealth, resolveCardUrl, resolveEmbed, resolveVerifierProvider, rgbToString, roadmapIssueUrl, sanitizeDisplayText, sanitizeHandleInput, searchResultDisambiguation, selfLabelsSchema, shouldRenderHighlights, singleDateExtractor, skillRefSchema, slugifyTitle, sortByActiveDateRange, sortByDateDesc, sortCertifications, sortEducation, sortHonors, sortLanguages, sortLanguagesByProficiency, sortPositions, sortProjects, sortPublications, splitCoursesByRole, streamAddressSchema, streamAuthorSchema, streamCardBodySchema, streamCardSubjectSchema, streamCardVMSchema, streamExternalLinkSchema, streamGeoSchema, streamMediaSchema, streamRichSegmentSchema, streamSourceSchema, streamThemeSchema, streamVerbSchema, stripHtmlToText, strongRefSchema, summarizePresentationDeliveries, summarizeProfileView, toStreamCardVM, toStreamCardVMs, truncateGraphemes, uriSchema, verbForCollection, visibleItems, youtubeThumbnailUrl, youtubeVideoId };
|
|
6700
6700
|
//# sourceMappingURL=index.js.map
|
package/dist/jev/index.cjs
CHANGED
|
@@ -92,7 +92,9 @@ var SIZE_BAND_CHOICE_QUESTION = choiceQuestion(
|
|
|
92
92
|
SIZE_BAND_TAXONOMY
|
|
93
93
|
);
|
|
94
94
|
function buildFirmographicState(org) {
|
|
95
|
-
|
|
95
|
+
const compacted = compact(org);
|
|
96
|
+
if (compacted.links && compacted.links.length === 0) delete compacted.links;
|
|
97
|
+
return compacted;
|
|
96
98
|
}
|
|
97
99
|
var FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;
|
|
98
100
|
function pickChoiceAboveFloor(answer, opts = {}) {
|
package/dist/jev/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/jev/index.ts"],"names":["z"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `@singi-labs/sifa-sdk/jev` -- TypeSafe System One (Jev) question builders,\n * taxonomies, result schemas, and threshold predicates for Sifa's organisation\n * intelligence.\n *\n * This subpath is pure: it defines the typed *questions* to ask Jev, validates\n * the *answers*, and holds the policy constants (thresholds, taxonomies) that\n * turn a raw judgment into a decision. It performs no network calls and needs\n * no API key, so it is safe to import anywhere. The actual `systemOne` call\n * lives server-side in sifa-api, which passes an API key that must never reach\n * the browser.\n *\n * Judgment shapes:\n * - Duplicate-org detection: a single Noul over a candidate pair.\n * - Firmographic classification: independent Choice questions (industry, type,\n * size) asked together over one org's state.\n *\n * See docs.typesafe.ai for the wire contract. Typed output guarantees the\n * interface, not truth: validate thresholds on labelled Sifa data before\n * enabling any write.\n */\n\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Wire question types\n// ---------------------------------------------------------------------------\n\n/** A yes/no judgment. `criteria` clarifies what true vs false means. */\nexport interface NoulQuestion {\n type: 'noul';\n instructions: string;\n criteria?: { true: string; false: string };\n}\n\n/** A single-selection judgment. `criteria` maps each option to its description. */\nexport interface ChoiceQuestion {\n type: 'choice';\n instructions: string;\n criteria: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Answer schemas (validate Jev's response before code trusts it)\n// ---------------------------------------------------------------------------\n\n/** Jev's Noul answer: `noul` is the probability that the answer is yes (0..1). */\nexport const noulAnswerSchema = z.object({\n type: z.literal('noul'),\n noul: z.number().min(0).max(1),\n});\nexport type NoulAnswer = z.infer<typeof noulAnswerSchema>;\n\n/**\n * Jev's Choice answer: `choice` is the highest-probability option, `confidence`\n * summarises how concentrated the distribution is, `probabilities` sums to 1.\n */\nexport const choiceAnswerSchema = z.object({\n type: z.literal('choice'),\n choice: z.string(),\n confidence: z.number().min(0).max(1),\n probabilities: z.record(z.string(), z.number()),\n});\nexport type ChoiceAnswer = z.infer<typeof choiceAnswerSchema>;\n\n// ---------------------------------------------------------------------------\n// Duplicate-org detection (Noul) -- ticket #560\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields we compare when judging whether two orgs are the same. */\nexport interface OrgRecordInput {\n name: string;\n domain?: string;\n location?: string;\n description?: string;\n}\n\n/** Build a plain object with only the defined keys (Jev sees no `undefined`). */\nfunction compact<T extends object>(obj: T): Partial<T> {\n const out: Partial<T> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined && v !== null && v !== '') out[k as keyof T] = v as T[keyof T];\n }\n return out;\n}\n\n/**\n * State for the org-pair judgment. Both records live under stable named fields\n * so the question can reference them unambiguously.\n */\nexport function buildOrgPairState(\n a: OrgRecordInput,\n b: OrgRecordInput,\n): { orgA: Partial<OrgRecordInput>; orgB: Partial<OrgRecordInput> } {\n return { orgA: compact(a), orgB: compact(b) };\n}\n\n/** Static Noul question paired with {@link buildOrgPairState}. */\nexport const ORG_PAIR_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds two organisation records, orgA and orgB. Do they refer to the same real-world organisation?',\n criteria: {\n true: 'Same organisation, e.g. a legal-suffix or abbreviation variant of the same name, or the same primary domain.',\n false:\n 'Distinct organisations, e.g. a shared word in the name but different domains, locations, or lines of business.',\n },\n};\n\n/**\n * Threshold favouring precision: a wrong merge is worse than a missed one.\n * Validate on labelled pairs (ticket #560) before enabling merges in prod.\n */\nexport const SAME_ORG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the merge threshold. */\nexport function isLikelySameOrg(noul: number, threshold: number = SAME_ORG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n// ---------------------------------------------------------------------------\n// Firmographic classification (Choice) -- ticket #561\n// ---------------------------------------------------------------------------\n\n/** Industry taxonomy. Single source of truth; `other` is the no-match option. */\nexport const INDUSTRY_TAXONOMY: Record<string, string> = {\n technology: 'Software, IT services, internet, hardware, telecoms',\n finance: 'Banking, insurance, investment, fintech, accounting',\n healthcare: 'Medical care, pharma, biotech, health services',\n education: 'Schools, universities, training, edtech',\n manufacturing: 'Industrial production, hardware, materials, automotive',\n retail: 'Retail, ecommerce, consumer goods, hospitality, food',\n media: 'Media, publishing, entertainment, marketing, advertising',\n energy: 'Energy, utilities, oil and gas, renewables',\n construction: 'Construction, real estate, architecture, engineering',\n transport: 'Transport, logistics, shipping, aviation',\n professional_services: 'Legal, consulting, HR, business services',\n public_sector: 'Government, defence, public administration',\n nonprofit: 'Charities, NGOs, foundations, associations',\n agriculture: 'Agriculture, farming, fishing, forestry',\n other: 'None of the above, or not enough information to decide',\n};\n\n/**\n * Organisation type (the org's FORM, not its sector -- a hospital's type is\n * healthcare_provider while its industry is healthcare). `other` is the no-match\n * option. Prefer the registry `legalForm` where it already encodes the form.\n */\nexport const ORG_TYPE_TAXONOMY: Record<string, string> = {\n company: 'For-profit company or corporation',\n cooperative: 'Member-owned co-operative (e.g. eG, SCOP, mutual, credit union)',\n nonprofit: 'Nonprofit, charity, or NGO',\n foundation: 'Grant-making or endowed foundation (e.g. Stiftung, fonds)',\n government: 'Government body, agency, or public-sector authority',\n educational: 'School, college, or university',\n research_institute: 'Independent research institute or laboratory',\n healthcare_provider: 'Hospital, clinic, or healthcare provider',\n professional_association: 'Professional association, trade body, or trade union',\n agency: 'Agency, consultancy, or professional-services firm',\n religious: 'Religious organisation',\n political: 'Political party or organisation',\n self_employed: 'Sole proprietor, freelancer, or self-employed individual',\n other: 'None of the above, or not enough information to decide',\n};\n\n/** Ordered employee-size bands; `unknown` is the no-match option. */\nexport const SIZE_BAND_TAXONOMY: Record<string, string> = {\n solo: '1 person',\n small: '2 to 10 people',\n medium: '11 to 50 people',\n large: '51 to 250 people',\n enterprise: 'More than 250 people',\n unknown: 'Not enough information to decide',\n};\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const INDUSTRY_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single industry best describes this organisation?',\n INDUSTRY_TAXONOMY,\n);\n\nexport const ORG_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single organisation type (its form, not its industry sector) best describes this organisation?',\n ORG_TYPE_TAXONOMY,\n);\n\nexport const SIZE_BAND_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which employee-size band best fits this organisation?',\n SIZE_BAND_TAXONOMY,\n);\n\n/** Input for firmographic classification. Enrichment text is the primary evidence. */\nexport interface FirmographicInput {\n name: string;\n domain?: string;\n enrichmentText: string;\n}\n\n/** State for the firmographic questions. */\nexport function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput> {\n return compact(org);\n}\n\n/**\n * Below this confidence a firmographic field is left unset rather than guessed.\n * Validate per field on labelled orgs (ticket #561).\n */\nexport const FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;\n\n/**\n * Select the classified option, or `null` to leave the field unset. Returns\n * null when confidence is below the floor, or when the selected option is the\n * taxonomy's no-match option.\n */\nexport function pickChoiceAboveFloor(\n answer: ChoiceAnswer,\n opts: { floor?: number; noMatch?: string } = {},\n): string | null {\n const floor = opts.floor ?? FIRMOGRAPHIC_CONFIDENCE_FLOOR;\n if (answer.confidence < floor) return null;\n if (opts.noMatch !== undefined && answer.choice === opts.noMatch) return null;\n return answer.choice;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/jev/index.ts"],"names":["z"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `@singi-labs/sifa-sdk/jev` -- TypeSafe System One (Jev) question builders,\n * taxonomies, result schemas, and threshold predicates for Sifa's organisation\n * intelligence.\n *\n * This subpath is pure: it defines the typed *questions* to ask Jev, validates\n * the *answers*, and holds the policy constants (thresholds, taxonomies) that\n * turn a raw judgment into a decision. It performs no network calls and needs\n * no API key, so it is safe to import anywhere. The actual `systemOne` call\n * lives server-side in sifa-api, which passes an API key that must never reach\n * the browser.\n *\n * Judgment shapes:\n * - Duplicate-org detection: a single Noul over a candidate pair.\n * - Firmographic classification: independent Choice questions (industry, type,\n * size) asked together over one org's state.\n *\n * See docs.typesafe.ai for the wire contract. Typed output guarantees the\n * interface, not truth: validate thresholds on labelled Sifa data before\n * enabling any write.\n */\n\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Wire question types\n// ---------------------------------------------------------------------------\n\n/** A yes/no judgment. `criteria` clarifies what true vs false means. */\nexport interface NoulQuestion {\n type: 'noul';\n instructions: string;\n criteria?: { true: string; false: string };\n}\n\n/** A single-selection judgment. `criteria` maps each option to its description. */\nexport interface ChoiceQuestion {\n type: 'choice';\n instructions: string;\n criteria: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Answer schemas (validate Jev's response before code trusts it)\n// ---------------------------------------------------------------------------\n\n/** Jev's Noul answer: `noul` is the probability that the answer is yes (0..1). */\nexport const noulAnswerSchema = z.object({\n type: z.literal('noul'),\n noul: z.number().min(0).max(1),\n});\nexport type NoulAnswer = z.infer<typeof noulAnswerSchema>;\n\n/**\n * Jev's Choice answer: `choice` is the highest-probability option, `confidence`\n * summarises how concentrated the distribution is, `probabilities` sums to 1.\n */\nexport const choiceAnswerSchema = z.object({\n type: z.literal('choice'),\n choice: z.string(),\n confidence: z.number().min(0).max(1),\n probabilities: z.record(z.string(), z.number()),\n});\nexport type ChoiceAnswer = z.infer<typeof choiceAnswerSchema>;\n\n// ---------------------------------------------------------------------------\n// Duplicate-org detection (Noul) -- ticket #560\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields we compare when judging whether two orgs are the same. */\nexport interface OrgRecordInput {\n name: string;\n domain?: string;\n location?: string;\n description?: string;\n}\n\n/** Build a plain object with only the defined keys (Jev sees no `undefined`). */\nfunction compact<T extends object>(obj: T): Partial<T> {\n const out: Partial<T> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined && v !== null && v !== '') out[k as keyof T] = v as T[keyof T];\n }\n return out;\n}\n\n/**\n * State for the org-pair judgment. Both records live under stable named fields\n * so the question can reference them unambiguously.\n */\nexport function buildOrgPairState(\n a: OrgRecordInput,\n b: OrgRecordInput,\n): { orgA: Partial<OrgRecordInput>; orgB: Partial<OrgRecordInput> } {\n return { orgA: compact(a), orgB: compact(b) };\n}\n\n/** Static Noul question paired with {@link buildOrgPairState}. */\nexport const ORG_PAIR_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds two organisation records, orgA and orgB. Do they refer to the same real-world organisation?',\n criteria: {\n true: 'Same organisation, e.g. a legal-suffix or abbreviation variant of the same name, or the same primary domain.',\n false:\n 'Distinct organisations, e.g. a shared word in the name but different domains, locations, or lines of business.',\n },\n};\n\n/**\n * Threshold favouring precision: a wrong merge is worse than a missed one.\n * Validate on labelled pairs (ticket #560) before enabling merges in prod.\n */\nexport const SAME_ORG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the merge threshold. */\nexport function isLikelySameOrg(noul: number, threshold: number = SAME_ORG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n// ---------------------------------------------------------------------------\n// Firmographic classification (Choice) -- ticket #561\n// ---------------------------------------------------------------------------\n\n/** Industry taxonomy. Single source of truth; `other` is the no-match option. */\nexport const INDUSTRY_TAXONOMY: Record<string, string> = {\n technology: 'Software, IT services, internet, hardware, telecoms',\n finance: 'Banking, insurance, investment, fintech, accounting',\n healthcare: 'Medical care, pharma, biotech, health services',\n education: 'Schools, universities, training, edtech',\n manufacturing: 'Industrial production, hardware, materials, automotive',\n retail: 'Retail, ecommerce, consumer goods, hospitality, food',\n media: 'Media, publishing, entertainment, marketing, advertising',\n energy: 'Energy, utilities, oil and gas, renewables',\n construction: 'Construction, real estate, architecture, engineering',\n transport: 'Transport, logistics, shipping, aviation',\n professional_services: 'Legal, consulting, HR, business services',\n public_sector: 'Government, defence, public administration',\n nonprofit: 'Charities, NGOs, foundations, associations',\n agriculture: 'Agriculture, farming, fishing, forestry',\n other: 'None of the above, or not enough information to decide',\n};\n\n/**\n * Organisation type (the org's FORM, not its sector -- a hospital's type is\n * healthcare_provider while its industry is healthcare). `other` is the no-match\n * option. Prefer the registry `legalForm` where it already encodes the form.\n */\nexport const ORG_TYPE_TAXONOMY: Record<string, string> = {\n company: 'For-profit company or corporation',\n cooperative: 'Member-owned co-operative (e.g. eG, SCOP, mutual, credit union)',\n nonprofit: 'Nonprofit, charity, or NGO',\n foundation: 'Grant-making or endowed foundation (e.g. Stiftung, fonds)',\n government: 'Government body, agency, or public-sector authority',\n educational: 'School, college, or university',\n research_institute: 'Independent research institute or laboratory',\n healthcare_provider: 'Hospital, clinic, or healthcare provider',\n professional_association: 'Professional association, trade body, or trade union',\n agency: 'Agency, consultancy, or professional-services firm',\n religious: 'Religious organisation',\n political: 'Political party or organisation',\n self_employed: 'Sole proprietor, freelancer, or self-employed individual',\n other: 'None of the above, or not enough information to decide',\n};\n\n/** Ordered employee-size bands; `unknown` is the no-match option. */\nexport const SIZE_BAND_TAXONOMY: Record<string, string> = {\n solo: '1 person',\n small: '2 to 10 people',\n medium: '11 to 50 people',\n large: '51 to 250 people',\n enterprise: 'More than 250 people',\n unknown: 'Not enough information to decide',\n};\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const INDUSTRY_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single industry best describes this organisation?',\n INDUSTRY_TAXONOMY,\n);\n\nexport const ORG_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single organisation type (its form, not its industry sector) best describes this organisation?',\n ORG_TYPE_TAXONOMY,\n);\n\nexport const SIZE_BAND_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which employee-size band best fits this organisation?',\n SIZE_BAND_TAXONOMY,\n);\n\n/**\n * Input for firmographic classification. `enrichmentText` (the org's description)\n * is the strongest evidence, but it is optional: many registry orgs have only a\n * name, domain, country, or link set. Passing those lets Jev classify from the\n * signal we hold rather than skipping the org for want of a description. A bare\n * name alone rarely suffices; a name plus a domain or country often does.\n */\nexport interface FirmographicInput {\n name: string;\n domain?: string;\n country?: string;\n /** Featured/external link labels or hosts (e.g. 'linkedin', 'wikipedia'). */\n links?: string[];\n /** An already-known industry, when set (helps disambiguate type/size). */\n industry?: string;\n enrichmentText?: string;\n}\n\n/**\n * State for the firmographic questions. Drops absent/empty fields (including\n * empty arrays) so Jev never sees a null or `[]` it would have to reason about.\n */\nexport function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput> {\n const compacted = compact(org);\n if (compacted.links && compacted.links.length === 0) delete compacted.links;\n return compacted;\n}\n\n/**\n * Below this confidence a firmographic field is left unset rather than guessed.\n * Validate per field on labelled orgs (ticket #561).\n */\nexport const FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;\n\n/**\n * Select the classified option, or `null` to leave the field unset. Returns\n * null when confidence is below the floor, or when the selected option is the\n * taxonomy's no-match option.\n */\nexport function pickChoiceAboveFloor(\n answer: ChoiceAnswer,\n opts: { floor?: number; noMatch?: string } = {},\n): string | null {\n const floor = opts.floor ?? FIRMOGRAPHIC_CONFIDENCE_FLOOR;\n if (answer.confidence < floor) return null;\n if (opts.noMatch !== undefined && answer.choice === opts.noMatch) return null;\n return answer.choice;\n}\n"]}
|
package/dist/jev/index.d.cts
CHANGED
|
@@ -91,13 +91,27 @@ declare const SIZE_BAND_TAXONOMY: Record<string, string>;
|
|
|
91
91
|
declare const INDUSTRY_CHOICE_QUESTION: ChoiceQuestion;
|
|
92
92
|
declare const ORG_TYPE_CHOICE_QUESTION: ChoiceQuestion;
|
|
93
93
|
declare const SIZE_BAND_CHOICE_QUESTION: ChoiceQuestion;
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Input for firmographic classification. `enrichmentText` (the org's description)
|
|
96
|
+
* is the strongest evidence, but it is optional: many registry orgs have only a
|
|
97
|
+
* name, domain, country, or link set. Passing those lets Jev classify from the
|
|
98
|
+
* signal we hold rather than skipping the org for want of a description. A bare
|
|
99
|
+
* name alone rarely suffices; a name plus a domain or country often does.
|
|
100
|
+
*/
|
|
95
101
|
interface FirmographicInput {
|
|
96
102
|
name: string;
|
|
97
103
|
domain?: string;
|
|
98
|
-
|
|
104
|
+
country?: string;
|
|
105
|
+
/** Featured/external link labels or hosts (e.g. 'linkedin', 'wikipedia'). */
|
|
106
|
+
links?: string[];
|
|
107
|
+
/** An already-known industry, when set (helps disambiguate type/size). */
|
|
108
|
+
industry?: string;
|
|
109
|
+
enrichmentText?: string;
|
|
99
110
|
}
|
|
100
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* State for the firmographic questions. Drops absent/empty fields (including
|
|
113
|
+
* empty arrays) so Jev never sees a null or `[]` it would have to reason about.
|
|
114
|
+
*/
|
|
101
115
|
declare function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput>;
|
|
102
116
|
/**
|
|
103
117
|
* Below this confidence a firmographic field is left unset rather than guessed.
|
package/dist/jev/index.d.ts
CHANGED
|
@@ -91,13 +91,27 @@ declare const SIZE_BAND_TAXONOMY: Record<string, string>;
|
|
|
91
91
|
declare const INDUSTRY_CHOICE_QUESTION: ChoiceQuestion;
|
|
92
92
|
declare const ORG_TYPE_CHOICE_QUESTION: ChoiceQuestion;
|
|
93
93
|
declare const SIZE_BAND_CHOICE_QUESTION: ChoiceQuestion;
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Input for firmographic classification. `enrichmentText` (the org's description)
|
|
96
|
+
* is the strongest evidence, but it is optional: many registry orgs have only a
|
|
97
|
+
* name, domain, country, or link set. Passing those lets Jev classify from the
|
|
98
|
+
* signal we hold rather than skipping the org for want of a description. A bare
|
|
99
|
+
* name alone rarely suffices; a name plus a domain or country often does.
|
|
100
|
+
*/
|
|
95
101
|
interface FirmographicInput {
|
|
96
102
|
name: string;
|
|
97
103
|
domain?: string;
|
|
98
|
-
|
|
104
|
+
country?: string;
|
|
105
|
+
/** Featured/external link labels or hosts (e.g. 'linkedin', 'wikipedia'). */
|
|
106
|
+
links?: string[];
|
|
107
|
+
/** An already-known industry, when set (helps disambiguate type/size). */
|
|
108
|
+
industry?: string;
|
|
109
|
+
enrichmentText?: string;
|
|
99
110
|
}
|
|
100
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* State for the firmographic questions. Drops absent/empty fields (including
|
|
113
|
+
* empty arrays) so Jev never sees a null or `[]` it would have to reason about.
|
|
114
|
+
*/
|
|
101
115
|
declare function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput>;
|
|
102
116
|
/**
|
|
103
117
|
* Below this confidence a firmographic field is left unset rather than guessed.
|
package/dist/jev/index.js
CHANGED
|
@@ -90,7 +90,9 @@ var SIZE_BAND_CHOICE_QUESTION = choiceQuestion(
|
|
|
90
90
|
SIZE_BAND_TAXONOMY
|
|
91
91
|
);
|
|
92
92
|
function buildFirmographicState(org) {
|
|
93
|
-
|
|
93
|
+
const compacted = compact(org);
|
|
94
|
+
if (compacted.links && compacted.links.length === 0) delete compacted.links;
|
|
95
|
+
return compacted;
|
|
94
96
|
}
|
|
95
97
|
var FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;
|
|
96
98
|
function pickChoiceAboveFloor(answer, opts = {}) {
|
package/dist/jev/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/jev/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `@singi-labs/sifa-sdk/jev` -- TypeSafe System One (Jev) question builders,\n * taxonomies, result schemas, and threshold predicates for Sifa's organisation\n * intelligence.\n *\n * This subpath is pure: it defines the typed *questions* to ask Jev, validates\n * the *answers*, and holds the policy constants (thresholds, taxonomies) that\n * turn a raw judgment into a decision. It performs no network calls and needs\n * no API key, so it is safe to import anywhere. The actual `systemOne` call\n * lives server-side in sifa-api, which passes an API key that must never reach\n * the browser.\n *\n * Judgment shapes:\n * - Duplicate-org detection: a single Noul over a candidate pair.\n * - Firmographic classification: independent Choice questions (industry, type,\n * size) asked together over one org's state.\n *\n * See docs.typesafe.ai for the wire contract. Typed output guarantees the\n * interface, not truth: validate thresholds on labelled Sifa data before\n * enabling any write.\n */\n\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Wire question types\n// ---------------------------------------------------------------------------\n\n/** A yes/no judgment. `criteria` clarifies what true vs false means. */\nexport interface NoulQuestion {\n type: 'noul';\n instructions: string;\n criteria?: { true: string; false: string };\n}\n\n/** A single-selection judgment. `criteria` maps each option to its description. */\nexport interface ChoiceQuestion {\n type: 'choice';\n instructions: string;\n criteria: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Answer schemas (validate Jev's response before code trusts it)\n// ---------------------------------------------------------------------------\n\n/** Jev's Noul answer: `noul` is the probability that the answer is yes (0..1). */\nexport const noulAnswerSchema = z.object({\n type: z.literal('noul'),\n noul: z.number().min(0).max(1),\n});\nexport type NoulAnswer = z.infer<typeof noulAnswerSchema>;\n\n/**\n * Jev's Choice answer: `choice` is the highest-probability option, `confidence`\n * summarises how concentrated the distribution is, `probabilities` sums to 1.\n */\nexport const choiceAnswerSchema = z.object({\n type: z.literal('choice'),\n choice: z.string(),\n confidence: z.number().min(0).max(1),\n probabilities: z.record(z.string(), z.number()),\n});\nexport type ChoiceAnswer = z.infer<typeof choiceAnswerSchema>;\n\n// ---------------------------------------------------------------------------\n// Duplicate-org detection (Noul) -- ticket #560\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields we compare when judging whether two orgs are the same. */\nexport interface OrgRecordInput {\n name: string;\n domain?: string;\n location?: string;\n description?: string;\n}\n\n/** Build a plain object with only the defined keys (Jev sees no `undefined`). */\nfunction compact<T extends object>(obj: T): Partial<T> {\n const out: Partial<T> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined && v !== null && v !== '') out[k as keyof T] = v as T[keyof T];\n }\n return out;\n}\n\n/**\n * State for the org-pair judgment. Both records live under stable named fields\n * so the question can reference them unambiguously.\n */\nexport function buildOrgPairState(\n a: OrgRecordInput,\n b: OrgRecordInput,\n): { orgA: Partial<OrgRecordInput>; orgB: Partial<OrgRecordInput> } {\n return { orgA: compact(a), orgB: compact(b) };\n}\n\n/** Static Noul question paired with {@link buildOrgPairState}. */\nexport const ORG_PAIR_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds two organisation records, orgA and orgB. Do they refer to the same real-world organisation?',\n criteria: {\n true: 'Same organisation, e.g. a legal-suffix or abbreviation variant of the same name, or the same primary domain.',\n false:\n 'Distinct organisations, e.g. a shared word in the name but different domains, locations, or lines of business.',\n },\n};\n\n/**\n * Threshold favouring precision: a wrong merge is worse than a missed one.\n * Validate on labelled pairs (ticket #560) before enabling merges in prod.\n */\nexport const SAME_ORG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the merge threshold. */\nexport function isLikelySameOrg(noul: number, threshold: number = SAME_ORG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n// ---------------------------------------------------------------------------\n// Firmographic classification (Choice) -- ticket #561\n// ---------------------------------------------------------------------------\n\n/** Industry taxonomy. Single source of truth; `other` is the no-match option. */\nexport const INDUSTRY_TAXONOMY: Record<string, string> = {\n technology: 'Software, IT services, internet, hardware, telecoms',\n finance: 'Banking, insurance, investment, fintech, accounting',\n healthcare: 'Medical care, pharma, biotech, health services',\n education: 'Schools, universities, training, edtech',\n manufacturing: 'Industrial production, hardware, materials, automotive',\n retail: 'Retail, ecommerce, consumer goods, hospitality, food',\n media: 'Media, publishing, entertainment, marketing, advertising',\n energy: 'Energy, utilities, oil and gas, renewables',\n construction: 'Construction, real estate, architecture, engineering',\n transport: 'Transport, logistics, shipping, aviation',\n professional_services: 'Legal, consulting, HR, business services',\n public_sector: 'Government, defence, public administration',\n nonprofit: 'Charities, NGOs, foundations, associations',\n agriculture: 'Agriculture, farming, fishing, forestry',\n other: 'None of the above, or not enough information to decide',\n};\n\n/**\n * Organisation type (the org's FORM, not its sector -- a hospital's type is\n * healthcare_provider while its industry is healthcare). `other` is the no-match\n * option. Prefer the registry `legalForm` where it already encodes the form.\n */\nexport const ORG_TYPE_TAXONOMY: Record<string, string> = {\n company: 'For-profit company or corporation',\n cooperative: 'Member-owned co-operative (e.g. eG, SCOP, mutual, credit union)',\n nonprofit: 'Nonprofit, charity, or NGO',\n foundation: 'Grant-making or endowed foundation (e.g. Stiftung, fonds)',\n government: 'Government body, agency, or public-sector authority',\n educational: 'School, college, or university',\n research_institute: 'Independent research institute or laboratory',\n healthcare_provider: 'Hospital, clinic, or healthcare provider',\n professional_association: 'Professional association, trade body, or trade union',\n agency: 'Agency, consultancy, or professional-services firm',\n religious: 'Religious organisation',\n political: 'Political party or organisation',\n self_employed: 'Sole proprietor, freelancer, or self-employed individual',\n other: 'None of the above, or not enough information to decide',\n};\n\n/** Ordered employee-size bands; `unknown` is the no-match option. */\nexport const SIZE_BAND_TAXONOMY: Record<string, string> = {\n solo: '1 person',\n small: '2 to 10 people',\n medium: '11 to 50 people',\n large: '51 to 250 people',\n enterprise: 'More than 250 people',\n unknown: 'Not enough information to decide',\n};\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const INDUSTRY_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single industry best describes this organisation?',\n INDUSTRY_TAXONOMY,\n);\n\nexport const ORG_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single organisation type (its form, not its industry sector) best describes this organisation?',\n ORG_TYPE_TAXONOMY,\n);\n\nexport const SIZE_BAND_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which employee-size band best fits this organisation?',\n SIZE_BAND_TAXONOMY,\n);\n\n/** Input for firmographic classification. Enrichment text is the primary evidence. */\nexport interface FirmographicInput {\n name: string;\n domain?: string;\n enrichmentText: string;\n}\n\n/** State for the firmographic questions. */\nexport function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput> {\n return compact(org);\n}\n\n/**\n * Below this confidence a firmographic field is left unset rather than guessed.\n * Validate per field on labelled orgs (ticket #561).\n */\nexport const FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;\n\n/**\n * Select the classified option, or `null` to leave the field unset. Returns\n * null when confidence is below the floor, or when the selected option is the\n * taxonomy's no-match option.\n */\nexport function pickChoiceAboveFloor(\n answer: ChoiceAnswer,\n opts: { floor?: number; noMatch?: string } = {},\n): string | null {\n const floor = opts.floor ?? FIRMOGRAPHIC_CONFIDENCE_FLOOR;\n if (answer.confidence < floor) return null;\n if (opts.noMatch !== undefined && answer.choice === opts.noMatch) return null;\n return answer.choice;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/jev/index.ts"],"names":[],"mappings":";;;AA+CO,IAAM,gBAAA,GAAmB,EAAE,MAAA,CAAO;AAAA,EACvC,IAAA,EAAM,CAAA,CAAE,OAAA,CAAQ,MAAM,CAAA;AAAA,EACtB,IAAA,EAAM,EAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,CAAC;AAC/B,CAAC;AAOM,IAAM,kBAAA,GAAqB,EAAE,MAAA,CAAO;AAAA,EACzC,IAAA,EAAM,CAAA,CAAE,OAAA,CAAQ,QAAQ,CAAA;AAAA,EACxB,MAAA,EAAQ,EAAE,MAAA,EAAO;AAAA,EACjB,UAAA,EAAY,EAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA,CAAE,IAAI,CAAC,CAAA;AAAA,EACnC,aAAA,EAAe,EAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,QAAQ;AAChD,CAAC;AAgBD,SAAS,QAA0B,GAAA,EAAoB;AACrD,EAAA,MAAM,MAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,EAAG;AACxC,IAAA,IAAI,CAAA,KAAM,UAAa,CAAA,KAAM,IAAA,IAAQ,MAAM,EAAA,EAAI,GAAA,CAAI,CAAY,CAAA,GAAI,CAAA;AAAA,EACrE;AACA,EAAA,OAAO,GAAA;AACT;AAMO,SAAS,iBAAA,CACd,GACA,CAAA,EACkE;AAClE,EAAA,OAAO,EAAE,MAAM,OAAA,CAAQ,CAAC,GAAG,IAAA,EAAM,OAAA,CAAQ,CAAC,CAAA,EAAE;AAC9C;AAGO,IAAM,sBAAA,GAAuC;AAAA,EAClD,IAAA,EAAM,MAAA;AAAA,EACN,YAAA,EACE,6GAAA;AAAA,EACF,QAAA,EAAU;AAAA,IACR,IAAA,EAAM,8GAAA;AAAA,IACN,KAAA,EACE;AAAA;AAEN;AAMO,IAAM,kBAAA,GAAqB;AAG3B,SAAS,eAAA,CAAgB,IAAA,EAAc,SAAA,GAAoB,kBAAA,EAA6B;AAC7F,EAAA,OAAO,IAAA,IAAQ,SAAA;AACjB;AAOO,IAAM,iBAAA,GAA4C;AAAA,EACvD,UAAA,EAAY,qDAAA;AAAA,EACZ,OAAA,EAAS,qDAAA;AAAA,EACT,UAAA,EAAY,gDAAA;AAAA,EACZ,SAAA,EAAW,yCAAA;AAAA,EACX,aAAA,EAAe,wDAAA;AAAA,EACf,MAAA,EAAQ,sDAAA;AAAA,EACR,KAAA,EAAO,0DAAA;AAAA,EACP,MAAA,EAAQ,4CAAA;AAAA,EACR,YAAA,EAAc,sDAAA;AAAA,EACd,SAAA,EAAW,0CAAA;AAAA,EACX,qBAAA,EAAuB,0CAAA;AAAA,EACvB,aAAA,EAAe,4CAAA;AAAA,EACf,SAAA,EAAW,4CAAA;AAAA,EACX,WAAA,EAAa,yCAAA;AAAA,EACb,KAAA,EAAO;AACT;AAOO,IAAM,iBAAA,GAA4C;AAAA,EACvD,OAAA,EAAS,mCAAA;AAAA,EACT,WAAA,EAAa,iEAAA;AAAA,EACb,SAAA,EAAW,4BAAA;AAAA,EACX,UAAA,EAAY,2DAAA;AAAA,EACZ,UAAA,EAAY,qDAAA;AAAA,EACZ,WAAA,EAAa,gCAAA;AAAA,EACb,kBAAA,EAAoB,8CAAA;AAAA,EACpB,mBAAA,EAAqB,0CAAA;AAAA,EACrB,wBAAA,EAA0B,sDAAA;AAAA,EAC1B,MAAA,EAAQ,oDAAA;AAAA,EACR,SAAA,EAAW,wBAAA;AAAA,EACX,SAAA,EAAW,iCAAA;AAAA,EACX,aAAA,EAAe,0DAAA;AAAA,EACf,KAAA,EAAO;AACT;AAGO,IAAM,kBAAA,GAA6C;AAAA,EACxD,IAAA,EAAM,UAAA;AAAA,EACN,KAAA,EAAO,gBAAA;AAAA,EACP,MAAA,EAAQ,iBAAA;AAAA,EACR,KAAA,EAAO,kBAAA;AAAA,EACP,UAAA,EAAY,sBAAA;AAAA,EACZ,OAAA,EAAS;AACX;AAEA,SAAS,cAAA,CAAe,cAAsB,QAAA,EAAkD;AAC9F,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,YAAA,EAAc,QAAA,EAAS;AAClD;AAEO,IAAM,wBAAA,GAA2B,cAAA;AAAA,EACtC,iFAAA;AAAA,EACA;AACF;AAEO,IAAM,wBAAA,GAA2B,cAAA;AAAA,EACtC,8HAAA;AAAA,EACA;AACF;AAEO,IAAM,yBAAA,GAA4B,cAAA;AAAA,EACvC,+EAAA;AAAA,EACA;AACF;AAwBO,SAAS,uBAAuB,GAAA,EAAoD;AACzF,EAAA,MAAM,SAAA,GAAY,QAAQ,GAAG,CAAA;AAC7B,EAAA,IAAI,UAAU,KAAA,IAAS,SAAA,CAAU,MAAM,MAAA,KAAW,CAAA,SAAU,SAAA,CAAU,KAAA;AACtE,EAAA,OAAO,SAAA;AACT;AAMO,IAAM,6BAAA,GAAgC;AAOtC,SAAS,oBAAA,CACd,MAAA,EACA,IAAA,GAA6C,EAAC,EAC/B;AACf,EAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,IAAS,6BAAA;AAC5B,EAAA,IAAI,MAAA,CAAO,UAAA,GAAa,KAAA,EAAO,OAAO,IAAA;AACtC,EAAA,IAAI,KAAK,OAAA,KAAY,MAAA,IAAa,OAAO,MAAA,KAAW,IAAA,CAAK,SAAS,OAAO,IAAA;AACzE,EAAA,OAAO,MAAA,CAAO,MAAA;AAChB","file":"index.js","sourcesContent":["/**\n * `@singi-labs/sifa-sdk/jev` -- TypeSafe System One (Jev) question builders,\n * taxonomies, result schemas, and threshold predicates for Sifa's organisation\n * intelligence.\n *\n * This subpath is pure: it defines the typed *questions* to ask Jev, validates\n * the *answers*, and holds the policy constants (thresholds, taxonomies) that\n * turn a raw judgment into a decision. It performs no network calls and needs\n * no API key, so it is safe to import anywhere. The actual `systemOne` call\n * lives server-side in sifa-api, which passes an API key that must never reach\n * the browser.\n *\n * Judgment shapes:\n * - Duplicate-org detection: a single Noul over a candidate pair.\n * - Firmographic classification: independent Choice questions (industry, type,\n * size) asked together over one org's state.\n *\n * See docs.typesafe.ai for the wire contract. Typed output guarantees the\n * interface, not truth: validate thresholds on labelled Sifa data before\n * enabling any write.\n */\n\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Wire question types\n// ---------------------------------------------------------------------------\n\n/** A yes/no judgment. `criteria` clarifies what true vs false means. */\nexport interface NoulQuestion {\n type: 'noul';\n instructions: string;\n criteria?: { true: string; false: string };\n}\n\n/** A single-selection judgment. `criteria` maps each option to its description. */\nexport interface ChoiceQuestion {\n type: 'choice';\n instructions: string;\n criteria: Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Answer schemas (validate Jev's response before code trusts it)\n// ---------------------------------------------------------------------------\n\n/** Jev's Noul answer: `noul` is the probability that the answer is yes (0..1). */\nexport const noulAnswerSchema = z.object({\n type: z.literal('noul'),\n noul: z.number().min(0).max(1),\n});\nexport type NoulAnswer = z.infer<typeof noulAnswerSchema>;\n\n/**\n * Jev's Choice answer: `choice` is the highest-probability option, `confidence`\n * summarises how concentrated the distribution is, `probabilities` sums to 1.\n */\nexport const choiceAnswerSchema = z.object({\n type: z.literal('choice'),\n choice: z.string(),\n confidence: z.number().min(0).max(1),\n probabilities: z.record(z.string(), z.number()),\n});\nexport type ChoiceAnswer = z.infer<typeof choiceAnswerSchema>;\n\n// ---------------------------------------------------------------------------\n// Duplicate-org detection (Noul) -- ticket #560\n// ---------------------------------------------------------------------------\n\n/** Public, non-user fields we compare when judging whether two orgs are the same. */\nexport interface OrgRecordInput {\n name: string;\n domain?: string;\n location?: string;\n description?: string;\n}\n\n/** Build a plain object with only the defined keys (Jev sees no `undefined`). */\nfunction compact<T extends object>(obj: T): Partial<T> {\n const out: Partial<T> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined && v !== null && v !== '') out[k as keyof T] = v as T[keyof T];\n }\n return out;\n}\n\n/**\n * State for the org-pair judgment. Both records live under stable named fields\n * so the question can reference them unambiguously.\n */\nexport function buildOrgPairState(\n a: OrgRecordInput,\n b: OrgRecordInput,\n): { orgA: Partial<OrgRecordInput>; orgB: Partial<OrgRecordInput> } {\n return { orgA: compact(a), orgB: compact(b) };\n}\n\n/** Static Noul question paired with {@link buildOrgPairState}. */\nexport const ORG_PAIR_NOUL_QUESTION: NoulQuestion = {\n type: 'noul',\n instructions:\n 'The state holds two organisation records, orgA and orgB. Do they refer to the same real-world organisation?',\n criteria: {\n true: 'Same organisation, e.g. a legal-suffix or abbreviation variant of the same name, or the same primary domain.',\n false:\n 'Distinct organisations, e.g. a shared word in the name but different domains, locations, or lines of business.',\n },\n};\n\n/**\n * Threshold favouring precision: a wrong merge is worse than a missed one.\n * Validate on labelled pairs (ticket #560) before enabling merges in prod.\n */\nexport const SAME_ORG_THRESHOLD = 0.85;\n\n/** True when the Noul probability clears the merge threshold. */\nexport function isLikelySameOrg(noul: number, threshold: number = SAME_ORG_THRESHOLD): boolean {\n return noul >= threshold;\n}\n\n// ---------------------------------------------------------------------------\n// Firmographic classification (Choice) -- ticket #561\n// ---------------------------------------------------------------------------\n\n/** Industry taxonomy. Single source of truth; `other` is the no-match option. */\nexport const INDUSTRY_TAXONOMY: Record<string, string> = {\n technology: 'Software, IT services, internet, hardware, telecoms',\n finance: 'Banking, insurance, investment, fintech, accounting',\n healthcare: 'Medical care, pharma, biotech, health services',\n education: 'Schools, universities, training, edtech',\n manufacturing: 'Industrial production, hardware, materials, automotive',\n retail: 'Retail, ecommerce, consumer goods, hospitality, food',\n media: 'Media, publishing, entertainment, marketing, advertising',\n energy: 'Energy, utilities, oil and gas, renewables',\n construction: 'Construction, real estate, architecture, engineering',\n transport: 'Transport, logistics, shipping, aviation',\n professional_services: 'Legal, consulting, HR, business services',\n public_sector: 'Government, defence, public administration',\n nonprofit: 'Charities, NGOs, foundations, associations',\n agriculture: 'Agriculture, farming, fishing, forestry',\n other: 'None of the above, or not enough information to decide',\n};\n\n/**\n * Organisation type (the org's FORM, not its sector -- a hospital's type is\n * healthcare_provider while its industry is healthcare). `other` is the no-match\n * option. Prefer the registry `legalForm` where it already encodes the form.\n */\nexport const ORG_TYPE_TAXONOMY: Record<string, string> = {\n company: 'For-profit company or corporation',\n cooperative: 'Member-owned co-operative (e.g. eG, SCOP, mutual, credit union)',\n nonprofit: 'Nonprofit, charity, or NGO',\n foundation: 'Grant-making or endowed foundation (e.g. Stiftung, fonds)',\n government: 'Government body, agency, or public-sector authority',\n educational: 'School, college, or university',\n research_institute: 'Independent research institute or laboratory',\n healthcare_provider: 'Hospital, clinic, or healthcare provider',\n professional_association: 'Professional association, trade body, or trade union',\n agency: 'Agency, consultancy, or professional-services firm',\n religious: 'Religious organisation',\n political: 'Political party or organisation',\n self_employed: 'Sole proprietor, freelancer, or self-employed individual',\n other: 'None of the above, or not enough information to decide',\n};\n\n/** Ordered employee-size bands; `unknown` is the no-match option. */\nexport const SIZE_BAND_TAXONOMY: Record<string, string> = {\n solo: '1 person',\n small: '2 to 10 people',\n medium: '11 to 50 people',\n large: '51 to 250 people',\n enterprise: 'More than 250 people',\n unknown: 'Not enough information to decide',\n};\n\nfunction choiceQuestion(instructions: string, criteria: Record<string, string>): ChoiceQuestion {\n return { type: 'choice', instructions, criteria };\n}\n\nexport const INDUSTRY_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single industry best describes this organisation?',\n INDUSTRY_TAXONOMY,\n);\n\nexport const ORG_TYPE_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which single organisation type (its form, not its industry sector) best describes this organisation?',\n ORG_TYPE_TAXONOMY,\n);\n\nexport const SIZE_BAND_CHOICE_QUESTION = choiceQuestion(\n 'Based on the org state, which employee-size band best fits this organisation?',\n SIZE_BAND_TAXONOMY,\n);\n\n/**\n * Input for firmographic classification. `enrichmentText` (the org's description)\n * is the strongest evidence, but it is optional: many registry orgs have only a\n * name, domain, country, or link set. Passing those lets Jev classify from the\n * signal we hold rather than skipping the org for want of a description. A bare\n * name alone rarely suffices; a name plus a domain or country often does.\n */\nexport interface FirmographicInput {\n name: string;\n domain?: string;\n country?: string;\n /** Featured/external link labels or hosts (e.g. 'linkedin', 'wikipedia'). */\n links?: string[];\n /** An already-known industry, when set (helps disambiguate type/size). */\n industry?: string;\n enrichmentText?: string;\n}\n\n/**\n * State for the firmographic questions. Drops absent/empty fields (including\n * empty arrays) so Jev never sees a null or `[]` it would have to reason about.\n */\nexport function buildFirmographicState(org: FirmographicInput): Partial<FirmographicInput> {\n const compacted = compact(org);\n if (compacted.links && compacted.links.length === 0) delete compacted.links;\n return compacted;\n}\n\n/**\n * Below this confidence a firmographic field is left unset rather than guessed.\n * Validate per field on labelled orgs (ticket #561).\n */\nexport const FIRMOGRAPHIC_CONFIDENCE_FLOOR = 0.6;\n\n/**\n * Select the classified option, or `null` to leave the field unset. Returns\n * null when confidence is below the floor, or when the selected option is the\n * taxonomy's no-match option.\n */\nexport function pickChoiceAboveFloor(\n answer: ChoiceAnswer,\n opts: { floor?: number; noMatch?: string } = {},\n): string | null {\n const floor = opts.floor ?? FIRMOGRAPHIC_CONFIDENCE_FLOOR;\n if (answer.confidence < floor) return null;\n if (opts.noMatch !== undefined && answer.choice === opts.noMatch) return null;\n return answer.choice;\n}\n"]}
|
package/package.json
CHANGED