@takuhon/core 0.1.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.
@@ -0,0 +1,1843 @@
1
+ var $schema = "https://json-schema.org/draft/2020-12/schema";
2
+ var $id = "https://takuhon.example/schemas/0.1.0/takuhon.schema.json";
3
+ var title = "Takuhon Profile";
4
+ var description = "Portable profile data format consumed by @takuhon/core. The canonical contract for profile content authored as takuhon.json.";
5
+ var type = "object";
6
+ var additionalProperties = false;
7
+ var required = [
8
+ "schemaVersion",
9
+ "profile",
10
+ "links",
11
+ "careers",
12
+ "projects",
13
+ "skills",
14
+ "contact",
15
+ "settings",
16
+ "meta"
17
+ ];
18
+ var properties = {
19
+ schemaVersion: {
20
+ type: "string",
21
+ pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.-]+)?$",
22
+ description: "Semantic version of the takuhon schema this document conforms to."
23
+ },
24
+ profile: {
25
+ $ref: "#/$defs/Profile"
26
+ },
27
+ links: {
28
+ type: "array",
29
+ maxItems: 100,
30
+ items: {
31
+ $ref: "#/$defs/Link"
32
+ }
33
+ },
34
+ careers: {
35
+ type: "array",
36
+ maxItems: 50,
37
+ items: {
38
+ $ref: "#/$defs/Career"
39
+ }
40
+ },
41
+ projects: {
42
+ type: "array",
43
+ maxItems: 100,
44
+ items: {
45
+ $ref: "#/$defs/Project"
46
+ }
47
+ },
48
+ skills: {
49
+ type: "array",
50
+ maxItems: 200,
51
+ items: {
52
+ $ref: "#/$defs/Skill"
53
+ }
54
+ },
55
+ contact: {
56
+ $ref: "#/$defs/Contact"
57
+ },
58
+ settings: {
59
+ $ref: "#/$defs/Settings"
60
+ },
61
+ meta: {
62
+ $ref: "#/$defs/Meta"
63
+ }
64
+ };
65
+ var $defs = {
66
+ LocaleTag: {
67
+ type: "string",
68
+ minLength: 2,
69
+ maxLength: 35,
70
+ pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$",
71
+ description: "BCP-47 language tag (e.g., 'en', 'ja', 'zh-Hant', 'pt-BR')."
72
+ },
73
+ Iso3166Alpha2: {
74
+ type: "string",
75
+ pattern: "^[A-Z]{2}$",
76
+ description: "ISO 3166-1 alpha-2 country code (uppercase, two letters)."
77
+ },
78
+ YearMonth: {
79
+ type: "string",
80
+ pattern: "^[0-9]{4}-(0[1-9]|1[0-2])$",
81
+ description: "Year-month in 'YYYY-MM' format (Gregorian calendar)."
82
+ },
83
+ IsoDateTime: {
84
+ type: "string",
85
+ format: "date-time",
86
+ description: "ISO 8601 date-time (e.g., 2026-05-11T12:34:56Z)."
87
+ },
88
+ Url: {
89
+ type: "string",
90
+ format: "uri",
91
+ maxLength: 2048
92
+ },
93
+ Email: {
94
+ type: "string",
95
+ format: "email",
96
+ maxLength: 254
97
+ },
98
+ Slug: {
99
+ type: "string",
100
+ minLength: 1,
101
+ maxLength: 64,
102
+ pattern: "^[a-z0-9][a-z0-9-]*$",
103
+ description: "URL-safe identifier (lowercase alphanumerics and hyphens, must start with alphanumeric)."
104
+ },
105
+ LocalizedTitle: {
106
+ type: "object",
107
+ minProperties: 1,
108
+ propertyNames: {
109
+ type: "string",
110
+ pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"
111
+ },
112
+ additionalProperties: {
113
+ type: "string",
114
+ minLength: 1,
115
+ maxLength: 200
116
+ },
117
+ description: "Map of BCP-47 locale tag to short title-like string (max 200 chars per value)."
118
+ },
119
+ LocalizedBody: {
120
+ type: "object",
121
+ minProperties: 1,
122
+ propertyNames: {
123
+ type: "string",
124
+ pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"
125
+ },
126
+ additionalProperties: {
127
+ type: "string",
128
+ minLength: 1,
129
+ maxLength: 5000
130
+ },
131
+ description: "Map of BCP-47 locale tag to body-length string (max 5000 chars per value)."
132
+ },
133
+ LinkType: {
134
+ type: "string",
135
+ "enum": [
136
+ "website",
137
+ "blog",
138
+ "github",
139
+ "gitlab",
140
+ "linkedin",
141
+ "x",
142
+ "mastodon",
143
+ "bluesky",
144
+ "instagram",
145
+ "youtube",
146
+ "threads",
147
+ "facebook",
148
+ "email",
149
+ "rss",
150
+ "custom"
151
+ ],
152
+ description: "Identifies the kind of link. 'custom' requires iconUrl."
153
+ },
154
+ Profile: {
155
+ type: "object",
156
+ additionalProperties: true,
157
+ required: [
158
+ "displayName"
159
+ ],
160
+ properties: {
161
+ displayName: {
162
+ $ref: "#/$defs/LocalizedTitle"
163
+ },
164
+ tagline: {
165
+ $ref: "#/$defs/LocalizedTitle"
166
+ },
167
+ bio: {
168
+ $ref: "#/$defs/LocalizedBody"
169
+ },
170
+ avatar: {
171
+ $ref: "#/$defs/Avatar"
172
+ },
173
+ location: {
174
+ $ref: "#/$defs/Address"
175
+ }
176
+ }
177
+ },
178
+ Avatar: {
179
+ type: "object",
180
+ additionalProperties: true,
181
+ required: [
182
+ "url"
183
+ ],
184
+ properties: {
185
+ url: {
186
+ type: "string",
187
+ format: "uri-reference",
188
+ maxLength: 2048,
189
+ description: "URL or path to the avatar image."
190
+ },
191
+ alt: {
192
+ $ref: "#/$defs/LocalizedTitle"
193
+ }
194
+ }
195
+ },
196
+ Address: {
197
+ type: "object",
198
+ additionalProperties: true,
199
+ properties: {
200
+ country: {
201
+ $ref: "#/$defs/Iso3166Alpha2"
202
+ },
203
+ region: {
204
+ type: "string",
205
+ maxLength: 100
206
+ },
207
+ locality: {
208
+ $ref: "#/$defs/LocalizedTitle"
209
+ },
210
+ display: {
211
+ $ref: "#/$defs/LocalizedTitle"
212
+ }
213
+ }
214
+ },
215
+ Link: {
216
+ type: "object",
217
+ additionalProperties: false,
218
+ required: [
219
+ "id",
220
+ "type",
221
+ "url"
222
+ ],
223
+ properties: {
224
+ id: {
225
+ $ref: "#/$defs/Slug"
226
+ },
227
+ type: {
228
+ $ref: "#/$defs/LinkType"
229
+ },
230
+ label: {
231
+ $ref: "#/$defs/LocalizedTitle"
232
+ },
233
+ url: {
234
+ $ref: "#/$defs/Url"
235
+ },
236
+ featured: {
237
+ type: "boolean"
238
+ },
239
+ order: {
240
+ type: "integer",
241
+ minimum: 0
242
+ },
243
+ iconUrl: {
244
+ $ref: "#/$defs/Url"
245
+ }
246
+ },
247
+ allOf: [
248
+ {
249
+ "if": {
250
+ properties: {
251
+ type: {
252
+ "const": "custom"
253
+ }
254
+ },
255
+ required: [
256
+ "type"
257
+ ]
258
+ },
259
+ then: {
260
+ properties: {
261
+ iconUrl: {
262
+ $ref: "#/$defs/Url"
263
+ }
264
+ },
265
+ required: [
266
+ "iconUrl"
267
+ ]
268
+ }
269
+ }
270
+ ]
271
+ },
272
+ Career: {
273
+ type: "object",
274
+ additionalProperties: true,
275
+ required: [
276
+ "id",
277
+ "organization",
278
+ "role",
279
+ "startDate"
280
+ ],
281
+ properties: {
282
+ id: {
283
+ $ref: "#/$defs/Slug"
284
+ },
285
+ organization: {
286
+ $ref: "#/$defs/LocalizedTitle"
287
+ },
288
+ role: {
289
+ $ref: "#/$defs/LocalizedTitle"
290
+ },
291
+ description: {
292
+ $ref: "#/$defs/LocalizedBody"
293
+ },
294
+ startDate: {
295
+ $ref: "#/$defs/YearMonth"
296
+ },
297
+ endDate: {
298
+ anyOf: [
299
+ {
300
+ $ref: "#/$defs/YearMonth"
301
+ },
302
+ {
303
+ type: "null"
304
+ }
305
+ ]
306
+ },
307
+ isCurrent: {
308
+ type: "boolean"
309
+ },
310
+ url: {
311
+ $ref: "#/$defs/Url"
312
+ },
313
+ location: {
314
+ $ref: "#/$defs/Address"
315
+ },
316
+ order: {
317
+ type: "integer",
318
+ minimum: 0
319
+ }
320
+ }
321
+ },
322
+ Project: {
323
+ type: "object",
324
+ additionalProperties: true,
325
+ required: [
326
+ "id",
327
+ "title"
328
+ ],
329
+ properties: {
330
+ id: {
331
+ $ref: "#/$defs/Slug"
332
+ },
333
+ title: {
334
+ $ref: "#/$defs/LocalizedTitle"
335
+ },
336
+ description: {
337
+ $ref: "#/$defs/LocalizedBody"
338
+ },
339
+ url: {
340
+ $ref: "#/$defs/Url"
341
+ },
342
+ tags: {
343
+ type: "array",
344
+ maxItems: 30,
345
+ items: {
346
+ type: "string",
347
+ minLength: 1,
348
+ maxLength: 50
349
+ }
350
+ },
351
+ relatedCareerId: {
352
+ $ref: "#/$defs/Slug"
353
+ },
354
+ startDate: {
355
+ $ref: "#/$defs/YearMonth"
356
+ },
357
+ endDate: {
358
+ anyOf: [
359
+ {
360
+ $ref: "#/$defs/YearMonth"
361
+ },
362
+ {
363
+ type: "null"
364
+ }
365
+ ]
366
+ },
367
+ highlighted: {
368
+ type: "boolean"
369
+ },
370
+ order: {
371
+ type: "integer",
372
+ minimum: 0
373
+ }
374
+ }
375
+ },
376
+ Skill: {
377
+ type: "object",
378
+ additionalProperties: true,
379
+ required: [
380
+ "id",
381
+ "label"
382
+ ],
383
+ properties: {
384
+ id: {
385
+ $ref: "#/$defs/Slug"
386
+ },
387
+ label: {
388
+ type: "string",
389
+ minLength: 1,
390
+ maxLength: 100
391
+ },
392
+ category: {
393
+ type: "string",
394
+ minLength: 1,
395
+ maxLength: 64,
396
+ description: "Recommended values (extensible): programming, design, business, communication, language, music, art, sports, other."
397
+ },
398
+ order: {
399
+ type: "integer",
400
+ minimum: 0
401
+ }
402
+ }
403
+ },
404
+ Contact: {
405
+ type: "object",
406
+ additionalProperties: true,
407
+ properties: {
408
+ email: {
409
+ $ref: "#/$defs/Email"
410
+ },
411
+ showEmail: {
412
+ type: "boolean",
413
+ "default": false
414
+ },
415
+ formUrl: {
416
+ $ref: "#/$defs/Url"
417
+ }
418
+ }
419
+ },
420
+ Settings: {
421
+ type: "object",
422
+ additionalProperties: true,
423
+ required: [
424
+ "defaultLocale",
425
+ "availableLocales"
426
+ ],
427
+ properties: {
428
+ defaultLocale: {
429
+ $ref: "#/$defs/LocaleTag"
430
+ },
431
+ fallbackLocale: {
432
+ $ref: "#/$defs/LocaleTag"
433
+ },
434
+ availableLocales: {
435
+ type: "array",
436
+ minItems: 1,
437
+ maxItems: 50,
438
+ uniqueItems: true,
439
+ items: {
440
+ $ref: "#/$defs/LocaleTag"
441
+ }
442
+ },
443
+ theme: {
444
+ type: "string",
445
+ minLength: 1,
446
+ maxLength: 64,
447
+ description: "UI theme identifier. 'default' is the built-in theme; adapters may add more."
448
+ },
449
+ showPoweredBy: {
450
+ type: "boolean",
451
+ "default": true,
452
+ description: "Display the 'Powered by takuhon' attribution in the rendered profile."
453
+ },
454
+ enableJsonLd: {
455
+ type: "boolean",
456
+ "default": true,
457
+ description: "Emit Schema.org JSON-LD on the rendered profile page."
458
+ },
459
+ enableApi: {
460
+ type: "boolean",
461
+ "default": true,
462
+ description: "Expose the public read API endpoints (GET /api/profile, /api/jsonld, /api/schema, /takuhon.json)."
463
+ },
464
+ enableAnalytics: {
465
+ type: "boolean",
466
+ "default": false,
467
+ description: "Opt-in flag for first-party analytics. Default is false to keep takuhon privacy-respecting by default."
468
+ }
469
+ }
470
+ },
471
+ Meta: {
472
+ type: "object",
473
+ additionalProperties: true,
474
+ required: [
475
+ "contentLicense"
476
+ ],
477
+ properties: {
478
+ createdAt: {
479
+ $ref: "#/$defs/IsoDateTime"
480
+ },
481
+ updatedAt: {
482
+ $ref: "#/$defs/IsoDateTime"
483
+ },
484
+ generator: {
485
+ type: "string",
486
+ minLength: 1,
487
+ maxLength: 100,
488
+ description: "Tool that produced this document (e.g. 'Takuhon', 'create-takuhon@0.1.0')."
489
+ },
490
+ contentLicense: {
491
+ $ref: "#/$defs/ContentLicense"
492
+ }
493
+ }
494
+ },
495
+ ContentLicense: {
496
+ type: "object",
497
+ additionalProperties: false,
498
+ required: [
499
+ "spdxId"
500
+ ],
501
+ properties: {
502
+ spdxId: {
503
+ type: "string",
504
+ minLength: 1,
505
+ maxLength: 64,
506
+ description: "SPDX identifier (e.g., 'CC-BY-4.0', 'CC0-1.0') or 'Proprietary'. No default; the profile owner must choose explicitly."
507
+ },
508
+ url: {
509
+ $ref: "#/$defs/Url"
510
+ },
511
+ attribution: {
512
+ type: "object",
513
+ additionalProperties: false,
514
+ properties: {
515
+ name: {
516
+ type: "string",
517
+ minLength: 1,
518
+ maxLength: 200
519
+ },
520
+ url: {
521
+ $ref: "#/$defs/Url"
522
+ }
523
+ }
524
+ },
525
+ rights: {
526
+ type: "string",
527
+ minLength: 1,
528
+ maxLength: 1000,
529
+ description: "Free-form rights statement (used when spdxId='Proprietary' or for additional notices)."
530
+ }
531
+ }
532
+ }
533
+ };
534
+ var schemaJson = {
535
+ $schema: $schema,
536
+ $id: $id,
537
+ title: title,
538
+ description: description,
539
+ type: type,
540
+ additionalProperties: additionalProperties,
541
+ required: required,
542
+ properties: properties,
543
+ $defs: $defs
544
+ };
545
+
546
+ /**
547
+ * Re-exports the canonical JSON Schema for takuhon profiles.
548
+ *
549
+ * The schema source of truth lives at `packages/core/takuhon.schema.json`
550
+ * (also distributed via the `@takuhon/core/schema.json` sub-path). This module
551
+ * is the convenient ESM-side entry point for consumers that want to feed it
552
+ * directly to a JSON Schema validator (e.g. Ajv) without a filesystem read.
553
+ */
554
+
555
+ declare const schema: {
556
+ $schema: string;
557
+ $id: string;
558
+ title: string;
559
+ description: string;
560
+ type: string;
561
+ additionalProperties: boolean;
562
+ required: string[];
563
+ properties: {
564
+ schemaVersion: {
565
+ type: string;
566
+ pattern: string;
567
+ description: string;
568
+ };
569
+ profile: {
570
+ $ref: string;
571
+ };
572
+ links: {
573
+ type: string;
574
+ maxItems: number;
575
+ items: {
576
+ $ref: string;
577
+ };
578
+ };
579
+ careers: {
580
+ type: string;
581
+ maxItems: number;
582
+ items: {
583
+ $ref: string;
584
+ };
585
+ };
586
+ projects: {
587
+ type: string;
588
+ maxItems: number;
589
+ items: {
590
+ $ref: string;
591
+ };
592
+ };
593
+ skills: {
594
+ type: string;
595
+ maxItems: number;
596
+ items: {
597
+ $ref: string;
598
+ };
599
+ };
600
+ contact: {
601
+ $ref: string;
602
+ };
603
+ settings: {
604
+ $ref: string;
605
+ };
606
+ meta: {
607
+ $ref: string;
608
+ };
609
+ };
610
+ $defs: {
611
+ LocaleTag: {
612
+ type: string;
613
+ minLength: number;
614
+ maxLength: number;
615
+ pattern: string;
616
+ description: string;
617
+ };
618
+ Iso3166Alpha2: {
619
+ type: string;
620
+ pattern: string;
621
+ description: string;
622
+ };
623
+ YearMonth: {
624
+ type: string;
625
+ pattern: string;
626
+ description: string;
627
+ };
628
+ IsoDateTime: {
629
+ type: string;
630
+ format: string;
631
+ description: string;
632
+ };
633
+ Url: {
634
+ type: string;
635
+ format: string;
636
+ maxLength: number;
637
+ };
638
+ Email: {
639
+ type: string;
640
+ format: string;
641
+ maxLength: number;
642
+ };
643
+ Slug: {
644
+ type: string;
645
+ minLength: number;
646
+ maxLength: number;
647
+ pattern: string;
648
+ description: string;
649
+ };
650
+ LocalizedTitle: {
651
+ type: string;
652
+ minProperties: number;
653
+ propertyNames: {
654
+ type: string;
655
+ pattern: string;
656
+ };
657
+ additionalProperties: {
658
+ type: string;
659
+ minLength: number;
660
+ maxLength: number;
661
+ };
662
+ description: string;
663
+ };
664
+ LocalizedBody: {
665
+ type: string;
666
+ minProperties: number;
667
+ propertyNames: {
668
+ type: string;
669
+ pattern: string;
670
+ };
671
+ additionalProperties: {
672
+ type: string;
673
+ minLength: number;
674
+ maxLength: number;
675
+ };
676
+ description: string;
677
+ };
678
+ LinkType: {
679
+ type: string;
680
+ enum: string[];
681
+ description: string;
682
+ };
683
+ Profile: {
684
+ type: string;
685
+ additionalProperties: boolean;
686
+ required: string[];
687
+ properties: {
688
+ displayName: {
689
+ $ref: string;
690
+ };
691
+ tagline: {
692
+ $ref: string;
693
+ };
694
+ bio: {
695
+ $ref: string;
696
+ };
697
+ avatar: {
698
+ $ref: string;
699
+ };
700
+ location: {
701
+ $ref: string;
702
+ };
703
+ };
704
+ };
705
+ Avatar: {
706
+ type: string;
707
+ additionalProperties: boolean;
708
+ required: string[];
709
+ properties: {
710
+ url: {
711
+ type: string;
712
+ format: string;
713
+ maxLength: number;
714
+ description: string;
715
+ };
716
+ alt: {
717
+ $ref: string;
718
+ };
719
+ };
720
+ };
721
+ Address: {
722
+ type: string;
723
+ additionalProperties: boolean;
724
+ properties: {
725
+ country: {
726
+ $ref: string;
727
+ };
728
+ region: {
729
+ type: string;
730
+ maxLength: number;
731
+ };
732
+ locality: {
733
+ $ref: string;
734
+ };
735
+ display: {
736
+ $ref: string;
737
+ };
738
+ };
739
+ };
740
+ Link: {
741
+ type: string;
742
+ additionalProperties: boolean;
743
+ required: string[];
744
+ properties: {
745
+ id: {
746
+ $ref: string;
747
+ };
748
+ type: {
749
+ $ref: string;
750
+ };
751
+ label: {
752
+ $ref: string;
753
+ };
754
+ url: {
755
+ $ref: string;
756
+ };
757
+ featured: {
758
+ type: string;
759
+ };
760
+ order: {
761
+ type: string;
762
+ minimum: number;
763
+ };
764
+ iconUrl: {
765
+ $ref: string;
766
+ };
767
+ };
768
+ allOf: {
769
+ if: {
770
+ properties: {
771
+ type: {
772
+ const: string;
773
+ };
774
+ };
775
+ required: string[];
776
+ };
777
+ then: {
778
+ properties: {
779
+ iconUrl: {
780
+ $ref: string;
781
+ };
782
+ };
783
+ required: string[];
784
+ };
785
+ }[];
786
+ };
787
+ Career: {
788
+ type: string;
789
+ additionalProperties: boolean;
790
+ required: string[];
791
+ properties: {
792
+ id: {
793
+ $ref: string;
794
+ };
795
+ organization: {
796
+ $ref: string;
797
+ };
798
+ role: {
799
+ $ref: string;
800
+ };
801
+ description: {
802
+ $ref: string;
803
+ };
804
+ startDate: {
805
+ $ref: string;
806
+ };
807
+ endDate: {
808
+ anyOf: ({
809
+ $ref: string;
810
+ type?: undefined;
811
+ } | {
812
+ type: string;
813
+ $ref?: undefined;
814
+ })[];
815
+ };
816
+ isCurrent: {
817
+ type: string;
818
+ };
819
+ url: {
820
+ $ref: string;
821
+ };
822
+ location: {
823
+ $ref: string;
824
+ };
825
+ order: {
826
+ type: string;
827
+ minimum: number;
828
+ };
829
+ };
830
+ };
831
+ Project: {
832
+ type: string;
833
+ additionalProperties: boolean;
834
+ required: string[];
835
+ properties: {
836
+ id: {
837
+ $ref: string;
838
+ };
839
+ title: {
840
+ $ref: string;
841
+ };
842
+ description: {
843
+ $ref: string;
844
+ };
845
+ url: {
846
+ $ref: string;
847
+ };
848
+ tags: {
849
+ type: string;
850
+ maxItems: number;
851
+ items: {
852
+ type: string;
853
+ minLength: number;
854
+ maxLength: number;
855
+ };
856
+ };
857
+ relatedCareerId: {
858
+ $ref: string;
859
+ };
860
+ startDate: {
861
+ $ref: string;
862
+ };
863
+ endDate: {
864
+ anyOf: ({
865
+ $ref: string;
866
+ type?: undefined;
867
+ } | {
868
+ type: string;
869
+ $ref?: undefined;
870
+ })[];
871
+ };
872
+ highlighted: {
873
+ type: string;
874
+ };
875
+ order: {
876
+ type: string;
877
+ minimum: number;
878
+ };
879
+ };
880
+ };
881
+ Skill: {
882
+ type: string;
883
+ additionalProperties: boolean;
884
+ required: string[];
885
+ properties: {
886
+ id: {
887
+ $ref: string;
888
+ };
889
+ label: {
890
+ type: string;
891
+ minLength: number;
892
+ maxLength: number;
893
+ };
894
+ category: {
895
+ type: string;
896
+ minLength: number;
897
+ maxLength: number;
898
+ description: string;
899
+ };
900
+ order: {
901
+ type: string;
902
+ minimum: number;
903
+ };
904
+ };
905
+ };
906
+ Contact: {
907
+ type: string;
908
+ additionalProperties: boolean;
909
+ properties: {
910
+ email: {
911
+ $ref: string;
912
+ };
913
+ showEmail: {
914
+ type: string;
915
+ default: boolean;
916
+ };
917
+ formUrl: {
918
+ $ref: string;
919
+ };
920
+ };
921
+ };
922
+ Settings: {
923
+ type: string;
924
+ additionalProperties: boolean;
925
+ required: string[];
926
+ properties: {
927
+ defaultLocale: {
928
+ $ref: string;
929
+ };
930
+ fallbackLocale: {
931
+ $ref: string;
932
+ };
933
+ availableLocales: {
934
+ type: string;
935
+ minItems: number;
936
+ maxItems: number;
937
+ uniqueItems: boolean;
938
+ items: {
939
+ $ref: string;
940
+ };
941
+ };
942
+ theme: {
943
+ type: string;
944
+ minLength: number;
945
+ maxLength: number;
946
+ description: string;
947
+ };
948
+ showPoweredBy: {
949
+ type: string;
950
+ default: boolean;
951
+ description: string;
952
+ };
953
+ enableJsonLd: {
954
+ type: string;
955
+ default: boolean;
956
+ description: string;
957
+ };
958
+ enableApi: {
959
+ type: string;
960
+ default: boolean;
961
+ description: string;
962
+ };
963
+ enableAnalytics: {
964
+ type: string;
965
+ default: boolean;
966
+ description: string;
967
+ };
968
+ };
969
+ };
970
+ Meta: {
971
+ type: string;
972
+ additionalProperties: boolean;
973
+ required: string[];
974
+ properties: {
975
+ createdAt: {
976
+ $ref: string;
977
+ };
978
+ updatedAt: {
979
+ $ref: string;
980
+ };
981
+ generator: {
982
+ type: string;
983
+ minLength: number;
984
+ maxLength: number;
985
+ description: string;
986
+ };
987
+ contentLicense: {
988
+ $ref: string;
989
+ };
990
+ };
991
+ };
992
+ ContentLicense: {
993
+ type: string;
994
+ additionalProperties: boolean;
995
+ required: string[];
996
+ properties: {
997
+ spdxId: {
998
+ type: string;
999
+ minLength: number;
1000
+ maxLength: number;
1001
+ description: string;
1002
+ };
1003
+ url: {
1004
+ $ref: string;
1005
+ };
1006
+ attribution: {
1007
+ type: string;
1008
+ additionalProperties: boolean;
1009
+ properties: {
1010
+ name: {
1011
+ type: string;
1012
+ minLength: number;
1013
+ maxLength: number;
1014
+ };
1015
+ url: {
1016
+ $ref: string;
1017
+ };
1018
+ };
1019
+ };
1020
+ rights: {
1021
+ type: string;
1022
+ minLength: number;
1023
+ maxLength: number;
1024
+ description: string;
1025
+ };
1026
+ };
1027
+ };
1028
+ };
1029
+ };
1030
+ type Schema = typeof schemaJson;
1031
+
1032
+ /**
1033
+ * TypeScript types for takuhon profile data.
1034
+ *
1035
+ * These mirror the canonical contract defined in `takuhon.schema.json`. The
1036
+ * published shape is sanity-checked at commit 1 by `__tests__/schema.test.ts`
1037
+ * (top-level keys, `$defs`, required fields, hybrid `additionalProperties`
1038
+ * splits, Spec §6 invariants) and by `__tests__/example.test.ts` (the bundled
1039
+ * fixture is assigned to `Takuhon` via a boundary cast, and per-Spec invariants
1040
+ * are asserted at runtime). Those tests catch the kind of drift that changes
1041
+ * the published shape, but they do not enforce field-by-field parity between
1042
+ * JSON Schema `properties` and TypeScript members — that stronger guarantee
1043
+ * arrives with the Ajv-backed validator in commit 2.
1044
+ *
1045
+ * When the schema changes, update these types accordingly and add a migration
1046
+ * entry under `src/migrations/` for the next minor version.
1047
+ *
1048
+ * Public surface scope (hybrid `additionalProperties` strategy):
1049
+ *
1050
+ * - **Closed** in the schema (no forward-compatible extras): the document
1051
+ * root, `ContentLicense`, and every `Link` variant.
1052
+ * - **Open** in the schema (`additionalProperties: true`): `Profile`,
1053
+ * `Settings`, `Meta`, `Career`, `Project`, `Skill`, `Contact`, `Avatar`,
1054
+ * `Address`, and the locale-keyed map shapes `LocalizedTitle` /
1055
+ * `LocalizedBody`.
1056
+ *
1057
+ * The public TypeScript surface intentionally omits an `[key: string]: unknown`
1058
+ * index signature on the open containers. Declared properties stay accurately
1059
+ * typed regardless of the choice — what such a signature would change is
1060
+ * access to *undeclared* keys: it would let consumers spell arbitrary property
1061
+ * names without an error and force `unknown` narrowing for those reads, and it
1062
+ * would dilute IDE autocomplete on every dotted access. Keeping the types
1063
+ * focused on the canonical members preserves that ergonomics. Consumers that
1064
+ * need to attach custom fields should extend the relevant interface locally:
1065
+ *
1066
+ * interface MyProfile extends Profile {
1067
+ * customField: string;
1068
+ * }
1069
+ */
1070
+ /** BCP-47 language tag, e.g. 'en', 'ja', 'zh-Hant', 'pt-BR'. */
1071
+ type LocaleTag = string;
1072
+ /** ISO 3166-1 alpha-2 country code, uppercase, two letters (e.g. 'JP', 'PT'). */
1073
+ type Iso3166Alpha2 = string;
1074
+ /** Year-month in `YYYY-MM` format. */
1075
+ type YearMonth = string;
1076
+ /** ISO 8601 date-time string (e.g. `2026-05-12T12:34:56Z`). */
1077
+ type IsoDateTime = string;
1078
+ /** Identifier matching `^[a-z0-9][a-z0-9-]*$`, max 64 chars. */
1079
+ type Slug = string;
1080
+ /** Map from BCP-47 locale tag to a short localized string (≤200 chars). */
1081
+ type LocalizedTitle = Record<LocaleTag, string>;
1082
+ /** Map from BCP-47 locale tag to a body-length localized string (≤5000 chars). */
1083
+ type LocalizedBody = Record<LocaleTag, string>;
1084
+ type LinkType = 'website' | 'blog' | 'github' | 'gitlab' | 'linkedin' | 'x' | 'mastodon' | 'bluesky' | 'instagram' | 'youtube' | 'threads' | 'facebook' | 'email' | 'rss' | 'custom';
1085
+ interface Avatar {
1086
+ url: string;
1087
+ alt?: LocalizedTitle;
1088
+ }
1089
+ interface Address {
1090
+ country?: Iso3166Alpha2;
1091
+ region?: string;
1092
+ locality?: LocalizedTitle;
1093
+ display?: LocalizedTitle;
1094
+ }
1095
+ interface Profile {
1096
+ displayName: LocalizedTitle;
1097
+ tagline?: LocalizedTitle;
1098
+ bio?: LocalizedBody;
1099
+ avatar?: Avatar;
1100
+ location?: Address;
1101
+ }
1102
+ interface LinkCommon {
1103
+ id: Slug;
1104
+ label?: LocalizedTitle;
1105
+ url: string;
1106
+ featured?: boolean;
1107
+ order?: number;
1108
+ }
1109
+ /**
1110
+ * A link of a built-in `type` (anything other than `'custom'`). The schema
1111
+ * permits an optional `iconUrl` on these entries — for example, to override
1112
+ * the default platform icon.
1113
+ */
1114
+ interface LinkBuiltin extends LinkCommon {
1115
+ type: Exclude<LinkType, 'custom'>;
1116
+ iconUrl?: string;
1117
+ }
1118
+ /**
1119
+ * A user-defined link (`type: 'custom'`). The schema requires `iconUrl` for
1120
+ * these entries; modelling that constraint as a discriminated union here lets
1121
+ * TypeScript reject `{ type: 'custom', ... }` literals that forget the icon
1122
+ * before Ajv validation runs in commit 2.
1123
+ */
1124
+ interface LinkCustom extends LinkCommon {
1125
+ type: 'custom';
1126
+ iconUrl: string;
1127
+ }
1128
+ /** A profile link. Discriminated on `type`; see `LinkBuiltin` / `LinkCustom`. */
1129
+ type Link = LinkBuiltin | LinkCustom;
1130
+ interface Career {
1131
+ id: Slug;
1132
+ organization: LocalizedTitle;
1133
+ role: LocalizedTitle;
1134
+ description?: LocalizedBody;
1135
+ startDate: YearMonth;
1136
+ /** `null` denotes an unbounded current position; omit if not applicable. */
1137
+ endDate?: YearMonth | null;
1138
+ isCurrent?: boolean;
1139
+ url?: string;
1140
+ location?: Address;
1141
+ order?: number;
1142
+ }
1143
+ interface Project {
1144
+ id: Slug;
1145
+ title: LocalizedTitle;
1146
+ description?: LocalizedBody;
1147
+ url?: string;
1148
+ tags?: string[];
1149
+ relatedCareerId?: Slug;
1150
+ startDate?: YearMonth;
1151
+ endDate?: YearMonth | null;
1152
+ highlighted?: boolean;
1153
+ order?: number;
1154
+ }
1155
+ interface Skill {
1156
+ id: Slug;
1157
+ label: string;
1158
+ /**
1159
+ * Recommended values (extensible): programming, design, business, communication,
1160
+ * language, music, art, sports, other.
1161
+ */
1162
+ category?: string;
1163
+ order?: number;
1164
+ }
1165
+ interface Contact {
1166
+ email?: string;
1167
+ showEmail?: boolean;
1168
+ formUrl?: string;
1169
+ }
1170
+ interface Settings {
1171
+ defaultLocale: LocaleTag;
1172
+ fallbackLocale?: LocaleTag;
1173
+ availableLocales: LocaleTag[];
1174
+ /** UI theme identifier. `'default'` is the built-in theme. */
1175
+ theme?: string;
1176
+ /** Display the 'Powered by takuhon' attribution on the rendered profile. */
1177
+ showPoweredBy?: boolean;
1178
+ /** Emit Schema.org JSON-LD on the rendered profile page. */
1179
+ enableJsonLd?: boolean;
1180
+ /** Expose the public read API endpoints. */
1181
+ enableApi?: boolean;
1182
+ /** Opt-in flag for first-party analytics. Default is false. */
1183
+ enableAnalytics?: boolean;
1184
+ }
1185
+ interface ContentLicenseAttribution {
1186
+ name?: string;
1187
+ url?: string;
1188
+ }
1189
+ interface ContentLicense {
1190
+ /** SPDX identifier (e.g. 'CC-BY-4.0', 'CC0-1.0') or 'Proprietary'. No default. */
1191
+ spdxId: string;
1192
+ url?: string;
1193
+ attribution?: ContentLicenseAttribution;
1194
+ rights?: string;
1195
+ }
1196
+ interface Meta {
1197
+ createdAt?: IsoDateTime;
1198
+ updatedAt?: IsoDateTime;
1199
+ /** Tool that produced this document (e.g. `'Takuhon'`, `'create-takuhon@0.1.0'`). */
1200
+ generator?: string;
1201
+ contentLicense: ContentLicense;
1202
+ }
1203
+ /** A complete takuhon profile document. */
1204
+ interface Takuhon {
1205
+ schemaVersion: string;
1206
+ profile: Profile;
1207
+ links: Link[];
1208
+ careers: Career[];
1209
+ projects: Project[];
1210
+ skills: Skill[];
1211
+ contact: Contact;
1212
+ settings: Settings;
1213
+ meta: Meta;
1214
+ }
1215
+ /**
1216
+ * A {@link Takuhon} document that has been canonicalized by `normalize()`:
1217
+ * arrays sorted by `order`, and empty localized-field entries removed.
1218
+ *
1219
+ * Structurally identical to {@link Takuhon}; the alias is a documentation hook
1220
+ * for downstream consumers that want to express "must run through normalize
1221
+ * first". A nominal branded form may replace this alias in a later phase if
1222
+ * OSS adopters need the static guarantee.
1223
+ */
1224
+ type NormalizedTakuhon = Takuhon;
1225
+ /**
1226
+ * Address with localized fields collapsed to single strings — the shape
1227
+ * `resolveLocale()` produces for `profile.location`.
1228
+ */
1229
+ interface LocalizedAddress {
1230
+ country?: Iso3166Alpha2;
1231
+ region?: string;
1232
+ locality?: string;
1233
+ display?: string;
1234
+ }
1235
+ /** Avatar with `alt` collapsed to a single string. */
1236
+ interface LocalizedAvatar {
1237
+ url: string;
1238
+ alt?: string;
1239
+ }
1240
+ /** Profile with every localized field collapsed to a single string. */
1241
+ interface LocalizedProfile {
1242
+ displayName: string;
1243
+ tagline?: string;
1244
+ bio?: string;
1245
+ avatar?: LocalizedAvatar;
1246
+ location?: LocalizedAddress;
1247
+ }
1248
+ interface LocalizedLinkCommon {
1249
+ id: Slug;
1250
+ label?: string;
1251
+ url: string;
1252
+ featured?: boolean;
1253
+ order?: number;
1254
+ }
1255
+ interface LocalizedLinkBuiltin extends LocalizedLinkCommon {
1256
+ type: Exclude<LinkType, 'custom'>;
1257
+ iconUrl?: string;
1258
+ }
1259
+ interface LocalizedLinkCustom extends LocalizedLinkCommon {
1260
+ type: 'custom';
1261
+ iconUrl: string;
1262
+ }
1263
+ /** Link with `label` collapsed to a single string. */
1264
+ type LocalizedLink = LocalizedLinkBuiltin | LocalizedLinkCustom;
1265
+ /** Career with `organization`, `role`, `description` collapsed to single strings. */
1266
+ interface LocalizedCareer {
1267
+ id: Slug;
1268
+ organization: string;
1269
+ role: string;
1270
+ description?: string;
1271
+ startDate: YearMonth;
1272
+ endDate?: YearMonth | null;
1273
+ isCurrent?: boolean;
1274
+ url?: string;
1275
+ location?: LocalizedAddress;
1276
+ order?: number;
1277
+ }
1278
+ /** Project with `title`, `description` collapsed to single strings. */
1279
+ interface LocalizedProject {
1280
+ id: Slug;
1281
+ title: string;
1282
+ description?: string;
1283
+ url?: string;
1284
+ tags?: string[];
1285
+ relatedCareerId?: Slug;
1286
+ startDate?: YearMonth;
1287
+ endDate?: YearMonth | null;
1288
+ highlighted?: boolean;
1289
+ order?: number;
1290
+ }
1291
+ /**
1292
+ * A takuhon document with every localized map flattened to a single string,
1293
+ * plus a `resolvedLocale` field recording which tag was actually used as the
1294
+ * head of the fallback chain. `resolveLocale()` returns this shape.
1295
+ *
1296
+ * `Skill`, `Contact`, `Settings`, and `Meta` carry no localized fields and
1297
+ * pass through unchanged.
1298
+ */
1299
+ interface LocalizedTakuhon {
1300
+ schemaVersion: string;
1301
+ profile: LocalizedProfile;
1302
+ links: LocalizedLink[];
1303
+ careers: LocalizedCareer[];
1304
+ projects: LocalizedProject[];
1305
+ skills: Skill[];
1306
+ contact: Contact;
1307
+ settings: Settings;
1308
+ meta: Meta;
1309
+ /**
1310
+ * The locale tag that was matched first by the fallback chain and used as
1311
+ * the head of per-field resolution. Equals one of the candidates derived
1312
+ * from the request arguments or `settings`; an empty string only when no
1313
+ * candidate was usable (theoretical — `validate()` rejects such inputs).
1314
+ */
1315
+ resolvedLocale: LocaleTag;
1316
+ }
1317
+
1318
+ /**
1319
+ * Schema validation for takuhon profile documents.
1320
+ *
1321
+ * Compiles the canonical {@link import('../takuhon.schema.json')} once at module
1322
+ * load and exposes a Result-style {@link validate} that returns either the
1323
+ * narrowed {@link Takuhon} value or a list of structured {@link ValidationError}s.
1324
+ * The validator is the canonical correctness boundary inside `@takuhon/core`:
1325
+ * `normalize` (commit 3) and the API layer (commit 11+) both rely on this
1326
+ * function to know the shape they are working with.
1327
+ *
1328
+ * Design notes:
1329
+ * - Returns a discriminated union rather than throwing so callers (CLI,
1330
+ * normalize, RFC 7807 Problem Details adapters) can route errors however they
1331
+ * like without paying for stack capture on every failure.
1332
+ * - Errors carry the RFC 6901 JSON Pointer of the offending value plus the
1333
+ * failing Ajv keyword, so consumers can render messages or look up the
1334
+ * relevant Spec section without leaking Ajv-specific types.
1335
+ * - The Ajv 2020 build is used because the schema declares
1336
+ * `$schema: "https://json-schema.org/draft/2020-12/schema"`.
1337
+ */
1338
+
1339
+ /**
1340
+ * Schema versions this build of `@takuhon/core` accepts directly.
1341
+ *
1342
+ * The migration registry (Phase 1 commit 6+) will translate older `schemaVersion`
1343
+ * values into the current one before validation runs, so this list reflects the
1344
+ * versions whose JSON Schema this package literally bundles, not the full
1345
+ * support window seen by end users.
1346
+ */
1347
+ declare const SUPPORTED_SCHEMA_VERSIONS: readonly ["0.1.0"];
1348
+ /**
1349
+ * A single validation failure.
1350
+ *
1351
+ * The shape is intentionally schema-agnostic: an API layer can adapt it into an
1352
+ * RFC 7807 Problem Details payload, a CLI can render the message, and a future
1353
+ * spec-section lookup table can join on {@link pointer} to surface
1354
+ * documentation references. A `specSection` field will be added in a later
1355
+ * commit; this minimal surface is what commit 2 ships.
1356
+ */
1357
+ interface ValidationError {
1358
+ /** RFC 6901 JSON Pointer to the offending value, e.g. `"/links/4/iconUrl"`. */
1359
+ pointer: string;
1360
+ /** Human-readable failure description (sourced from Ajv when available). */
1361
+ message: string;
1362
+ /**
1363
+ * The schema keyword that failed: `'required'`, `'enum'`, `'pattern'`,
1364
+ * `'additionalProperties'`, `'format'`, `'maxItems'`, `'maxLength'`, etc.
1365
+ * The value `'schemaVersion'` is reserved for documents whose
1366
+ * `schemaVersion` lies outside {@link SUPPORTED_SCHEMA_VERSIONS}.
1367
+ */
1368
+ keyword: string;
1369
+ /** JSON Pointer into the schema for the failing rule, e.g. `"#/$defs/Link/required"`. */
1370
+ schemaPointer?: string;
1371
+ }
1372
+ /** Result of {@link validate}. Narrow on `ok` to access `data` or `errors`. */
1373
+ type ValidationResult = {
1374
+ ok: true;
1375
+ data: Takuhon;
1376
+ } | {
1377
+ ok: false;
1378
+ errors: ValidationError[];
1379
+ };
1380
+ /**
1381
+ * Validate an arbitrary value against the bundled takuhon schema.
1382
+ *
1383
+ * @param data unknown JSON-like value (typically parsed from a `takuhon.json` file)
1384
+ * @returns A discriminated result. On success `data` is narrowed to {@link Takuhon};
1385
+ * on failure `errors` is a non-empty list of {@link ValidationError}s.
1386
+ */
1387
+ declare function validate(data: unknown): ValidationResult;
1388
+
1389
+ /**
1390
+ * Canonicalize a {@link Takuhon} document into the form downstream consumers
1391
+ * (`@takuhon/api`, `@takuhon/ui`, `@takuhon/jsonld`) can rely on without
1392
+ * re-checking shape invariants.
1393
+ *
1394
+ * Two transformations only:
1395
+ * - **Empty-entry cleanup** on every localized field (`LocalizedTitle` /
1396
+ * `LocalizedBody`): entries whose string value is empty or whitespace-only
1397
+ * are removed so that locale fallback works field-by-field. When the
1398
+ * cleanup leaves an optional localized map empty, the map itself is
1399
+ * removed (the schema requires `minProperties: 1`).
1400
+ * - **Stable sort by `order`** on every list field (`links`, `careers`,
1401
+ * `projects`, `skills`). Items without an `order` move to the end while
1402
+ * preserving their original relative position (ES2019 stable sort).
1403
+ *
1404
+ * Design notes:
1405
+ * - The input is deep-cloned via `structuredClone`; the original is never
1406
+ * mutated. Callers may safely keep a reference to the value they passed in.
1407
+ * - `normalize` does *not* canonicalize BCP-47 tag casing nor trim string
1408
+ * content. The first is covered by the schema's `propertyNames` pattern and
1409
+ * `resolveLocale`'s case-insensitive lookup; the second would silently rewrite
1410
+ * author input and is out of scope for Phase 1.
1411
+ * - `normalize(normalize(x))` deep-equals `normalize(x)` (idempotent), and the
1412
+ * output re-validates against `takuhon.schema.json`. Both invariants are
1413
+ * enforced by the unit tests.
1414
+ */
1415
+
1416
+ /**
1417
+ * Return a normalized copy of `data`.
1418
+ *
1419
+ * @param data A takuhon document that has already passed {@link validate}.
1420
+ * @returns A new {@link NormalizedTakuhon} with localized empties dropped and
1421
+ * list fields sorted by `order`.
1422
+ */
1423
+ declare function normalize(data: Takuhon): NormalizedTakuhon;
1424
+
1425
+ /**
1426
+ * Reduce a multi-locale {@link Takuhon} document to a single requested locale.
1427
+ *
1428
+ * Builds a flat candidate chain from the function arguments and `data.settings`,
1429
+ * expanding each entry's regional subtag (e.g. `'en-US' → ['en-US', 'en']`),
1430
+ * deduplicating case-insensitively, then walks the chain **per field**. The
1431
+ * first candidate whose value is non-blank wins for that field; an empty entry
1432
+ * (`""` / whitespace-only) falls through to the next candidate just like a
1433
+ * missing entry. This matches the spec semantics in [api.md §3.4] where empty
1434
+ * values are equivalent to absence.
1435
+ *
1436
+ * Design notes:
1437
+ * - Function arguments (`locale`, `fallbackLocale`) take precedence over
1438
+ * `settings.*`, in line with the spec's 7-tier list: HTTP-derived locales
1439
+ * (#1-#4) are resolved upstream by `@takuhon/api` and arrive here as
1440
+ * `locale` / `fallbackLocale`; `settings.defaultLocale` (#5),
1441
+ * `settings.fallbackLocale` (#6), and `settings.availableLocales[0]` (#7)
1442
+ * fill the tail.
1443
+ * - Invalid tags (`'zz_invalid'`, `'_'`, etc.) are silently dropped rather
1444
+ * than throwing. Resolution is best-effort: throwing on a malformed
1445
+ * `?lang=` query would push error handling responsibilities back into the
1446
+ * API layer for a recoverable case.
1447
+ * - A final rescue step appends every `availableLocales` entry so a request
1448
+ * with no matching candidate still produces a populated document. If even
1449
+ * that fails (only possible for hand-crafted inputs that bypassed
1450
+ * `validate`), required strings fall back to `''` and the caller's tests
1451
+ * catch the document-level regression.
1452
+ * - `resolvedLocale` records the tag that produced `profile.displayName`,
1453
+ * which is the field most consumers expose as the canonical locale of the
1454
+ * response (e.g. `meta.locale` in API responses, `<html lang>` in UI).
1455
+ */
1456
+
1457
+ /**
1458
+ * Resolve a takuhon document to a single locale.
1459
+ *
1460
+ * @param data A takuhon document (validated; ideally normalized first).
1461
+ * @param locale Caller-resolved request locale (e.g. from `?lang=` or
1462
+ * `Accept-Language`). Invalid tags are ignored.
1463
+ * @param fallbackLocale Caller-supplied secondary candidate when `locale`
1464
+ * misses. Invalid tags are ignored.
1465
+ */
1466
+ declare function resolveLocale(data: Takuhon, locale?: string, fallbackLocale?: string): LocalizedTakuhon;
1467
+
1468
+ /**
1469
+ * Generate Schema.org JSON-LD from a {@link LocalizedTakuhon} document.
1470
+ *
1471
+ * Emits a `ProfilePage` whose `mainEntity` is the `Person` described by the
1472
+ * input. Mapping rules follow the published schema.org mapping spec; the
1473
+ * relevant invariants are exercised by the unit tests.
1474
+ *
1475
+ * Design notes:
1476
+ * - Input is the output of `resolveLocale()`, so every localized field is
1477
+ * already a single string. No locale fallback happens here.
1478
+ * - All optional keys are omitted (not set to `null` / `undefined`) when their
1479
+ * source value is absent or empty, so consumers can shallow-merge or
1480
+ * `JSON.stringify` the result without post-processing.
1481
+ * - The canonical URL surfaced as `ProfilePage.url`, `Person.@id`, and
1482
+ * `Person.url` is derived from a single `links[]` entry: `type: 'website'`
1483
+ * with `featured: true`. The first match wins (stable after `normalize()`).
1484
+ * When no such link exists the three URL-bearing keys are omitted entirely;
1485
+ * no placeholder is fabricated.
1486
+ * - `profile.tagline` is intentionally not surfaced. `description` carries
1487
+ * `profile.bio` only, matching the spec exemplar. Phase 2 may revisit.
1488
+ * - `contact.email` is surfaced only when `contact.showEmail === true`
1489
+ * (privacy by default).
1490
+ * - URLs pass through verbatim. Relative paths in the input remain relative
1491
+ * in the output; absolutization is the API/UI layer's responsibility.
1492
+ * - Field insertion order on each emitted object is fixed so
1493
+ * `JSON.stringify(result)` is deterministic for a given input.
1494
+ */
1495
+
1496
+ /**
1497
+ * Build the `Person` JSON-LD object for `data`.
1498
+ *
1499
+ * @param data A locale-resolved takuhon document.
1500
+ * @returns A Schema.org `Person` object. `@context` is included so the
1501
+ * returned object is valid as a standalone JSON-LD document.
1502
+ */
1503
+ declare function generatePersonJsonLd(data: LocalizedTakuhon): object;
1504
+ /**
1505
+ * Build the `ProfilePage` JSON-LD object for `data`, with `Person` inlined
1506
+ * as `mainEntity`.
1507
+ *
1508
+ * @param data A locale-resolved takuhon document.
1509
+ */
1510
+ declare function generateProfilePageJsonLd(data: LocalizedTakuhon): object;
1511
+ /**
1512
+ * Build the array of JSON-LD objects to embed in a single
1513
+ * `<script type="application/ld+json">` tag.
1514
+ *
1515
+ * Phase 1 emits a single-element array containing the `ProfilePage`; the
1516
+ * `Person` is inlined there as `mainEntity`. The array shape leaves room
1517
+ * for later additions (e.g. `WebSite`) without changing the public surface.
1518
+ */
1519
+ declare function generateJsonLd(data: LocalizedTakuhon): object[];
1520
+
1521
+ /**
1522
+ * Export and import for takuhon profile documents.
1523
+ *
1524
+ * {@link exportTakuhon} serialises a {@link Takuhon} document into a transport
1525
+ * form ({@link ExportedTakuhon}) that can be persisted to a file, an API
1526
+ * response, or any other byte-oriented sink. {@link importTakuhon} is the
1527
+ * inverse: it validates the input and returns a {@link Takuhon}.
1528
+ *
1529
+ * Scope of these helpers (deliberately narrow):
1530
+ * - Pure, in-memory data transforms — no I/O, no storage adapter coupling.
1531
+ * - {@link importTakuhon} **does not** auto-migrate older `schemaVersion`
1532
+ * values. Cross-version handling belongs to the CLI / API layer, which
1533
+ * composes `importTakuhon` + {@link migrateTakuhon} + storage adapters as
1534
+ * spelled out in operational-lifecycle §5.3.
1535
+ * - Round-trip equivalence (operational-lifecycle §5.1) is preserved up to
1536
+ * the documented `meta.updatedAt` exception.
1537
+ *
1538
+ * Asset embedding (Base64) and backup creation are out of scope here; both
1539
+ * are the storage / API layer's responsibility.
1540
+ */
1541
+
1542
+ /**
1543
+ * Structural alias of {@link Takuhon}: the transport form is the document
1544
+ * itself. A wrapping envelope (e.g. `{ format, version, data, hash }`) is
1545
+ * intentionally avoided in Phase 1 — adding one later would be a breaking
1546
+ * change to the `GET /api/export` response shape and would require a major
1547
+ * version bump of `@takuhon/core`.
1548
+ */
1549
+ type ExportedTakuhon = Takuhon;
1550
+ /** Options for {@link exportTakuhon}. */
1551
+ interface ExportOptions {
1552
+ /**
1553
+ * When `true` (default), `meta.updatedAt` is overwritten with the current
1554
+ * ISO-8601 timestamp. Set to `false` for byte-for-byte reproducible
1555
+ * exports (e.g. roundtrip tests).
1556
+ *
1557
+ * Round-trip equivalence per operational-lifecycle §5.1 explicitly lists
1558
+ * `meta.updatedAt` as the allowed exception.
1559
+ */
1560
+ updateTimestamp?: boolean;
1561
+ }
1562
+ /**
1563
+ * Thrown by {@link importTakuhon} when the input fails schema validation
1564
+ * (including an unsupported `schemaVersion`). The `errors` field carries
1565
+ * the same {@link ValidationError} list that `validate()` would have
1566
+ * returned, so the API layer can map them onto RFC 7807.
1567
+ */
1568
+ declare class ImportError extends Error {
1569
+ readonly errors?: ValidationError[];
1570
+ constructor(message: string, options?: {
1571
+ cause?: unknown;
1572
+ errors?: ValidationError[];
1573
+ });
1574
+ }
1575
+ /**
1576
+ * Serialise a {@link Takuhon} into its transport form. The input is
1577
+ * deep-cloned via `JSON.parse(JSON.stringify(...))`; the original is never
1578
+ * mutated.
1579
+ */
1580
+ declare function exportTakuhon(data: Takuhon, options?: ExportOptions): ExportedTakuhon;
1581
+ /**
1582
+ * Validate an {@link ExportedTakuhon} and return it as a {@link Takuhon}.
1583
+ *
1584
+ * On schema validation failure (including an unsupported `schemaVersion`)
1585
+ * throws an {@link ImportError} with the structured `errors` attached. The
1586
+ * input is not mutated. The return value is a deep clone, so subsequent
1587
+ * caller mutations cannot reach back into the supplied document.
1588
+ *
1589
+ * Cross-version inputs (older `schemaVersion`) are out of scope: callers
1590
+ * (CLI / API layer) should run {@link migrateTakuhon} before calling this.
1591
+ */
1592
+ declare function importTakuhon(data: ExportedTakuhon): Takuhon;
1593
+
1594
+ /**
1595
+ * Forward migration entry point for takuhon documents.
1596
+ *
1597
+ * {@link migrateTakuhon} composes a chain of {@link Migration} entries from
1598
+ * the registry (`./migrations`) and applies them in order. Phase 1 ships
1599
+ * with an empty registry, so any non-identity migration currently throws
1600
+ * {@link MigrationError}; the first concrete entry will land alongside
1601
+ * the v0.2.0 schema bump.
1602
+ *
1603
+ * Scope (deliberately narrow, mirroring `export.ts`):
1604
+ * - Pure data transform — no I/O, no backup creation, no storage write.
1605
+ * - Backup-before-migrate (operational-lifecycle §3.1) is the storage /
1606
+ * API layer's responsibility; this function only transforms the
1607
+ * in-memory document.
1608
+ * - Forward only (operational-lifecycle §2.4); downgrade is via restore.
1609
+ */
1610
+
1611
+ /**
1612
+ * Thrown by {@link migrateTakuhon} when no forward chain connects the
1613
+ * source `schemaVersion` to `targetVersion`. The message includes both
1614
+ * versions so the API layer can surface an actionable RFC 7807 problem.
1615
+ */
1616
+ declare class MigrationError extends Error {
1617
+ constructor(message: string, options?: {
1618
+ cause?: unknown;
1619
+ });
1620
+ }
1621
+ /**
1622
+ * Migrate a takuhon document forward to `targetVersion`. Returns a deep
1623
+ * clone; the input is never mutated, even when a migration throws.
1624
+ *
1625
+ * @throws {MigrationError} when no forward chain exists from
1626
+ * `data.schemaVersion` to `targetVersion`.
1627
+ */
1628
+ declare function migrateTakuhon(data: Takuhon, targetVersion: string): Takuhon;
1629
+
1630
+ /**
1631
+ * Forward migration registry for `@takuhon/core`.
1632
+ *
1633
+ * Each migration is a pure function from a takuhon document at version `from`
1634
+ * to one at version `to`. The registry is consulted by {@link migrateTakuhon}
1635
+ * to build a chain when the requested target is more than one step away
1636
+ * (`0.1.0 → 0.3.0` is composed of `0.1.0→0.2.0` and `0.2.0→0.3.0`).
1637
+ *
1638
+ * Authoring conventions:
1639
+ * - File name: `vX.Y.Z-to-vA.B.C.ts`
1640
+ * - Pure function: must not mutate input, must not perform I/O
1641
+ * - Forward only: downgrades are not provided; recovery is via the backup
1642
+ * restore path (operational-lifecycle §4)
1643
+ * - Each entry ships with a unit test: sample input/output, idempotency
1644
+ * when applicable, and schema-pass against the target version's schema
1645
+ *
1646
+ * The chain-building algorithm lives in `_chain.ts` and is intentionally
1647
+ * not re-exported from `@takuhon/core` — it is an implementation detail of
1648
+ * {@link migrateTakuhon}.
1649
+ */
1650
+
1651
+ /**
1652
+ * A forward migration entry. `from` and `to` are semver strings matching
1653
+ * the `schemaVersion` field of the input and output documents. `migrate`
1654
+ * is pure: it must not mutate `data`.
1655
+ */
1656
+ interface Migration<From, To> {
1657
+ from: string;
1658
+ to: string;
1659
+ migrate(data: From): To;
1660
+ }
1661
+ /**
1662
+ * Forward migrations bundled with this build of `@takuhon/core`. Empty in
1663
+ * Phase 1; the first entry will land alongside the v0.2.0 schema bump.
1664
+ */
1665
+ declare const migrations: readonly Migration<Takuhon, Takuhon>[];
1666
+
1667
+ /**
1668
+ * Persistence contracts for takuhon profile documents and binary assets.
1669
+ *
1670
+ * Adapters (KV / R2 / filesystem / SQLite / …) implement these interfaces to
1671
+ * plug into `@takuhon/api`. All methods are async; failures surface as
1672
+ * exceptions in the {@link StorageError} family so the API layer can map them
1673
+ * onto RFC 7807 problem details.
1674
+ *
1675
+ * Design notes:
1676
+ * - `version` is an opaque ETag-like token (UUID, hash, monotonic counter —
1677
+ * the adapter chooses). It powers HTTP `If-Match` style optimistic locking
1678
+ * and is unrelated to {@link Takuhon.schemaVersion}, which describes the
1679
+ * document's data-model version.
1680
+ * - `getProfile()` returns a raw {@link Takuhon}, not a normalized or
1681
+ * locale-resolved one. Normalization and locale resolution belong to the
1682
+ * API / render layer; storage only persists.
1683
+ * - `saveProfile(data, ifMatch?)` rejects with {@link ConflictError} when
1684
+ * `ifMatch` is supplied and does not equal the current stored version.
1685
+ * When `ifMatch` is omitted, the adapter's policy decides whether to
1686
+ * overwrite unconditionally; per-implementation docs spell this out.
1687
+ * - `TakuhonAssetStorage` is intentionally a separate interface so deployments
1688
+ * that don't host user-uploaded media (e.g. static export) can omit it.
1689
+ * - The naming standardises on the lowercase "Takuhon" word (cf.
1690
+ * {@link Takuhon}, {@link LocalizedTakuhon}, `normalize`, `validate`) even
1691
+ * where upstream documents write "Takuhon".
1692
+ */
1693
+
1694
+ /**
1695
+ * Persistence contract for the single profile document of a takuhon instance.
1696
+ *
1697
+ * Implementations: Cloudflare KV (Phase 3), filesystem (Phase 3+), in-memory
1698
+ * test doubles, and future SQL adapters.
1699
+ */
1700
+ interface TakuhonStorage {
1701
+ /**
1702
+ * Read the current profile document and its opaque version token.
1703
+ *
1704
+ * @throws {NotFoundError} when no profile has been saved yet.
1705
+ */
1706
+ getProfile(): Promise<{
1707
+ data: Takuhon;
1708
+ version: string;
1709
+ }>;
1710
+ /**
1711
+ * Replace the profile document. The returned `version` is the new opaque
1712
+ * token to supply as the next `ifMatch`.
1713
+ *
1714
+ * @param data the document to persist (raw, not normalized)
1715
+ * @param ifMatch when set, the adapter rejects the write unless the
1716
+ * current stored version equals this token
1717
+ * @throws {ConflictError} when `ifMatch` is supplied and does not equal
1718
+ * the current stored version
1719
+ */
1720
+ saveProfile(data: Takuhon, ifMatch?: string): Promise<{
1721
+ version: string;
1722
+ }>;
1723
+ /**
1724
+ * Remove the profile document. Idempotent: no error when nothing is stored.
1725
+ */
1726
+ deleteProfile(): Promise<void>;
1727
+ }
1728
+ /**
1729
+ * Metadata for a stored binary asset, returned by {@link TakuhonAssetStorage}.
1730
+ *
1731
+ * `url` is the relative path used inside the document (`/assets/...`);
1732
+ * `publicUrl` is the absolute URL a browser can fetch. The two are kept
1733
+ * distinct because adapters may serve assets from a different host than the
1734
+ * profile (e.g. Cloudflare R2 + custom CDN).
1735
+ */
1736
+ interface AssetRecord {
1737
+ id: string;
1738
+ url: string;
1739
+ publicUrl: string;
1740
+ mimeType: string;
1741
+ size: number;
1742
+ width?: number;
1743
+ height?: number;
1744
+ /** ISO-8601 timestamp. */
1745
+ createdAt?: string;
1746
+ }
1747
+ /**
1748
+ * Caller hints for {@link TakuhonAssetStorage.putAsset}. Both fields are
1749
+ * optional; when omitted, the adapter falls back to the `File` / `Blob`
1750
+ * metadata. Adapters are responsible for magic-byte verification, EXIF
1751
+ * stripping, and dimension limits — callers must not pre-process.
1752
+ */
1753
+ interface AssetOptions {
1754
+ filename?: string;
1755
+ contentType?: string;
1756
+ }
1757
+ /**
1758
+ * Persistence contract for binary assets (avatars, project images, …).
1759
+ *
1760
+ * `listAssets()` is unbounded by design for the MVP; later phases may
1761
+ * introduce paginated semantics with a different return shape. `getPublicUrl()`
1762
+ * takes only an `assetId` today; a future options object (e.g. `expiresIn`
1763
+ * for signed URLs) would be added in a backward-compatible way.
1764
+ */
1765
+ interface TakuhonAssetStorage {
1766
+ putAsset(file: File | Blob, options?: AssetOptions): Promise<AssetRecord>;
1767
+ /** @throws {NotFoundError} when no asset exists for `assetId`. */
1768
+ getPublicUrl(assetId: string): Promise<string>;
1769
+ /** Idempotent: no error when the asset is already absent. */
1770
+ deleteAsset(assetId: string): Promise<void>;
1771
+ listAssets(): Promise<AssetRecord[]>;
1772
+ }
1773
+ /**
1774
+ * Base class for errors thrown by storage adapters. Catch this to handle
1775
+ * any storage-layer failure uniformly; check `instanceof` of a subclass
1776
+ * (e.g. {@link NotFoundError}, {@link ConflictError}) to discriminate.
1777
+ */
1778
+ declare class StorageError extends Error {
1779
+ constructor(message: string, options?: {
1780
+ cause?: unknown;
1781
+ });
1782
+ }
1783
+ /** Thrown when a requested resource (profile or asset) does not exist. */
1784
+ declare class NotFoundError extends StorageError {
1785
+ constructor(message: string, options?: {
1786
+ cause?: unknown;
1787
+ });
1788
+ }
1789
+ /**
1790
+ * Thrown when an optimistic-locking precondition fails: the caller supplied
1791
+ * an `ifMatch` token that does not equal the current stored version.
1792
+ *
1793
+ * `currentVersion` (when set) carries the actual current version so the
1794
+ * caller can decide between refetch-and-retry and surfacing a 409 to the
1795
+ * end user without an extra round trip.
1796
+ */
1797
+ declare class ConflictError extends StorageError {
1798
+ readonly currentVersion?: string;
1799
+ constructor(message: string, options?: {
1800
+ currentVersion?: string;
1801
+ cause?: unknown;
1802
+ });
1803
+ }
1804
+
1805
+ /**
1806
+ * @takuhon/core — canonical JSON Schema, hand-written TypeScript types,
1807
+ * Ajv-backed validation, document normalization, and locale resolution for
1808
+ * takuhon profile data.
1809
+ *
1810
+ * Public surface (Phase 1):
1811
+ * - {@link schema}: the JSON Schema 2020-12 contract bundled with this build.
1812
+ * - {@link SCHEMA_VERSION}: the version of that schema (matches the `$id`).
1813
+ * - {@link validate} / {@link ValidationResult} / {@link ValidationError} /
1814
+ * {@link SUPPORTED_SCHEMA_VERSIONS}: Result-style validator backed by Ajv.
1815
+ * - {@link normalize} / {@link NormalizedTakuhon}: canonicalize a validated
1816
+ * document (sort lists by `order`, drop blank localized entries).
1817
+ * - {@link resolveLocale} / {@link LocalizedTakuhon}: collapse a multi-locale
1818
+ * document to a single requested locale with BCP-47 regional fallback.
1819
+ * - {@link generateJsonLd} / {@link generatePersonJsonLd} /
1820
+ * {@link generateProfilePageJsonLd}: emit Schema.org JSON-LD
1821
+ * (`ProfilePage` wrapping `Person`) from a locale-resolved document.
1822
+ * - {@link TakuhonStorage} / {@link TakuhonAssetStorage}: persistence contracts
1823
+ * for adapters (KV / R2 / filesystem / SQLite / …), with the
1824
+ * {@link StorageError} / {@link NotFoundError} / {@link ConflictError}
1825
+ * exception family for optimistic-locking and not-found signalling.
1826
+ * - {@link exportTakuhon} / {@link importTakuhon} / {@link ExportOptions} /
1827
+ * {@link ExportedTakuhon} / {@link ImportError}: roundtrip-stable
1828
+ * serialisation for transport (file, API response, …).
1829
+ * - {@link migrateTakuhon} / {@link Migration} / {@link migrations} /
1830
+ * {@link MigrationError}: forward-only migration registry. Empty in
1831
+ * Phase 1; first entry lands with the v0.2.0 schema bump.
1832
+ * - Domain types: {@link Takuhon} and its constituent shapes (`Profile`,
1833
+ * `Settings`, `Career`, `Project`, `Link` discriminated union, etc.).
1834
+ */
1835
+
1836
+ /**
1837
+ * Version of the takuhon schema bundled with this build of `@takuhon/core`.
1838
+ * A takuhon profile document's `schemaVersion` field must be migrate-compatible
1839
+ * with this version. See operational-lifecycle docs for the migration policy.
1840
+ */
1841
+ declare const SCHEMA_VERSION = "0.1.0";
1842
+
1843
+ export { type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type ExportOptions, type ExportedTakuhon, ImportError, type Iso3166Alpha2, type IsoDateTime, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedProfile, type LocalizedProject, type LocalizedTakuhon, type LocalizedTitle, type Meta, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Profile, type Project, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type ValidationError, type ValidationResult, type YearMonth, exportTakuhon, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, migrateTakuhon, migrations, normalize, resolveLocale, schema, validate };