@gitkraken/milestones-tools 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/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-07-09
10
+
11
+ ### Added
12
+
13
+ - Initial scaffold of `@gitkraken/milestones-tools`, the v1 client library for the milestone and demographic surface of identity-service.
14
+ - `MilestoneClient` (via `createMilestoneClient`) - loads achieved milestones (cached through the injected `StoragePort`), observes host actions against the embedded manifest, enqueues newly-detected milestones, flushes them to identity-service, and fires `onConfirmed` callbacks on acceptance.
15
+ - `DemographicClient` (via `createDemographicClient`) - reads and writes the user's demographic metadata and the enum sets that constrain each field.
16
+ - `createMilestonesClient` - one-call factory that wires both clients to a shared `MilestonesSdkConfig`.
17
+ - Embedded canonical manifest (the v1 event taxonomy: `commit_created`, `branch_created`, `pull_request_created`, `ai_feature_used`) with `eventForMilestone` / `eventByName` helpers, validated with zod at module load.
18
+ - Request-function transport: the host injects a `request(options) => { body, status, headers }` function (the GitKraken house pattern, e.g. `@gitkraken/provider-apis` via `contextFreeMakeRequest`); when omitted, a default `fetch`-based requester is used. The transport attaches a fresh bearer token per request and maps non-2xx (or enveloped-error) responses to a typed `HttpError`. String bodies are JSON-coerced before envelope unwrapping.
19
+ - Durable, at-least-once action queue persisted through the `StoragePort`, with linear backoff retries and drop-on-422 (the milestone is not active yet, so backfill covers it).
20
+ - Transport unwraps the identity-service `{ data, error }` response envelope (surfacing a non-null `error` as `HttpError`); `loadMilestones` maps the returned milestone array into the achieved map. Verified end-to-end against the dev environment.
21
+
22
+ ### Fixed
23
+
24
+ - Serialized the cache read-modify-write sections of `recordAchieved` and `loadMilestones` so a confirmation and a refresh can no longer interleave and drop one another's writes to the achieved-state key; `loadMilestones` now merges server rows into the cached state instead of overwriting it.
25
+ - A failed send no longer maps back to `done` via its bare status: a thrown `HttpError` (including a 2xx whose envelope carried an `error`) is never recorded as achieved. Terminal statuses (the 422 gate and other 4xx) drop instead of retrying to exhaustion; only transient statuses (408, 429, 5xx) retry.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Core Tools License
2
+
3
+ Copyright (c) 2025-2026 Axosoft, LLC dba GitKraken ("GitKraken")
4
+
5
+ This software and associated documentation files (the "Software") may be compiled as part of GitKraken software products, including the gitkraken/vscode-gitlens open source project (collectively, the "GitKraken Products"), to the extent the Software is a required component of the GitKraken Products; provided, however, that the Software and its functionality may only be used if you (and any entity that you represent) have agreed to, and are in compliance with, the GitKraken End User License Agreement, available at https://gitkraken.com/eula (the "EULA"), or other agreement governing the use of the Software, as agreed by you and GitKraken, and otherwise have a valid subscription for the correct number of user seats for the applicable version of the Software (e.g., GitLens Free+, GitLens Pro, GitLens Teams, and GitLens Enterprise).
6
+
7
+ The Software is licensed, not sold. This license only gives you some rights to use the Software. GitKraken reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the Software only as expressly permitted in this license. In doing so, you must comply with any technical limitations in the Software that only allow you to use it in certain ways. You may not (i) work around any technical limitations in the Software, (ii) reverse engineer, decompile or disassemble the Software, or otherwise attempt to derive the source code for the Software, except and to the extent required by third party licensing terms governing use of certain open source components that may be included with the Software, (iii) remove, minimize, block or modify any notices of GitKraken or its suppliers in the Software, (iv) use the software in any way that is against the law, (v) host, share, publish, rent or lease the Software; (vi) distribute the Software as a stand-alone or integrated offering or combine it with any of your applications for others to use, (vii) use the Software other than in connection with validly licensed GitKraken Products or (viii) use any portion of the Software to create software with the same or similar functionality.
8
+
9
+ You agree that GitKraken and/or its licensors (as applicable) retain all right, title and interest in and to the Software and all modifications and/or patches thereto. You are not granted any other rights beyond what is expressly stated herein. Except as set forth above, it is forbidden to copy, merge, publish, distribute, sublicense, modify and/or sell the Software.
10
+
11
+ If you give feedback about the Software to GitKraken, you give to GitKraken, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires GitKraken to license its software or documentation to third parties because we include your feedback in them. These rights survive this License.
12
+
13
+ The full text of this Core Tools License shall be included in all copies or substantial portions of the Software.
14
+
15
+ This License and the EULA, if applicable, are the entire agreement for the Software. You must comply with all domestic and international export laws and regulations that apply to the Software, which include restrictions on destinations, end users and end use. This License is governed and construed by the laws of the State of Arizona, United States without regard to any choice or conflict of law principles or rules.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL GITKRAKEN, THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
+
19
+ For all third party components incorporated into the Software, those components are licensed under the original license provided by the owner of the applicable component.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # @gitkraken/milestones-tools
2
+
3
+ Client library for GitKraken user milestones and demographic metadata, served by identity-service.
4
+
5
+ ## What it is
6
+
7
+ This package wraps the v1 milestone and demographic surface of identity-service in a runtime-agnostic client. It does two things:
8
+
9
+ - **Milestones** - detects when a user earns a milestone (first commit, first branch, first PR, first AI event) from actions the host already observes, queues each detection durably, and confirms it with identity-service. Detection runs against an embedded copy of the canonical event manifest, so the client and the server agree on the taxonomy without a round-trip.
10
+ - **Demographics** - reads and writes the user's demographic metadata and the enum sets that constrain each field.
11
+
12
+ The library never performs HTTP itself, never reads a token from ambient state, and never persists anything directly. The host injects all three: a `request` function (the same `{ body, status, headers }` shape GitKraken's other client SDKs use, e.g. `@gitkraken/provider-apis`), a token getter, and a storage port.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @gitkraken/milestones-tools
18
+ ```
19
+
20
+ ## Provide the ports
21
+
22
+ Everything the library needs from the outside world is passed through `MilestonesSdkConfig`:
23
+
24
+ ```typescript
25
+ import { createMilestonesClient } from '@gitkraken/milestones-tools';
26
+
27
+ const client = createMilestonesClient({
28
+ baseUrl: 'https://api.gitkraken.dev',
29
+ // Identifies the host surface reporting milestones (e.g. 'gitlens').
30
+ sourceClient: 'gitlens',
31
+ // Injected request function - omit to use a default fetch-based requester.
32
+ // Plug in your own (e.g. GitKraken's contextFreeMakeRequest) to own the network.
33
+ // `headers` is a Web `Headers` instance (case-insensitive lookups). A fetch
34
+ // response already exposes one; wrap a plain object with `new Headers(obj)`.
35
+ request: async ({ method, url, headers, body }) => {
36
+ const response = await myHttp({ method, url, headers, body });
37
+ return { body: response.body, status: response.status, headers: new Headers(response.headers) };
38
+ },
39
+ // Called per request to supply a fresh bearer token.
40
+ getToken: async () => await myAuth.getAccessToken(),
41
+ // Durable key/value store - back it with disk, localStorage, etc.
42
+ storage: {
43
+ get: async (key) => myStore.get(key) ?? null,
44
+ set: async (key, value) => myStore.set(key, value),
45
+ delete: async (key) => myStore.delete(key),
46
+ },
47
+ // Optional - how long loadMilestones() trusts its cache (default 5 min).
48
+ cacheTtlMs: 5 * 60 * 1000,
49
+ // Optional structured logger.
50
+ logger: console,
51
+ });
52
+ ```
53
+
54
+ `StoragePort` is the only stateful dependency. The library uses it to cache milestone state and to persist the pending-action queue, so detections survive process restarts and are delivered at least once.
55
+
56
+ ## Milestones
57
+
58
+ ```typescript
59
+ const { milestones } = client;
60
+
61
+ // Load the user's achieved milestones (cached for cacheTtlMs).
62
+ const state = await milestones.loadMilestones();
63
+ console.log(Object.keys(state.achieved));
64
+
65
+ // Get notified when a queued milestone is confirmed by the server.
66
+ const unsubscribe = milestones.onConfirmed((milestoneId) => {
67
+ showCelebration(milestoneId);
68
+ });
69
+
70
+ // Feed actions the host already observes. The client maps each action to a
71
+ // milestone via the embedded manifest, skips ones already achieved, enqueues
72
+ // the rest, and flushes to identity-service.
73
+ await milestones.observe({
74
+ type: 'commit_created',
75
+ at: new Date().toISOString(),
76
+ payload: { surface_name: 'gitlens', kind: 'create' },
77
+ });
78
+
79
+ // Cheap local check against the cached state.
80
+ if (await milestones.isAchieved('first_commit')) {
81
+ hideFirstCommitHint();
82
+ }
83
+
84
+ unsubscribe();
85
+ ```
86
+
87
+ A `commit_created` with `kind` of `cherrypick` or `revert` does not earn `first_commit` - the embedded manifest's `milestone_filter` only credits `create` and `amend`. Unknown action types are ignored.
88
+
89
+ ## Demographics
90
+
91
+ ```typescript
92
+ const { demographic } = client;
93
+
94
+ const enums = await demographic.getEnums();
95
+ const current = await demographic.get();
96
+ const updated = await demographic.set({ role: 'softwareEngineer', gitComfortLevel: 'very' });
97
+ ```
98
+
99
+ ## Canonical manifest
100
+
101
+ The canonical v1 event taxonomy is embedded in the package and exported as `manifest`, alongside the `eventForMilestone(milestoneId)` and `eventByName(name)` helpers. It is validated with zod at module load, so a malformed manifest fails fast rather than producing silent mis-detections.
102
+
103
+ ```typescript
104
+ import { manifest, eventForMilestone } from '@gitkraken/milestones-tools';
105
+
106
+ console.log(manifest.events.map((event) => event.event_name));
107
+ console.log(eventForMilestone('first_commit')?.event_name); // 'commit_created'
108
+ ```
109
+
110
+ ## Contributing
111
+
112
+ This package follows the monorepo's standard tooling - see the root [CONTRIBUTING.md](../../CONTRIBUTING.md). Local commands:
113
+
114
+ ```bash
115
+ pnpm --filter @gitkraken/milestones-tools build
116
+ pnpm --filter @gitkraken/milestones-tools typecheck
117
+ pnpm --filter @gitkraken/milestones-tools test
118
+ ```
@@ -0,0 +1,14 @@
1
+ import { MilestonesSdkConfig } from "./config.js";
2
+ import { DemographicClient } from "./demographic/demographic-client.js";
3
+ import { MilestoneClient } from "./milestones/milestone-client.js";
4
+
5
+ //#region src/client.d.ts
6
+ /** The milestone and demographic clients, wired to one shared config. */
7
+ interface MilestonesClient {
8
+ milestones: MilestoneClient;
9
+ demographic: DemographicClient;
10
+ }
11
+ /** Constructs both the milestone and demographic clients from one SDK config. */
12
+ declare function createMilestonesClient(config: MilestonesSdkConfig): MilestonesClient;
13
+ //#endregion
14
+ export { MilestonesClient, createMilestonesClient };
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ import{createDemographicClient as e}from"./demographic/demographic-client.js";import{createMilestoneClient as t}from"./milestones/milestone-client.js";function n(n){return{milestones:t(n),demographic:e(n)}}export{n as createMilestonesClient};
@@ -0,0 +1,37 @@
1
+ import { ApiClientConfig } from "@gitkraken/api-tools";
2
+
3
+ //#region src/config.d.ts
4
+ /**
5
+ * Durable key/value store the library uses to cache milestone state and hold
6
+ * the pending-action queue. Implemented by the host (e.g. backed by disk,
7
+ * `localStorage`, or an in-memory map for tests).
8
+ */
9
+ interface StoragePort {
10
+ get(key: string): Promise<string | null>;
11
+ set(key: string, value: string): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ }
14
+ /** The host surfaces identity-service accepts as a milestone `sourceClient`. */
15
+ declare const SourceClient: {
16
+ readonly GitKrakenClient: "GitKraken Client";
17
+ readonly GitLens: "gitlens";
18
+ readonly GitKrakenCliV2: "GitKraken CLI V2";
19
+ };
20
+ type SourceClient = (typeof SourceClient)[keyof typeof SourceClient];
21
+ /**
22
+ * Configuration for the milestones and demographic clients. Composes the
23
+ * transport config from `@gitkraken/api-tools` (`baseUrl`, `getToken`,
24
+ * optional `request`/`logger`) and adds the milestone-specific fields.
25
+ *
26
+ * `request` is injected so the host owns the network (proxies, on-prem hosts,
27
+ * custom TLS); when omitted the library uses a default `fetch`-based requester.
28
+ * `getToken` is called per request to supply a fresh bearer token.
29
+ */
30
+ interface MilestonesSdkConfig extends ApiClientConfig {
31
+ /** Identifies the host surface reporting milestones (e.g. `'gitlens'`). */
32
+ sourceClient: SourceClient;
33
+ storage: StoragePort;
34
+ cacheTtlMs?: number;
35
+ }
36
+ //#endregion
37
+ export { MilestonesSdkConfig, SourceClient, StoragePort };
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ const e={GitKrakenClient:`GitKraken Client`,GitLens:`gitlens`,GitKrakenCliV2:`GitKraken CLI V2`};export{e as SourceClient};
@@ -0,0 +1,22 @@
1
+ import { MilestonesSdkConfig } from "../config.js";
2
+ import { DemographicEnums, DemographicInput, UserDemographic } from "./types.js";
3
+
4
+ //#region src/demographic/demographic-client.d.ts
5
+ /**
6
+ * Client for the v1 demographic surface of identity-service.
7
+ *
8
+ * Reads and writes the user's demographic metadata and the enum sets that
9
+ * constrain each field. Every call carries a bearer token.
10
+ */
11
+ declare class DemographicClient {
12
+ private readonly transport;
13
+ constructor(config: MilestonesSdkConfig);
14
+ getEnums(signal?: AbortSignal): Promise<DemographicEnums>;
15
+ get(signal?: AbortSignal): Promise<UserDemographic>;
16
+ set(input: DemographicInput, signal?: AbortSignal): Promise<UserDemographic>;
17
+ private requireBody;
18
+ }
19
+ /** Constructs a {@link DemographicClient} from the SDK config. */
20
+ declare function createDemographicClient(config: MilestonesSdkConfig): DemographicClient;
21
+ //#endregion
22
+ export { DemographicClient, createDemographicClient };
@@ -0,0 +1 @@
1
+ import{demographicEnumsSchema as e,userDemographicSchema as t}from"./schemas.js";import{HttpError as n,Transport as r}from"@gitkraken/api-tools";var i=class{transport;constructor(e){this.transport=new r(e)}async getEnums(t){return this.requireBody({method:`GET`,path:`/v1/demographic/enums`,signal:t,parse:t=>e.parse(t)})}async get(e){return this.requireBody({method:`GET`,path:`/v1/demographic`,signal:e,parse:e=>t.parse(e)})}async set(e,n){return this.requireBody({method:`POST`,path:`/v1/demographic`,body:e,signal:n,parse:e=>t.parse(e)})}async requireBody(e){let{data:t,status:r,headers:i}=await this.transport.request(e);if(t===void 0)throw new n(r,null,i);return t}};function a(e){return new i(e)}export{i as DemographicClient,a as createDemographicClient};
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/demographic/schemas.d.ts
4
+ declare const userDemographicSchema: z.ZodObject<{
5
+ userId: z.ZodString;
6
+ role: z.ZodOptional<z.ZodEnum<{
7
+ softwareEngineer: "softwareEngineer";
8
+ engineeringManager: "engineeringManager";
9
+ devOps: "devOps";
10
+ dataAi: "dataAi";
11
+ designer: "designer";
12
+ productManager: "productManager";
13
+ student: "student";
14
+ educator: "educator";
15
+ hobbyist: "hobbyist";
16
+ other: "other";
17
+ none: "none";
18
+ }>>;
19
+ roleUpdatedAt: z.ZodOptional<z.ZodString>;
20
+ gitComfortLevel: z.ZodOptional<z.ZodEnum<{
21
+ notAtAll: "notAtAll";
22
+ somewhat: "somewhat";
23
+ very: "very";
24
+ extremely: "extremely";
25
+ }>>;
26
+ gitComfortLevelUpdatedAt: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>;
28
+ declare const demographicEnumsSchema: z.ZodObject<{
29
+ role: z.ZodArray<z.ZodEnum<{
30
+ softwareEngineer: "softwareEngineer";
31
+ engineeringManager: "engineeringManager";
32
+ devOps: "devOps";
33
+ dataAi: "dataAi";
34
+ designer: "designer";
35
+ productManager: "productManager";
36
+ student: "student";
37
+ educator: "educator";
38
+ hobbyist: "hobbyist";
39
+ other: "other";
40
+ none: "none";
41
+ }>>;
42
+ gitComfortLevel: z.ZodArray<z.ZodEnum<{
43
+ notAtAll: "notAtAll";
44
+ somewhat: "somewhat";
45
+ very: "very";
46
+ extremely: "extremely";
47
+ }>>;
48
+ }, z.core.$strip>;
49
+ //#endregion
50
+ export { demographicEnumsSchema, userDemographicSchema };
@@ -0,0 +1 @@
1
+ import{GitComfortLevel as e,UserRole as t}from"./types.js";import{z as n}from"zod";const r=n.enum(Object.values(t)),i=n.enum(Object.values(e)),a=n.object({userId:n.string(),role:r.optional(),roleUpdatedAt:n.string().optional(),gitComfortLevel:i.optional(),gitComfortLevelUpdatedAt:n.string().optional()}),o=n.object({role:n.array(r),gitComfortLevel:n.array(i)});export{o as demographicEnumsSchema,a as userDemographicSchema};
@@ -0,0 +1,47 @@
1
+ //#region src/demographic/types.d.ts
2
+ /** The user's self-reported role, as constrained by identity-service. */
3
+ declare const UserRole: {
4
+ readonly SoftwareEngineer: "softwareEngineer";
5
+ readonly EngineeringManager: "engineeringManager";
6
+ readonly DevOps: "devOps";
7
+ readonly DataAi: "dataAi";
8
+ readonly Designer: "designer";
9
+ readonly ProductManager: "productManager";
10
+ readonly Student: "student";
11
+ readonly Educator: "educator";
12
+ readonly Hobbyist: "hobbyist";
13
+ readonly Other: "other";
14
+ readonly None: "none";
15
+ };
16
+ type UserRole = (typeof UserRole)[keyof typeof UserRole];
17
+ /** The user's self-reported comfort with Git, as constrained by identity-service. */
18
+ declare const GitComfortLevel: {
19
+ readonly NotAtAll: "notAtAll";
20
+ readonly Somewhat: "somewhat";
21
+ readonly Very: "very";
22
+ readonly Extremely: "extremely";
23
+ };
24
+ type GitComfortLevel = (typeof GitComfortLevel)[keyof typeof GitComfortLevel];
25
+ /** The user's stored demographic metadata. */
26
+ interface UserDemographic {
27
+ userId: string;
28
+ role?: UserRole;
29
+ roleUpdatedAt?: string;
30
+ gitComfortLevel?: GitComfortLevel;
31
+ gitComfortLevelUpdatedAt?: string;
32
+ }
33
+ /**
34
+ * The writable subset of {@link UserDemographic} accepted by `set`. Passing
35
+ * `'skip'` for `gitComfortLevel` clears the stored value server-side.
36
+ */
37
+ interface DemographicInput {
38
+ role?: UserRole;
39
+ gitComfortLevel?: GitComfortLevel | 'skip';
40
+ }
41
+ /** The allowed values for each demographic field, as served by identity-service. */
42
+ interface DemographicEnums {
43
+ role: UserRole[];
44
+ gitComfortLevel: GitComfortLevel[];
45
+ }
46
+ //#endregion
47
+ export { DemographicEnums, DemographicInput, GitComfortLevel, UserDemographic, UserRole };
@@ -0,0 +1 @@
1
+ const e={SoftwareEngineer:`softwareEngineer`,EngineeringManager:`engineeringManager`,DevOps:`devOps`,DataAi:`dataAi`,Designer:`designer`,ProductManager:`productManager`,Student:`student`,Educator:`educator`,Hobbyist:`hobbyist`,Other:`other`,None:`none`},t={NotAtAll:`notAtAll`,Somewhat:`somewhat`,Very:`very`,Extremely:`extremely`};export{t as GitComfortLevel,e as UserRole};
@@ -0,0 +1,11 @@
1
+ import { MilestonesSdkConfig, SourceClient, StoragePort } from "./config.js";
2
+ import { DemographicEnums, DemographicInput, GitComfortLevel, UserDemographic, UserRole } from "./demographic/types.js";
3
+ import { DemographicClient, createDemographicClient } from "./demographic/demographic-client.js";
4
+ import { CanonicalEventName, ClientAction, DetectionMethod, MilestoneAchieved, MilestoneId, MilestoneState, Unsubscribe } from "./milestones/types.js";
5
+ import { DropCallback, DropReason, DurableQueue, QueueItem, SendOutcome, outcomeForError, outcomeForStatus } from "./milestones/queue.js";
6
+ import { ConfirmedCallback, DroppedCallback, MilestoneClient, createMilestoneClient } from "./milestones/milestone-client.js";
7
+ import { MilestonesClient, createMilestonesClient } from "./client.js";
8
+ import { CanonicalEvent, Manifest, PayloadSchema, Surface, eventByName, eventForMilestone, manifest } from "./manifest/manifest.js";
9
+ import { detect } from "./milestones/detection.js";
10
+ import { ApiClientConfig, ApiRequest, ApiRequest as MilestonesRequest, ApiRequestOptions, ApiRequestOptions as MilestonesRequestOptions, ApiResponse, ApiResponse as MilestonesResponse, HttpError, LoggerPort, RequestInput } from "@gitkraken/api-tools";
11
+ export { type ApiClientConfig, type ApiRequest, type ApiRequestOptions, type ApiResponse, type CanonicalEvent, CanonicalEventName, type ClientAction, type ConfirmedCallback, DemographicClient, type DemographicEnums, type DemographicInput, DetectionMethod, type DropCallback, type DropReason, type DroppedCallback, DurableQueue, GitComfortLevel, HttpError, type LoggerPort, type Manifest, type MilestoneAchieved, MilestoneClient, MilestoneId, type MilestoneState, type MilestonesClient, type MilestonesRequest, type MilestonesRequestOptions, type MilestonesResponse, type MilestonesSdkConfig, type PayloadSchema, type QueueItem, type RequestInput, type SendOutcome, SourceClient, type StoragePort, type Surface, type Unsubscribe, type UserDemographic, UserRole, createDemographicClient, createMilestoneClient, createMilestonesClient, detect, eventByName, eventForMilestone, manifest, outcomeForError, outcomeForStatus };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{GitComfortLevel as e,UserRole as t}from"./demographic/types.js";import{DemographicClient as n,createDemographicClient as r}from"./demographic/demographic-client.js";import{eventByName as i,eventForMilestone as a,manifest as o}from"./manifest/manifest.js";import{detect as s}from"./milestones/detection.js";import{DurableQueue as c,outcomeForError as l,outcomeForStatus as u}from"./milestones/queue.js";import{CanonicalEventName as d,DetectionMethod as f,MilestoneId as p}from"./milestones/types.js";import{MilestoneClient as m,createMilestoneClient as h}from"./milestones/milestone-client.js";import{createMilestonesClient as g}from"./client.js";import{SourceClient as _}from"./config.js";import{HttpError as v}from"@gitkraken/api-tools";export{d as CanonicalEventName,n as DemographicClient,f as DetectionMethod,c as DurableQueue,e as GitComfortLevel,v as HttpError,m as MilestoneClient,p as MilestoneId,_ as SourceClient,t as UserRole,r as createDemographicClient,h as createMilestoneClient,g as createMilestonesClient,s as detect,i as eventByName,a as eventForMilestone,o as manifest,l as outcomeForError,u as outcomeForStatus};
@@ -0,0 +1,43 @@
1
+ import { CanonicalEventName, MilestoneId } from "../milestones/types.js";
2
+
3
+ //#region src/manifest/manifest.d.ts
4
+ /** A milestone-emitting surface (GitKraken Client, GitLens, CLI V2). */
5
+ interface Surface {
6
+ id: string;
7
+ service_name: string;
8
+ source_client: string;
9
+ }
10
+ /** A JSON-Schema-ish payload contract for a canonical event. */
11
+ interface PayloadSchema {
12
+ type: 'object';
13
+ required: string[];
14
+ additionalProperties: boolean;
15
+ properties: Record<string, unknown>;
16
+ }
17
+ /**
18
+ * One canonical event in the v1 milestone taxonomy. `milestone_filter`
19
+ * narrows which payloads count toward the milestone (e.g. only commit kinds
20
+ * `create`/`amend` earn `first_commit`).
21
+ */
22
+ interface CanonicalEvent {
23
+ event_name: CanonicalEventName;
24
+ milestone: MilestoneId;
25
+ description: string;
26
+ payload_schema: PayloadSchema;
27
+ milestone_filter: Record<string, string[]> | null;
28
+ }
29
+ /** The canonical manifest the client uses for real-time milestone detection. */
30
+ interface Manifest {
31
+ version: string;
32
+ description: string;
33
+ surfaces: Surface[];
34
+ events: CanonicalEvent[];
35
+ }
36
+ /** The validated canonical manifest. Shape is parsed at module load. */
37
+ declare const manifest: Manifest;
38
+ /** Returns the canonical event whose `milestone` matches `milestoneId`, or `undefined`. */
39
+ declare function eventForMilestone(milestoneId: MilestoneId): CanonicalEvent | undefined;
40
+ /** Returns the canonical event whose `event_name` matches `name`, or `undefined`. */
41
+ declare function eventByName(name: string): CanonicalEvent | undefined;
42
+ //#endregion
43
+ export { CanonicalEvent, Manifest, PayloadSchema, Surface, eventByName, eventForMilestone, manifest };
@@ -0,0 +1,105 @@
1
+ //#region src/manifest/manifest.data.d.ts
2
+ declare const manifestData: {
3
+ version: string;
4
+ description: string;
5
+ surfaces: {
6
+ id: string;
7
+ service_name: string;
8
+ source_client: string;
9
+ }[];
10
+ events: ({
11
+ event_name: "commit_created";
12
+ milestone: "first_commit";
13
+ description: string;
14
+ payload_schema: {
15
+ type: "object";
16
+ required: string[];
17
+ additionalProperties: true;
18
+ properties: {
19
+ surface_name: {
20
+ type: string;
21
+ enum: string[];
22
+ };
23
+ kind: {
24
+ type: string;
25
+ enum: string[];
26
+ };
27
+ client_version: {
28
+ type: string;
29
+ };
30
+ feature?: undefined;
31
+ };
32
+ };
33
+ milestone_filter: {
34
+ kind: string[];
35
+ };
36
+ } | {
37
+ event_name: "branch_created";
38
+ milestone: "first_branch";
39
+ description: string;
40
+ payload_schema: {
41
+ type: "object";
42
+ required: string[];
43
+ additionalProperties: true;
44
+ properties: {
45
+ surface_name: {
46
+ type: string;
47
+ enum: string[];
48
+ };
49
+ client_version: {
50
+ type: string;
51
+ };
52
+ kind?: undefined;
53
+ feature?: undefined;
54
+ };
55
+ };
56
+ milestone_filter: null;
57
+ } | {
58
+ event_name: "pull_request_created";
59
+ milestone: "first_pr";
60
+ description: string;
61
+ payload_schema: {
62
+ type: "object";
63
+ required: string[];
64
+ additionalProperties: true;
65
+ properties: {
66
+ surface_name: {
67
+ type: string;
68
+ enum: string[];
69
+ };
70
+ client_version: {
71
+ type: string;
72
+ };
73
+ kind?: undefined;
74
+ feature?: undefined;
75
+ };
76
+ };
77
+ milestone_filter: null;
78
+ } | {
79
+ event_name: "ai_feature_used";
80
+ milestone: "first_ai_event";
81
+ description: string;
82
+ payload_schema: {
83
+ type: "object";
84
+ required: string[];
85
+ additionalProperties: true;
86
+ properties: {
87
+ surface_name: {
88
+ type: string;
89
+ enum: string[];
90
+ };
91
+ feature: {
92
+ type: string;
93
+ enum: string[];
94
+ };
95
+ client_version: {
96
+ type: string;
97
+ };
98
+ kind?: undefined;
99
+ };
100
+ };
101
+ milestone_filter: null;
102
+ })[];
103
+ };
104
+ //#endregion
105
+ export { manifestData };
@@ -0,0 +1 @@
1
+ const e={version:`1`,description:`Canonical event taxonomy for the v1 user milestone system. Each event has one canonical name across all surfaces (GitKraken Client, GitLens, GitKraken CLI V2). Surface teams emit these events going forward; the per-surface legacy events used for historical backfill live in backfill_legacy.json and are not part of this contract. Consumed by client SDKs (real-time milestone detection) and by forward-going Airflow compute DAGs.`,surfaces:[{id:`GitKraken Client`,service_name:`GitKraken Client`,source_client:`GitKraken Client`},{id:`gitlens`,service_name:`gitlens`,source_client:`gitlens`},{id:`GitKraken CLI V2`,service_name:`GitKraken CLI V2`,source_client:`GitKraken CLI V2`}],events:[{event_name:`commit_created`,milestone:`first_commit`,description:`Fired when the user completes a commit in any GitKraken surface. All kinds (including cherrypick and revert) count toward the commits aggregate. The first_commit milestone only counts kinds create and amend.`,payload_schema:{type:`object`,required:[`surface_name`,`kind`],additionalProperties:!0,properties:{surface_name:{type:`string`,enum:[`GitKraken Client`,`gitlens`,`GitKraken CLI V2`]},kind:{type:`string`,enum:[`create`,`amend`,`cherrypick`,`revert`]},client_version:{type:`string`}}},milestone_filter:{kind:[`create`,`amend`]}},{event_name:`branch_created`,milestone:`first_branch`,description:`Fired when the user creates a branch in any GitKraken surface.`,payload_schema:{type:`object`,required:[`surface_name`],additionalProperties:!0,properties:{surface_name:{type:`string`,enum:[`GitKraken Client`,`gitlens`,`GitKraken CLI V2`]},client_version:{type:`string`}}},milestone_filter:null},{event_name:`pull_request_created`,milestone:`first_pr`,description:`Fired when the user creates a pull request in any GitKraken surface. Emitted whether or not an AI feature was used to author the PR; AI usage is reported in parallel via ai_feature_used and is orthogonal to artifact creation.`,payload_schema:{type:`object`,required:[`surface_name`],additionalProperties:!0,properties:{surface_name:{type:`string`,enum:[`GitKraken Client`,`gitlens`,`GitKraken CLI V2`]},client_version:{type:`string`}}},milestone_filter:null},{event_name:`ai_feature_used`,milestone:`first_ai_event`,description:`Fired when the user invokes a supported AI feature. Independent of whether an artifact (commit, PR, etc.) is ultimately produced; the AI invocation itself is the tracked signal. feature is an advisory classification with an other catch-all, not a gate: any non-empty feature earns the milestone and counts toward the aggregate.`,payload_schema:{type:`object`,required:[`surface_name`,`feature`],additionalProperties:!0,properties:{surface_name:{type:`string`,enum:[`GitKraken Client`,`gitlens`,`GitKraken CLI V2`]},feature:{type:`string`,enum:[`commit_message`,`pr_description`,`explain`,`conflict_resolve`,`feedback`,`agent_hook`,`other`]},client_version:{type:`string`}}},milestone_filter:null}]};export{e as manifestData};
@@ -0,0 +1 @@
1
+ import{manifestData as e}from"./manifest.data.js";import{z as t}from"zod";const n=t.object({type:t.literal(`object`),required:t.array(t.string()),additionalProperties:t.boolean(),properties:t.record(t.string(),t.unknown())}),r=t.object({event_name:t.enum([`commit_created`,`branch_created`,`pull_request_created`,`ai_feature_used`]),milestone:t.enum([`first_commit`,`first_branch`,`first_pr`,`first_ai_event`]),description:t.string(),payload_schema:n,milestone_filter:t.record(t.string(),t.array(t.string())).nullable()}),i=t.object({version:t.string(),description:t.string(),surfaces:t.array(t.object({id:t.string(),service_name:t.string(),source_client:t.string()})).min(1),events:t.array(r).min(1)}).parse(e);function a(e){return i.events.find(t=>t.milestone===e)}function o(e){return i.events.find(t=>t.event_name===e)}export{o as eventByName,a as eventForMilestone,i as manifest};
@@ -0,0 +1,17 @@
1
+ import { ClientAction, MilestoneId } from "./types.js";
2
+ import { Manifest } from "../manifest/manifest.js";
3
+
4
+ //#region src/milestones/detection.d.ts
5
+ /**
6
+ * Maps an observed action to the milestone it earns, or `null` when it earns
7
+ * none.
8
+ *
9
+ * The action's `type` must match a canonical event's `event_name`. When that
10
+ * event declares a `milestone_filter`, every listed field in the action's
11
+ * payload must hold one of the allowed values; otherwise the action counts
12
+ * toward the aggregate but does not earn the milestone. An unknown action type
13
+ * yields `null`.
14
+ */
15
+ declare function detect(action: ClientAction, manifest: Manifest): MilestoneId | null;
16
+ //#endregion
17
+ export { detect };
@@ -0,0 +1 @@
1
+ function e(e,n){let r=n.events.find(t=>t.event_name===e.type);return!r||!t(r,e)?null:r.milestone}function t(e,t){let n=e.milestone_filter;if(!n)return!0;let r=t.payload??{};for(let[e,t]of Object.entries(n)){let n=r[e];if(typeof n!=`string`||!t.includes(n))return!1}return!0}export{e as detect};
@@ -0,0 +1,56 @@
1
+ import { MilestonesSdkConfig } from "../config.js";
2
+ import { ClientAction, MilestoneId, MilestoneState, Unsubscribe } from "./types.js";
3
+ import { DropReason } from "./queue.js";
4
+
5
+ //#region src/milestones/milestone-client.d.ts
6
+ /** Fired when identity-service confirms a queued milestone. */
7
+ type ConfirmedCallback = (milestoneId: MilestoneId) => void;
8
+ /** Fired when the queue drops a milestone terminally (server rejection or exhausted retries). */
9
+ type DroppedCallback = (milestoneId: MilestoneId, reason: DropReason) => void;
10
+ /**
11
+ * High-level client for the v1 milestone surface of identity-service.
12
+ *
13
+ * `loadMilestones` reads the user's achieved milestones (cached through the
14
+ * {@link StoragePort} for `cacheTtlMs`). `observe` runs an action through the
15
+ * embedded manifest, skips milestones already achieved, enqueues the rest, and
16
+ * triggers a flush. `onConfirmed` fires once when a milestone is first accepted
17
+ * by the server; `onDropped` fires when the queue gives up on an item.
18
+ */
19
+ declare class MilestoneClient {
20
+ private readonly config;
21
+ private readonly transport;
22
+ private readonly queue;
23
+ private readonly manifest;
24
+ private readonly cacheTtlMs;
25
+ private readonly confirmedCallbacks;
26
+ private readonly droppedCallbacks;
27
+ private cacheChain;
28
+ constructor(config: MilestonesSdkConfig);
29
+ loadMilestones(options?: {
30
+ force?: boolean;
31
+ signal?: AbortSignal;
32
+ }): Promise<MilestoneState>;
33
+ isAchieved(milestoneId: MilestoneId): Promise<boolean>;
34
+ observe(action: ClientAction): Promise<void>;
35
+ flush(signal?: AbortSignal): Promise<void>;
36
+ onConfirmed(cb: ConfirmedCallback): Unsubscribe;
37
+ onDropped(cb: DroppedCallback): Unsubscribe;
38
+ private send;
39
+ private recordAchieved;
40
+ private emitConfirmed;
41
+ private emitDropped;
42
+ private readCache;
43
+ private writeCache;
44
+ /**
45
+ * Serializes the cache read-modify-write sections of `recordAchieved` and
46
+ * `loadMilestones` against each other so a confirmation and a refresh cannot
47
+ * interleave and drop one another's writes to `STATE_KEY`. The network fetch
48
+ * in `loadMilestones` stays outside this chain; only its merge-and-write runs
49
+ * inside.
50
+ */
51
+ private serializeCache;
52
+ }
53
+ /** Constructs a {@link MilestoneClient} from the SDK config. */
54
+ declare function createMilestoneClient(config: MilestonesSdkConfig): MilestoneClient;
55
+ //#endregion
56
+ export { ConfirmedCallback, DroppedCallback, MilestoneClient, createMilestoneClient };
@@ -0,0 +1 @@
1
+ import{manifest as e}from"../manifest/manifest.js";import{detect as t}from"./detection.js";import{DurableQueue as n,outcomeForError as r,outcomeForStatus as i}from"./queue.js";import{milestoneAchievedListSchema as a}from"./schemas.js";import{Transport as o}from"@gitkraken/api-tools";const s=`gk.milestones.state.v1`;var c=class{config;transport;queue;manifest;cacheTtlMs;confirmedCallbacks=new Set;droppedCallbacks=new Set;cacheChain=Promise.resolve();constructor(t){this.config=t,this.transport=new o(t),this.queue=new n(t.storage,t.logger),this.manifest=e,this.cacheTtlMs=t.cacheTtlMs??3e5}async loadMilestones(e){if(!e?.force){let e=await this.readCache();if(e&&Date.now()-e.fetchedAt<this.cacheTtlMs)return e.state}let{data:t}=await this.transport.request({method:`GET`,path:`/v1/milestones`,signal:e?.signal,parse:e=>a.parse(e)});return this.serializeCache(async()=>{let e={...(await this.readCache())?.state.achieved??{}};for(let n of t??[])e[n.milestoneId]=n;let n={achieved:e};return await this.writeCache(n),n})}async isAchieved(e){return!!(await this.readCache())?.state.achieved[e]}async observe(e){let n=t(e,this.manifest);n&&(await this.isAchieved(n)||(await this.queue.enqueue({id:crypto.randomUUID(),milestoneId:n,action:{type:e.type,at:e.at,payload:e.payload}}),await this.flush()))}async flush(e){await this.queue.flush(t=>this.send(t,e),{onDrop:(e,t)=>this.emitDropped(e,t)})}onConfirmed(e){return this.confirmedCallbacks.add(e),()=>{this.confirmedCallbacks.delete(e)}}onDropped(e){return this.droppedCallbacks.add(e),()=>{this.droppedCallbacks.delete(e)}}async send(e,t){let n;try{n=i((await this.transport.request({method:`POST`,path:`/v1/milestones`,body:{milestone:e.milestoneId,sourceClient:this.config.sourceClient},signal:t})).status)}catch(t){if(!l(t))return this.config.logger?.warn(`milestone send failed before response`,{id:e.id,milestoneId:e.milestoneId,error:t}),{kind:`retry`};n=r(t.status)}return n.kind===`done`&&await this.recordAchieved(e.milestoneId)&&this.emitConfirmed(e.milestoneId),n}async recordAchieved(e){return this.serializeCache(async()=>{let t=(await this.readCache())?.state??{achieved:{}};return t.achieved[e]?!1:(t.achieved[e]={milestoneId:e,achievedAt:new Date().toISOString(),sourceSurface:this.config.sourceClient,detectionMethod:`live`},await this.writeCache(t),!0)})}emitConfirmed(e){for(let t of this.confirmedCallbacks)try{t(e)}catch(e){this.config.logger?.error(`onConfirmed callback threw`,e)}}emitDropped(e,t){for(let n of this.droppedCallbacks)try{n(e,t)}catch(e){this.config.logger?.error(`onDropped callback threw`,e)}}async readCache(){let e=await this.config.storage.get(s);if(!e)return null;try{return JSON.parse(e)}catch{return null}}async writeCache(e){let t={fetchedAt:Date.now(),state:e};await this.config.storage.set(s,JSON.stringify(t))}serializeCache(e){let t=this.cacheChain.then(e,e);return this.cacheChain=t.catch(()=>void 0),t}};function l(e){return typeof e==`object`&&!!e&&typeof e.status==`number`}function u(e){return new c(e)}export{c as MilestoneClient,u as createMilestoneClient};
@@ -0,0 +1,66 @@
1
+ import { StoragePort } from "../config.js";
2
+ import { ClientAction, MilestoneId } from "./types.js";
3
+ import { LoggerPort } from "@gitkraken/api-tools";
4
+
5
+ //#region src/milestones/queue.d.ts
6
+ /** Why an item left the queue without being confirmed. */
7
+ type DropReason = 'rejected' | 'exhausted';
8
+ /** Reports a milestone item that the queue dropped terminally. */
9
+ type DropCallback = (milestoneId: MilestoneId, reason: DropReason) => void;
10
+ /** A queued milestone action awaiting confirmation by identity-service. */
11
+ interface QueueItem {
12
+ id: string;
13
+ milestoneId: MilestoneId;
14
+ action: ClientAction;
15
+ attempts: number;
16
+ nextAttemptAt: number;
17
+ }
18
+ /** The result of attempting to send one item to identity-service. */
19
+ type SendOutcome = {
20
+ kind: 'done';
21
+ status: number;
22
+ } | {
23
+ kind: 'drop';
24
+ status: number;
25
+ } | {
26
+ kind: 'retry';
27
+ status?: number;
28
+ };
29
+ /** Maps the status of a successfully-resolved send (a clean 2xx) to an outcome. */
30
+ declare function outcomeForStatus(status: number): SendOutcome;
31
+ /**
32
+ * Maps the status of a *thrown* send (a non-2xx, or a 2xx whose envelope carried
33
+ * an `error`) to an outcome. A thrown send is never a success, so this never
34
+ * returns `done`: transient statuses (408, 429, 5xx) retry; everything else
35
+ * (the 422 activation gate, other 4xx, and a 2xx-with-error) drops terminally,
36
+ * since retrying cannot recover them.
37
+ */
38
+ declare function outcomeForError(status: number): SendOutcome;
39
+ /**
40
+ * A durable, at-least-once queue persisted through the {@link StoragePort}.
41
+ *
42
+ * `flush` walks the queue oldest-first, calling `send` for each item whose
43
+ * backoff window has elapsed. A `done` outcome removes the item; `drop` (a 422:
44
+ * the milestone is not active yet) also removes it, since retrying cannot
45
+ * succeed before the cutover flag flips and backfill covers pre-cutover
46
+ * actions; `retry` schedules the next attempt with linear backoff until
47
+ * `MAX_ATTEMPTS`, after which the item is dropped. Reads,
48
+ * mutations, and writes are serialized through an in-instance promise chain so
49
+ * concurrent `enqueue`/`flush` calls cannot lose an item.
50
+ */
51
+ declare class DurableQueue {
52
+ private readonly storage;
53
+ private readonly logger?;
54
+ private chain;
55
+ constructor(storage: StoragePort, logger?: LoggerPort);
56
+ enqueue(item: Omit<QueueItem, 'attempts' | 'nextAttemptAt'>): Promise<void>;
57
+ flush(send: (item: QueueItem) => Promise<SendOutcome>, options?: {
58
+ now?: number;
59
+ onDrop?: DropCallback;
60
+ }): Promise<void>;
61
+ read(): Promise<QueueItem[]>;
62
+ private write;
63
+ private serialize;
64
+ }
65
+ //#endregion
66
+ export { DropCallback, DropReason, DurableQueue, QueueItem, SendOutcome, outcomeForError, outcomeForStatus };
@@ -0,0 +1 @@
1
+ const e=`gk.milestones.queue.v1`,t=`${e}.corrupt`;function n(e){return e===200||e===201?{kind:`done`,status:e}:e===422?{kind:`drop`,status:e}:{kind:`retry`,status:e}}function r(e){return e===408||e===429||e>=500?{kind:`retry`,status:e}:{kind:`drop`,status:e}}var i=class{storage;logger;chain=Promise.resolve();constructor(e,t){this.storage=e,this.logger=t}async enqueue(e){await this.serialize(async()=>{let t=await this.read();t.push({...e,attempts:0,nextAttemptAt:0}),await this.write(t)})}async flush(e,t){let n=t?.now??Date.now();await this.serialize(async()=>{let r=await this.read();if(r.length===0)return;let i=[];for(let a of r){if(a.nextAttemptAt>n){i.push(a);continue}let r;try{r=await e(a)}catch(e){this.logger?.warn(`milestone queue send threw`,e),r={kind:`retry`}}if(r.kind===`done`)continue;if(r.kind===`drop`){this.logger?.warn(`milestone queue item rejected; dropping`,{id:a.id,milestoneId:a.milestoneId,status:r.status}),t?.onDrop?.(a.milestoneId,`rejected`);continue}let o=a.attempts+1;if(o>=6){this.logger?.error(`milestone queue item exhausted retries`,{id:a.id,milestoneId:a.milestoneId,attempts:o}),t?.onDrop?.(a.milestoneId,`exhausted`);continue}i.push({...a,attempts:o,nextAttemptAt:n+o*1e3})}await this.write(i)})}async read(){let n=await this.storage.get(e);if(!n)return[];try{let e=JSON.parse(n);return Array.isArray(e)?e:[]}catch(r){return await this.storage.set(t,n),this.logger?.error(`milestone queue storage was corrupt; quarantined and dropped`,{key:e,corruptKey:t,raw:n,error:r}),[]}}async write(t){if(t.length===0){await this.storage.delete(e);return}await this.storage.set(e,JSON.stringify(t))}serialize(e){let t=this.chain.then(e,e);return this.chain=t.catch(()=>void 0),t}};export{i as DurableQueue,r as outcomeForError,n as outcomeForStatus};
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/milestones/schemas.d.ts
4
+ declare const milestoneAchievedSchema: z.ZodObject<{
5
+ milestoneId: z.ZodEnum<{
6
+ first_commit: "first_commit";
7
+ first_branch: "first_branch";
8
+ first_pr: "first_pr";
9
+ first_ai_event: "first_ai_event";
10
+ }>;
11
+ achievedAt: z.ZodString;
12
+ sourceSurface: z.ZodString;
13
+ detectionMethod: z.ZodEnum<{
14
+ live: "live";
15
+ backfill: "backfill";
16
+ backfill_corrected: "backfill_corrected";
17
+ }>;
18
+ }, z.core.$strip>;
19
+ declare const milestoneAchievedListSchema: z.ZodNullable<z.ZodArray<z.ZodObject<{
20
+ milestoneId: z.ZodEnum<{
21
+ first_commit: "first_commit";
22
+ first_branch: "first_branch";
23
+ first_pr: "first_pr";
24
+ first_ai_event: "first_ai_event";
25
+ }>;
26
+ achievedAt: z.ZodString;
27
+ sourceSurface: z.ZodString;
28
+ detectionMethod: z.ZodEnum<{
29
+ live: "live";
30
+ backfill: "backfill";
31
+ backfill_corrected: "backfill_corrected";
32
+ }>;
33
+ }, z.core.$strip>>>;
34
+ //#endregion
35
+ export { milestoneAchievedListSchema, milestoneAchievedSchema };
@@ -0,0 +1 @@
1
+ import{DetectionMethod as e,MilestoneId as t}from"./types.js";import{z as n}from"zod";const r=n.enum(Object.values(t)),i=n.enum(Object.values(e)),a=n.object({milestoneId:r,achievedAt:n.string(),sourceSurface:n.string(),detectionMethod:i}),o=n.array(a).nullable();export{o as milestoneAchievedListSchema,a as milestoneAchievedSchema};
@@ -0,0 +1,49 @@
1
+ //#region src/milestones/types.d.ts
2
+ /** How a milestone achievement was detected, as recorded by identity-service. */
3
+ declare const DetectionMethod: {
4
+ readonly Live: "live";
5
+ readonly Backfill: "backfill";
6
+ readonly BackfillCorrected: "backfill_corrected";
7
+ };
8
+ type DetectionMethod = (typeof DetectionMethod)[keyof typeof DetectionMethod];
9
+ /** A canonical event name in the v1 milestone taxonomy. */
10
+ declare const CanonicalEventName: {
11
+ readonly CommitCreated: "commit_created";
12
+ readonly BranchCreated: "branch_created";
13
+ readonly PullRequestCreated: "pull_request_created";
14
+ readonly AiFeatureUsed: "ai_feature_used";
15
+ };
16
+ type CanonicalEventName = (typeof CanonicalEventName)[keyof typeof CanonicalEventName];
17
+ /** The closed set of milestones a client can earn in the v1 taxonomy. */
18
+ declare const MilestoneId: {
19
+ readonly FirstCommit: "first_commit";
20
+ readonly FirstBranch: "first_branch";
21
+ readonly FirstPr: "first_pr";
22
+ readonly FirstAiEvent: "first_ai_event";
23
+ };
24
+ type MilestoneId = (typeof MilestoneId)[keyof typeof MilestoneId];
25
+ /** A single achieved milestone as reported by identity-service. */
26
+ interface MilestoneAchieved {
27
+ milestoneId: MilestoneId;
28
+ achievedAt: string;
29
+ sourceSurface: string;
30
+ detectionMethod: DetectionMethod;
31
+ }
32
+ /** The user's full milestone state: which milestones have been achieved. */
33
+ interface MilestoneState {
34
+ achieved: Partial<Record<MilestoneId, MilestoneAchieved>>;
35
+ }
36
+ /**
37
+ * An action the host observed, passed to {@link MilestoneClient.observe}. The
38
+ * `type` matches a canonical event name (e.g. `commit_created`); `payload`
39
+ * carries the fields the manifest filters against (e.g. commit `kind`).
40
+ */
41
+ interface ClientAction {
42
+ type: CanonicalEventName;
43
+ at: string;
44
+ payload?: Record<string, unknown>;
45
+ }
46
+ /** Cancels a subscription registered via `onConfirmed`. */
47
+ type Unsubscribe = () => void;
48
+ //#endregion
49
+ export { CanonicalEventName, ClientAction, DetectionMethod, MilestoneAchieved, MilestoneId, MilestoneState, Unsubscribe };
@@ -0,0 +1 @@
1
+ const e={Live:`live`,Backfill:`backfill`,BackfillCorrected:`backfill_corrected`},t={CommitCreated:`commit_created`,BranchCreated:`branch_created`,PullRequestCreated:`pull_request_created`,AiFeatureUsed:`ai_feature_used`},n={FirstCommit:`first_commit`,FirstBranch:`first_branch`,FirstPr:`first_pr`,FirstAiEvent:`first_ai_event`};export{t as CanonicalEventName,e as DetectionMethod,n as MilestoneId};
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@gitkraken/milestones-tools",
3
+ "version": "0.1.0",
4
+ "description": "Client library for GitKraken user milestones and demographic metadata (identity-service).",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": "Axosoft, LLC dba GitKraken",
7
+ "engines": {
8
+ "node": ">= 22"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "dependencies": {
23
+ "zod": "^4.3.6",
24
+ "@gitkraken/api-tools": "0.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "typescript": "^6.0.2",
29
+ "vitest": "^4.1.1"
30
+ },
31
+ "scripts": {
32
+ "build": "tsdown --config-loader=unrun",
33
+ "build:dev": "tsdown --config tsdown.dev.config.ts --config-loader=unrun",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run"
36
+ }
37
+ }