@takuhon/core 0.1.0 → 0.2.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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // takuhon.schema.json
2
2
  var takuhon_schema_default = {
3
3
  $schema: "https://json-schema.org/draft/2020-12/schema",
4
- $id: "https://takuhon.example/schemas/0.1.0/takuhon.schema.json",
4
+ $id: "https://takuhon.example/schemas/0.2.0/takuhon.schema.json",
5
5
  title: "Takuhon Profile",
6
6
  description: "Portable profile data format consumed by @takuhon/core. The canonical contract for profile content authored as takuhon.json.",
7
7
  type: "object",
@@ -44,6 +44,51 @@ var takuhon_schema_default = {
44
44
  maxItems: 200,
45
45
  items: { $ref: "#/$defs/Skill" }
46
46
  },
47
+ certifications: {
48
+ type: "array",
49
+ maxItems: 50,
50
+ items: { $ref: "#/$defs/Certification" }
51
+ },
52
+ memberships: {
53
+ type: "array",
54
+ maxItems: 50,
55
+ items: { $ref: "#/$defs/Membership" }
56
+ },
57
+ volunteering: {
58
+ type: "array",
59
+ maxItems: 50,
60
+ items: { $ref: "#/$defs/Volunteering" }
61
+ },
62
+ honors: {
63
+ type: "array",
64
+ maxItems: 50,
65
+ items: { $ref: "#/$defs/Honor" }
66
+ },
67
+ education: {
68
+ type: "array",
69
+ maxItems: 30,
70
+ items: { $ref: "#/$defs/Education" }
71
+ },
72
+ publications: {
73
+ type: "array",
74
+ maxItems: 100,
75
+ items: { $ref: "#/$defs/Publication" }
76
+ },
77
+ languages: {
78
+ type: "array",
79
+ maxItems: 30,
80
+ items: { $ref: "#/$defs/Language" }
81
+ },
82
+ courses: {
83
+ type: "array",
84
+ maxItems: 100,
85
+ items: { $ref: "#/$defs/Course" }
86
+ },
87
+ patents: {
88
+ type: "array",
89
+ maxItems: 50,
90
+ items: { $ref: "#/$defs/Patent" }
91
+ },
47
92
  contact: { $ref: "#/$defs/Contact" },
48
93
  settings: { $ref: "#/$defs/Settings" },
49
94
  meta: { $ref: "#/$defs/Meta" }
@@ -321,7 +366,8 @@ var takuhon_schema_default = {
321
366
  maxLength: 100,
322
367
  description: "Tool that produced this document (e.g. 'Takuhon', 'create-takuhon@0.1.0')."
323
368
  },
324
- contentLicense: { $ref: "#/$defs/ContentLicense" }
369
+ contentLicense: { $ref: "#/$defs/ContentLicense" },
370
+ privacy: { $ref: "#/$defs/MetaPrivacy" }
325
371
  }
326
372
  },
327
373
  ContentLicense: {
@@ -351,6 +397,214 @@ var takuhon_schema_default = {
351
397
  description: "Free-form rights statement (used when spdxId='Proprietary' or for additional notices)."
352
398
  }
353
399
  }
