@singi-labs/sifa-sdk 0.19.111 → 0.20.0

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 CHANGED
@@ -8044,7 +8044,7 @@ function describeSifaRecord(collection, value) {
8044
8044
  }
8045
8045
 
8046
8046
  // src/index.ts
8047
- var SIFA_SDK_VERSION = "0.19.111";
8047
+ var SIFA_SDK_VERSION = "0.20.0";
8048
8048
 
8049
8049
  exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
8050
8050
  exports.ACTIVITY_VERBS = ACTIVITY_VERBS;
package/dist/index.js CHANGED
@@ -393,7 +393,7 @@ import {
393
393
  roadmapIssueUrl
394
394
  } from "./roadmap/index.js";
395
395
  import { SIFA_REPO_GROUPS, describeSifaRecord, repoGroupForCollection } from "./repo/index.js";
396
- var SIFA_SDK_VERSION = "0.19.111";
396
+ var SIFA_SDK_VERSION = "0.20.0";
397
397
  export {
398
398
  ACTIVITY_TIERS,
399
399
  ACTIVITY_VERBS,
@@ -22,7 +22,7 @@ var FUNDER_CV_GROUPS = {
22
22
  function funderCvItemId(kind, rkey) {
23
23
  return `${kind}:${rkey}`;
24
24
  }
25
- var ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;
25
+ var ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:/-]{1,512}$/;
26
26
  function kindOf(id) {
27
27
  const kind = id.slice(0, id.indexOf(":"));
28
28
  return FUNDER_CV_ITEM_KINDS.includes(kind) ? kind : void 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/resume/funder-cv.ts"],"sourcesContent":["/**\n * Funder-format CV documents: the NIH Biographical Sketch (Common Form plus\n * NIH Supplement) and the ERC Part B1 \"Curriculum vitae and Track Record\".\n *\n * A pure mapper from a Sifa profile and the owner's pick of items to a plain\n * JSON document a typesetter lays out. Like the JSON Resume emitter next door,\n * nothing here fetches or renders.\n *\n * Funders cap how many items a CV may list. The caps live in\n * `FUNDER_CV_GROUPS`, so the export page that offers the choice and the server\n * that enforces it read one number. Sections a funder asks for that Sifa holds\n * no records for (a personal statement, contributions to science, career\n * breaks) are emitted as `null` or empty, for the template to show as a\n * placeholder the applicant fills in.\n */\n\nimport { z } from 'zod';\n\nimport { formatLocation } from '../format/location-utils.js';\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport { isTeachingCourse } from '../taxonomy/course-role.js';\nimport type {\n ExternalAccount,\n ProfileCourse,\n ProfileEducation,\n ProfileHonor,\n ProfilePosition,\n ProfilePresentation,\n ProfilePublication,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\nimport { toResumeDate } from './json-resume.js';\n\nexport const FUNDER_CV_FORMATS = ['nih', 'erc'] as const;\nexport type FunderCvFormat = (typeof FUNDER_CV_FORMATS)[number];\n\nexport const FUNDER_CV_ITEM_KINDS = ['publication', 'presentation', 'honor'] as const;\nexport type FunderCvItemKind = (typeof FUNDER_CV_ITEM_KINDS)[number];\n\nexport interface FunderCvGroupSpec {\n readonly kinds: readonly FunderCvItemKind[];\n /** Most items the funder's form takes for this group. */\n readonly max: number;\n}\n\n/**\n * The pickable groups per format, with the funder's item limits.\n *\n * - NIH Common Form \"Products\": up to 5 closely related to the proposed\n * project and up to 5 other significant products. Publications, conference\n * papers and presentations all count. NIH Supplement \"Honors\": up to 15.\n * - ERC Part B1 \"Research achievements\": up to ten research outputs. \"Peer\n * recognition\" is \"selected examples\" with no number; 15 is Sifa's own cap,\n * to keep the section inside the 4-page limit.\n */\nexport const FUNDER_CV_GROUPS = {\n nih: {\n related: { kinds: ['publication', 'presentation'], max: 5 },\n other: { kinds: ['publication', 'presentation'], max: 5 },\n honors: { kinds: ['honor'], max: 15 },\n },\n erc: {\n outputs: { kinds: ['publication'], max: 10 },\n recognition: { kinds: ['honor', 'presentation'], max: 15 },\n },\n} as const satisfies Record<FunderCvFormat, Record<string, FunderCvGroupSpec>>;\n\nexport type FunderCvGroupName<F extends FunderCvFormat> = keyof (typeof FUNDER_CV_GROUPS)[F] &\n string;\n\n/** A picked item: `<kind>:<rkey>`, e.g. `publication:3kxyz`. */\nexport function funderCvItemId(kind: FunderCvItemKind, rkey: string): string {\n return `${kind}:${rkey}`;\n}\n\n/** Record key syntax (atproto): 1-512 of A-Z a-z 0-9 . - _ : ~ */\nconst ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;\n\nfunction kindOf(id: string): FunderCvItemKind | undefined {\n const kind = id.slice(0, id.indexOf(':'));\n return (FUNDER_CV_ITEM_KINDS as readonly string[]).includes(kind)\n ? (kind as FunderCvItemKind)\n : undefined;\n}\n\nfunction groupSchema(spec: FunderCvGroupSpec) {\n return z\n .array(\n z\n .string()\n .regex(ITEM_ID_RE)\n .refine((id) => spec.kinds.includes(kindOf(id) as FunderCvItemKind), {\n message: `Only ${spec.kinds.join(', ')} items fit here`,\n }),\n )\n .max(spec.max)\n .default([]);\n}\n\nfunction noRepeats(lists: readonly string[][]): boolean {\n const all = lists.flat();\n return new Set(all).size === all.length;\n}\n\nconst nihSelectionSchema = z\n .object({\n format: z.literal('nih'),\n related: groupSchema(FUNDER_CV_GROUPS.nih.related),\n other: groupSchema(FUNDER_CV_GROUPS.nih.other),\n honors: groupSchema(FUNDER_CV_GROUPS.nih.honors),\n })\n .refine((s) => noRepeats([s.related, s.other, s.honors]), {\n message: 'An item can be listed once',\n });\n\nconst ercSelectionSchema = z\n .object({\n format: z.literal('erc'),\n outputs: groupSchema(FUNDER_CV_GROUPS.erc.outputs),\n recognition: groupSchema(FUNDER_CV_GROUPS.erc.recognition),\n })\n .refine((s) => noRepeats([s.outputs, s.recognition]), {\n message: 'An item can be listed once',\n });\n\n/**\n * The owner's pick for one format. Enforces the shape and the funder's limits;\n * whether each id is one of the owner's own visible records is a separate\n * check against the profile, `findUnknownFunderCvItems`.\n */\nexport const funderCvSelectionSchema = z.discriminatedUnion('format', [\n nihSelectionSchema,\n ercSelectionSchema,\n]);\n\nexport type FunderCvSelection = z.infer<typeof funderCvSelectionSchema>;\n\n/** The subset of `Profile` the funder mapper reads. */\nexport interface FunderCvProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly headline?: string;\n readonly website?: string;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly publications?: ProfilePublication[];\n readonly presentations?: ProfilePresentation[];\n readonly honors?: ProfileHonor[];\n readonly courses?: ProfileCourse[];\n readonly volunteering?: ProfileVolunteering[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface FunderCvCandidate {\n readonly id: string;\n readonly kind: FunderCvItemKind;\n readonly title: string;\n /** `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; absent when the record has no date. */\n readonly date?: string;\n}\n\n/** The most recent visible delivery of a talk, which dates it. */\nfunction latestDeliveryRecord(presentation: ProfilePresentation) {\n const dateOf = (d: { date?: string | null }) => toResumeDate(d.date ?? undefined) ?? '';\n return [...filterHidden(presentation.deliveries)]\n .sort((a, b) => dateOf(a).localeCompare(dateOf(b)))\n .at(-1);\n}\n\nfunction latestDelivery(presentation: ProfilePresentation): string | undefined {\n return toResumeDate(latestDeliveryRecord(presentation)?.date ?? undefined);\n}\n\nfunction candidatesOf(profile: FunderCvProfileInput, kind: FunderCvItemKind): FunderCvCandidate[] {\n switch (kind) {\n case 'publication':\n return filterHidden(profile.publications).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: toResumeDate(p.date),\n }));\n case 'presentation':\n return filterHidden(profile.presentations).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: latestDelivery(p),\n }));\n case 'honor':\n return filterHidden(profile.honors).map((h) => ({\n id: funderCvItemId(kind, h.rkey),\n kind,\n title: h.title,\n date: toResumeDate(h.date),\n }));\n }\n}\n\n/**\n * The owner's visible items of the given kinds, most recent first. Undated\n * items go last, in profile order. The sort is stable, so equal dates keep\n * profile order too.\n */\nexport function listFunderCvCandidates(\n profile: FunderCvProfileInput,\n kinds: readonly FunderCvItemKind[],\n): FunderCvCandidate[] {\n const all = kinds.flatMap((kind) => candidatesOf(profile, kind));\n return all.sort((a, b) => {\n if (a.date && b.date) return b.date.localeCompare(a.date);\n if (a.date) return -1;\n if (b.date) return 1;\n return 0;\n });\n}\n\n/**\n * What the export page pre-ticks: the most recent publications up to each\n * limit (for NIH, the first five as closely related and the next five as\n * other), and the most recent honors. Talks are offered but not pre-ticked.\n */\nexport function defaultFunderCvSelection(\n profile: FunderCvProfileInput,\n format: FunderCvFormat,\n): FunderCvSelection {\n const publications = listFunderCvCandidates(profile, ['publication']).map((c) => c.id);\n const honors = listFunderCvCandidates(profile, ['honor']).map((c) => c.id);\n if (format === 'nih') {\n const { related, other } = FUNDER_CV_GROUPS.nih;\n return {\n format,\n related: publications.slice(0, related.max),\n other: publications.slice(related.max, related.max + other.max),\n honors: honors.slice(0, FUNDER_CV_GROUPS.nih.honors.max),\n };\n }\n return {\n format,\n outputs: publications.slice(0, FUNDER_CV_GROUPS.erc.outputs.max),\n recognition: honors.slice(0, FUNDER_CV_GROUPS.erc.recognition.max),\n };\n}\n\nfunction selectedIds(selection: FunderCvSelection): string[] {\n return selection.format === 'nih'\n ? [...selection.related, ...selection.other, ...selection.honors]\n : [...selection.outputs, ...selection.recognition];\n}\n\n/**\n * Ids in the selection that are not visible records on this profile. The\n * profile is the owner's own, so an empty result means every pick is theirs.\n */\nexport function findUnknownFunderCvItems(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n): string[] {\n const known = new Set(listFunderCvCandidates(profile, FUNDER_CV_ITEM_KINDS).map((c) => c.id));\n return selectedIds(selection).filter((id) => !known.has(id));\n}\n\n// ---------------------------------------------------------------------------\n// Document shapes\n// ---------------------------------------------------------------------------\n\nexport interface FunderCvPerson {\n /** Full name as the profile shows it; the handle when there is no name. */\n readonly name: string;\n readonly givenName?: string;\n readonly familyName?: string;\n /** Bare ORCID iD, e.g. `0000-0002-1825-0097`. */\n readonly orcid?: string;\n readonly website?: string;\n}\n\n/**\n * The ERC person, with the contact and personal details the owner typed for\n * this export. The NIH Biographical Sketch has no such block, so only ERC carries them.\n */\nexport interface ErcFunderCvPerson extends FunderCvPerson {\n readonly email?: string;\n readonly phone?: string;\n /** Lines are separated by `\\n`. */\n readonly address?: string;\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n}\n\nexport interface FunderCvOptions {\n /**\n * Contact details the owner typed for this export. Sifa stores none of\n * them. Used for ERC only.\n */\n readonly contact?: ExportContact;\n}\n\n/** One product or research output, ready to cite. */\nexport interface FunderCvCitation {\n readonly kind: 'publication' | 'presentation';\n readonly title: string;\n /** Authors in record order, comma separated. For a talk, the speaker. */\n readonly authors?: string;\n /** Journal, publisher or event. */\n readonly venue?: string;\n /** Year. */\n readonly date?: string;\n readonly doi?: string;\n readonly url?: string;\n}\n\nexport interface FunderCvHonor {\n readonly year?: string;\n readonly title: string;\n readonly organization?: string;\n}\n\nexport interface NihEducation {\n readonly organization: string;\n readonly location?: string;\n readonly degree?: string;\n /** Start date, `MM/YYYY` or `YYYY`. */\n readonly start?: string;\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly end?: string;\n readonly field?: string;\n}\n\nexport interface FunderCvAppointment {\n /** `YYYY`. */\n readonly start?: string;\n /** `YYYY`, or null for a current appointment. */\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n readonly location?: string;\n}\n\n/** NIH Biographical Sketch: Common Form plus NIH Supplement. */\nexport interface NihFunderCv {\n readonly format: 'nih';\n readonly person: FunderCvPerson;\n readonly positionTitle?: string;\n /** Primary organization: the current position's. */\n readonly organization?: string;\n readonly location?: string;\n readonly education: NihEducation[];\n readonly appointments: FunderCvAppointment[];\n readonly products: { readonly related: FunderCvCitation[]; readonly other: FunderCvCitation[] };\n readonly honors: FunderCvHonor[];\n /** Supplement \"Personal Statement\". Sifa holds no record for it. */\n readonly personalStatement: string | null;\n /** Supplement \"Contributions to Science\". Sifa holds no record for them. */\n readonly contributions: string[];\n}\n\nexport interface ErcEducation {\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly date?: string;\n readonly degree?: string;\n readonly field?: string;\n readonly institution: string;\n}\n\nexport interface ErcRecognition {\n readonly kind: 'honor' | 'presentation';\n readonly year?: string;\n readonly title: string;\n /** Awarding body, or the event a talk was given at. */\n readonly organization?: string;\n}\n\nexport interface ErcContribution {\n readonly kind: 'teaching' | 'service';\n readonly start?: string;\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n}\n\n/** ERC Part B1, section \"Curriculum vitae and Track Record\". */\nexport interface ErcFunderCv {\n readonly format: 'erc';\n readonly person: ErcFunderCvPerson;\n readonly education: ErcEducation[];\n readonly currentPositions: FunderCvAppointment[];\n readonly previousPositions: FunderCvAppointment[];\n readonly outputs: FunderCvCitation[];\n readonly recognition: ErcRecognition[];\n readonly communityContributions: ErcContribution[];\n /** Proposal acronym for the page header. Not a profile fact. */\n readonly acronym: string | null;\n /** \"Career breaks, diverse career paths and major life events\". */\n readonly careerBreaks: string | null;\n}\n\nexport type FunderCvDocument = NihFunderCv | ErcFunderCv;\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction text(value: string | null | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\nfunction year(value: string | null | undefined): string | undefined {\n return toResumeDate(value ?? undefined)?.slice(0, 4);\n}\n\n/** `MM/YYYY` when the month is known, else `YYYY`. */\nfunction monthYear(value: string | null | undefined): string | undefined {\n const date = toResumeDate(value ?? undefined);\n if (!date) return undefined;\n return date.length >= 7 ? `${date.slice(5, 7)}/${date.slice(0, 4)}` : date;\n}\n\n/** Newest start first; undated entries last, in profile order. */\nfunction byStartDesc<T>(items: T[], start: (item: T) => string | undefined): T[] {\n return [...items].sort((a, b) => {\n const da = toResumeDate(start(a));\n const db = toResumeDate(start(b));\n if (da && db) return db.localeCompare(da);\n if (da) return -1;\n if (db) return 1;\n return 0;\n });\n}\n\nconst ORCID_RE = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/;\n\nfunction orcidOf(accounts: ExternalAccount[] | undefined): string | undefined {\n const account = filterHidden(accounts).find((a) => a.platform.toLowerCase() === 'orcid');\n return account ? ORCID_RE.exec(account.url)?.[0] : undefined;\n}\n\nfunction personOf(profile: FunderCvProfileInput): FunderCvPerson {\n return compact<FunderCvPerson>({\n name:\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName) ??\n profile.handle,\n givenName: text(profile.givenName),\n familyName: text(profile.familyName),\n orcid: orcidOf(profile.externalAccounts),\n website: text(profile.website),\n });\n}\n\nfunction appointment(position: ProfilePosition): FunderCvAppointment {\n return compact<FunderCvAppointment>({\n start: year(position.startedAt),\n end: year(position.endedAt) ?? null,\n title: position.title,\n organization: text(position.entityName) ?? text(position.company),\n location: text(formatLocation(position.location)),\n });\n}\n\n/** The current position to name on the form: the flagged primary, else the newest. */\nfunction currentPosition(positions: ProfilePosition[]): ProfilePosition | undefined {\n const current = byStartDesc(\n positions.filter((p) => !text(p.endedAt)),\n (p) => p.startedAt,\n );\n return current.find((p) => p.primary) ?? current[0];\n}\n\nfunction lookup(profile: FunderCvProfileInput) {\n const publications = new Map(filterHidden(profile.publications).map((p) => [p.rkey, p]));\n const presentations = new Map(filterHidden(profile.presentations).map((p) => [p.rkey, p]));\n const honors = new Map(filterHidden(profile.honors).map((h) => [h.rkey, h]));\n const split = (id: string) => {\n const at = id.indexOf(':');\n return { kind: id.slice(0, at), rkey: id.slice(at + 1) };\n };\n return { publications, presentations, honors, split };\n}\n\nfunction citations(\n ids: readonly string[],\n profile: FunderCvProfileInput,\n person: FunderCvPerson,\n): FunderCvCitation[] {\n const { publications, presentations, split } = lookup(profile);\n return ids.flatMap((id): FunderCvCitation[] => {\n const { kind, rkey } = split(id);\n if (kind === 'publication') {\n const p = publications.get(rkey);\n if (!p) return [];\n const authors = (p.contributors ?? []).map((c) => c.name.trim()).filter(Boolean);\n return [\n compact<FunderCvCitation>({\n kind: 'publication',\n title: p.title,\n authors: authors.length > 0 ? authors.join(', ') : undefined,\n venue: text(p.publisher),\n date: year(p.date),\n doi: text(p.doi),\n url: text(p.url),\n }),\n ];\n }\n if (kind === 'presentation') {\n const talk = presentations.get(rkey);\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<FunderCvCitation>({\n kind: 'presentation',\n title: talk.title,\n authors: person.name,\n venue: text(delivery?.eventName),\n date: year(delivery?.date),\n url: text(talk.links?.[0]?.uri),\n }),\n ];\n }\n return [];\n });\n}\n\nfunction honorsOf(ids: readonly string[], profile: FunderCvProfileInput): FunderCvHonor[] {\n const { honors, split } = lookup(profile);\n return ids.flatMap((id) => {\n const { kind, rkey } = split(id);\n const h = kind === 'honor' ? honors.get(rkey) : undefined;\n if (!h) return [];\n return [\n compact<FunderCvHonor>({\n year: year(h.date),\n title: h.title,\n organization: text(h.entityName) ?? text(h.issuer),\n }),\n ];\n });\n}\n\n/**\n * Build the funder document from the profile and the owner's selection.\n *\n * Hidden records never appear. Selected ids that match no visible record are\n * skipped; callers that must refuse them check `findUnknownFunderCvItems`\n * first. Selected items keep the order they were picked in.\n */\nexport function profileToFunderCv(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n options: FunderCvOptions = {},\n): FunderCvDocument {\n const person = personOf(profile);\n const positions = filterHidden(profile.positions);\n const education = byStartDesc(filterHidden(profile.education), (e) => e.startedAt);\n\n if (selection.format === 'nih') {\n const current = currentPosition(positions);\n return compact<NihFunderCv>({\n format: 'nih',\n person,\n positionTitle: text(current?.title),\n organization: current ? appointment(current).organization : undefined,\n location: text(formatLocation(current?.location)),\n education: education.map((e) =>\n compact<NihEducation>({\n organization: text(e.entityName) ?? e.institution,\n degree: text(e.degree),\n start: monthYear(e.startedAt),\n end: monthYear(e.endedAt),\n field: text(e.fieldOfStudy),\n }),\n ),\n appointments: byStartDesc(positions, (p) => p.startedAt).map(appointment),\n products: {\n related: citations(selection.related, profile, person),\n other: citations(selection.other, profile, person),\n },\n honors: honorsOf(selection.honors, profile),\n personalStatement: null,\n contributions: [],\n });\n }\n\n const appointments = byStartDesc(positions, (p) => p.startedAt);\n const { presentations, split } = lookup(profile);\n\n const recognition = selection.recognition.flatMap((id): ErcRecognition[] => {\n const { kind, rkey } = split(id);\n if (kind === 'honor') {\n const [honor] = honorsOf([id], profile);\n return honor ? [{ kind: 'honor', ...honor }] : [];\n }\n const talk = kind === 'presentation' ? presentations.get(rkey) : undefined;\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<ErcRecognition>({\n kind: 'presentation',\n year: year(delivery?.date),\n title: talk.title,\n organization: text(delivery?.eventName),\n }),\n ];\n });\n\n const teaching = filterHidden(profile.courses)\n .filter(isTeachingCourse)\n .map((c) =>\n compact<ErcContribution>({\n kind: 'teaching',\n start: year(c.startedAt),\n end: year(c.endedAt) ?? null,\n title: c.name,\n organization: text(c.entityName) ?? text(c.institution),\n }),\n );\n const service = filterHidden(profile.volunteering).map((v) =>\n compact<ErcContribution>({\n kind: 'service',\n start: year(v.startDate),\n end: year(v.endDate) ?? null,\n title: text(v.role) ?? text(v.entityName) ?? v.organization,\n organization: text(v.entityName) ?? text(v.organization),\n }),\n );\n\n return {\n format: 'erc',\n person: { ...person, ...normalizeExportContact(options.contact ?? {}) },\n education: education.map((e) =>\n compact<ErcEducation>({\n date: monthYear(e.endedAt) ?? monthYear(e.startedAt),\n degree: text(e.degree),\n field: text(e.fieldOfStudy),\n institution: text(e.entityName) ?? e.institution,\n }),\n ),\n currentPositions: appointments.filter((p) => !text(p.endedAt)).map(appointment),\n previousPositions: appointments.filter((p) => !!text(p.endedAt)).map(appointment),\n outputs: citations(selection.outputs, profile, person),\n recognition,\n communityContributions: [...teaching, ...service],\n acronym: null,\n careerBreaks: null,\n };\n}\n"],"mappings":";AAgBA,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAWjC,SAAS,8BAAkD;AAC3D,SAAS,oBAAoB;AAEtB,IAAM,oBAAoB,CAAC,OAAO,KAAK;AAGvC,IAAM,uBAAuB,CAAC,eAAe,gBAAgB,OAAO;AAmBpE,IAAM,mBAAmB;AAAA,EAC9B,KAAK;AAAA,IACH,SAAS,EAAE,OAAO,CAAC,eAAe,cAAc,GAAG,KAAK,EAAE;AAAA,IAC1D,OAAO,EAAE,OAAO,CAAC,eAAe,cAAc,GAAG,KAAK,EAAE;AAAA,IACxD,QAAQ,EAAE,OAAO,CAAC,OAAO,GAAG,KAAK,GAAG;AAAA,EACtC;AAAA,EACA,KAAK;AAAA,IACH,SAAS,EAAE,OAAO,CAAC,aAAa,GAAG,KAAK,GAAG;AAAA,IAC3C,aAAa,EAAE,OAAO,CAAC,SAAS,cAAc,GAAG,KAAK,GAAG;AAAA,EAC3D;AACF;AAMO,SAAS,eAAe,MAAwB,MAAsB;AAC3E,SAAO,GAAG,IAAI,IAAI,IAAI;AACxB;AAGA,IAAM,aAAa;AAEnB,SAAS,OAAO,IAA0C;AACxD,QAAM,OAAO,GAAG,MAAM,GAAG,GAAG,QAAQ,GAAG,CAAC;AACxC,SAAQ,qBAA2C,SAAS,IAAI,IAC3D,OACD;AACN;AAEA,SAAS,YAAY,MAAyB;AAC5C,SAAO,EACJ;AAAA,IACC,EACG,OAAO,EACP,MAAM,UAAU,EAChB,OAAO,CAAC,OAAO,KAAK,MAAM,SAAS,OAAO,EAAE,CAAqB,GAAG;AAAA,MACnE,SAAS,QAAQ,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,IACxC,CAAC;AAAA,EACL,EACC,IAAI,KAAK,GAAG,EACZ,QAAQ,CAAC,CAAC;AACf;AAEA,SAAS,UAAU,OAAqC;AACtD,QAAM,MAAM,MAAM,KAAK;AACvB,SAAO,IAAI,IAAI,GAAG,EAAE,SAAS,IAAI;AACnC;AAEA,IAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACvB,SAAS,YAAY,iBAAiB,IAAI,OAAO;AAAA,EACjD,OAAO,YAAY,iBAAiB,IAAI,KAAK;AAAA,EAC7C,QAAQ,YAAY,iBAAiB,IAAI,MAAM;AACjD,CAAC,EACA,OAAO,CAAC,MAAM,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG;AAAA,EACxD,SAAS;AACX,CAAC;AAEH,IAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACvB,SAAS,YAAY,iBAAiB,IAAI,OAAO;AAAA,EACjD,aAAa,YAAY,iBAAiB,IAAI,WAAW;AAC3D,CAAC,EACA,OAAO,CAAC,MAAM,UAAU,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,GAAG;AAAA,EACpD,SAAS;AACX,CAAC;AAOI,IAAM,0BAA0B,EAAE,mBAAmB,UAAU;AAAA,EACpE;AAAA,EACA;AACF,CAAC;AA+BD,SAAS,qBAAqB,cAAmC;AAC/D,QAAM,SAAS,CAAC,MAAgC,aAAa,EAAE,QAAQ,MAAS,KAAK;AACrF,SAAO,CAAC,GAAG,aAAa,aAAa,UAAU,CAAC,EAC7C,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,cAAc,OAAO,CAAC,CAAC,CAAC,EACjD,GAAG,EAAE;AACV;AAEA,SAAS,eAAe,cAAuD;AAC7E,SAAO,aAAa,qBAAqB,YAAY,GAAG,QAAQ,MAAS;AAC3E;AAEA,SAAS,aAAa,SAA+B,MAA6C;AAChG,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,aAAa,QAAQ,YAAY,EAAE,IAAI,CAAC,OAAO;AAAA,QACpD,IAAI,eAAe,MAAM,EAAE,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAM,aAAa,EAAE,IAAI;AAAA,MAC3B,EAAE;AAAA,IACJ,KAAK;AACH,aAAO,aAAa,QAAQ,aAAa,EAAE,IAAI,CAAC,OAAO;AAAA,QACrD,IAAI,eAAe,MAAM,EAAE,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAM,eAAe,CAAC;AAAA,MACxB,EAAE;AAAA,IACJ,KAAK;AACH,aAAO,aAAa,QAAQ,MAAM,EAAE,IAAI,CAAC,OAAO;AAAA,QAC9C,IAAI,eAAe,MAAM,EAAE,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAM,aAAa,EAAE,IAAI;AAAA,MAC3B,EAAE;AAAA,EACN;AACF;AAOO,SAAS,uBACd,SACA,OACqB;AACrB,QAAM,MAAM,MAAM,QAAQ,CAAC,SAAS,aAAa,SAAS,IAAI,CAAC;AAC/D,SAAO,IAAI,KAAK,CAAC,GAAG,MAAM;AACxB,QAAI,EAAE,QAAQ,EAAE,KAAM,QAAO,EAAE,KAAK,cAAc,EAAE,IAAI;AACxD,QAAI,EAAE,KAAM,QAAO;AACnB,QAAI,EAAE,KAAM,QAAO;AACnB,WAAO;AAAA,EACT,CAAC;AACH;AAOO,SAAS,yBACd,SACA,QACmB;AACnB,QAAM,eAAe,uBAAuB,SAAS,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AACrF,QAAM,SAAS,uBAAuB,SAAS,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AACzE,MAAI,WAAW,OAAO;AACpB,UAAM,EAAE,SAAS,MAAM,IAAI,iBAAiB;AAC5C,WAAO;AAAA,MACL;AAAA,MACA,SAAS,aAAa,MAAM,GAAG,QAAQ,GAAG;AAAA,MAC1C,OAAO,aAAa,MAAM,QAAQ,KAAK,QAAQ,MAAM,MAAM,GAAG;AAAA,MAC9D,QAAQ,OAAO,MAAM,GAAG,iBAAiB,IAAI,OAAO,GAAG;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,SAAS,aAAa,MAAM,GAAG,iBAAiB,IAAI,QAAQ,GAAG;AAAA,IAC/D,aAAa,OAAO,MAAM,GAAG,iBAAiB,IAAI,YAAY,GAAG;AAAA,EACnE;AACF;AAEA,SAAS,YAAY,WAAwC;AAC3D,SAAO,UAAU,WAAW,QACxB,CAAC,GAAG,UAAU,SAAS,GAAG,UAAU,OAAO,GAAG,UAAU,MAAM,IAC9D,CAAC,GAAG,UAAU,SAAS,GAAG,UAAU,WAAW;AACrD;AAMO,SAAS,yBACd,SACA,WACU;AACV,QAAM,QAAQ,IAAI,IAAI,uBAAuB,SAAS,oBAAoB,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC5F,SAAO,YAAY,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;AAC7D;AA+IA,SAAS,KAAK,OAAsD;AAClE,QAAM,UAAU,OAAO,KAAK;AAC5B,SAAO,UAAU,UAAU;AAC7B;AAEA,SAAS,QAA0B,OAAa;AAC9C,SAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,MAAS,CAAC;AACpF;AAEA,SAAS,KAAK,OAAsD;AAClE,SAAO,aAAa,SAAS,MAAS,GAAG,MAAM,GAAG,CAAC;AACrD;AAGA,SAAS,UAAU,OAAsD;AACvE,QAAM,OAAO,aAAa,SAAS,MAAS;AAC5C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,UAAU,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK;AACxE;AAGA,SAAS,YAAe,OAAY,OAA6C;AAC/E,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM;AAC/B,UAAM,KAAK,aAAa,MAAM,CAAC,CAAC;AAChC,UAAM,KAAK,aAAa,MAAM,CAAC,CAAC;AAChC,QAAI,MAAM,GAAI,QAAO,GAAG,cAAc,EAAE;AACxC,QAAI,GAAI,QAAO;AACf,QAAI,GAAI,QAAO;AACf,WAAO;AAAA,EACT,CAAC;AACH;AAEA,IAAM,WAAW;AAEjB,SAAS,QAAQ,UAA6D;AAC5E,QAAM,UAAU,aAAa,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY,MAAM,OAAO;AACvF,SAAO,UAAU,SAAS,KAAK,QAAQ,GAAG,IAAI,CAAC,IAAI;AACrD;AAEA,SAAS,SAAS,SAA+C;AAC/D,SAAO,QAAwB;AAAA,IAC7B,MACE,qBAAqB,QAAQ,WAAW,QAAQ,UAAU,KAC1D,KAAK,QAAQ,WAAW,KACxB,QAAQ;AAAA,IACV,WAAW,KAAK,QAAQ,SAAS;AAAA,IACjC,YAAY,KAAK,QAAQ,UAAU;AAAA,IACnC,OAAO,QAAQ,QAAQ,gBAAgB;AAAA,IACvC,SAAS,KAAK,QAAQ,OAAO;AAAA,EAC/B,CAAC;AACH;AAEA,SAAS,YAAY,UAAgD;AACnE,SAAO,QAA6B;AAAA,IAClC,OAAO,KAAK,SAAS,SAAS;AAAA,IAC9B,KAAK,KAAK,SAAS,OAAO,KAAK;AAAA,IAC/B,OAAO,SAAS;AAAA,IAChB,cAAc,KAAK,SAAS,UAAU,KAAK,KAAK,SAAS,OAAO;AAAA,IAChE,UAAU,KAAK,eAAe,SAAS,QAAQ,CAAC;AAAA,EAClD,CAAC;AACH;AAGA,SAAS,gBAAgB,WAA2D;AAClF,QAAM,UAAU;AAAA,IACd,UAAU,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC;AAAA,IACxC,CAAC,MAAM,EAAE;AAAA,EACX;AACA,SAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ,CAAC;AACpD;AAEA,SAAS,OAAO,SAA+B;AAC7C,QAAM,eAAe,IAAI,IAAI,aAAa,QAAQ,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACvF,QAAM,gBAAgB,IAAI,IAAI,aAAa,QAAQ,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACzF,QAAM,SAAS,IAAI,IAAI,aAAa,QAAQ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC3E,QAAM,QAAQ,CAAC,OAAe;AAC5B,UAAM,KAAK,GAAG,QAAQ,GAAG;AACzB,WAAO,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE,GAAG,MAAM,GAAG,MAAM,KAAK,CAAC,EAAE;AAAA,EACzD;AACA,SAAO,EAAE,cAAc,eAAe,QAAQ,MAAM;AACtD;AAEA,SAAS,UACP,KACA,SACA,QACoB;AACpB,QAAM,EAAE,cAAc,eAAe,MAAM,IAAI,OAAO,OAAO;AAC7D,SAAO,IAAI,QAAQ,CAAC,OAA2B;AAC7C,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE;AAC/B,QAAI,SAAS,eAAe;AAC1B,YAAM,IAAI,aAAa,IAAI,IAAI;AAC/B,UAAI,CAAC,EAAG,QAAO,CAAC;AAChB,YAAM,WAAW,EAAE,gBAAgB,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC,EAAE,OAAO,OAAO;AAC/E,aAAO;AAAA,QACL,QAA0B;AAAA,UACxB,MAAM;AAAA,UACN,OAAO,EAAE;AAAA,UACT,SAAS,QAAQ,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI;AAAA,UACnD,OAAO,KAAK,EAAE,SAAS;AAAA,UACvB,MAAM,KAAK,EAAE,IAAI;AAAA,UACjB,KAAK,KAAK,EAAE,GAAG;AAAA,UACf,KAAK,KAAK,EAAE,GAAG;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,SAAS,gBAAgB;AAC3B,YAAM,OAAO,cAAc,IAAI,IAAI;AACnC,UAAI,CAAC,KAAM,QAAO,CAAC;AACnB,YAAM,WAAW,qBAAqB,IAAI;AAC1C,aAAO;AAAA,QACL,QAA0B;AAAA,UACxB,MAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,SAAS,OAAO;AAAA,UAChB,OAAO,KAAK,UAAU,SAAS;AAAA,UAC/B,MAAM,KAAK,UAAU,IAAI;AAAA,UACzB,KAAK,KAAK,KAAK,QAAQ,CAAC,GAAG,GAAG;AAAA,QAChC,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO,CAAC;AAAA,EACV,CAAC;AACH;AAEA,SAAS,SAAS,KAAwB,SAAgD;AACxF,QAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,OAAO;AACxC,SAAO,IAAI,QAAQ,CAAC,OAAO;AACzB,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE;AAC/B,UAAM,IAAI,SAAS,UAAU,OAAO,IAAI,IAAI,IAAI;AAChD,QAAI,CAAC,EAAG,QAAO,CAAC;AAChB,WAAO;AAAA,MACL,QAAuB;AAAA,QACrB,MAAM,KAAK,EAAE,IAAI;AAAA,QACjB,OAAO,EAAE;AAAA,QACT,cAAc,KAAK,EAAE,UAAU,KAAK,KAAK,EAAE,MAAM;AAAA,MACnD,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AASO,SAAS,kBACd,SACA,WACA,UAA2B,CAAC,GACV;AAClB,QAAM,SAAS,SAAS,OAAO;AAC/B,QAAM,YAAY,aAAa,QAAQ,SAAS;AAChD,QAAM,YAAY,YAAY,aAAa,QAAQ,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS;AAEjF,MAAI,UAAU,WAAW,OAAO;AAC9B,UAAM,UAAU,gBAAgB,SAAS;AACzC,WAAO,QAAqB;AAAA,MAC1B,QAAQ;AAAA,MACR;AAAA,MACA,eAAe,KAAK,SAAS,KAAK;AAAA,MAClC,cAAc,UAAU,YAAY,OAAO,EAAE,eAAe;AAAA,MAC5D,UAAU,KAAK,eAAe,SAAS,QAAQ,CAAC;AAAA,MAChD,WAAW,UAAU;AAAA,QAAI,CAAC,MACxB,QAAsB;AAAA,UACpB,cAAc,KAAK,EAAE,UAAU,KAAK,EAAE;AAAA,UACtC,QAAQ,KAAK,EAAE,MAAM;AAAA,UACrB,OAAO,UAAU,EAAE,SAAS;AAAA,UAC5B,KAAK,UAAU,EAAE,OAAO;AAAA,UACxB,OAAO,KAAK,EAAE,YAAY;AAAA,QAC5B,CAAC;AAAA,MACH;AAAA,MACA,cAAc,YAAY,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,WAAW;AAAA,MACxE,UAAU;AAAA,QACR,SAAS,UAAU,UAAU,SAAS,SAAS,MAAM;AAAA,QACrD,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;AAAA,MACnD;AAAA,MACA,QAAQ,SAAS,UAAU,QAAQ,OAAO;AAAA,MAC1C,mBAAmB;AAAA,MACnB,eAAe,CAAC;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY,WAAW,CAAC,MAAM,EAAE,SAAS;AAC9D,QAAM,EAAE,eAAe,MAAM,IAAI,OAAO,OAAO;AAE/C,QAAM,cAAc,UAAU,YAAY,QAAQ,CAAC,OAAyB;AAC1E,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE;AAC/B,QAAI,SAAS,SAAS;AACpB,YAAM,CAAC,KAAK,IAAI,SAAS,CAAC,EAAE,GAAG,OAAO;AACtC,aAAO,QAAQ,CAAC,EAAE,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC;AAAA,IAClD;AACA,UAAM,OAAO,SAAS,iBAAiB,cAAc,IAAI,IAAI,IAAI;AACjE,QAAI,CAAC,KAAM,QAAO,CAAC;AACnB,UAAM,WAAW,qBAAqB,IAAI;AAC1C,WAAO;AAAA,MACL,QAAwB;AAAA,QACtB,MAAM;AAAA,QACN,MAAM,KAAK,UAAU,IAAI;AAAA,QACzB,OAAO,KAAK;AAAA,QACZ,cAAc,KAAK,UAAU,SAAS;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,QAAM,WAAW,aAAa,QAAQ,OAAO,EAC1C,OAAO,gBAAgB,EACvB;AAAA,IAAI,CAAC,MACJ,QAAyB;AAAA,MACvB,MAAM;AAAA,MACN,OAAO,KAAK,EAAE,SAAS;AAAA,MACvB,KAAK,KAAK,EAAE,OAAO,KAAK;AAAA,MACxB,OAAO,EAAE;AAAA,MACT,cAAc,KAAK,EAAE,UAAU,KAAK,KAAK,EAAE,WAAW;AAAA,IACxD,CAAC;AAAA,EACH;AACF,QAAM,UAAU,aAAa,QAAQ,YAAY,EAAE;AAAA,IAAI,CAAC,MACtD,QAAyB;AAAA,MACvB,MAAM;AAAA,MACN,OAAO,KAAK,EAAE,SAAS;AAAA,MACvB,KAAK,KAAK,EAAE,OAAO,KAAK;AAAA,MACxB,OAAO,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE,UAAU,KAAK,EAAE;AAAA,MAC/C,cAAc,KAAK,EAAE,UAAU,KAAK,KAAK,EAAE,YAAY;AAAA,IACzD,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,EAAE,GAAG,QAAQ,GAAG,uBAAuB,QAAQ,WAAW,CAAC,CAAC,EAAE;AAAA,IACtE,WAAW,UAAU;AAAA,MAAI,CAAC,MACxB,QAAsB;AAAA,QACpB,MAAM,UAAU,EAAE,OAAO,KAAK,UAAU,EAAE,SAAS;AAAA,QACnD,QAAQ,KAAK,EAAE,MAAM;AAAA,QACrB,OAAO,KAAK,EAAE,YAAY;AAAA,QAC1B,aAAa,KAAK,EAAE,UAAU,KAAK,EAAE;AAAA,MACvC,CAAC;AAAA,IACH;AAAA,IACA,kBAAkB,aAAa,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,IAAI,WAAW;AAAA,IAC9E,mBAAmB,aAAa,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,IAAI,WAAW;AAAA,IAChF,SAAS,UAAU,UAAU,SAAS,SAAS,MAAM;AAAA,IACrD;AAAA,IACA,wBAAwB,CAAC,GAAG,UAAU,GAAG,OAAO;AAAA,IAChD,SAAS;AAAA,IACT,cAAc;AAAA,EAChB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/resume/funder-cv.ts"],"sourcesContent":["/**\n * Funder-format CV documents: the NIH Biographical Sketch (Common Form plus\n * NIH Supplement) and the ERC Part B1 \"Curriculum vitae and Track Record\".\n *\n * A pure mapper from a Sifa profile and the owner's pick of items to a plain\n * JSON document a typesetter lays out. Like the JSON Resume emitter next door,\n * nothing here fetches or renders.\n *\n * Funders cap how many items a CV may list. The caps live in\n * `FUNDER_CV_GROUPS`, so the export page that offers the choice and the server\n * that enforces it read one number. Sections a funder asks for that Sifa holds\n * no records for (a personal statement, contributions to science, career\n * breaks) are emitted as `null` or empty, for the template to show as a\n * placeholder the applicant fills in.\n */\n\nimport { z } from 'zod';\n\nimport { formatLocation } from '../format/location-utils.js';\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport { isTeachingCourse } from '../taxonomy/course-role.js';\nimport type {\n ExternalAccount,\n ProfileCourse,\n ProfileEducation,\n ProfileHonor,\n ProfilePosition,\n ProfilePresentation,\n ProfilePublication,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\nimport { toResumeDate } from './json-resume.js';\n\nexport const FUNDER_CV_FORMATS = ['nih', 'erc'] as const;\nexport type FunderCvFormat = (typeof FUNDER_CV_FORMATS)[number];\n\nexport const FUNDER_CV_ITEM_KINDS = ['publication', 'presentation', 'honor'] as const;\nexport type FunderCvItemKind = (typeof FUNDER_CV_ITEM_KINDS)[number];\n\nexport interface FunderCvGroupSpec {\n readonly kinds: readonly FunderCvItemKind[];\n /** Most items the funder's form takes for this group. */\n readonly max: number;\n}\n\n/**\n * The pickable groups per format, with the funder's item limits.\n *\n * - NIH Common Form \"Products\": up to 5 closely related to the proposed\n * project and up to 5 other significant products. Publications, conference\n * papers and presentations all count. NIH Supplement \"Honors\": up to 15.\n * - ERC Part B1 \"Research achievements\": up to ten research outputs. \"Peer\n * recognition\" is \"selected examples\" with no number; 15 is Sifa's own cap,\n * to keep the section inside the 4-page limit.\n */\nexport const FUNDER_CV_GROUPS = {\n nih: {\n related: { kinds: ['publication', 'presentation'], max: 5 },\n other: { kinds: ['publication', 'presentation'], max: 5 },\n honors: { kinds: ['honor'], max: 15 },\n },\n erc: {\n outputs: { kinds: ['publication'], max: 10 },\n recognition: { kinds: ['honor', 'presentation'], max: 15 },\n },\n} as const satisfies Record<FunderCvFormat, Record<string, FunderCvGroupSpec>>;\n\nexport type FunderCvGroupName<F extends FunderCvFormat> = keyof (typeof FUNDER_CV_GROUPS)[F] &\n string;\n\n/** A picked item: `<kind>:<rkey>`, e.g. `publication:3kxyz`. */\nexport function funderCvItemId(kind: FunderCvItemKind, rkey: string): string {\n return `${kind}:${rkey}`;\n}\n\n/**\n * Record key syntax (atproto: 1-512 of A-Z a-z 0-9 . - _ : ~), plus `/`:\n * publications from Standard.site are keyed by their AT URI (`std-at://...`).\n */\nconst ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:/-]{1,512}$/;\n\nfunction kindOf(id: string): FunderCvItemKind | undefined {\n const kind = id.slice(0, id.indexOf(':'));\n return (FUNDER_CV_ITEM_KINDS as readonly string[]).includes(kind)\n ? (kind as FunderCvItemKind)\n : undefined;\n}\n\nfunction groupSchema(spec: FunderCvGroupSpec) {\n return z\n .array(\n z\n .string()\n .regex(ITEM_ID_RE)\n .refine((id) => spec.kinds.includes(kindOf(id) as FunderCvItemKind), {\n message: `Only ${spec.kinds.join(', ')} items fit here`,\n }),\n )\n .max(spec.max)\n .default([]);\n}\n\nfunction noRepeats(lists: readonly string[][]): boolean {\n const all = lists.flat();\n return new Set(all).size === all.length;\n}\n\nconst nihSelectionSchema = z\n .object({\n format: z.literal('nih'),\n related: groupSchema(FUNDER_CV_GROUPS.nih.related),\n other: groupSchema(FUNDER_CV_GROUPS.nih.other),\n honors: groupSchema(FUNDER_CV_GROUPS.nih.honors),\n })\n .refine((s) => noRepeats([s.related, s.other, s.honors]), {\n message: 'An item can be listed once',\n });\n\nconst ercSelectionSchema = z\n .object({\n format: z.literal('erc'),\n outputs: groupSchema(FUNDER_CV_GROUPS.erc.outputs),\n recognition: groupSchema(FUNDER_CV_GROUPS.erc.recognition),\n })\n .refine((s) => noRepeats([s.outputs, s.recognition]), {\n message: 'An item can be listed once',\n });\n\n/**\n * The owner's pick for one format. Enforces the shape and the funder's limits;\n * whether each id is one of the owner's own visible records is a separate\n * check against the profile, `findUnknownFunderCvItems`.\n */\nexport const funderCvSelectionSchema = z.discriminatedUnion('format', [\n nihSelectionSchema,\n ercSelectionSchema,\n]);\n\nexport type FunderCvSelection = z.infer<typeof funderCvSelectionSchema>;\n\n/** The subset of `Profile` the funder mapper reads. */\nexport interface FunderCvProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly headline?: string;\n readonly website?: string;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly publications?: ProfilePublication[];\n readonly presentations?: ProfilePresentation[];\n readonly honors?: ProfileHonor[];\n readonly courses?: ProfileCourse[];\n readonly volunteering?: ProfileVolunteering[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface FunderCvCandidate {\n readonly id: string;\n readonly kind: FunderCvItemKind;\n readonly title: string;\n /** `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; absent when the record has no date. */\n readonly date?: string;\n}\n\n/** The most recent visible delivery of a talk, which dates it. */\nfunction latestDeliveryRecord(presentation: ProfilePresentation) {\n const dateOf = (d: { date?: string | null }) => toResumeDate(d.date ?? undefined) ?? '';\n return [...filterHidden(presentation.deliveries)]\n .sort((a, b) => dateOf(a).localeCompare(dateOf(b)))\n .at(-1);\n}\n\nfunction latestDelivery(presentation: ProfilePresentation): string | undefined {\n return toResumeDate(latestDeliveryRecord(presentation)?.date ?? undefined);\n}\n\nfunction candidatesOf(profile: FunderCvProfileInput, kind: FunderCvItemKind): FunderCvCandidate[] {\n switch (kind) {\n case 'publication':\n return filterHidden(profile.publications).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: toResumeDate(p.date),\n }));\n case 'presentation':\n return filterHidden(profile.presentations).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: latestDelivery(p),\n }));\n case 'honor':\n return filterHidden(profile.honors).map((h) => ({\n id: funderCvItemId(kind, h.rkey),\n kind,\n title: h.title,\n date: toResumeDate(h.date),\n }));\n }\n}\n\n/**\n * The owner's visible items of the given kinds, most recent first. Undated\n * items go last, in profile order. The sort is stable, so equal dates keep\n * profile order too.\n */\nexport function listFunderCvCandidates(\n profile: FunderCvProfileInput,\n kinds: readonly FunderCvItemKind[],\n): FunderCvCandidate[] {\n const all = kinds.flatMap((kind) => candidatesOf(profile, kind));\n return all.sort((a, b) => {\n if (a.date && b.date) return b.date.localeCompare(a.date);\n if (a.date) return -1;\n if (b.date) return 1;\n return 0;\n });\n}\n\n/**\n * What the export page pre-ticks: the most recent publications up to each\n * limit (for NIH, the first five as closely related and the next five as\n * other), and the most recent honors. Talks are offered but not pre-ticked.\n */\nexport function defaultFunderCvSelection(\n profile: FunderCvProfileInput,\n format: FunderCvFormat,\n): FunderCvSelection {\n const publications = listFunderCvCandidates(profile, ['publication']).map((c) => c.id);\n const honors = listFunderCvCandidates(profile, ['honor']).map((c) => c.id);\n if (format === 'nih') {\n const { related, other } = FUNDER_CV_GROUPS.nih;\n return {\n format,\n related: publications.slice(0, related.max),\n other: publications.slice(related.max, related.max + other.max),\n honors: honors.slice(0, FUNDER_CV_GROUPS.nih.honors.max),\n };\n }\n return {\n format,\n outputs: publications.slice(0, FUNDER_CV_GROUPS.erc.outputs.max),\n recognition: honors.slice(0, FUNDER_CV_GROUPS.erc.recognition.max),\n };\n}\n\nfunction selectedIds(selection: FunderCvSelection): string[] {\n return selection.format === 'nih'\n ? [...selection.related, ...selection.other, ...selection.honors]\n : [...selection.outputs, ...selection.recognition];\n}\n\n/**\n * Ids in the selection that are not visible records on this profile. The\n * profile is the owner's own, so an empty result means every pick is theirs.\n */\nexport function findUnknownFunderCvItems(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n): string[] {\n const known = new Set(listFunderCvCandidates(profile, FUNDER_CV_ITEM_KINDS).map((c) => c.id));\n return selectedIds(selection).filter((id) => !known.has(id));\n}\n\n// ---------------------------------------------------------------------------\n// Document shapes\n// ---------------------------------------------------------------------------\n\nexport interface FunderCvPerson {\n /** Full name as the profile shows it; the handle when there is no name. */\n readonly name: string;\n readonly givenName?: string;\n readonly familyName?: string;\n /** Bare ORCID iD, e.g. `0000-0002-1825-0097`. */\n readonly orcid?: string;\n readonly website?: string;\n}\n\n/**\n * The ERC person, with the contact and personal details the owner typed for\n * this export. The NIH Biographical Sketch has no such block, so only ERC carries them.\n */\nexport interface ErcFunderCvPerson extends FunderCvPerson {\n readonly email?: string;\n readonly phone?: string;\n /** Lines are separated by `\\n`. */\n readonly address?: string;\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n}\n\nexport interface FunderCvOptions {\n /**\n * Contact details the owner typed for this export. Sifa stores none of\n * them. Used for ERC only.\n */\n readonly contact?: ExportContact;\n}\n\n/** One product or research output, ready to cite. */\nexport interface FunderCvCitation {\n readonly kind: 'publication' | 'presentation';\n readonly title: string;\n /** Authors in record order, comma separated. For a talk, the speaker. */\n readonly authors?: string;\n /** Journal, publisher or event. */\n readonly venue?: string;\n /** Year. */\n readonly date?: string;\n readonly doi?: string;\n readonly url?: string;\n}\n\nexport interface FunderCvHonor {\n readonly year?: string;\n readonly title: string;\n readonly organization?: string;\n}\n\nexport interface NihEducation {\n readonly organization: string;\n readonly location?: string;\n readonly degree?: string;\n /** Start date, `MM/YYYY` or `YYYY`. */\n readonly start?: string;\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly end?: string;\n readonly field?: string;\n}\n\nexport interface FunderCvAppointment {\n /** `YYYY`. */\n readonly start?: string;\n /** `YYYY`, or null for a current appointment. */\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n readonly location?: string;\n}\n\n/** NIH Biographical Sketch: Common Form plus NIH Supplement. */\nexport interface NihFunderCv {\n readonly format: 'nih';\n readonly person: FunderCvPerson;\n readonly positionTitle?: string;\n /** Primary organization: the current position's. */\n readonly organization?: string;\n readonly location?: string;\n readonly education: NihEducation[];\n readonly appointments: FunderCvAppointment[];\n readonly products: { readonly related: FunderCvCitation[]; readonly other: FunderCvCitation[] };\n readonly honors: FunderCvHonor[];\n /** Supplement \"Personal Statement\". Sifa holds no record for it. */\n readonly personalStatement: string | null;\n /** Supplement \"Contributions to Science\". Sifa holds no record for them. */\n readonly contributions: string[];\n}\n\nexport interface ErcEducation {\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly date?: string;\n readonly degree?: string;\n readonly field?: string;\n readonly institution: string;\n}\n\nexport interface ErcRecognition {\n readonly kind: 'honor' | 'presentation';\n readonly year?: string;\n readonly title: string;\n /** Awarding body, or the event a talk was given at. */\n readonly organization?: string;\n}\n\nexport interface ErcContribution {\n readonly kind: 'teaching' | 'service';\n readonly start?: string;\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n}\n\n/** ERC Part B1, section \"Curriculum vitae and Track Record\". */\nexport interface ErcFunderCv {\n readonly format: 'erc';\n readonly person: ErcFunderCvPerson;\n readonly education: ErcEducation[];\n readonly currentPositions: FunderCvAppointment[];\n readonly previousPositions: FunderCvAppointment[];\n readonly outputs: FunderCvCitation[];\n readonly recognition: ErcRecognition[];\n readonly communityContributions: ErcContribution[];\n /** Proposal acronym for the page header. Not a profile fact. */\n readonly acronym: string | null;\n /** \"Career breaks, diverse career paths and major life events\". */\n readonly careerBreaks: string | null;\n}\n\nexport type FunderCvDocument = NihFunderCv | ErcFunderCv;\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction text(value: string | null | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\nfunction year(value: string | null | undefined): string | undefined {\n return toResumeDate(value ?? undefined)?.slice(0, 4);\n}\n\n/** `MM/YYYY` when the month is known, else `YYYY`. */\nfunction monthYear(value: string | null | undefined): string | undefined {\n const date = toResumeDate(value ?? undefined);\n if (!date) return undefined;\n return date.length >= 7 ? `${date.slice(5, 7)}/${date.slice(0, 4)}` : date;\n}\n\n/** Newest start first; undated entries last, in profile order. */\nfunction byStartDesc<T>(items: T[], start: (item: T) => string | undefined): T[] {\n return [...items].sort((a, b) => {\n const da = toResumeDate(start(a));\n const db = toResumeDate(start(b));\n if (da && db) return db.localeCompare(da);\n if (da) return -1;\n if (db) return 1;\n return 0;\n });\n}\n\nconst ORCID_RE = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/;\n\nfunction orcidOf(accounts: ExternalAccount[] | undefined): string | undefined {\n const account = filterHidden(accounts).find((a) => a.platform.toLowerCase() === 'orcid');\n return account ? ORCID_RE.exec(account.url)?.[0] : undefined;\n}\n\nfunction personOf(profile: FunderCvProfileInput): FunderCvPerson {\n return compact<FunderCvPerson>({\n name:\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName) ??\n profile.handle,\n givenName: text(profile.givenName),\n familyName: text(profile.familyName),\n orcid: orcidOf(profile.externalAccounts),\n website: text(profile.website),\n });\n}\n\nfunction appointment(position: ProfilePosition): FunderCvAppointment {\n return compact<FunderCvAppointment>({\n start: year(position.startedAt),\n end: year(position.endedAt) ?? null,\n title: position.title,\n organization: text(position.entityName) ?? text(position.company),\n location: text(formatLocation(position.location)),\n });\n}\n\n/** The current position to name on the form: the flagged primary, else the newest. */\nfunction currentPosition(positions: ProfilePosition[]): ProfilePosition | undefined {\n const current = byStartDesc(\n positions.filter((p) => !text(p.endedAt)),\n (p) => p.startedAt,\n );\n return current.find((p) => p.primary) ?? current[0];\n}\n\nfunction lookup(profile: FunderCvProfileInput) {\n const publications = new Map(filterHidden(profile.publications).map((p) => [p.rkey, p]));\n const presentations = new Map(filterHidden(profile.presentations).map((p) => [p.rkey, p]));\n const honors = new Map(filterHidden(profile.honors).map((h) => [h.rkey, h]));\n const split = (id: string) => {\n const at = id.indexOf(':');\n return { kind: id.slice(0, at), rkey: id.slice(at + 1) };\n };\n return { publications, presentations, honors, split };\n}\n\nfunction citations(\n ids: readonly string[],\n profile: FunderCvProfileInput,\n person: FunderCvPerson,\n): FunderCvCitation[] {\n const { publications, presentations, split } = lookup(profile);\n return ids.flatMap((id): FunderCvCitation[] => {\n const { kind, rkey } = split(id);\n if (kind === 'publication') {\n const p = publications.get(rkey);\n if (!p) return [];\n const authors = (p.contributors ?? []).map((c) => c.name.trim()).filter(Boolean);\n return [\n compact<FunderCvCitation>({\n kind: 'publication',\n title: p.title,\n authors: authors.length > 0 ? authors.join(', ') : undefined,\n venue: text(p.publisher),\n date: year(p.date),\n doi: text(p.doi),\n url: text(p.url),\n }),\n ];\n }\n if (kind === 'presentation') {\n const talk = presentations.get(rkey);\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<FunderCvCitation>({\n kind: 'presentation',\n title: talk.title,\n authors: person.name,\n venue: text(delivery?.eventName),\n date: year(delivery?.date),\n url: text(talk.links?.[0]?.uri),\n }),\n ];\n }\n return [];\n });\n}\n\nfunction honorsOf(ids: readonly string[], profile: FunderCvProfileInput): FunderCvHonor[] {\n const { honors, split } = lookup(profile);\n return ids.flatMap((id) => {\n const { kind, rkey } = split(id);\n const h = kind === 'honor' ? honors.get(rkey) : undefined;\n if (!h) return [];\n return [\n compact<FunderCvHonor>({\n year: year(h.date),\n title: h.title,\n organization: text(h.entityName) ?? text(h.issuer),\n }),\n ];\n });\n}\n\n/**\n * Build the funder document from the profile and the owner's selection.\n *\n * Hidden records never appear. Selected ids that match no visible record are\n * skipped; callers that must refuse them check `findUnknownFunderCvItems`\n * first. Selected items keep the order they were picked in.\n */\nexport function profileToFunderCv(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n options: FunderCvOptions = {},\n): FunderCvDocument {\n const person = personOf(profile);\n const positions = filterHidden(profile.positions);\n const education = byStartDesc(filterHidden(profile.education), (e) => e.startedAt);\n\n if (selection.format === 'nih') {\n const current = currentPosition(positions);\n return compact<NihFunderCv>({\n format: 'nih',\n person,\n positionTitle: text(current?.title),\n organization: current ? appointment(current).organization : undefined,\n location: text(formatLocation(current?.location)),\n education: education.map((e) =>\n compact<NihEducation>({\n organization: text(e.entityName) ?? e.institution,\n degree: text(e.degree),\n start: monthYear(e.startedAt),\n end: monthYear(e.endedAt),\n field: text(e.fieldOfStudy),\n }),\n ),\n appointments: byStartDesc(positions, (p) => p.startedAt).map(appointment),\n products: {\n related: citations(selection.related, profile, person),\n other: citations(selection.other, profile, person),\n },\n honors: honorsOf(selection.honors, profile),\n personalStatement: null,\n contributions: [],\n });\n }\n\n const appointments = byStartDesc(positions, (p) => p.startedAt);\n const { presentations, split } = lookup(profile);\n\n const recognition = selection.recognition.flatMap((id): ErcRecognition[] => {\n const { kind, rkey } = split(id);\n if (kind === 'honor') {\n const [honor] = honorsOf([id], profile);\n return honor ? [{ kind: 'honor', ...honor }] : [];\n }\n const talk = kind === 'presentation' ? presentations.get(rkey) : undefined;\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<ErcRecognition>({\n kind: 'presentation',\n year: year(delivery?.date),\n title: talk.title,\n organization: text(delivery?.eventName),\n }),\n ];\n });\n\n const teaching = filterHidden(profile.courses)\n .filter(isTeachingCourse)\n .map((c) =>\n compact<ErcContribution>({\n kind: 'teaching',\n start: year(c.startedAt),\n end: year(c.endedAt) ?? null,\n title: c.name,\n organization: text(c.entityName) ?? text(c.institution),\n }),\n );\n const service = filterHidden(profile.volunteering).map((v) =>\n compact<ErcContribution>({\n kind: 'service',\n start: year(v.startDate),\n end: year(v.endDate) ?? null,\n title: text(v.role) ?? text(v.entityName) ?? v.organization,\n organization: text(v.entityName) ?? text(v.organization),\n }),\n );\n\n return {\n format: 'erc',\n person: { ...person, ...normalizeExportContact(options.contact ?? {}) },\n education: education.map((e) =>\n compact<ErcEducation>({\n date: monthYear(e.endedAt) ?? monthYear(e.startedAt),\n degree: text(e.degree),\n field: text(e.fieldOfStudy),\n institution: text(e.entityName) ?? e.institution,\n }),\n ),\n currentPositions: appointments.filter((p) => !text(p.endedAt)).map(appointment),\n previousPositions: appointments.filter((p) => !!text(p.endedAt)).map(appointment),\n outputs: citations(selection.outputs, profile, person),\n recognition,\n communityContributions: [...teaching, ...service],\n acronym: null,\n careerBreaks: null,\n };\n}\n"],"mappings":";AAgBA,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAWjC,SAAS,8BAAkD;AAC3D,SAAS,oBAAoB;AAEtB,IAAM,oBAAoB,CAAC,OAAO,KAAK;AAGvC,IAAM,uBAAuB,CAAC,eAAe,gBAAgB,OAAO;AAmBpE,IAAM,mBAAmB;AAAA,EAC9B,KAAK;AAAA,IACH,SAAS,EAAE,OAAO,CAAC,eAAe,cAAc,GAAG,KAAK,EAAE;AAAA,IAC1D,OAAO,EAAE,OAAO,CAAC,eAAe,cAAc,GAAG,KAAK,EAAE;AAAA,IACxD,QAAQ,EAAE,OAAO,CAAC,OAAO,GAAG,KAAK,GAAG;AAAA,EACtC;AAAA,EACA,KAAK;AAAA,IACH,SAAS,EAAE,OAAO,CAAC,aAAa,GAAG,KAAK,GAAG;AAAA,IAC3C,aAAa,EAAE,OAAO,CAAC,SAAS,cAAc,GAAG,KAAK,GAAG;AAAA,EAC3D;AACF;AAMO,SAAS,eAAe,MAAwB,MAAsB;AAC3E,SAAO,GAAG,IAAI,IAAI,IAAI;AACxB;AAMA,IAAM,aAAa;AAEnB,SAAS,OAAO,IAA0C;AACxD,QAAM,OAAO,GAAG,MAAM,GAAG,GAAG,QAAQ,GAAG,CAAC;AACxC,SAAQ,qBAA2C,SAAS,IAAI,IAC3D,OACD;AACN;AAEA,SAAS,YAAY,MAAyB;AAC5C,SAAO,EACJ;AAAA,IACC,EACG,OAAO,EACP,MAAM,UAAU,EAChB,OAAO,CAAC,OAAO,KAAK,MAAM,SAAS,OAAO,EAAE,CAAqB,GAAG;AAAA,MACnE,SAAS,QAAQ,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,IACxC,CAAC;AAAA,EACL,EACC,IAAI,KAAK,GAAG,EACZ,QAAQ,CAAC,CAAC;AACf;AAEA,SAAS,UAAU,OAAqC;AACtD,QAAM,MAAM,MAAM,KAAK;AACvB,SAAO,IAAI,IAAI,GAAG,EAAE,SAAS,IAAI;AACnC;AAEA,IAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACvB,SAAS,YAAY,iBAAiB,IAAI,OAAO;AAAA,EACjD,OAAO,YAAY,iBAAiB,IAAI,KAAK;AAAA,EAC7C,QAAQ,YAAY,iBAAiB,IAAI,MAAM;AACjD,CAAC,EACA,OAAO,CAAC,MAAM,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG;AAAA,EACxD,SAAS;AACX,CAAC;AAEH,IAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACvB,SAAS,YAAY,iBAAiB,IAAI,OAAO;AAAA,EACjD,aAAa,YAAY,iBAAiB,IAAI,WAAW;AAC3D,CAAC,EACA,OAAO,CAAC,MAAM,UAAU,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC,GAAG;AAAA,EACpD,SAAS;AACX,CAAC;AAOI,IAAM,0BAA0B,EAAE,mBAAmB,UAAU;AAAA,EACpE;AAAA,EACA;AACF,CAAC;AA+BD,SAAS,qBAAqB,cAAmC;AAC/D,QAAM,SAAS,CAAC,MAAgC,aAAa,EAAE,QAAQ,MAAS,KAAK;AACrF,SAAO,CAAC,GAAG,aAAa,aAAa,UAAU,CAAC,EAC7C,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,cAAc,OAAO,CAAC,CAAC,CAAC,EACjD,GAAG,EAAE;AACV;AAEA,SAAS,eAAe,cAAuD;AAC7E,SAAO,aAAa,qBAAqB,YAAY,GAAG,QAAQ,MAAS;AAC3E;AAEA,SAAS,aAAa,SAA+B,MAA6C;AAChG,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,aAAa,QAAQ,YAAY,EAAE,IAAI,CAAC,OAAO;AAAA,QACpD,IAAI,eAAe,MAAM,EAAE,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAM,aAAa,EAAE,IAAI;AAAA,MAC3B,EAAE;AAAA,IACJ,KAAK;AACH,aAAO,aAAa,QAAQ,aAAa,EAAE,IAAI,CAAC,OAAO;AAAA,QACrD,IAAI,eAAe,MAAM,EAAE,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAM,eAAe,CAAC;AAAA,MACxB,EAAE;AAAA,IACJ,KAAK;AACH,aAAO,aAAa,QAAQ,MAAM,EAAE,IAAI,CAAC,OAAO;AAAA,QAC9C,IAAI,eAAe,MAAM,EAAE,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO,EAAE;AAAA,QACT,MAAM,aAAa,EAAE,IAAI;AAAA,MAC3B,EAAE;AAAA,EACN;AACF;AAOO,SAAS,uBACd,SACA,OACqB;AACrB,QAAM,MAAM,MAAM,QAAQ,CAAC,SAAS,aAAa,SAAS,IAAI,CAAC;AAC/D,SAAO,IAAI,KAAK,CAAC,GAAG,MAAM;AACxB,QAAI,EAAE,QAAQ,EAAE,KAAM,QAAO,EAAE,KAAK,cAAc,EAAE,IAAI;AACxD,QAAI,EAAE,KAAM,QAAO;AACnB,QAAI,EAAE,KAAM,QAAO;AACnB,WAAO;AAAA,EACT,CAAC;AACH;AAOO,SAAS,yBACd,SACA,QACmB;AACnB,QAAM,eAAe,uBAAuB,SAAS,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AACrF,QAAM,SAAS,uBAAuB,SAAS,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AACzE,MAAI,WAAW,OAAO;AACpB,UAAM,EAAE,SAAS,MAAM,IAAI,iBAAiB;AAC5C,WAAO;AAAA,MACL;AAAA,MACA,SAAS,aAAa,MAAM,GAAG,QAAQ,GAAG;AAAA,MAC1C,OAAO,aAAa,MAAM,QAAQ,KAAK,QAAQ,MAAM,MAAM,GAAG;AAAA,MAC9D,QAAQ,OAAO,MAAM,GAAG,iBAAiB,IAAI,OAAO,GAAG;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA,SAAS,aAAa,MAAM,GAAG,iBAAiB,IAAI,QAAQ,GAAG;AAAA,IAC/D,aAAa,OAAO,MAAM,GAAG,iBAAiB,IAAI,YAAY,GAAG;AAAA,EACnE;AACF;AAEA,SAAS,YAAY,WAAwC;AAC3D,SAAO,UAAU,WAAW,QACxB,CAAC,GAAG,UAAU,SAAS,GAAG,UAAU,OAAO,GAAG,UAAU,MAAM,IAC9D,CAAC,GAAG,UAAU,SAAS,GAAG,UAAU,WAAW;AACrD;AAMO,SAAS,yBACd,SACA,WACU;AACV,QAAM,QAAQ,IAAI,IAAI,uBAAuB,SAAS,oBAAoB,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC5F,SAAO,YAAY,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;AAC7D;AA+IA,SAAS,KAAK,OAAsD;AAClE,QAAM,UAAU,OAAO,KAAK;AAC5B,SAAO,UAAU,UAAU;AAC7B;AAEA,SAAS,QAA0B,OAAa;AAC9C,SAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,MAAS,CAAC;AACpF;AAEA,SAAS,KAAK,OAAsD;AAClE,SAAO,aAAa,SAAS,MAAS,GAAG,MAAM,GAAG,CAAC;AACrD;AAGA,SAAS,UAAU,OAAsD;AACvE,QAAM,OAAO,aAAa,SAAS,MAAS;AAC5C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,UAAU,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK;AACxE;AAGA,SAAS,YAAe,OAAY,OAA6C;AAC/E,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM;AAC/B,UAAM,KAAK,aAAa,MAAM,CAAC,CAAC;AAChC,UAAM,KAAK,aAAa,MAAM,CAAC,CAAC;AAChC,QAAI,MAAM,GAAI,QAAO,GAAG,cAAc,EAAE;AACxC,QAAI,GAAI,QAAO;AACf,QAAI,GAAI,QAAO;AACf,WAAO;AAAA,EACT,CAAC;AACH;AAEA,IAAM,WAAW;AAEjB,SAAS,QAAQ,UAA6D;AAC5E,QAAM,UAAU,aAAa,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY,MAAM,OAAO;AACvF,SAAO,UAAU,SAAS,KAAK,QAAQ,GAAG,IAAI,CAAC,IAAI;AACrD;AAEA,SAAS,SAAS,SAA+C;AAC/D,SAAO,QAAwB;AAAA,IAC7B,MACE,qBAAqB,QAAQ,WAAW,QAAQ,UAAU,KAC1D,KAAK,QAAQ,WAAW,KACxB,QAAQ;AAAA,IACV,WAAW,KAAK,QAAQ,SAAS;AAAA,IACjC,YAAY,KAAK,QAAQ,UAAU;AAAA,IACnC,OAAO,QAAQ,QAAQ,gBAAgB;AAAA,IACvC,SAAS,KAAK,QAAQ,OAAO;AAAA,EAC/B,CAAC;AACH;AAEA,SAAS,YAAY,UAAgD;AACnE,SAAO,QAA6B;AAAA,IAClC,OAAO,KAAK,SAAS,SAAS;AAAA,IAC9B,KAAK,KAAK,SAAS,OAAO,KAAK;AAAA,IAC/B,OAAO,SAAS;AAAA,IAChB,cAAc,KAAK,SAAS,UAAU,KAAK,KAAK,SAAS,OAAO;AAAA,IAChE,UAAU,KAAK,eAAe,SAAS,QAAQ,CAAC;AAAA,EAClD,CAAC;AACH;AAGA,SAAS,gBAAgB,WAA2D;AAClF,QAAM,UAAU;AAAA,IACd,UAAU,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC;AAAA,IACxC,CAAC,MAAM,EAAE;AAAA,EACX;AACA,SAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ,CAAC;AACpD;AAEA,SAAS,OAAO,SAA+B;AAC7C,QAAM,eAAe,IAAI,IAAI,aAAa,QAAQ,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACvF,QAAM,gBAAgB,IAAI,IAAI,aAAa,QAAQ,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACzF,QAAM,SAAS,IAAI,IAAI,aAAa,QAAQ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC3E,QAAM,QAAQ,CAAC,OAAe;AAC5B,UAAM,KAAK,GAAG,QAAQ,GAAG;AACzB,WAAO,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE,GAAG,MAAM,GAAG,MAAM,KAAK,CAAC,EAAE;AAAA,EACzD;AACA,SAAO,EAAE,cAAc,eAAe,QAAQ,MAAM;AACtD;AAEA,SAAS,UACP,KACA,SACA,QACoB;AACpB,QAAM,EAAE,cAAc,eAAe,MAAM,IAAI,OAAO,OAAO;AAC7D,SAAO,IAAI,QAAQ,CAAC,OAA2B;AAC7C,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE;AAC/B,QAAI,SAAS,eAAe;AAC1B,YAAM,IAAI,aAAa,IAAI,IAAI;AAC/B,UAAI,CAAC,EAAG,QAAO,CAAC;AAChB,YAAM,WAAW,EAAE,gBAAgB,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC,EAAE,OAAO,OAAO;AAC/E,aAAO;AAAA,QACL,QAA0B;AAAA,UACxB,MAAM;AAAA,UACN,OAAO,EAAE;AAAA,UACT,SAAS,QAAQ,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI;AAAA,UACnD,OAAO,KAAK,EAAE,SAAS;AAAA,UACvB,MAAM,KAAK,EAAE,IAAI;AAAA,UACjB,KAAK,KAAK,EAAE,GAAG;AAAA,UACf,KAAK,KAAK,EAAE,GAAG;AAAA,QACjB,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,SAAS,gBAAgB;AAC3B,YAAM,OAAO,cAAc,IAAI,IAAI;AACnC,UAAI,CAAC,KAAM,QAAO,CAAC;AACnB,YAAM,WAAW,qBAAqB,IAAI;AAC1C,aAAO;AAAA,QACL,QAA0B;AAAA,UACxB,MAAM;AAAA,UACN,OAAO,KAAK;AAAA,UACZ,SAAS,OAAO;AAAA,UAChB,OAAO,KAAK,UAAU,SAAS;AAAA,UAC/B,MAAM,KAAK,UAAU,IAAI;AAAA,UACzB,KAAK,KAAK,KAAK,QAAQ,CAAC,GAAG,GAAG;AAAA,QAChC,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO,CAAC;AAAA,EACV,CAAC;AACH;AAEA,SAAS,SAAS,KAAwB,SAAgD;AACxF,QAAM,EAAE,QAAQ,MAAM,IAAI,OAAO,OAAO;AACxC,SAAO,IAAI,QAAQ,CAAC,OAAO;AACzB,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE;AAC/B,UAAM,IAAI,SAAS,UAAU,OAAO,IAAI,IAAI,IAAI;AAChD,QAAI,CAAC,EAAG,QAAO,CAAC;AAChB,WAAO;AAAA,MACL,QAAuB;AAAA,QACrB,MAAM,KAAK,EAAE,IAAI;AAAA,QACjB,OAAO,EAAE;AAAA,QACT,cAAc,KAAK,EAAE,UAAU,KAAK,KAAK,EAAE,MAAM;AAAA,MACnD,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;AASO,SAAS,kBACd,SACA,WACA,UAA2B,CAAC,GACV;AAClB,QAAM,SAAS,SAAS,OAAO;AAC/B,QAAM,YAAY,aAAa,QAAQ,SAAS;AAChD,QAAM,YAAY,YAAY,aAAa,QAAQ,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS;AAEjF,MAAI,UAAU,WAAW,OAAO;AAC9B,UAAM,UAAU,gBAAgB,SAAS;AACzC,WAAO,QAAqB;AAAA,MAC1B,QAAQ;AAAA,MACR;AAAA,MACA,eAAe,KAAK,SAAS,KAAK;AAAA,MAClC,cAAc,UAAU,YAAY,OAAO,EAAE,eAAe;AAAA,MAC5D,UAAU,KAAK,eAAe,SAAS,QAAQ,CAAC;AAAA,MAChD,WAAW,UAAU;AAAA,QAAI,CAAC,MACxB,QAAsB;AAAA,UACpB,cAAc,KAAK,EAAE,UAAU,KAAK,EAAE;AAAA,UACtC,QAAQ,KAAK,EAAE,MAAM;AAAA,UACrB,OAAO,UAAU,EAAE,SAAS;AAAA,UAC5B,KAAK,UAAU,EAAE,OAAO;AAAA,UACxB,OAAO,KAAK,EAAE,YAAY;AAAA,QAC5B,CAAC;AAAA,MACH;AAAA,MACA,cAAc,YAAY,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,WAAW;AAAA,MACxE,UAAU;AAAA,QACR,SAAS,UAAU,UAAU,SAAS,SAAS,MAAM;AAAA,QACrD,OAAO,UAAU,UAAU,OAAO,SAAS,MAAM;AAAA,MACnD;AAAA,MACA,QAAQ,SAAS,UAAU,QAAQ,OAAO;AAAA,MAC1C,mBAAmB;AAAA,MACnB,eAAe,CAAC;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,YAAY,WAAW,CAAC,MAAM,EAAE,SAAS;AAC9D,QAAM,EAAE,eAAe,MAAM,IAAI,OAAO,OAAO;AAE/C,QAAM,cAAc,UAAU,YAAY,QAAQ,CAAC,OAAyB;AAC1E,UAAM,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE;AAC/B,QAAI,SAAS,SAAS;AACpB,YAAM,CAAC,KAAK,IAAI,SAAS,CAAC,EAAE,GAAG,OAAO;AACtC,aAAO,QAAQ,CAAC,EAAE,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC;AAAA,IAClD;AACA,UAAM,OAAO,SAAS,iBAAiB,cAAc,IAAI,IAAI,IAAI;AACjE,QAAI,CAAC,KAAM,QAAO,CAAC;AACnB,UAAM,WAAW,qBAAqB,IAAI;AAC1C,WAAO;AAAA,MACL,QAAwB;AAAA,QACtB,MAAM;AAAA,QACN,MAAM,KAAK,UAAU,IAAI;AAAA,QACzB,OAAO,KAAK;AAAA,QACZ,cAAc,KAAK,UAAU,SAAS;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,QAAM,WAAW,aAAa,QAAQ,OAAO,EAC1C,OAAO,gBAAgB,EACvB;AAAA,IAAI,CAAC,MACJ,QAAyB;AAAA,MACvB,MAAM;AAAA,MACN,OAAO,KAAK,EAAE,SAAS;AAAA,MACvB,KAAK,KAAK,EAAE,OAAO,KAAK;AAAA,MACxB,OAAO,EAAE;AAAA,MACT,cAAc,KAAK,EAAE,UAAU,KAAK,KAAK,EAAE,WAAW;AAAA,IACxD,CAAC;AAAA,EACH;AACF,QAAM,UAAU,aAAa,QAAQ,YAAY,EAAE;AAAA,IAAI,CAAC,MACtD,QAAyB;AAAA,MACvB,MAAM;AAAA,MACN,OAAO,KAAK,EAAE,SAAS;AAAA,MACvB,KAAK,KAAK,EAAE,OAAO,KAAK;AAAA,MACxB,OAAO,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE,UAAU,KAAK,EAAE;AAAA,MAC/C,cAAc,KAAK,EAAE,UAAU,KAAK,KAAK,EAAE,YAAY;AAAA,IACzD,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,EAAE,GAAG,QAAQ,GAAG,uBAAuB,QAAQ,WAAW,CAAC,CAAC,EAAE;AAAA,IACtE,WAAW,UAAU;AAAA,MAAI,CAAC,MACxB,QAAsB;AAAA,QACpB,MAAM,UAAU,EAAE,OAAO,KAAK,UAAU,EAAE,SAAS;AAAA,QACnD,QAAQ,KAAK,EAAE,MAAM;AAAA,QACrB,OAAO,KAAK,EAAE,YAAY;AAAA,QAC1B,aAAa,KAAK,EAAE,UAAU,KAAK,EAAE;AAAA,MACvC,CAAC;AAAA,IACH;AAAA,IACA,kBAAkB,aAAa,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,IAAI,WAAW;AAAA,IAC9E,mBAAmB,aAAa,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,IAAI,WAAW;AAAA,IAChF,SAAS,UAAU,UAAU,SAAS,SAAS,MAAM;AAAA,IACrD;AAAA,IACA,wBAAwB,CAAC,GAAG,UAAU,GAAG,OAAO;AAAA,IAChD,SAAS;AAAA,IACT,cAAc;AAAA,EAChB;AACF;","names":[]}
