@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.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +112 -0
- package/dist/index.d.ts +1843 -0
- package/dist/index.js +919 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/takuhon.schema.json +355 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|