400
+ },
401
+ MetaPrivacy: {
402
+ type: "object",
403
+ additionalProperties: true,
404
+ description: "Opt-out flags that strip personally identifying fields from public API output (GET /api/profile, /api/jsonld, /takuhon.json). Admin endpoints (PUT /api/admin/*, GET /api/export) ignore these flags. Privacy-by-default: omitting the object or individual flags is equivalent to true.",
405
+ properties: {
406
+ hideCredentialIds: {
407
+ type: "boolean",
408
+ default: true,
409
+ description: "When true (default), strip certifications[*].credentialId from public responses."
410
+ },
411
+ hideEducationGrades: {
412
+ type: "boolean",
413
+ default: true,
414
+ description: "When true (default), strip education[*].grade from public responses."
415
+ }
416
+ }
417
+ },
418
+ Certification: {
419
+ type: "object",
420
+ additionalProperties: true,
421
+ required: ["id", "title", "issuingOrganization", "issueDate"],
422
+ properties: {
423
+ id: { $ref: "#/$defs/Slug" },
424
+ title: { $ref: "#/$defs/LocalizedTitle" },
425
+ issuingOrganization: { $ref: "#/$defs/LocalizedTitle" },
426
+ issueDate: { $ref: "#/$defs/YearMonth" },
427
+ expirationDate: {
428
+ anyOf: [{ $ref: "#/$defs/YearMonth" }, { type: "null" }],
429
+ description: "null = explicitly 'no expiration'. Omit if unknown/unstated."
430
+ },
431
+ credentialId: {
432
+ type: "string",
433
+ minLength: 1,
434
+ maxLength: 100,
435
+ description: "License or certificate number. Public exposure controlled by meta.privacy.hideCredentialIds."
436
+ },
437
+ url: { $ref: "#/$defs/Url" },
438
+ order: { type: "integer", minimum: 0 }
439
+ }
440
+ },
441
+ Membership: {
442
+ type: "object",
443
+ additionalProperties: true,
444
+ required: ["id", "organization", "startDate"],
445
+ properties: {
446
+ id: { $ref: "#/$defs/Slug" },
447
+ organization: { $ref: "#/$defs/LocalizedTitle" },
448
+ role: { $ref: "#/$defs/LocalizedTitle" },
449
+ description: { $ref: "#/$defs/LocalizedBody" },
450
+ startDate: { $ref: "#/$defs/YearMonth" },
451
+ endDate: {
452
+ anyOf: [{ $ref: "#/$defs/YearMonth" }, { type: "null" }],
453
+ description: "null = ongoing. Omit if unknown."
454
+ },
455
+ isCurrent: { type: "boolean" },
456
+ url: { $ref: "#/$defs/Url" },
457
+ order: { type: "integer", minimum: 0 }
458
+ }
459
+ },
460
+ Volunteering: {
461
+ type: "object",
462
+ additionalProperties: true,
463
+ required: ["id", "organization", "role", "startDate"],
464
+ properties: {
465
+ id: { $ref: "#/$defs/Slug" },
466
+ organization: { $ref: "#/$defs/LocalizedTitle" },
467
+ role: { $ref: "#/$defs/LocalizedTitle" },
468
+ cause: { $ref: "#/$defs/LocalizedTitle" },
469
+ description: { $ref: "#/$defs/LocalizedBody" },
470
+ startDate: { $ref: "#/$defs/YearMonth" },
471
+ endDate: {
472
+ anyOf: [{ $ref: "#/$defs/YearMonth" }, { type: "null" }]
473
+ },
474
+ isCurrent: { type: "boolean" },
475
+ url: { $ref: "#/$defs/Url" },
476
+ order: { type: "integer", minimum: 0 }
477
+ }
478
+ },
479
+ Honor: {
480
+ type: "object",
481
+ additionalProperties: true,
482
+ required: ["id", "title", "issuer", "date"],
483
+ properties: {
484
+ id: { $ref: "#/$defs/Slug" },
485
+ title: { $ref: "#/$defs/LocalizedTitle" },
486
+ issuer: { $ref: "#/$defs/LocalizedTitle" },
487
+ description: { $ref: "#/$defs/LocalizedBody" },
488
+ date: { $ref: "#/$defs/YearMonth" },
489
+ url: { $ref: "#/$defs/Url" },
490
+ order: { type: "integer", minimum: 0 }
491
+ }
492
+ },
493
+ Education: {
494
+ type: "object",
495
+ additionalProperties: true,
496
+ required: ["id", "institution", "startDate"],
497
+ properties: {
498
+ id: { $ref: "#/$defs/Slug" },
499
+ institution: { $ref: "#/$defs/LocalizedTitle" },
500
+ degree: { $ref: "#/$defs/LocalizedTitle" },
501
+ fieldOfStudy: { $ref: "#/$defs/LocalizedTitle" },
502
+ description: { $ref: "#/$defs/LocalizedBody" },
503
+ grade: {
504
+ type: "string",
505
+ minLength: 1,
506
+ maxLength: 50,
507
+ description: "Free-form grade / class / GPA. Public exposure controlled by meta.privacy.hideEducationGrades."
508
+ },
509
+ startDate: { $ref: "#/$defs/YearMonth" },
510
+ endDate: {
511
+ anyOf: [{ $ref: "#/$defs/YearMonth" }, { type: "null" }],
512
+ description: "null = currently enrolled."
513
+ },
514
+ isCurrent: { type: "boolean" },
515
+ url: { $ref: "#/$defs/Url" },
516
+ order: { type: "integer", minimum: 0 }
517
+ }
518
+ },
519
+ Publication: {
520
+ type: "object",
521
+ additionalProperties: true,
522
+ required: ["id", "title", "date"],
523
+ properties: {
524
+ id: { $ref: "#/$defs/Slug" },
525
+ title: { $ref: "#/$defs/LocalizedTitle" },
526
+ publisher: { $ref: "#/$defs/LocalizedTitle" },
527
+ description: { $ref: "#/$defs/LocalizedBody" },
528
+ date: { $ref: "#/$defs/YearMonth" },
529
+ url: { $ref: "#/$defs/Url" },
530
+ doi: {
531
+ type: "string",
532
+ minLength: 1,
533
+ maxLength: 200,
534
+ description: "DOI identifier (e.g. '10.1145/3548643.3548644'). The full URL goes in 'url'."
535
+ },
536
+ coAuthors: {
537
+ type: "array",
538
+ maxItems: 50,
539
+ items: { type: "string", minLength: 1, maxLength: 100 },
540
+ description: "Co-author names in original script. Excludes the profile owner."
541
+ },
542
+ order: { type: "integer", minimum: 0 }
543
+ }
544
+ },
545
+ Language: {
546
+ type: "object",
547
+ additionalProperties: true,
548
+ required: ["id", "language", "proficiency"],
549
+ properties: {
550
+ id: { $ref: "#/$defs/Slug" },
551
+ language: { $ref: "#/$defs/LocaleTag" },
552
+ displayName: { $ref: "#/$defs/LocalizedTitle" },
553
+ proficiency: {
554
+ type: "string",
555
+ enum: ["native", "fluent", "professional", "intermediate", "basic"],
556
+ description: "LinkedIn-compatible 5-level proficiency."
557
+ },
558
+ order: { type: "integer", minimum: 0 }
559
+ }
560
+ },
561
+ Course: {
562
+ type: "object",
563
+ additionalProperties: true,
564
+ required: ["id", "title"],
565
+ properties: {
566
+ id: { $ref: "#/$defs/Slug" },
567
+ title: { $ref: "#/$defs/LocalizedTitle" },
568
+ provider: { $ref: "#/$defs/LocalizedTitle" },
569
+ courseNumber: { type: "string", minLength: 1, maxLength: 50 },
570
+ description: { $ref: "#/$defs/LocalizedBody" },
571
+ completionDate: { $ref: "#/$defs/YearMonth" },
572
+ certificateUrl: { $ref: "#/$defs/Url" },
573
+ relatedEducationId: {
574
+ $ref: "#/$defs/Slug",
575
+ description: "Optional reference to an education[].id (e.g. for university coursework)."
576
+ },
577
+ order: { type: "integer", minimum: 0 }
578
+ }
579
+ },
580
+ Patent: {
581
+ type: "object",
582
+ additionalProperties: true,
583
+ required: ["id", "title", "patentNumber", "status"],
584
+ properties: {
585
+ id: { $ref: "#/$defs/Slug" },
586
+ title: { $ref: "#/$defs/LocalizedTitle" },
587
+ patentNumber: { type: "string", minLength: 1, maxLength: 100 },
588
+ office: {
589
+ type: "string",
590
+ maxLength: 100,
591
+ description: "Patent office name (e.g. 'USPTO', 'JPO', 'EPO')."
592
+ },
593
+ status: {
594
+ type: "string",
595
+ enum: ["pending", "issued", "expired", "abandoned"]
596
+ },
597
+ description: { $ref: "#/$defs/LocalizedBody" },
598
+ filingDate: { $ref: "#/$defs/YearMonth" },
599
+ grantDate: { $ref: "#/$defs/YearMonth" },
600
+ url: { $ref: "#/$defs/Url" },
601
+ coInventors: {
602
+ type: "array",
603
+ maxItems: 20,
604
+ items: { type: "string", minLength: 1, maxLength: 100 }
605
+ },
606
+ order: { type: "integer", minimum: 0 }
607
+ }
354
608
  }
