@thinkai/tai-api-contract 2.34.0 → 2.35.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.
@@ -395,7 +395,7 @@ export interface paths {
395
395
  };
396
396
  /**
397
397
  * Productivity insights — contributor drilldown
398
- * @description Weekly and daily activity rollups for a single contributor on `/phase-c/c4/contributors/:id`. Distributes weekly merged PR counts from precomputed person metrics across weekdays; includes fail-rate signals and period-over-period summary deltas. Accepts org-chart person ids (typically email) and synthetic GitHub scopes (`__github:{login}`) for contributors not in the org chart.
398
+ * @description Weekly and daily activity rollups for a single contributor on `/insights/productivity/contributors/:loginOrId`. Accepts GitHub login slugs (preferred public URL), org-chart person ids (typically email), and synthetic GitHub scopes (`__github:{login}`) for contributors not in the org chart.
399
399
  */
400
400
  get: operations["getProductivityInsightsContributor"];
401
401
  put?: never;
@@ -642,7 +642,7 @@ export interface paths {
642
642
  put?: never;
643
643
  /**
644
644
  * Create GitHub App installation URL
645
- * @description Creates a GitHub App installation URL scoped to the workspace and caller. Body is optional; when omitted, defaults to `/phase-a/p4` return path. Optional `pickerSession` (from user OAuth org picker) recovers `returnPath` from the server-side picker snapshot when the multi-hop authorize→install flow should return to the page where OAuth started.
645
+ * @description Creates a GitHub App installation URL scoped to the workspace and caller. Body is optional; when omitted, defaults to `/onboarding/integrations` return path. Optional `pickerSession` (from user OAuth org picker) recovers `returnPath` from the server-side picker snapshot when the multi-hop authorize→install flow should return to the page where OAuth started.
646
646
  */
647
647
  post: operations["postGithubInstallUrl"];
648
648
  delete?: never;
@@ -1121,6 +1121,26 @@ export interface paths {
1121
1121
  patch?: never;
1122
1122
  trace?: never;
1123
1123
  };
1124
+ "/workspaces/{workspaceId}/readiness/repos/by-slug/{providerSlug}": {
1125
+ parameters: {
1126
+ query?: never;
1127
+ header?: never;
1128
+ path?: never;
1129
+ cookie?: never;
1130
+ };
1131
+ /**
1132
+ * Single repo readiness by provider slug
1133
+ * @description Returns one stored repository scorecard keyed by `providerSlug` (`owner/repo`). Same response as `GET .../readiness/repos/{repoId}`. Unknown slug returns `404`.
1134
+ */
1135
+ get: operations["getReadinessRepoBySlug"];
1136
+ put?: never;
1137
+ post?: never;
1138
+ delete?: never;
1139
+ options?: never;
1140
+ head?: never;
1141
+ patch?: never;
1142
+ trace?: never;
1143
+ };
1124
1144
  "/workspaces/{workspaceId}/readiness/repos/{repoId}": {
1125
1145
  parameters: {
1126
1146
  query?: never;
@@ -1901,7 +1921,7 @@ export interface paths {
1901
1921
  };
1902
1922
  /**
1903
1923
  * Workspace journey step completion
1904
- * @description Returns the workspace-scoped journey progress state. Returns an empty `completedSteps` list with a synthetic `updatedAt` if no row exists yet. Covers issue #271.
1924
+ * @description Returns the workspace-scoped journey progress state. Returns an empty `completedStepIds` array when no row exists yet (updatedAt is synthetic in that case). Values are canonical step IDs (see `JourneyStepId`); legacy route strings are accepted on PUT and normalized. Covers issue #271.
1905
1925
  */
1906
1926
  get: operations["getYourAiJourneyProgress"];
1907
1927
  /**
@@ -3763,14 +3783,31 @@ export interface components {
3763
3783
  AiStrategyPlanDto: {
3764
3784
  phases: components["schemas"]["AiStrategyPhaseBlockDto"][];
3765
3785
  };
3786
+ /**
3787
+ * @description Canonical journey step identifier (stable across URL reshuffles).
3788
+ * @enum {string}
3789
+ */
3790
+ JourneyStepId: "onboarding.workspace" | "onboarding.ai_readiness" | "onboarding.integrations" | "readiness.dashboard" | "readiness.fix_queue" | "readiness.activity" | "insights.productivity" | "insights.productivity.by_user" | "insights.productivity.by_team" | "insights.ai_usage" | "insights.ai_usage.by_user" | "insights.ai_usage.by_team" | "insights.ai_usage.by_model";
3766
3791
  JourneyProgressDto: {
3767
- completedSteps: string[];
3792
+ /** @description Canonical completed journey step IDs. */
3793
+ completedStepIds: components["schemas"]["JourneyStepId"][];
3794
+ /**
3795
+ * @deprecated
3796
+ * @description Deprecated alias of completedStepIds (legacy clients). Omitted on new responses when empty.
3797
+ */
3798
+ completedSteps?: string[];
3768
3799
  /** Format: date-time */
3769
3800
  updatedAt: string;
3770
3801
  };
3771
3802
  JourneyProgressUpdateDto: {
3772
- completedSteps: string[];
3773
- };
3803
+ /** @description Idempotent replace-all of completed step IDs. */
3804
+ completedStepIds?: components["schemas"]["JourneyStepId"][];
3805
+ /**
3806
+ * @deprecated
3807
+ * @description Deprecated input alias — route strings and legacy `/phase-*` paths are normalized to step IDs.
3808
+ */
3809
+ completedSteps?: string[];
3810
+ } | unknown | unknown;
3774
3811
  /** @enum {string} */
3775
3812
  ThemePreference: "light" | "dark" | "system";
3776
3813
  NotificationPreferencesDto: {
@@ -3892,7 +3929,9 @@ export interface components {
3892
3929
  /** Format: date-time */
3893
3930
  windowEnd: string;
3894
3931
  journey: {
3895
- completedSteps: string[];
3932
+ completedStepIds: components["schemas"]["JourneyStepId"][];
3933
+ /** @deprecated */
3934
+ completedSteps?: string[];
3896
3935
  /** Format: date-time */
3897
3936
  updatedAt: string | null;
3898
3937
  };
@@ -4215,6 +4254,8 @@ export interface components {
4215
4254
  parameters: {
4216
4255
  WorkspaceId: string;
4217
4256
  RepoId: string;
4257
+ /** @description Provider repository slug in `owner/repo` form (GitHub). Used for shareable public URLs; resolves to the same row as `repoId` UUID internally. */
4258
+ ProviderSlug: string;
4218
4259
  ProjectId: string;
4219
4260
  NotificationId: string;
4220
4261
  /** @description Workspace member user id (`tai_users.id`). */
@@ -4424,6 +4465,7 @@ export type AiStrategyRecommendBodyDto = components['schemas']['AiStrategyRecomm
4424
4465
  export type AiStrategyTaskRecommendationDto = components['schemas']['AiStrategyTaskRecommendationDto'];
4425
4466
  export type AiStrategyPhaseBlockDto = components['schemas']['AiStrategyPhaseBlockDto'];
4426
4467
  export type AiStrategyPlanDto = components['schemas']['AiStrategyPlanDto'];
4468
+ export type JourneyStepId = components['schemas']['JourneyStepId'];
4427
4469
  export type JourneyProgressDto = components['schemas']['JourneyProgressDto'];
4428
4470
  export type JourneyProgressUpdateDto = components['schemas']['JourneyProgressUpdateDto'];
4429
4471
  export type ThemePreference = components['schemas']['ThemePreference'];
@@ -4461,6 +4503,7 @@ export type ResponseUnauthorized = components['responses']['Unauthorized'];
4461
4503
  export type ResponseForbidden = components['responses']['Forbidden'];
4462
4504
  export type ParameterWorkspaceId = components['parameters']['WorkspaceId'];
4463
4505
  export type ParameterRepoId = components['parameters']['RepoId'];
4506
+ export type ParameterProviderSlug = components['parameters']['ProviderSlug'];
4464
4507
  export type ParameterProjectId = components['parameters']['ProjectId'];
4465
4508
  export type ParameterNotificationId = components['parameters']['NotificationId'];
4466
4509
  export type ParameterWorkspaceMemberId = components['parameters']['WorkspaceMemberId'];
@@ -5510,7 +5553,7 @@ export interface operations {
5510
5553
  header?: never;
5511
5554
  path: {
5512
5555
  workspaceId: components["parameters"]["WorkspaceId"];
5513
- /** @description Org-chart person id or synthetic GitHub scope id (`__github:{login}`). */
5556
+ /** @description GitHub login slug, org-chart person id, or synthetic `__github:{login}` scope id. */
5514
5557
  contributorId: string;
5515
5558
  };
5516
5559
  cookie?: never;
@@ -7319,6 +7362,50 @@ export interface operations {
7319
7362
  };
7320
7363
  };
7321
7364
  };
7365
+ getReadinessRepoBySlug: {
7366
+ parameters: {
7367
+ query?: never;
7368
+ header?: never;
7369
+ path: {
7370
+ workspaceId: components["parameters"]["WorkspaceId"];
7371
+ /** @description Provider repository slug in `owner/repo` form (GitHub). Used for shareable public URLs; resolves to the same row as `repoId` UUID internally. */
7372
+ providerSlug: components["parameters"]["ProviderSlug"];
7373
+ };
7374
+ cookie?: never;
7375
+ };
7376
+ requestBody?: never;
7377
+ responses: {
7378
+ /** @description Repo score */
7379
+ 200: {
7380
+ headers: {
7381
+ [name: string]: unknown;
7382
+ };
7383
+ content: {
7384
+ "application/json": components["schemas"]["RepoReadinessScoreDetailDto"];
7385
+ };
7386
+ };
7387
+ /** @description Malformed providerSlug */
7388
+ 400: {
7389
+ headers: {
7390
+ [name: string]: unknown;
7391
+ };
7392
+ content: {
7393
+ "application/json": components["schemas"]["ErrorMessageDto"];
7394
+ };
7395
+ };
7396
+ 401: components["responses"]["Unauthorized"];
7397
+ 403: components["responses"]["Forbidden"];
7398
+ /** @description Workspace or repository not found */
7399
+ 404: {
7400
+ headers: {
7401
+ [name: string]: unknown;
7402
+ };
7403
+ content: {
7404
+ "application/json": components["schemas"]["ErrorMessageDto"];
7405
+ };
7406
+ };
7407
+ };
7408
+ };
7322
7409
  getReadinessRepo: {
7323
7410
  parameters: {
7324
7411
  query?: never;
@@ -8909,7 +8996,7 @@ export interface operations {
8909
8996
  };
8910
8997
  requestBody?: never;
8911
8998
  responses: {
8912
- /** @description Progress (empty completedSteps when no row exists yet; updatedAt is synthetic in that case) */
8999
+ /** @description Progress (empty completedStepIds when no row exists yet; updatedAt is synthetic in that case) */
8913
9000
  200: {
8914
9001
  headers: {
8915
9002
  [name: string]: unknown;
package/src/index.ts CHANGED
@@ -1084,3 +1084,13 @@ export interface HrisTriggerSyncResultDto {
1084
1084
  /** Per-provider results when one or more HRIS sources are configured. */
1085
1085
  providers?: HrisProviderSyncResultDto[];
1086
1086
  }
1087
+
1088
+ export {
1089
+ JOURNEY_STEP_IDS,
1090
+ JOURNEY_STEP_REGISTRY,
1091
+ normalizeStepIdList,
1092
+ normalizeToStepId,
1093
+ parseJourneyStepInputs,
1094
+ stepIdToRoute,
1095
+ type JourneyStepId,
1096
+ } from "./journeySteps.js";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Unit tests for journey step ID registry and normalization.
3
+ */
4
+ import {
5
+ JOURNEY_STEP_IDS,
6
+ normalizeStepIdList,
7
+ normalizeToStepId,
8
+ parseJourneyStepInputs,
9
+ stepIdToRoute,
10
+ } from "./journeySteps.js";
11
+
12
+ if (normalizeToStepId("/phase-a/p1") !== "onboarding.workspace") {
13
+ throw new Error("expected /phase-a/p1 → onboarding.workspace");
14
+ }
15
+ if (normalizeToStepId("/onboarding/workspace") !== "onboarding.workspace") {
16
+ throw new Error("expected semantic route alias");
17
+ }
18
+ if (normalizeToStepId("readiness.dashboard") !== "readiness.dashboard") {
19
+ throw new Error("expected canonical step id passthrough");
20
+ }
21
+ if (normalizeToStepId("/unknown/route") !== null) {
22
+ throw new Error("unknown route should normalize to null");
23
+ }
24
+
25
+ const deduped = normalizeStepIdList(["/phase-a/p1", "/onboarding/workspace", "/phase-a/p1"]);
26
+ if (deduped.length !== 1 || deduped[0] !== "onboarding.workspace") {
27
+ throw new Error(`dedupe failed: ${JSON.stringify(deduped)}`);
28
+ }
29
+
30
+ const droppedUnknown = parseJourneyStepInputs(["/phase-a/p1", "/stale/route"]);
31
+ if (droppedUnknown.error || droppedUnknown.value?.length !== 1) {
32
+ throw new Error(`unknown should drop silently by default: ${JSON.stringify(droppedUnknown)}`);
33
+ }
34
+ if (droppedUnknown.unknown?.length !== 1 || droppedUnknown.unknown[0] !== "/stale/route") {
35
+ throw new Error("unknown list should report dropped entries");
36
+ }
37
+
38
+ const rejectUnknown = parseJourneyStepInputs(["/stale/route"], { rejectUnknown: true });
39
+ if (rejectUnknown.error !== "Unknown journey step" || rejectUnknown.unknown?.[0] !== "/stale/route") {
40
+ throw new Error(`rejectUnknown failed: ${JSON.stringify(rejectUnknown)}`);
41
+ }
42
+
43
+ const blankStep = parseJourneyStepInputs([" "], { rejectUnknown: true });
44
+ if (blankStep.error !== "Each completed step must be a non-empty string") {
45
+ throw new Error(`blank step guard failed: ${JSON.stringify(blankStep)}`);
46
+ }
47
+
48
+ const atCap = parseJourneyStepInputs(JOURNEY_STEP_IDS, { rejectUnknown: true });
49
+ if (atCap.error || atCap.value?.length !== JOURNEY_STEP_IDS.length) {
50
+ throw new Error(`full registry parse failed: ${JSON.stringify(atCap)}`);
51
+ }
52
+
53
+ if (stepIdToRoute("onboarding.workspace") !== "/onboarding/workspace") {
54
+ throw new Error("stepIdToRoute failed for onboarding.workspace");
55
+ }
56
+ if (stepIdToRoute("not.a.step") !== null) {
57
+ throw new Error("stepIdToRoute should return null for unknown id");
58
+ }
59
+
60
+ console.log("journeySteps.test: OK");
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Canonical journey step IDs — stable keys for journey progress storage.
3
+ * Semantic routes are the SPA URL surface; legacy /phase-* paths normalize here.
4
+ */
5
+
6
+ export const JOURNEY_STEP_IDS = [
7
+ "onboarding.workspace",
8
+ "onboarding.ai_readiness",
9
+ "onboarding.integrations",
10
+ "readiness.dashboard",
11
+ "readiness.fix_queue",
12
+ "readiness.activity",
13
+ "insights.productivity",
14
+ "insights.productivity.by_user",
15
+ "insights.productivity.by_team",
16
+ "insights.ai_usage",
17
+ "insights.ai_usage.by_user",
18
+ "insights.ai_usage.by_team",
19
+ "insights.ai_usage.by_model",
20
+ ] as const;
21
+
22
+ export type JourneyStepId = (typeof JOURNEY_STEP_IDS)[number];
23
+
24
+ type StepDef = {
25
+ id: JourneyStepId;
26
+ route: string;
27
+ aliases: readonly string[];
28
+ };
29
+
30
+ /** Registry order matches product journey A → R → C. */
31
+ export const JOURNEY_STEP_REGISTRY: readonly StepDef[] = [
32
+ {
33
+ id: "onboarding.workspace",
34
+ route: "/onboarding/workspace",
35
+ aliases: ["/phase-a/p1"],
36
+ },
37
+ {
38
+ id: "onboarding.ai_readiness",
39
+ route: "/onboarding/ai-readiness",
40
+ aliases: ["/phase-a/p2"],
41
+ },
42
+ {
43
+ id: "onboarding.integrations",
44
+ route: "/onboarding/integrations",
45
+ aliases: [
46
+ "/onboarding/platform",
47
+ "/phase-a/p3",
48
+ "/phase-a/p4",
49
+ "/phase-a/p5",
50
+ "/phase-a/p6",
51
+ "/phase-a/p7",
52
+ ],
53
+ },
54
+ {
55
+ id: "readiness.dashboard",
56
+ route: "/agentic-foundation",
57
+ aliases: ["/phase-r/r1"],
58
+ },
59
+ {
60
+ id: "readiness.fix_queue",
61
+ route: "/agentic-foundation/fix-queue",
62
+ aliases: ["/phase-r/r3"],
63
+ },
64
+ {
65
+ id: "readiness.activity",
66
+ route: "/agentic-foundation/activity",
67
+ aliases: ["/phase-r/r4"],
68
+ },
69
+ {
70
+ id: "insights.productivity",
71
+ route: "/insights/productivity",
72
+ aliases: ["/insights/productivity/overview", "/phase-c/c4"],
73
+ },
74
+ {
75
+ id: "insights.productivity.by_user",
76
+ route: "/insights/productivity/by-user",
77
+ aliases: ["/phase-c/c4/by-user"],
78
+ },
79
+ {
80
+ id: "insights.productivity.by_team",
81
+ route: "/insights/productivity/by-team",
82
+ aliases: ["/phase-c/c4/by-team"],
83
+ },
84
+ {
85
+ id: "insights.ai_usage",
86
+ route: "/insights/ai-usage",
87
+ aliases: ["/insights/ai-usage/overview", "/phase-c/c7"],
88
+ },
89
+ {
90
+ id: "insights.ai_usage.by_user",
91
+ route: "/insights/ai-usage/by-user",
92
+ aliases: ["/phase-c/c7/ai-usage-by-user"],
93
+ },
94
+ {
95
+ id: "insights.ai_usage.by_team",
96
+ route: "/insights/ai-usage/by-team",
97
+ aliases: ["/phase-c/c7/ai-usage-by-team"],
98
+ },
99
+ {
100
+ id: "insights.ai_usage.by_model",
101
+ route: "/insights/ai-usage/by-model",
102
+ aliases: ["/phase-c/c7/costs-by-llm-model"],
103
+ },
104
+ ] as const;
105
+
106
+ const STEP_ID_SET = new Set<string>(JOURNEY_STEP_IDS);
107
+
108
+ const ALIAS_TO_STEP_ID = new Map<string, JourneyStepId>();
109
+
110
+ for (const def of JOURNEY_STEP_REGISTRY) {
111
+ ALIAS_TO_STEP_ID.set(def.id, def.id);
112
+ ALIAS_TO_STEP_ID.set(def.route, def.id);
113
+ for (const alias of def.aliases) {
114
+ ALIAS_TO_STEP_ID.set(alias, def.id);
115
+ }
116
+ }
117
+
118
+ const STEP_ID_TO_ROUTE = new Map<JourneyStepId, string>(
119
+ JOURNEY_STEP_REGISTRY.map((d) => [d.id, d.route]),
120
+ );
121
+
122
+ function normalizePathKey(input: string): string {
123
+ const trimmed = input.trim();
124
+ if (!trimmed) return "";
125
+ const noQuery = trimmed.split("?")[0]?.split("#")[0] ?? trimmed;
126
+ const collapsed = noQuery.replace(/\/+$/, "") || "/";
127
+ return collapsed;
128
+ }
129
+
130
+ /** Map step ID, semantic route, or legacy path to a canonical step ID. */
131
+ export function normalizeToStepId(input: string): JourneyStepId | null {
132
+ const key = normalizePathKey(input);
133
+ if (!key) return null;
134
+ return ALIAS_TO_STEP_ID.get(key) ?? null;
135
+ }
136
+
137
+ /** Resolve a canonical step ID to its primary semantic SPA route. */
138
+ export function stepIdToRoute(stepId: string): string | null {
139
+ if (!STEP_ID_SET.has(stepId)) return null;
140
+ return STEP_ID_TO_ROUTE.get(stepId as JourneyStepId) ?? null;
141
+ }
142
+
143
+ /** Normalize an array of mixed inputs to deduped canonical step IDs (drops unknowns). */
144
+ export function normalizeStepIdList(inputs: readonly string[]): JourneyStepId[] {
145
+ const out = new Set<JourneyStepId>();
146
+ for (const raw of inputs) {
147
+ const id = normalizeToStepId(raw);
148
+ if (id) out.add(id);
149
+ }
150
+ return [...out];
151
+ }
152
+
153
+ /** Accept step IDs and legacy/route strings; returns normalized IDs or an error message. */
154
+ export function parseJourneyStepInputs(
155
+ inputs: readonly string[],
156
+ options?: { rejectUnknown?: boolean },
157
+ ): { value?: JourneyStepId[]; error?: string; unknown?: string[] } {
158
+ const rejectUnknown = options?.rejectUnknown === true;
159
+ const unknown: string[] = [];
160
+ const out = new Set<JourneyStepId>();
161
+ for (const raw of inputs) {
162
+ if (typeof raw !== "string") {
163
+ return { error: "Each completed step must be a string" };
164
+ }
165
+ const trimmed = raw.trim();
166
+ if (!trimmed) {
167
+ return { error: "Each completed step must be a non-empty string" };
168
+ }
169
+ if (trimmed.length > 128) {
170
+ return { error: "Each completed step must be at most 128 characters" };
171
+ }
172
+ const id = normalizeToStepId(trimmed);
173
+ if (!id) {
174
+ unknown.push(trimmed);
175
+ continue;
176
+ }
177
+ out.add(id);
178
+ }
179
+ if (rejectUnknown && unknown.length > 0) {
180
+ return { error: "Unknown journey step", unknown };
181
+ }
182
+ const value = [...out];
183
+ if (value.length > 256) {
184
+ return { error: "completedStepIds must have at most 256 entries" };
185
+ }
186
+ if (unknown.length > 0) {
187
+ return { value, unknown };
188
+ }
189
+ return { value };
190
+ }