@thinkai/tai-api-contract 2.34.0 → 2.35.0-pr.811.d57af044
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/generated/openapi.d.ts +96 -9
- package/dist/generated/openapi.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journeySteps.d.ts +29 -0
- package/dist/journeySteps.d.ts.map +1 -0
- package/dist/journeySteps.js +171 -0
- package/dist/journeySteps.js.map +1 -0
- package/dist/journeySteps.test.d.ts +2 -0
- package/dist/journeySteps.test.d.ts.map +1 -0
- package/dist/journeySteps.test.js +47 -0
- package/dist/journeySteps.test.js.map +1 -0
- package/openapi/openapi.yaml +103 -13
- package/package.json +2 -1
- package/src/generated/openapi.ts +96 -9
- package/src/index.ts +10 -0
- package/src/journeySteps.test.ts +60 -0
- package/src/journeySteps.ts +190 -0
package/src/generated/openapi.ts
CHANGED
|
@@ -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 `/
|
|
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 `/
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|