355
609
  }
356
610
  };
@@ -361,7 +615,7 @@ var schema = takuhon_schema_default;
361
615
  // src/validate.ts
362
616
  import Ajv2020 from "ajv/dist/2020.js";
363
617
  import addFormats from "ajv-formats";
364
- var SUPPORTED_SCHEMA_VERSIONS = ["0.1.0"];
618
+ var SUPPORTED_SCHEMA_VERSIONS = ["0.1.0", "0.2.0"];
365
619
  var ajv = new Ajv2020({
366
620
  allErrors: true,
367
621
  strict: true
@@ -397,13 +651,58 @@ function validate(data) {
397
651
  };
398
652
  }
399
653
  if (compiled(data)) {
400
- return { ok: true, data };
654
+ const cloned = JSON.parse(JSON.stringify(data));
655
+ coerceMissingArrays(cloned);
656
+ const duplicate = findDuplicateLanguage(cloned.languages);
657
+ if (duplicate !== void 0) {
658
+ return {
659
+ ok: false,
660
+ errors: [
661
+ {
662
+ pointer: `/languages/${duplicate.index}/language`,
663
+ message: `Duplicate languages[].language value "${duplicate.tag}" \u2014 each entry must declare a unique BCP-47 tag.`,
664
+ keyword: "uniqueItems"
665
+ }
666
+ ]
667
+ };
668
+ }
669
+ return { ok: true, data: cloned };
401
670
  }
402
671
  return {
403
672
  ok: false,
404
673
  errors: (compiled.errors ?? []).map(toValidationError)
405
674
  };
406
675
  }
676
+ var COERCED_ARRAY_KEYS = [
677
+ "certifications",
678
+ "memberships",
679
+ "volunteering",
680
+ "honors",
681
+ "education",
682
+ "publications",
683
+ "languages",
684
+ "courses",
685
+ "patents"
686
+ ];
687
+ function coerceMissingArrays(data) {
688
+ const bag = data;
689
+ for (const key of COERCED_ARRAY_KEYS) {
690
+ if (!Array.isArray(bag[key])) {
691
+ bag[key] = [];
692
+ }
693
+ }
694
+ }
695
+ function findDuplicateLanguage(languages) {
696
+ const seen = /* @__PURE__ */ new Map();
697
+ for (let i = 0; i < languages.length; i++) {
698
+ const entry = languages[i];
699
+ if (entry === void 0) continue;
700
+ const key = entry.language.toLowerCase();
701
+ if (seen.has(key)) return { index: i, tag: entry.language };
702
+ seen.set(key, i);
703
+ }
704
+ return void 0;
705
+ }
407
706
  function isSupportedVersion(value) {
408
707
  return SUPPORTED_SCHEMA_VERSIONS.includes(value);
409
708
  }