@@ -198,6 +198,91 @@ var sortCertifications = (items) => sortByDateDesc(items, certDateExtractor);
198
198
  var sortHonors = (items) => sortByDateDesc(items, singleDateExtractor);
199
199
  var sortLanguages = (items) => sortLanguagesByProficiency(items);
200
200
 
201
+ // src/taxonomy/course-role.ts
202
+ var COURSE_ROLE_TAKEN = "id.sifa.defs#courseTaken";
203
+ var COURSE_ROLE_TAUGHT = "id.sifa.defs#courseTaught";
204
+ var COURSE_ROLE_TEACHING_ASSISTANT = "id.sifa.defs#courseTeachingAssistant";
205
+ var COURSE_ROLE_OPTIONS = [
206
+ { value: COURSE_ROLE_TAKEN, label: "Student" },
207
+ { value: COURSE_ROLE_TAUGHT, label: "Instructor" },
208
+ { value: COURSE_ROLE_TEACHING_ASSISTANT, label: "Teaching assistant" }
209
+ ];
210
+ Object.fromEntries(
211
+ COURSE_ROLE_OPTIONS.map((o) => [o.value, o.label])
212
+ );
213
+ var TEACHING_ROLES = /* @__PURE__ */ new Set([COURSE_ROLE_TAUGHT, COURSE_ROLE_TEACHING_ASSISTANT]);
214
+ function isTeachingCourse(course) {
215
+ return !!course.role && TEACHING_ROLES.has(course.role);
216
+ }
217
+
218
+ // src/taxonomy/involvement-kind.ts
219
+ var INVOLVEMENT_KIND_OPTIONS = [
220
+ { value: "id.sifa.defs#involvementOpenSource", label: "Open source", heading: "Open Source" },
221
+ { value: "id.sifa.defs#involvementCommunity", label: "Community", heading: "Community" },
222
+ { value: "id.sifa.defs#involvementCharity", label: "Volunteering", heading: "Volunteering" },
223
+ { value: "id.sifa.defs#involvementCivic", label: "Civic", heading: "Civic" },
224
+ { value: "id.sifa.defs#involvementOther", label: "Other", heading: "Other" }
225
+ ];
226
+ var INVOLVEMENT_KIND_LABELS = Object.fromEntries(
227
+ INVOLVEMENT_KIND_OPTIONS.map((o) => [o.value, o.label])
228
+ );
229
+ Object.fromEntries(
230
+ INVOLVEMENT_KIND_OPTIONS.map((o) => [o.value, o.heading])
231
+ );
232
+ function getInvolvementKindLabel(value) {
233
+ if (!value) return void 0;
234
+ return INVOLVEMENT_KIND_LABELS[value] ?? value;
235
+ }
236
+
237
+ // src/taxonomy/skill-categories.ts
238
+ var SKILL_CATEGORIES = [
239
+ { value: "technical", label: "Technical" },
240
+ { value: "business", label: "Business" },
241
+ { value: "creative", label: "Creative" },
242
+ { value: "interpersonal", label: "Interpersonal" },
243
+ { value: "industry", label: "Industry" },
244
+ { value: "community", label: "Community" },
245
+ { value: "security", label: "Security" }
246
+ ];
247
+ var CATEGORY_ORDER = SKILL_CATEGORIES.map((c) => c.value);
248
+ var CATEGORY_LABELS = {
249
+ ...Object.fromEntries(SKILL_CATEGORIES.map((c) => [c.value, c.label])),
250
+ other: "Other"
251
+ };
252
+ function normalizeSkillCategory(value) {
253
+ if (typeof value !== "string") return void 0;
254
+ const afterRef = value.includes("#") ? value.slice(value.lastIndexOf("#") + 1) : value;
255
+ const token = afterRef.trim().toLowerCase();
256
+ return token === "" ? void 0 : token;
257
+ }
258
+
259
+ // src/taxonomy/skill-grouping.ts
260
+ function groupSkillsByCategory(skills) {
261
+ const grouped = /* @__PURE__ */ new Map();
262
+ for (const skill of skills) {
263
+ const normalised = normalizeSkillCategory(skill.category) ?? "";
264
+ const key = CATEGORY_ORDER.includes(normalised) ? normalised : "other";
265
+ const bucket = grouped.get(key) ?? [];
266
+ bucket.push(skill);
267
+ grouped.set(key, bucket);
268
+ }
269
+ for (const [, groupSkills] of grouped) {
270
+ groupSkills.sort((a, b) => {
271
+ const countDiff = (b.endorsementCount ?? 0) - (a.endorsementCount ?? 0);
272
+ if (countDiff !== 0) return countDiff;
273
+ return compareText(a.name, b.name);
274
+ });
275
+ }
276
+ const ordered = [];
277
+ for (const cat of CATEGORY_ORDER) {
278
+ const group = grouped.get(cat);
279
+ if (group?.length) ordered.push([cat, group]);
280
+ }
281
+ const otherGroup = grouped.get("other");
282
+ if (otherGroup?.length) ordered.push(["other", otherGroup]);
283
+ return ordered;
284
+ }
285
+
201
286
  // src/jsonld/url.ts
