@takuhon/core 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +494 -9
- package/dist/index.js +909 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/takuhon.schema.json +54 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var $schema = "https://json-schema.org/draft/2020-12/schema";
|
|
2
|
-
var $id = "https://takuhon.example/schemas/0.
|
|
2
|
+
var $id = "https://takuhon.example/schemas/0.5.0/takuhon.schema.json";
|
|
3
3
|
var title = "Takuhon Profile";
|
|
4
4
|
var description = "Portable profile data format consumed by @takuhon/core. The canonical contract for profile content authored as takuhon.json.";
|
|
5
5
|
var type = "object";
|
|
@@ -542,6 +542,74 @@ var $defs = {
|
|
|
542
542
|
type: "boolean",
|
|
543
543
|
"default": false,
|
|
544
544
|
description: "Opt-in flag for first-party analytics. Default is false to keep takuhon privacy-respecting by default."
|
|
545
|
+
},
|
|
546
|
+
activity: {
|
|
547
|
+
$ref: "#/$defs/ActivitySettings"
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
ActivitySettings: {
|
|
552
|
+
type: "object",
|
|
553
|
+
additionalProperties: false,
|
|
554
|
+
description: "Opt-in developer-activity dashboard configuration (GitHub / WakaTime). Only the owner-curated settings live here; secrets are provisioned out of band and the synced metrics are stored in a separate document, never in takuhon.json.",
|
|
555
|
+
properties: {
|
|
556
|
+
enabled: {
|
|
557
|
+
type: "boolean",
|
|
558
|
+
"default": false,
|
|
559
|
+
description: "Master switch. When false (the default), the activity section is not rendered even if a snapshot exists."
|
|
560
|
+
},
|
|
561
|
+
github: {
|
|
562
|
+
type: "object",
|
|
563
|
+
additionalProperties: false,
|
|
564
|
+
required: [
|
|
565
|
+
"username"
|
|
566
|
+
],
|
|
567
|
+
properties: {
|
|
568
|
+
username: {
|
|
569
|
+
type: "string",
|
|
570
|
+
minLength: 1,
|
|
571
|
+
maxLength: 39,
|
|
572
|
+
description: "GitHub login whose public activity is summarized."
|
|
573
|
+
},
|
|
574
|
+
showLanguages: {
|
|
575
|
+
type: "boolean",
|
|
576
|
+
"default": true
|
|
577
|
+
},
|
|
578
|
+
showContributions: {
|
|
579
|
+
type: "boolean",
|
|
580
|
+
"default": true
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
wakatime: {
|
|
585
|
+
type: "object",
|
|
586
|
+
additionalProperties: false,
|
|
587
|
+
required: [
|
|
588
|
+
"username"
|
|
589
|
+
],
|
|
590
|
+
properties: {
|
|
591
|
+
username: {
|
|
592
|
+
type: "string",
|
|
593
|
+
minLength: 1,
|
|
594
|
+
maxLength: 255,
|
|
595
|
+
description: "WakaTime username whose coding-time stats are summarized."
|
|
596
|
+
},
|
|
597
|
+
showCodingTime: {
|
|
598
|
+
type: "boolean",
|
|
599
|
+
"default": true
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
showRank: {
|
|
604
|
+
type: "boolean",
|
|
605
|
+
"default": true,
|
|
606
|
+
description: "Display the derived activity rank / badge."
|
|
607
|
+
},
|
|
608
|
+
refreshHintHours: {
|
|
609
|
+
type: "integer",
|
|
610
|
+
minimum: 1,
|
|
611
|
+
maximum: 168,
|
|
612
|
+
description: "Advisory refresh cadence in hours. The real cadence is how often the sync step (CLI command or scheduled job) runs."
|
|
545
613
|
}
|
|
546
614
|
}
|
|
547
615
|
},
|
|
@@ -1664,6 +1732,70 @@ declare const schema: {
|
|
|
1664
1732
|
default: boolean;
|
|
1665
1733
|
description: string;
|
|
1666
1734
|
};
|
|
1735
|
+
activity: {
|
|
1736
|
+
$ref: string;
|
|
1737
|
+
};
|
|
1738
|
+
};
|
|
1739
|
+
};
|
|
1740
|
+
ActivitySettings: {
|
|
1741
|
+
type: string;
|
|
1742
|
+
additionalProperties: boolean;
|
|
1743
|
+
description: string;
|
|
1744
|
+
properties: {
|
|
1745
|
+
enabled: {
|
|
1746
|
+
type: string;
|
|
1747
|
+
default: boolean;
|
|
1748
|
+
description: string;
|
|
1749
|
+
};
|
|
1750
|
+
github: {
|
|
1751
|
+
type: string;
|
|
1752
|
+
additionalProperties: boolean;
|
|
1753
|
+
required: string[];
|
|
1754
|
+
properties: {
|
|
1755
|
+
username: {
|
|
1756
|
+
type: string;
|
|
1757
|
+
minLength: number;
|
|
1758
|
+
maxLength: number;
|
|
1759
|
+
description: string;
|
|
1760
|
+
};
|
|
1761
|
+
showLanguages: {
|
|
1762
|
+
type: string;
|
|
1763
|
+
default: boolean;
|
|
1764
|
+
};
|
|
1765
|
+
showContributions: {
|
|
1766
|
+
type: string;
|
|
1767
|
+
default: boolean;
|
|
1768
|
+
};
|
|
1769
|
+
};
|
|
1770
|
+
};
|
|
1771
|
+
wakatime: {
|
|
1772
|
+
type: string;
|
|
1773
|
+
additionalProperties: boolean;
|
|
1774
|
+
required: string[];
|
|
1775
|
+
properties: {
|
|
1776
|
+
username: {
|
|
1777
|
+
type: string;
|
|
1778
|
+
minLength: number;
|
|
1779
|
+
maxLength: number;
|
|
1780
|
+
description: string;
|
|
1781
|
+
};
|
|
1782
|
+
showCodingTime: {
|
|
1783
|
+
type: string;
|
|
1784
|
+
default: boolean;
|
|
1785
|
+
};
|
|
1786
|
+
};
|
|
1787
|
+
};
|
|
1788
|
+
showRank: {
|
|
1789
|
+
type: string;
|
|
1790
|
+
default: boolean;
|
|
1791
|
+
description: string;
|
|
1792
|
+
};
|
|
1793
|
+
refreshHintHours: {
|
|
1794
|
+
type: string;
|
|
1795
|
+
minimum: number;
|
|
1796
|
+
maximum: number;
|
|
1797
|
+
description: string;
|
|
1798
|
+
};
|
|
1667
1799
|
};
|
|
1668
1800
|
};
|
|
1669
1801
|
Meta: {
|
|
@@ -2515,6 +2647,32 @@ interface Settings {
|
|
|
2515
2647
|
enableApi?: boolean;
|
|
2516
2648
|
/** Opt-in flag for first-party analytics. Default is false. */
|
|
2517
2649
|
enableAnalytics?: boolean;
|
|
2650
|
+
/** Opt-in developer-activity dashboard configuration (added in 0.5.0). */
|
|
2651
|
+
activity?: ActivitySettings;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Owner-curated configuration for the developer-activity dashboard (GitHub /
|
|
2655
|
+
* WakaTime). Secrets (API tokens) are provisioned out of band and never stored
|
|
2656
|
+
* here; the synced metrics live in a separate document, not in `takuhon.json`.
|
|
2657
|
+
*/
|
|
2658
|
+
interface ActivitySettings {
|
|
2659
|
+
/** Master switch; the section is hidden when false (the default). */
|
|
2660
|
+
enabled?: boolean;
|
|
2661
|
+
github?: {
|
|
2662
|
+
/** GitHub login whose public activity is summarized. */
|
|
2663
|
+
username: string;
|
|
2664
|
+
showLanguages?: boolean;
|
|
2665
|
+
showContributions?: boolean;
|
|
2666
|
+
};
|
|
2667
|
+
wakatime?: {
|
|
2668
|
+
/** WakaTime username whose coding-time stats are summarized. */
|
|
2669
|
+
username: string;
|
|
2670
|
+
showCodingTime?: boolean;
|
|
2671
|
+
};
|
|
2672
|
+
/** Display the derived activity rank / badge. */
|
|
2673
|
+
showRank?: boolean;
|
|
2674
|
+
/** Advisory refresh cadence in hours (the real cadence is the sync schedule). */
|
|
2675
|
+
refreshHintHours?: number;
|
|
2518
2676
|
}
|
|
2519
2677
|
interface ContentLicenseAttribution {
|
|
2520
2678
|
name?: string;
|
|
@@ -2862,7 +3020,7 @@ interface LocalizedTakuhon {
|
|
|
2862
3020
|
* versions whose JSON Schema this package literally bundles, not the full
|
|
2863
3021
|
* support window seen by end users.
|
|
2864
3022
|
*/
|
|
2865
|
-
declare const SUPPORTED_SCHEMA_VERSIONS: readonly ["0.1.0", "0.2.0", "0.3.0", "0.4.0"];
|
|
3023
|
+
declare const SUPPORTED_SCHEMA_VERSIONS: readonly ["0.1.0", "0.2.0", "0.3.0", "0.4.0", "0.5.0"];
|
|
2866
3024
|
/**
|
|
2867
3025
|
* A single validation failure.
|
|
2868
3026
|
*
|
|
@@ -3169,9 +3327,9 @@ declare function applyPublicPrivacyFilter<T extends FilterableProfile>(profile:
|
|
|
3169
3327
|
*
|
|
3170
3328
|
* {@link migrateTakuhon} composes a chain of {@link Migration} entries from
|
|
3171
3329
|
* the registry (`./migrations`) and applies them in order. The registry
|
|
3172
|
-
* currently ships the `0.1.0 → 0.2.0`, `0.2.0 → 0.3.0`,
|
|
3173
|
-
* forward migrations; a request whose source has no chain to
|
|
3174
|
-
* throws {@link MigrationError}.
|
|
3330
|
+
* currently ships the `0.1.0 → 0.2.0`, `0.2.0 → 0.3.0`, `0.3.0 → 0.4.0`, and
|
|
3331
|
+
* `0.4.0 → 0.5.0` forward migrations; a request whose source has no chain to
|
|
3332
|
+
* the target throws {@link MigrationError}.
|
|
3175
3333
|
*
|
|
3176
3334
|
* Scope (deliberately narrow, mirroring `export.ts`):
|
|
3177
3335
|
* - Pure data transform — no I/O, no backup creation, no storage write.
|
|
@@ -3240,6 +3398,130 @@ interface Migration<From, To> {
|
|
|
3240
3398
|
*/
|
|
3241
3399
|
declare const migrations: readonly Migration<Takuhon, Takuhon>[];
|
|
3242
3400
|
|
|
3401
|
+
/**
|
|
3402
|
+
* Developer-activity snapshot: types, a runtime type-guard, and the pure,
|
|
3403
|
+
* network-free transforms that turn raw GitHub / WakaTime figures into the
|
|
3404
|
+
* stored, render-ready shape.
|
|
3405
|
+
*
|
|
3406
|
+
* This module is deliberately dependency-free and platform-independent — it
|
|
3407
|
+
* never performs I/O or network access. Fetching the raw figures from GitHub /
|
|
3408
|
+
* WakaTime lives in a separate, runtime-aware package (`@takuhon/activity`);
|
|
3409
|
+
* core only owns the data shape and the deterministic transforms, so every
|
|
3410
|
+
* runtime renders identical output from the same snapshot.
|
|
3411
|
+
*
|
|
3412
|
+
* The {@link ActivitySnapshot} is NOT part of the canonical `takuhon.json`
|
|
3413
|
+
* document. It is machine-written by a sync step (a CLI command or a scheduled
|
|
3414
|
+
* job), persisted in a sibling document via {@link ActivityStorage}, and read
|
|
3415
|
+
* by the renderer. Keeping it out of the canonical, owner-curated identity
|
|
3416
|
+
* document is intentional: externally-sourced, volatile metrics must not enter
|
|
3417
|
+
* the single source of truth a profile owner hand-maintains.
|
|
3418
|
+
*/
|
|
3419
|
+
/** One language's share of analyzed source, derived from byte counts. */
|
|
3420
|
+
interface LanguageBreakdown {
|
|
3421
|
+
/** Language name as reported by the source (e.g. `"TypeScript"`). */
|
|
3422
|
+
name: string;
|
|
3423
|
+
/** Total bytes attributed to this language across analyzed repositories. */
|
|
3424
|
+
bytes: number;
|
|
3425
|
+
/** Share of the analyzed total, 0–100, rounded to one decimal place. */
|
|
3426
|
+
percent: number;
|
|
3427
|
+
}
|
|
3428
|
+
/** A single day in a contribution calendar. */
|
|
3429
|
+
interface ContributionDay {
|
|
3430
|
+
/** ISO-8601 calendar date (`YYYY-MM-DD`). */
|
|
3431
|
+
date: string;
|
|
3432
|
+
/** Contribution count on that day. */
|
|
3433
|
+
count: number;
|
|
3434
|
+
}
|
|
3435
|
+
/** Contribution activity over a window (e.g. the trailing year). */
|
|
3436
|
+
interface ContributionCalendar {
|
|
3437
|
+
/** Total contributions across the window. */
|
|
3438
|
+
total: number;
|
|
3439
|
+
/** Per-day counts, in chronological order. */
|
|
3440
|
+
days: ContributionDay[];
|
|
3441
|
+
}
|
|
3442
|
+
/** Coding time decomposed into whole hours/minutes/seconds plus the raw total. */
|
|
3443
|
+
interface CodingTime {
|
|
3444
|
+
totalSeconds: number;
|
|
3445
|
+
hours: number;
|
|
3446
|
+
minutes: number;
|
|
3447
|
+
seconds: number;
|
|
3448
|
+
}
|
|
3449
|
+
/** Coarse activity tier label, highest (`S`) to lowest (`D`). */
|
|
3450
|
+
type RankTierLabel = 'S' | 'A' | 'B' | 'C' | 'D';
|
|
3451
|
+
/** A derived activity rank: a tier label plus the 0–100 score it came from. */
|
|
3452
|
+
interface RankTier {
|
|
3453
|
+
tier: RankTierLabel;
|
|
3454
|
+
/** Normalized 0–100 activity score the tier was derived from. */
|
|
3455
|
+
score: number;
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* A point-in-time snapshot of externally-sourced developer-activity metrics
|
|
3459
|
+
* (GitHub languages / contributions, WakaTime coding time, and a derived rank).
|
|
3460
|
+
*
|
|
3461
|
+
* Every metric field is optional so the snapshot degrades gracefully when a
|
|
3462
|
+
* source is unconfigured or temporarily unavailable: the renderer shows what is
|
|
3463
|
+
* present and omits the rest. Only {@link lastSyncedAt} is required so staleness
|
|
3464
|
+
* is always displayable.
|
|
3465
|
+
*/
|
|
3466
|
+
interface ActivitySnapshot {
|
|
3467
|
+
/** ISO-8601 timestamp of when this snapshot was produced. */
|
|
3468
|
+
lastSyncedAt: string;
|
|
3469
|
+
languages?: LanguageBreakdown[];
|
|
3470
|
+
contributions?: ContributionCalendar;
|
|
3471
|
+
codingTime?: CodingTime;
|
|
3472
|
+
rank?: RankTier;
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Validate that an unknown value (e.g. one read back from storage) is a
|
|
3476
|
+
* well-formed {@link ActivitySnapshot}. A lightweight runtime guard is
|
|
3477
|
+
* sufficient here — the snapshot is machine-written and self-owned, so it does
|
|
3478
|
+
* not warrant a full JSON Schema validator like `takuhon.json`. A malformed or
|
|
3479
|
+
* truncated snapshot fails the guard and the renderer treats it as absent.
|
|
3480
|
+
*/
|
|
3481
|
+
declare function isActivitySnapshot(value: unknown): value is ActivitySnapshot;
|
|
3482
|
+
/**
|
|
3483
|
+
* Turn per-language byte counts (as GitHub's `repos/.../languages` reports) into
|
|
3484
|
+
* sorted percentage breakdowns. Languages with zero bytes are dropped; the
|
|
3485
|
+
* result is sorted by bytes descending, ties broken by name, so the output is
|
|
3486
|
+
* deterministic. Percentages are rounded to one decimal place and need not sum
|
|
3487
|
+
* to exactly 100. Returns `[]` when there is nothing to attribute.
|
|
3488
|
+
*/
|
|
3489
|
+
declare function computeLanguagePercentages(bytesByLanguage: Readonly<Record<string, number>>): LanguageBreakdown[];
|
|
3490
|
+
/** Decompose a total number of seconds into whole h/m/s (negatives clamp to 0). */
|
|
3491
|
+
declare function formatCodingTime(totalSeconds: number): CodingTime;
|
|
3492
|
+
/** Inputs to {@link deriveRankTier}; any field may be absent. */
|
|
3493
|
+
interface RankInput {
|
|
3494
|
+
/** Contributions in the trailing window (e.g. the past year). */
|
|
3495
|
+
contributions?: number;
|
|
3496
|
+
/** Total coding time, in seconds. */
|
|
3497
|
+
codingSeconds?: number;
|
|
3498
|
+
}
|
|
3499
|
+
/** Contribution count that saturates the contribution sub-score at 100. */
|
|
3500
|
+
declare const RANK_FULL_CONTRIBUTIONS = 2000;
|
|
3501
|
+
/** Coding hours that saturate the coding-time sub-score at 100. */
|
|
3502
|
+
declare const RANK_FULL_CODING_HOURS = 2000;
|
|
3503
|
+
/**
|
|
3504
|
+
* Inclusive lower bounds for each tier on the 0–100 score (anything below `C`
|
|
3505
|
+
* is `D`). These are a deliberately generic, transparent default — not tuned to
|
|
3506
|
+
* any individual — and may be revisited.
|
|
3507
|
+
*/
|
|
3508
|
+
declare const RANK_TIER_THRESHOLDS: {
|
|
3509
|
+
readonly S: 80;
|
|
3510
|
+
readonly A: 60;
|
|
3511
|
+
readonly B: 40;
|
|
3512
|
+
readonly C: 20;
|
|
3513
|
+
};
|
|
3514
|
+
/**
|
|
3515
|
+
* Derive a coarse activity {@link RankTier} from the available signals. Each
|
|
3516
|
+
* present signal is mapped to a 0–100 sub-score via linear saturation
|
|
3517
|
+
* ({@link RANK_FULL_CONTRIBUTIONS} / {@link RANK_FULL_CODING_HOURS} reach 100);
|
|
3518
|
+
* the final score is the mean of the present sub-scores, so a profile is not
|
|
3519
|
+
* penalized for leaving a source unconfigured. With no signals the score is 0
|
|
3520
|
+
* (`D`). The function is deterministic — no clock or randomness — so renders are
|
|
3521
|
+
* reproducible.
|
|
3522
|
+
*/
|
|
3523
|
+
declare function deriveRankTier(input: RankInput): RankTier;
|
|
3524
|
+
|
|
3243
3525
|
/**
|
|
3244
3526
|
* Persistence contracts for takuhon profile documents and binary assets.
|
|
3245
3527
|
*
|
|
@@ -3323,8 +3605,14 @@ interface AssetRecord {
|
|
|
3323
3605
|
/**
|
|
3324
3606
|
* Caller hints for {@link TakuhonAssetStorage.putAsset}. Both fields are
|
|
3325
3607
|
* optional; when omitted, the adapter falls back to the `File` / `Blob`
|
|
3326
|
-
* metadata.
|
|
3327
|
-
*
|
|
3608
|
+
* metadata.
|
|
3609
|
+
*
|
|
3610
|
+
* The caller (`@takuhon/api`'s admin app) verifies the magic bytes, enforces
|
|
3611
|
+
* the size / dimension / frame limits, and strips metadata (EXIF / IPTC / XMP /
|
|
3612
|
+
* color profile) before calling `putAsset`, using the shared `@takuhon/core`
|
|
3613
|
+
* image helpers, so every adapter applies the same checks. An adapter therefore
|
|
3614
|
+
* persists the already-validated, already-stripped bytes verbatim and is
|
|
3615
|
+
* responsible only for storage and key/URL generation.
|
|
3328
3616
|
*/
|
|
3329
3617
|
interface AssetOptions {
|
|
3330
3618
|
filename?: string;
|
|
@@ -3346,6 +3634,27 @@ interface TakuhonAssetStorage {
|
|
|
3346
3634
|
deleteAsset(assetId: string): Promise<void>;
|
|
3347
3635
|
listAssets(): Promise<AssetRecord[]>;
|
|
3348
3636
|
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Persistence contract for the developer-activity snapshot (see
|
|
3639
|
+
* {@link ActivitySnapshot}). Like {@link TakuhonAssetStorage}, this is a
|
|
3640
|
+
* separate, optional interface so deployments that don't enable the activity
|
|
3641
|
+
* feature can omit it entirely.
|
|
3642
|
+
*
|
|
3643
|
+
* Unlike {@link TakuhonStorage}, there is no optimistic-locking `version`: the
|
|
3644
|
+
* snapshot is machine-written by a sync step (a CLI command or a scheduled job)
|
|
3645
|
+
* with last-writer-wins semantics and is never concurrently hand-edited.
|
|
3646
|
+
*/
|
|
3647
|
+
interface ActivityStorage {
|
|
3648
|
+
/**
|
|
3649
|
+
* Read the current activity snapshot, or `null` when none has been synced
|
|
3650
|
+
* yet. Returns `null` rather than throwing {@link NotFoundError}: an absent
|
|
3651
|
+
* snapshot is the normal opt-out / pre-sync state, and the renderer omits the
|
|
3652
|
+
* activity section gracefully when it is `null`.
|
|
3653
|
+
*/
|
|
3654
|
+
getActivitySnapshot(): Promise<ActivitySnapshot | null>;
|
|
3655
|
+
/** Replace the stored snapshot (last-writer-wins; no precondition). */
|
|
3656
|
+
saveActivitySnapshot(snapshot: ActivitySnapshot): Promise<void>;
|
|
3657
|
+
}
|
|
3349
3658
|
/**
|
|
3350
3659
|
* Base class for errors thrown by storage adapters. Catch this to handle
|
|
3351
3660
|
* any storage-layer failure uniformly; check `instanceof` of a subclass
|
|
@@ -3378,6 +3687,182 @@ declare class ConflictError extends StorageError {
|
|
|
3378
3687
|
});
|
|
3379
3688
|
}
|
|
3380
3689
|
|
|
3690
|
+
/**
|
|
3691
|
+
* Self-owned inline SVG rendering of an {@link ActivitySnapshot}.
|
|
3692
|
+
*
|
|
3693
|
+
* The dashboard is drawn from the stored snapshot only — no external badge
|
|
3694
|
+
* image, no network access — so a page embedding it keeps a strict
|
|
3695
|
+
* `img-src 'self'` Content-Security-Policy. The output is a deterministic pure
|
|
3696
|
+
* string: no clock, no randomness, no locale-dependent formatting — the same
|
|
3697
|
+
* snapshot always renders the same markup, so both rendering surfaces (the
|
|
3698
|
+
* static HTML export and the React profile) and their tests stay in lockstep.
|
|
3699
|
+
*
|
|
3700
|
+
* Every snapshot-derived string (language names, dates) is XML-escaped before
|
|
3701
|
+
* it reaches the markup: language names come from an external API and are
|
|
3702
|
+
* treated as untrusted. An empty snapshot (no metric fields) renders to `''`
|
|
3703
|
+
* so callers can omit the section with a truthiness check.
|
|
3704
|
+
*/
|
|
3705
|
+
|
|
3706
|
+
/**
|
|
3707
|
+
* Render the activity snapshot as a self-contained `<svg>` string, or `''`
|
|
3708
|
+
* when the snapshot carries no metric data (so callers can omit the section).
|
|
3709
|
+
* Sections render independently — whatever the snapshot holds is shown, the
|
|
3710
|
+
* rest is left out — and `lastSyncedAt`'s date is always stamped in the footer
|
|
3711
|
+
* so staleness stays visible.
|
|
3712
|
+
*/
|
|
3713
|
+
declare function renderActivitySvg(snapshot: ActivitySnapshot): string;
|
|
3714
|
+
|
|
3715
|
+
/**
|
|
3716
|
+
* Résumé / CV projection of a profile.
|
|
3717
|
+
*
|
|
3718
|
+
* {@link deriveCv} turns one locale-resolved {@link LocalizedTakuhon} into a
|
|
3719
|
+
* {@link CvDocument}: a header plus the CV-relevant sections in a fixed,
|
|
3720
|
+
* résumé-conventional order. It is a pure, deterministic selection-and-projection
|
|
3721
|
+
* — no clock, no randomness, no I/O — so the static export ({@link
|
|
3722
|
+
* import('@takuhon/cli')}) and a React `CvView` render identical output from the
|
|
3723
|
+
* same input.
|
|
3724
|
+
*
|
|
3725
|
+
* A CV is a curated *subset* of the profile: the web-page-specific sections
|
|
3726
|
+
* (links, recommendations, the activity dashboard, test scores) are omitted,
|
|
3727
|
+
* and the rest appear in the order a résumé conventionally uses. Entry order
|
|
3728
|
+
* within a section is preserved from the input (already normalized by `order`
|
|
3729
|
+
* upstream), so the owner's `order` field controls it — `deriveCv` never
|
|
3730
|
+
* re-sorts. Empty sections are dropped so the renderer can iterate without
|
|
3731
|
+
* emptiness checks.
|
|
3732
|
+
*
|
|
3733
|
+
* The input must already be privacy-filtered (`applyPublicPrivacyFilter`) by the
|
|
3734
|
+
* caller, exactly as the public render path is: `deriveCv` does not strip
|
|
3735
|
+
* anything itself, it only projects what it is given.
|
|
3736
|
+
*/
|
|
3737
|
+
|
|
3738
|
+
/** The header block of a CV: identity and contact, drawn from `profile` + `contact`. */
|
|
3739
|
+
interface CvHeader {
|
|
3740
|
+
displayName: string;
|
|
3741
|
+
tagline?: string;
|
|
3742
|
+
/** Human-readable location (the address `display` string), if present. */
|
|
3743
|
+
location?: string;
|
|
3744
|
+
bio?: string;
|
|
3745
|
+
/** Email, only when the profile exposed it (privacy filter already applied). */
|
|
3746
|
+
email?: string;
|
|
3747
|
+
formUrl?: string;
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* One CV section, discriminated by `kind`, carrying the locale-resolved entries
|
|
3751
|
+
* for that section. The entry types are reused verbatim from `LocalizedTakuhon`.
|
|
3752
|
+
*/
|
|
3753
|
+
type CvSection = {
|
|
3754
|
+
kind: 'experience';
|
|
3755
|
+
entries: LocalizedCareer[];
|
|
3756
|
+
} | {
|
|
3757
|
+
kind: 'education';
|
|
3758
|
+
entries: LocalizedEducation[];
|
|
3759
|
+
} | {
|
|
3760
|
+
kind: 'skills';
|
|
3761
|
+
entries: Skill[];
|
|
3762
|
+
} | {
|
|
3763
|
+
kind: 'certifications';
|
|
3764
|
+
entries: LocalizedCertification[];
|
|
3765
|
+
} | {
|
|
3766
|
+
kind: 'publications';
|
|
3767
|
+
entries: LocalizedPublication[];
|
|
3768
|
+
} | {
|
|
3769
|
+
kind: 'honors';
|
|
3770
|
+
entries: LocalizedHonor[];
|
|
3771
|
+
} | {
|
|
3772
|
+
kind: 'courses';
|
|
3773
|
+
entries: LocalizedCourse[];
|
|
3774
|
+
} | {
|
|
3775
|
+
kind: 'patents';
|
|
3776
|
+
entries: LocalizedPatent[];
|
|
3777
|
+
} | {
|
|
3778
|
+
kind: 'languages';
|
|
3779
|
+
entries: LocalizedLanguage[];
|
|
3780
|
+
} | {
|
|
3781
|
+
kind: 'volunteering';
|
|
3782
|
+
entries: LocalizedVolunteering[];
|
|
3783
|
+
} | {
|
|
3784
|
+
kind: 'memberships';
|
|
3785
|
+
entries: LocalizedMembership[];
|
|
3786
|
+
};
|
|
3787
|
+
/** Every `CvSection` discriminant, in the fixed résumé order `deriveCv` emits. */
|
|
3788
|
+
type CvSectionKind = CvSection['kind'];
|
|
3789
|
+
/** A résumé/CV view derived from a profile: a header plus ordered sections. */
|
|
3790
|
+
interface CvDocument {
|
|
3791
|
+
/** The locale this CV was resolved at (mirrors `LocalizedTakuhon.resolvedLocale`). */
|
|
3792
|
+
resolvedLocale: string;
|
|
3793
|
+
header: CvHeader;
|
|
3794
|
+
/** Non-empty sections only, in the fixed CV order. */
|
|
3795
|
+
sections: CvSection[];
|
|
3796
|
+
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Project a locale-resolved profile into a {@link CvDocument}. Sections appear
|
|
3799
|
+
* in a fixed résumé-conventional order and empty ones are omitted; entry order
|
|
3800
|
+
* within each section is preserved from the input. Pure and deterministic.
|
|
3801
|
+
*/
|
|
3802
|
+
declare function deriveCv(localized: LocalizedTakuhon): CvDocument;
|
|
3803
|
+
|
|
3804
|
+
/**
|
|
3805
|
+
* Image-asset helpers for uploaded media (avatars, project images): magic-byte
|
|
3806
|
+
* type detection, header-only dimension / frame reading, and metadata stripping
|
|
3807
|
+
* (EXIF / IPTC / XMP / color profile) for the four MIME types takuhon accepts.
|
|
3808
|
+
*
|
|
3809
|
+
* Every function parses only the container structure — it never decodes pixel
|
|
3810
|
+
* data — so the module is tiny, dependency-free, and runs identically on
|
|
3811
|
+
* Cloudflare Workers, Node, and in the browser. This is the deliberate
|
|
3812
|
+
* "no codec, no re-encode" approach: `security.md` §4 requires metadata removal
|
|
3813
|
+
* for privacy (EXIF GPS / device data on avatars) but leaves resizing to the UI
|
|
3814
|
+
* (§4.3 "aspect ratio cropped UI-side"), so no image codec is needed.
|
|
3815
|
+
*
|
|
3816
|
+
* AVIF is intentionally not handled yet — its ISO-BMFF metadata layout is more
|
|
3817
|
+
* involved — so {@link detectImageMime} returns `null` for it and uploads of it
|
|
3818
|
+
* are rejected until a later phase adds support.
|
|
3819
|
+
*
|
|
3820
|
+
* The functions assume well-formed input only as far as security requires:
|
|
3821
|
+
* {@link detectImageMime} authenticates the type from the leading bytes (so a
|
|
3822
|
+
* spoofed `Content-Type` cannot smuggle, say, an SVG), {@link readImageInfo}
|
|
3823
|
+
* returns `null` when it cannot find the dimensions, and
|
|
3824
|
+
* {@link stripImageMetadata} preserves the pixel data while dropping metadata
|
|
3825
|
+
* segments. A truncated or malformed file is the uploader's problem — the bytes
|
|
3826
|
+
* are stored and served verbatim, never decoded server-side.
|
|
3827
|
+
*/
|
|
3828
|
+
/** MIME types accepted for uploaded images (`security.md` §4.1, minus AVIF). */
|
|
3829
|
+
declare const ACCEPTED_IMAGE_MIME_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
3830
|
+
type AcceptedImageMime = (typeof ACCEPTED_IMAGE_MIME_TYPES)[number];
|
|
3831
|
+
/** Maximum accepted upload size, in bytes (`security.md` §4.3). */
|
|
3832
|
+
declare const MAX_IMAGE_BYTES: number;
|
|
3833
|
+
/** Maximum accepted width or height, in pixels (`security.md` §4.3). */
|
|
3834
|
+
declare const MAX_IMAGE_DIMENSION = 4096;
|
|
3835
|
+
/** Maximum accepted animation frame count (`security.md` §4.4). */
|
|
3836
|
+
declare const MAX_IMAGE_FRAMES = 100;
|
|
3837
|
+
/** Canonical file extension per accepted MIME, used in object keys. */
|
|
3838
|
+
declare const IMAGE_EXTENSIONS: Record<AcceptedImageMime, string>;
|
|
3839
|
+
/** Dimensions and animation frame count read from an image's container header. */
|
|
3840
|
+
interface ImageInfo {
|
|
3841
|
+
width: number;
|
|
3842
|
+
height: number;
|
|
3843
|
+
/** 1 for a still image; the frame count for an animated GIF / WebP / APNG. */
|
|
3844
|
+
frames: number;
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Identify an accepted image type from its leading bytes, independent of any
|
|
3848
|
+
* declared `Content-Type` (`security.md` §4.2). Returns `null` for unrecognized
|
|
3849
|
+
* or unsupported (e.g. AVIF, SVG) input.
|
|
3850
|
+
*/
|
|
3851
|
+
declare function detectImageMime(bytes: Uint8Array): AcceptedImageMime | null;
|
|
3852
|
+
/**
|
|
3853
|
+
* Read an image's dimensions and animation frame count from its container
|
|
3854
|
+
* header alone (no pixel decode). Returns `null` when the structure cannot be
|
|
3855
|
+
* parsed. `mime` must come from {@link detectImageMime}.
|
|
3856
|
+
*/
|
|
3857
|
+
declare function readImageInfo(bytes: Uint8Array, mime: AcceptedImageMime): ImageInfo | null;
|
|
3858
|
+
/**
|
|
3859
|
+
* Remove metadata (EXIF / IPTC / XMP / embedded color profile / comments) from
|
|
3860
|
+
* an image while preserving its pixel data, by editing only the container
|
|
3861
|
+
* structure (`security.md` §4.5). `mime` must come from {@link detectImageMime}.
|
|
3862
|
+
* Returns the original bytes unchanged when there is no metadata to remove.
|
|
3863
|
+
*/
|
|
3864
|
+
declare function stripImageMetadata(bytes: Uint8Array, mime: AcceptedImageMime): Uint8Array;
|
|
3865
|
+
|
|
3381
3866
|
/**
|
|
3382
3867
|
* @takuhon/core — canonical JSON Schema, hand-written TypeScript types,
|
|
3383
3868
|
* Ajv-backed validation, document normalization, and locale resolution for
|
|
@@ -3414,6 +3899,6 @@ declare class ConflictError extends StorageError {
|
|
|
3414
3899
|
* A takuhon profile document's `schemaVersion` field must be migrate-compatible
|
|
3415
3900
|
* with this version. See operational-lifecycle docs for the migration policy.
|
|
3416
3901
|
*/
|
|
3417
|
-
declare const SCHEMA_VERSION = "0.
|
|
3902
|
+
declare const SCHEMA_VERSION = "0.5.0";
|
|
3418
3903
|
|
|
3419
|
-
export { type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, type Certification, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type Course, type Education, type ExportOptions, type ExportedTakuhon, type Honor, ImportError, type Iso3166Alpha2, type IsoDateTime, type Language, type LanguageProficiency, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedCertification, type LocalizedCourse, type LocalizedEducation, type LocalizedHonor, type LocalizedLanguage, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedMembership, type LocalizedPatent, type LocalizedProfile, type LocalizedProject, type LocalizedPublication, type LocalizedRecommendation, type LocalizedRecommendationAuthor, type LocalizedTakuhon, type LocalizedTestScore, type LocalizedTitle, type LocalizedVolunteering, type Membership, type Meta, type MetaPrivacy, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Patent, type PatentStatus, type Profile, type Project, type Publication, type Recommendation, type RecommendationAuthor, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type TestScore, type ValidationError, type ValidationResult, type Volunteering, type YearMonth, applyPublicPrivacyFilter, exportTakuhon, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, migrateTakuhon, migrations, normalize, resolveLocale, schema, validate };
|
|
3904
|
+
export { ACCEPTED_IMAGE_MIME_TYPES, type AcceptedImageMime, type ActivitySettings, type ActivitySnapshot, type ActivityStorage, type Address, type AssetOptions, type AssetRecord, type Avatar, type Career, type Certification, type CodingTime, ConflictError, type Contact, type ContentLicense, type ContentLicenseAttribution, type ContributionCalendar, type ContributionDay, type Course, type CvDocument, type CvHeader, type CvSection, type CvSectionKind, type Education, type ExportOptions, type ExportedTakuhon, type Honor, IMAGE_EXTENSIONS, type ImageInfo, ImportError, type Iso3166Alpha2, type IsoDateTime, type Language, type LanguageBreakdown, type LanguageProficiency, type Link, type LinkBuiltin, type LinkCustom, type LinkType, type LocaleTag, type LocalizedAddress, type LocalizedAvatar, type LocalizedBody, type LocalizedCareer, type LocalizedCertification, type LocalizedCourse, type LocalizedEducation, type LocalizedHonor, type LocalizedLanguage, type LocalizedLink, type LocalizedLinkBuiltin, type LocalizedLinkCustom, type LocalizedMembership, type LocalizedPatent, type LocalizedProfile, type LocalizedProject, type LocalizedPublication, type LocalizedRecommendation, type LocalizedRecommendationAuthor, type LocalizedTakuhon, type LocalizedTestScore, type LocalizedTitle, type LocalizedVolunteering, MAX_IMAGE_BYTES, MAX_IMAGE_DIMENSION, MAX_IMAGE_FRAMES, type Membership, type Meta, type MetaPrivacy, type Migration, MigrationError, type NormalizedTakuhon, NotFoundError, type Patent, type PatentStatus, type Profile, type Project, type Publication, RANK_FULL_CODING_HOURS, RANK_FULL_CONTRIBUTIONS, RANK_TIER_THRESHOLDS, type RankInput, type RankTier, type RankTierLabel, type Recommendation, type RecommendationAuthor, SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, type Schema, type Settings, type Skill, type Slug, StorageError, type Takuhon, type TakuhonAssetStorage, type TakuhonStorage, type TestScore, type ValidationError, type ValidationResult, type Volunteering, type YearMonth, applyPublicPrivacyFilter, computeLanguagePercentages, deriveCv, deriveRankTier, detectImageMime, exportTakuhon, formatCodingTime, generateJsonLd, generatePersonJsonLd, generateProfilePageJsonLd, importTakuhon, isActivitySnapshot, migrateTakuhon, migrations, normalize, readImageInfo, renderActivitySvg, resolveLocale, schema, stripImageMetadata, validate };
|