@@ -434,6 +733,12 @@ function escapePointerSegment(segment) {
434
733
  // src/normalize.ts
435
734
  function normalize(data) {
436
735
  const out = JSON.parse(JSON.stringify(data));
736
+ const bag = out;
737
+ for (const key of NORMALIZED_ARRAYS) {
738
+ if (!Array.isArray(bag[key])) {
739
+ bag[key] = [];
740
+ }
741
+ }
437
742
  normalizeProfile(out.profile);
438
743
  for (const link of out.links) {
439
744
  cleanOptionalLocalized(link, "label");
@@ -451,6 +756,58 @@ function normalize(data) {
451
756
  }
452
757
  out.projects = stableSortByOrder(out.projects);
453
758
  out.skills = stableSortByOrder(out.skills);
759
+ for (const cert of out.certifications) {
760
+ cleanRequiredLocalized(cert.title);
761
+ cleanRequiredLocalized(cert.issuingOrganization);
762
+ }
763
+ out.certifications = stableSortByOrder(out.certifications);
764
+ for (const m of out.memberships) {
765
+ cleanRequiredLocalized(m.organization);
766
+ cleanOptionalLocalized(m, "role");
767
+ cleanOptionalLocalized(m, "description");
768
+ }
769
+ out.memberships = stableSortByOrder(out.memberships);
770
+ for (const v of out.volunteering) {
771
+ cleanRequiredLocalized(v.organization);
772
+ cleanRequiredLocalized(v.role);
773
+ cleanOptionalLocalized(v, "cause");
774
+ cleanOptionalLocalized(v, "description");
775
+ }
776
+ out.volunteering = stableSortByOrder(out.volunteering);
777
+ for (const h of out.honors) {
778
+ cleanRequiredLocalized(h.title);
779
+ cleanRequiredLocalized(h.issuer);
780
+ cleanOptionalLocalized(h, "description");
781
+ }
782
+ out.honors = stableSortByOrder(out.honors);
783
+ for (const e of out.education) {
784
+ cleanRequiredLocalized(e.institution);
785
+ cleanOptionalLocalized(e, "degree");
786
+ cleanOptionalLocalized(e, "fieldOfStudy");
787
+ cleanOptionalLocalized(e, "description");
788
+ }
789
+ out.education = stableSortByOrder(out.education);
790
+ for (const p of out.publications) {
791
+ cleanRequiredLocalized(p.title);
792
+ cleanOptionalLocalized(p, "publisher");
793
+ cleanOptionalLocalized(p, "description");
794
+ }
795
+ out.publications = stableSortByOrder(out.publications);
796
+ for (const l of out.languages) {
797
+ cleanOptionalLocalized(l, "displayName");
798
+ }
799
+ out.languages = stableSortByOrder(out.languages);
800
+ for (const c of out.courses) {
801
+ cleanRequiredLocalized(c.title);
802
+ cleanOptionalLocalized(c, "provider");
803
+ cleanOptionalLocalized(c, "description");
804
+ }
805
+ out.courses = stableSortByOrder(out.courses);
806
+ for (const p of out.patents) {
807
+ cleanRequiredLocalized(p.title);
808
+ cleanOptionalLocalized(p, "description");
809
+ }
810
+ out.patents = stableSortByOrder(out.patents);
454
811
  return out;
455
812
  }
456
813
  function normalizeProfile(profile) {
@@ -493,6 +850,17 @@ function stableSortByOrder(items) {
493
850
  return ao - bo;
494
851
  });
495
852
  }
853
+ var NORMALIZED_ARRAYS = [
854
+ "certifications",
855
+ "memberships",
856
+ "volunteering",
857
+ "honors",
858
+ "education",
859
+ "publications",
860
+ "languages",
861
+ "courses",
862
+ "patents"
863
+ ];
496
864
 
497
865
  // src/locale-tag.ts
498
866
  var BCP47_PATTERN = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/;
@@ -530,6 +898,15 @@ function resolveLocale(data, locale, fallbackLocale) {
530
898
  careers: data.careers.map((c) => resolveCareer(c, candidates)),
531
899
  projects: data.projects.map((p) => resolveProject(p, candidates)),
532
900
  skills: data.skills,
901
+ certifications: data.certifications.map((c) => resolveCertification(c, candidates)),
902
+ memberships: data.memberships.map((m) => resolveMembership(m, candidates)),
903
+ volunteering: data.volunteering.map((v) => resolveVolunteering(v, candidates)),
904
+ honors: data.honors.map((h) => resolveHonor(h, candidates)),
905
+ education: data.education.map((e) => resolveEducation(e, candidates)),
906
+ publications: data.publications.map((p) => resolvePublication(p, candidates)),
907
+ languages: data.languages.map((l) => resolveLanguage(l, candidates)),
908
+ courses: data.courses.map((c) => resolveCourse(c, candidates)),
909
+ patents: data.patents.map((p) => resolvePatent(p, candidates)),
533
910
  contact: data.contact,
534
911
  settings: data.settings,
535
912
  meta: data.meta,
@@ -657,6 +1034,144 @@ function resolveProject(project, candidates) {
657
1034
  if (project.order !== void 0) out.order = project.order;
658
1035
  return out;
659
1036
  }
1037
+ function resolveCertification(cert, candidates) {
1038
+ const out = {
1039
+ id: cert.id,
1040
+ title: pickLocalized(cert.title, candidates) ?? "",
1041
+ issuingOrganization: pickLocalized(cert.issuingOrganization, candidates) ?? "",
1042
+ issueDate: cert.issueDate
1043
+ };
1044
+ if (cert.expirationDate !== void 0) out.expirationDate = cert.expirationDate;
1045
+ if (cert.credentialId !== void 0) out.credentialId = cert.credentialId;
1046
+ if (cert.url !== void 0) out.url = cert.url;
1047
+ if (cert.order !== void 0) out.order = cert.order;
1048
+ return out;
1049
+ }
1050
+ function resolveMembership(membership, candidates) {
1051
+ const out = {
1052
+ id: membership.id,
1053
+ organization: pickLocalized(membership.organization, candidates) ?? "",
1054
+ startDate: membership.startDate
1055
+ };
1056
+ const role = pickLocalized(membership.role, candidates);
1057
+ if (role !== void 0) out.role = role;
1058
+ const description = pickLocalized(membership.description, candidates);
1059
+ if (description !== void 0) out.description = description;
1060
+ if (membership.endDate !== void 0) out.endDate = membership.endDate;
1061
+ if (membership.isCurrent !== void 0) out.isCurrent = membership.isCurrent;
1062
+ if (membership.url !== void 0) out.url = membership.url;
1063
+ if (membership.order !== void 0) out.order = membership.order;
1064
+ return out;
1065
+ }
1066
+ function resolveVolunteering(v, candidates) {
1067
+ const out = {
1068
+ id: v.id,
1069
+ organization: pickLocalized(v.organization, candidates) ?? "",
1070
+ role: pickLocalized(v.role, candidates) ?? "",
1071
+ startDate: v.startDate
1072
+ };
1073
+ const cause = pickLocalized(v.cause, candidates);
1074
+ if (cause !== void 0) out.cause = cause;
1075
+ const description = pickLocalized(v.description, candidates);
1076
+ if (description !== void 0) out.description = description;
1077
+ if (v.endDate !== void 0) out.endDate = v.endDate;
1078
+ if (v.isCurrent !== void 0) out.isCurrent = v.isCurrent;
1079
+ if (v.url !== void 0) out.url = v.url;
1080
+ if (v.order !== void 0) out.order = v.order;
1081
+ return out;
1082
+ }
1083
+ function resolveHonor(honor, candidates) {
1084
+ const out = {
1085
+ id: honor.id,
1086
+ title: pickLocalized(honor.title, candidates) ?? "",
1087
+ issuer: pickLocalized(honor.issuer, candidates) ?? "",
1088
+ date: honor.date
1089
+ };
1090
+ const description = pickLocalized(honor.description, candidates);
1091
+ if (description !== void 0) out.description = description;
1092
+ if (honor.url !== void 0) out.url = honor.url;
1093
+ if (honor.order !== void 0) out.order = honor.order;
1094
+ return out;
1095
+ }
1096
+ function resolveEducation(edu, candidates) {
1097
+ const out = {
1098
+ id: edu.id,
1099
+ institution: pickLocalized(edu.institution, candidates) ?? "",
1100
+ startDate: edu.startDate
1101
+ };
1102
+ const degree = pickLocalized(edu.degree, candidates);
1103
+ if (degree !== void 0) out.degree = degree;
1104
+ const fieldOfStudy = pickLocalized(edu.fieldOfStudy, candidates);
1105
+ if (fieldOfStudy !== void 0) out.fieldOfStudy = fieldOfStudy;
1106
+ const description = pickLocalized(edu.description, candidates);
1107
+ if (description !== void 0) out.description = description;
1108
+ if (edu.grade !== void 0) out.grade = edu.grade;
1109
+ if (edu.endDate !== void 0) out.endDate = edu.endDate;
1110
+ if (edu.isCurrent !== void 0) out.isCurrent = edu.isCurrent;
1111
+ if (edu.url !== void 0) out.url = edu.url;
1112
+ if (edu.order !== void 0) out.order = edu.order;
1113
+ return out;
1114
+ }
1115
+ function resolvePublication(pub, candidates) {
1116
+ const out = {
1117
+ id: pub.id,
1118
+ title: pickLocalized(pub.title, candidates) ?? "",
1119
+ date: pub.date
1120
+ };
1121
+ const publisher = pickLocalized(pub.publisher, candidates);
1122
+ if (publisher !== void 0) out.publisher = publisher;
1123
+ const description = pickLocalized(pub.description, candidates);
1124
+ if (description !== void 0) out.description = description;
1125
+ if (pub.url !== void 0) out.url = pub.url;
1126
+ if (pub.doi !== void 0) out.doi = pub.doi;
1127
+ if (pub.coAuthors !== void 0) out.coAuthors = pub.coAuthors;
1128
+ if (pub.order !== void 0) out.order = pub.order;
1129
+ return out;
1130
+ }
1131
+ function resolveLanguage(lang, candidates) {
1132
+ const out = {
1133
+ id: lang.id,
1134
+ language: lang.language,
1135
+ proficiency: lang.proficiency
1136
+ };
1137
+ const displayName = pickLocalized(lang.displayName, candidates);
1138
+ if (displayName !== void 0) out.displayName = displayName;
1139
+ if (lang.order !== void 0) out.order = lang.order;
1140
+ return out;
1141
+ }
1142
+ function resolveCourse(course, candidates) {
1143
+ const out = {
1144
+ id: course.id,
1145
+ title: pickLocalized(course.title, candidates) ?? ""
1146
+ };
1147
+ const provider = pickLocalized(course.provider, candidates);
1148
+ if (provider !== void 0) out.provider = provider;
1149
+ if (course.courseNumber !== void 0) out.courseNumber = course.courseNumber;
1150
+ const description = pickLocalized(course.description, candidates);
1151
+ if (description !== void 0) out.description = description;
1152
+ if (course.completionDate !== void 0) out.completionDate = course.completionDate;
1153
+ if (course.certificateUrl !== void 0) out.certificateUrl = course.certificateUrl;
1154
+ if (course.relatedEducationId !== void 0) out.relatedEducationId = course.relatedEducationId;
1155
+ if (course.order !== void 0) out.order = course.order;
1156
+ return out;
1157
+ }
1158
+ function resolvePatent(patent, candidates) {
1159
+ const out = {
1160
+ id: patent.id,
1161
+ title: pickLocalized(patent.title, candidates) ?? "",
1162
+ patentNumber: patent.patentNumber,
1163
+ status: patent.status
1164
+ };
1165
+ if (patent.office !== void 0) out.office = patent.office;
1166
+ const description = pickLocalized(patent.description, candidates);
1167
+ if (description !== void 0) out.description = description;
1168
+ if (patent.filingDate !== void 0) out.filingDate = patent.filingDate;
1169
+ if (patent.grantDate !== void 0) out.grantDate = patent.grantDate;
1170
+ if (patent.url !== void 0) out.url = patent.url;
1171
+ if (patent.coInventors !== void 0) out.coInventors = patent.coInventors;
1172
+ if (patent.order !== void 0) out.order = patent.order;
1173
+ return out;
1174
+ }
660
1175
 
661
1176
  // src/jsonld.ts
662
1177
  var SAMEAS_IDENTITY_TYPES = /* @__PURE__ */ new Set([
@@ -699,7 +1214,23 @@ function deriveCanonicalUrl(data) {
699
1214
  return featured?.url;
700
1215
  }
701
1216
  function buildPerson(data, canonicalUrl) {
702
- const { profile, careers, projects, links, skills, contact } = data;
1217
+ const {
1218
+ profile,
1219
+ careers,
1220
+ projects,
1221
+ links,
1222
+ skills,
1223
+ contact,
1224
+ certifications,
1225
+ memberships,
1226
+ volunteering,
1227
+ honors,
1228
+ education,
1229
+ publications,
1230
+ languages,
1231
+ courses,
1232
+ patents
1233
+ } = data;
703
1234
  const out = {};
704
1235
  out["@type"] = "Person";
705
1236
  if (canonicalUrl !== void 0) out["@id"] = `${canonicalUrl}#person`;
@@ -718,9 +1249,26 @@ function buildPerson(data, canonicalUrl) {
718
1249
  if (email !== void 0) out.email = email;
719
1250
  const knowsAbout = buildKnowsAbout(skills);
720
1251
  if (knowsAbout !== void 0) out.knowsAbout = knowsAbout;
1252
+ const knowsLanguage = buildKnowsLanguage(languages);
1253
+ if (knowsLanguage !== void 0) out.knowsLanguage = knowsLanguage;
1254
+ const hasCredential = buildCredentials(certifications);
1255
+ if (hasCredential !== void 0) out.hasCredential = hasCredential;
1256
+ const memberOf = buildMemberOf(memberships);
1257
+ if (memberOf !== void 0) out.memberOf = memberOf;
1258
+ const alumniOf = buildAlumniOf(education);
1259
+ if (alumniOf !== void 0) out.alumniOf = alumniOf;
1260
+ const award = buildAwards(honors);
1261
+ if (award !== void 0) out.award = award;
721
1262
  const sameAs = buildSameAs(links);
722
1263
  if (sameAs !== void 0) out.sameAs = sameAs;
723
- const subjectOf = [...buildPastRoles(past), ...buildProjects(projects)];
1264
+ const subjectOf = [
1265
+ ...buildPastRoles(past),
1266
+ ...buildProjects(projects),
1267
+ ...buildVolunteeringRoles(volunteering),
1268
+ ...buildPublications(publications),
1269
+ ...buildCourses(courses),
1270
+ ...buildPatentWorks(patents)
1271
+ ];
724
1272
  if (subjectOf.length > 0) out.subjectOf = subjectOf;
725
1273
  return out;
726
1274
  }
@@ -802,6 +1350,162 @@ function buildEmail(contact) {
802
1350
  if (typeof contact.email !== "string" || contact.email.length === 0) return void 0;
803
1351
  return contact.email;
804
1352
  }
1353
+ function buildKnowsLanguage(languages) {
1354
+ if (languages.length === 0) return void 0;
1355
+ return languages.map((l) => l.language);
1356
+ }
1357
+ function buildCredentials(certifications) {
1358
+ if (certifications.length === 0) return void 0;
1359
+ return certifications.map((c) => {
1360
+ const out = {};
1361
+ out["@type"] = "EducationalOccupationalCredential";
1362
+ if (c.title !== "") out.name = c.title;
1363
+ out.credentialCategory = "certification";
1364
+ if (c.issuingOrganization !== "") {
1365
+ out.recognizedBy = {
1366
+ "@type": "Organization",
1367
+ name: c.issuingOrganization
1368
+ };
1369
+ }
1370
+ out.dateCreated = c.issueDate;
1371
+ if (c.expirationDate !== void 0 && c.expirationDate !== null) {
1372
+ out.expires = c.expirationDate;
1373
+ }
1374
+ if (c.credentialId !== void 0) out.identifier = c.credentialId;
1375
+ if (c.url !== void 0) out.url = c.url;
1376
+ return out;
1377
+ });
1378
+ }
1379
+ function buildMemberOf(memberships) {
1380
+ if (memberships.length === 0) return void 0;
1381
+ return memberships.map((m) => {
1382
+ const out = {};
1383
+ out["@type"] = "OrganizationRole";
1384
+ if (m.role !== void 0) out.roleName = m.role;
1385
+ out.startDate = m.startDate;
1386
+ if (m.endDate !== void 0 && m.endDate !== null) out.endDate = m.endDate;
1387
+ if (m.description !== void 0) out.description = m.description;
1388
+ const org = { "@type": "Organization", name: m.organization };
1389
+ if (m.url !== void 0) org.url = m.url;
1390
+ out.memberOf = org;
1391
+ return out;
1392
+ });
1393
+ }
1394
+ function buildAlumniOf(education) {
1395
+ if (education.length === 0) return void 0;
1396
+ return education.map((e) => {
1397
+ const out = {};
1398
+ out["@type"] = "OrganizationRole";
1399
+ const roleName = composeRoleName(e.degree, e.fieldOfStudy);
1400
+ if (roleName !== void 0) out.roleName = roleName;
1401
+ out.startDate = e.startDate;
1402
+ if (e.endDate !== void 0 && e.endDate !== null) out.endDate = e.endDate;
1403
+ const description = composeEducationDescription(e.description, e.grade);
1404
+ if (description !== void 0) out.description = description;
1405
+ const org = {
1406
+ "@type": "EducationalOrganization",
1407
+ name: e.institution
1408
+ };
1409
+ if (e.url !== void 0) org.url = e.url;
1410
+ out.alumniOf = org;
1411
+ return out;
1412
+ });
1413
+ }
1414
+ function composeRoleName(degree, fieldOfStudy) {
1415
+ if (degree === void 0 && fieldOfStudy === void 0) return void 0;
1416
+ if (degree !== void 0 && fieldOfStudy !== void 0) return `${degree} (${fieldOfStudy})`;
1417
+ return degree ?? fieldOfStudy;
1418
+ }
1419
+ function composeEducationDescription(description, grade) {
1420
+ if (grade !== void 0 && description !== void 0) return `Grade: ${grade}. ${description}`;
1421
+ if (grade !== void 0) return `Grade: ${grade}`;
1422
+ return description;
1423
+ }
1424
+ function buildAwards(honors) {
1425
+ if (honors.length === 0) return void 0;
1426
+ return honors.map((h) => `${h.title} (${h.issuer}, ${h.date})`);
1427
+ }
1428
+ function buildVolunteeringRoles(volunteering) {
1429
+ return volunteering.map((v) => {
1430
+ const out = {};
1431
+ out["@type"] = "Role";
1432
+ out.roleName = v.role;
1433
+ out.startDate = v.startDate;
1434
+ if (v.endDate !== void 0 && v.endDate !== null) out.endDate = v.endDate;
1435
+ const description = composeVolunteeringDescription(v.cause, v.description);
1436
+ if (description !== void 0) out.description = description;
1437
+ const org = { "@type": "Organization", name: v.organization };
1438
+ if (v.url !== void 0) org.url = v.url;
1439
+ out.memberOf = org;
1440
+ return out;
1441
+ });
1442
+ }
1443
+ function composeVolunteeringDescription(cause, description) {
1444
+ if (cause !== void 0 && description !== void 0) return `Cause: ${cause}. ${description}`;
1445
+ if (cause !== void 0) return `Cause: ${cause}`;
1446
+ return description;
1447
+ }
1448
+ function buildPublications(publications) {
1449
+ return publications.map((p) => {
1450
+ const out = {};
1451
+ out["@type"] = "ScholarlyArticle";
1452
+ out.name = p.title;
1453
+ if (p.publisher !== void 0) {
1454
+ out.publisher = { "@type": "Organization", name: p.publisher };
1455
+ }
1456
+ out.datePublished = p.date;
1457
+ if (p.url !== void 0) out.url = p.url;
1458
+ if (p.doi !== void 0) {
1459
+ out.identifier = { "@type": "PropertyValue", propertyID: "DOI", value: p.doi };
1460
+ }
1461
+ if (p.coAuthors !== void 0 && p.coAuthors.length > 0) {
1462
+ out.author = p.coAuthors.map((name) => ({ "@type": "Person", name }));
1463
+ }
1464
+ return out;
1465
+ });
1466
+ }
1467
+ function buildCourses(courses) {
1468
+ return courses.map((c) => {
1469
+ const out = {};
1470
+ out["@type"] = "Course";
1471
+ out.name = c.title;
1472
+ if (c.provider !== void 0) {
1473
+ out.provider = { "@type": "Organization", name: c.provider };
1474
+ }
1475
+ if (c.courseNumber !== void 0) out.courseCode = c.courseNumber;
1476
+ if (c.certificateUrl !== void 0) out.url = c.certificateUrl;
1477
+ if (c.completionDate !== void 0) {
1478
+ out.hasCourseInstance = {
1479
+ "@type": "CourseInstance",
1480
+ endDate: c.completionDate
1481
+ };
1482
+ }
1483
+ return out;
1484
+ });
1485
+ }
1486
+ function buildPatentWorks(patents) {
1487
+ return patents.map((p) => {
1488
+ const out = {};
1489
+ out["@type"] = "CreativeWork";
1490
+ out.additionalType = "https://schema.org/Patent";
1491
+ out.name = p.title;
1492
+ out.identifier = p.patentNumber;
1493
+ if (p.office !== void 0) {
1494
+ out.publisher = { "@type": "Organization", name: p.office };
1495
+ }
1496
+ out.creativeWorkStatus = p.status;
1497
+ if (p.grantDate !== void 0) {
1498
+ out.datePublished = p.grantDate;
1499
+ } else if (p.filingDate !== void 0) {
1500
+ out.dateCreated = p.filingDate;
1501
+ }
1502
+ if (p.url !== void 0) out.url = p.url;
1503
+ if (p.coInventors !== void 0 && p.coInventors.length > 0) {
1504
+ out.author = p.coInventors.map((name) => ({ "@type": "Person", name }));
1505
+ }
1506
+ return out;
1507
+ });
1508
+ }
805
1509
 
806
1510
  // src/export.ts
807
1511
  var ImportError = class extends Error {
@@ -846,8 +1550,30 @@ function findMigrationChain(from, to, registry) {
846
1550
  return chain;
847
1551
  }
848
1552
 
1553
+ // src/migrations/v0.1.0-to-v0.2.0.ts
1554
+ var v0_1_0_to_v0_2_0 = {
1555
+ from: "0.1.0",
1556
+ to: "0.2.0",
1557
+ migrate(data) {
1558
+ const partial = data;
1559
+ return {
1560
+ ...data,
1561
+ schemaVersion: "0.2.0",
1562
+ certifications: partial.certifications ?? [],
1563
+ memberships: partial.memberships ?? [],
1564
+ volunteering: partial.volunteering ?? [],
1565
+ honors: partial.honors ?? [],
1566
+ education: partial.education ?? [],
1567
+ publications: partial.publications ?? [],
1568
+ languages: partial.languages ?? [],
1569
+ courses: partial.courses ?? [],
1570
+ patents: partial.patents ?? []
1571
+ };
1572
+ }
1573
+ };
1574
+
849
1575
  // src/migrations/index.ts
850
- var migrations = [];
1576
+ var migrations = [v0_1_0_to_v0_2_0];
851
1577
 
852
1578
  // src/migrate.ts
853
1579
  var MigrationError = class extends Error {
@@ -895,7 +1621,7 @@ var ConflictError = class extends StorageError {
895
1621
  };
896
1622
 
897
1623
  // src/index.ts
898
- var SCHEMA_VERSION = "0.1.0";
1624
+ var SCHEMA_VERSION = "0.2.0";
899
1625
  export {
900
1626
  ConflictError,
901
1627
  ImportError,