202
287
  var DEFAULT_BASE_URL = "https://sifa.id";
203
288
  function normaliseBaseUrl(baseUrl) {
@@ -269,6 +354,41 @@ function text(value, s) {
269
354
  function orgName(entityName, stored, s) {
270
355
  return text(entityName, s) ?? text(stored, s);
271
356
  }
357
+ var TALK_EVENTS_SHOWN = 3;
358
+ function byDateDesc(a, b) {
359
+ return (b.date ?? "").localeCompare(a.date ?? "");
360
+ }
361
+ function mapTalks(profile, s) {
362
+ const standalone = filterHidden(profile.presentationDeliveries).filter(
363
+ (d) => !d.presentationRkey
364
+ );
365
+ const talks = filterHidden(profile.presentations).map((talk) => {
366
+ const deliveries = filterHidden(talk.deliveries).sort(byDateDesc);
367
+ const events = [
368
+ ...new Set(deliveries.map((d) => text(d.eventName ?? void 0, s)).filter(Boolean))
369
+ ];
370
+ const shown = events.slice(0, TALK_EVENTS_SHOWN).join(", ");
371
+ const more = events.length - TALK_EVENTS_SHOWN;
372
+ return compact({
373
+ name: s(talk.title),
374
+ publisher: more > 0 ? `${shown} and ${more} more` : shown || void 0,
375
+ releaseDate: toResumeDate(deliveries[0]?.date ?? void 0),
376
+ url: link(talk.links?.[0]?.uri, s),
377
+ summary: text(talk.description ?? void 0, s),
378
+ type: "Talks"
379
+ });
380
+ });
381
+ const loose = standalone.map(
382
+ (d) => compact({
383
+ name: s(d.title?.trim() || d.eventName?.trim() || ""),
384
+ publisher: text(d.eventName ?? void 0, s),
385
+ releaseDate: toResumeDate(d.date ?? void 0),
386
+ url: link(d.links?.[0]?.uri, s),
387
+ type: "Talks"
388
+ })
389
+ );
390
+ return [...talks, ...loose].filter((t) => t.name).sort((a, b) => byDateDesc({ date: a.releaseDate }, { date: b.releaseDate }));
391
+ }
272
392
  function profileToJsonResume(profile, options = {}) {
273
393
  const s = options.sanitize ?? identity;
274
394
  const baseUrl = normaliseBaseUrl(options.baseUrl);
@@ -302,15 +422,22 @@ function profileToJsonResume(profile, options = {}) {
302
422
  endDate: toResumeDate(position.endedAt)
303
423
  })
304
424
  );
305
- const volunteer = sortByActiveDateRange(filterHidden(profile.volunteering)).map(
306
- (entry) => compact({
425
+ const volunteer = sortByActiveDateRange([
426
+ ...filterHidden(profile.volunteering).map((entry) => ({
307
427
  organization: orgName(entry.entityName, entry.organization, s),
308
428
  position: text(entry.role, s),
309
429
  summary: text(entry.description, s),
310
430
  startDate: toResumeDate(entry.startDate),
311
431
  endDate: toResumeDate(entry.endDate)
312
- })
313
- );
432
+ })),
433
+ ...filterHidden(profile.involvement).map((entry) => ({
434
+ organization: orgName(entry.entityName, entry.upstream, s),
435
+ position: text(entry.role, s) ?? text(getInvolvementKindLabel(entry.kind), s),
436
+ summary: text(entry.description, s),
437
+ startDate: toResumeDate(entry.startedAt),
438
+ endDate: toResumeDate(entry.endedAt)
439
+ }))
440
+ ]).map((entry) => compact(entry));
314
441
  const education = sortEducation(filterHidden(profile.education)).map(
315
442
  (entry) => compact({
316
443
  institution: orgName(entry.entityName, entry.institution, s),
@@ -339,6 +466,18 @@ function profileToJsonResume(profile, options = {}) {
339
466
  url: link(cert.credentialUrl, s)
340
467
  })
341
468
  );
469
+ const listedCredentials = new Set(filterHidden(profile.certifications).map((c) => c.rkey));
470
+ for (const course of filterHidden(profile.courses)) {
471
+ if (isTeachingCourse(course)) continue;
472
+ if (course.credentialRkey && listedCredentials.has(course.credentialRkey)) continue;
473
+ certificates.push(
474
+ compact({
475
+ name: s(course.name),
476
+ date: toResumeDate(course.completedAt),
477
+ issuer: orgName(course.entityName, course.institution, s)
478
+ })
479
+ );
480
+ }
342
481
  const publications = sortPublications(filterHidden(profile.publications)).map(
343
482
  (publication) => compact({
344
483
  name: s(publication.title),
@@ -348,7 +487,11 @@ function profileToJsonResume(profile, options = {}) {
348
487
  summary: text(publication.description, s)
349
488
  })
350
489
  );
351
- const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));
490
+ publications.push(...mapTalks(profile, s));
491
+ const skills = groupSkillsByCategory(profile.skills ?? []).map(([category, items]) => ({
492
+ name: CATEGORY_LABELS[category] ?? category,
493
+ keywords: items.map((skill) => s(skill.name))
494
+ }));
352
495
  const languages = sortLanguages(filterHidden(profile.languages)).map(
353
496
  (language) => compact({
354
497
  language: s(language.language),
@@ -378,25 +521,6 @@ function profileToJsonResume(profile, options = {}) {
378
521
  meta: { canonical, version: SCHEMA_VERSION }
379
522
  };
380
523
  }
381
-
382
- // src/taxonomy/course-role.ts
383
- var COURSE_ROLE_TAKEN = "id.sifa.defs#courseTaken";
384
- var COURSE_ROLE_TAUGHT = "id.sifa.defs#courseTaught";
385
- var COURSE_ROLE_TEACHING_ASSISTANT = "id.sifa.defs#courseTeachingAssistant";
386
- var COURSE_ROLE_OPTIONS = [
387
- { value: COURSE_ROLE_TAKEN, label: "Student" },
388
- { value: COURSE_ROLE_TAUGHT, label: "Instructor" },
389
- { value: COURSE_ROLE_TEACHING_ASSISTANT, label: "Teaching assistant" }
390
- ];
391
- Object.fromEntries(
392
- COURSE_ROLE_OPTIONS.map((o) => [o.value, o.label])
393
- );
394
- var TEACHING_ROLES = /* @__PURE__ */ new Set([COURSE_ROLE_TAUGHT, COURSE_ROLE_TEACHING_ASSISTANT]);
395
- function isTeachingCourse(course) {
396
- return !!course.role && TEACHING_ROLES.has(course.role);
397
- }
398
-
399
- // src/resume/funder-cv.ts
400
524
  var FUNDER_CV_FORMATS = ["nih", "erc"];
401
525
  var FUNDER_CV_ITEM_KINDS = ["publication", "presentation", "honor"];
402
526
  var FUNDER_CV_GROUPS = {
@@ -413,7 +537,7 @@ var FUNDER_CV_GROUPS = {
413
537
  function funderCvItemId(kind, rkey) {
414
538
  return `${kind}:${rkey}`;
415
539
  }
416
- var ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;
540
+ var ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:/-]{1,512}$/;
417
541
  function kindOf(id) {
418
542
  const kind = id.slice(0, id.indexOf(":"));
419
543
  return FUNDER_CV_ITEM_KINDS.includes(kind) ? kind : void 0;