@ttctl/core 0.0.0 → 0.1.0-rc.1

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.
Files changed (195) hide show
  1. package/README.md +49 -9
  2. package/dist/__generated__/gateway.d.ts +4546 -0
  3. package/dist/__generated__/gateway.d.ts.map +1 -0
  4. package/dist/__generated__/gateway.js +9 -0
  5. package/dist/__generated__/gateway.js.map +1 -0
  6. package/dist/__generated__/talent-profile-zod-schemas.d.ts +1187 -0
  7. package/dist/__generated__/talent-profile-zod-schemas.d.ts.map +1 -0
  8. package/dist/__generated__/talent-profile-zod-schemas.js +1136 -0
  9. package/dist/__generated__/talent-profile-zod-schemas.js.map +1 -0
  10. package/dist/__generated__/talent-profile.d.ts +1397 -0
  11. package/dist/__generated__/talent-profile.d.ts.map +1 -0
  12. package/dist/__generated__/talent-profile.js +9 -0
  13. package/dist/__generated__/talent-profile.js.map +1 -0
  14. package/dist/__generated__/zod-schemas.d.ts +2895 -0
  15. package/dist/__generated__/zod-schemas.d.ts.map +1 -0
  16. package/dist/__generated__/zod-schemas.js +3121 -0
  17. package/dist/__generated__/zod-schemas.js.map +1 -0
  18. package/dist/__tests__/fixtures/profile/builders.d.ts +74 -0
  19. package/dist/__tests__/fixtures/profile/builders.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/profile/builders.js +196 -0
  21. package/dist/__tests__/fixtures/profile/builders.js.map +1 -0
  22. package/dist/__tests__/fixtures/profile/data.d.ts +39 -0
  23. package/dist/__tests__/fixtures/profile/data.d.ts.map +1 -0
  24. package/dist/__tests__/fixtures/profile/data.js +230 -0
  25. package/dist/__tests__/fixtures/profile/data.js.map +1 -0
  26. package/dist/__tests__/fixtures/profile/index.d.ts +9 -0
  27. package/dist/__tests__/fixtures/profile/index.d.ts.map +1 -0
  28. package/dist/__tests__/fixtures/profile/index.js +10 -0
  29. package/dist/__tests__/fixtures/profile/index.js.map +1 -0
  30. package/dist/__tests__/fixtures/profile/types.d.ts +53 -0
  31. package/dist/__tests__/fixtures/profile/types.d.ts.map +1 -0
  32. package/dist/__tests__/fixtures/profile/types.js +4 -0
  33. package/dist/__tests__/fixtures/profile/types.js.map +1 -0
  34. package/dist/auth/errors.d.ts +82 -0
  35. package/dist/auth/errors.d.ts.map +1 -0
  36. package/dist/auth/errors.js +68 -0
  37. package/dist/auth/errors.js.map +1 -0
  38. package/dist/auth.d.ts +192 -0
  39. package/dist/auth.d.ts.map +1 -0
  40. package/dist/auth.js +294 -0
  41. package/dist/auth.js.map +1 -0
  42. package/dist/config.d.ts +212 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +349 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/configLock.d.ts +50 -0
  47. package/dist/configLock.d.ts.map +1 -0
  48. package/dist/configLock.js +88 -0
  49. package/dist/configLock.js.map +1 -0
  50. package/dist/configWriter.d.ts +97 -0
  51. package/dist/configWriter.d.ts.map +1 -0
  52. package/dist/configWriter.js +687 -0
  53. package/dist/configWriter.js.map +1 -0
  54. package/dist/index.d.ts +37 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +28 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/kill-switch.d.ts +161 -0
  59. package/dist/kill-switch.d.ts.map +1 -0
  60. package/dist/kill-switch.js +235 -0
  61. package/dist/kill-switch.js.map +1 -0
  62. package/dist/lib/date.d.ts +58 -0
  63. package/dist/lib/date.d.ts.map +1 -0
  64. package/dist/lib/date.js +104 -0
  65. package/dist/lib/date.js.map +1 -0
  66. package/dist/lib/diagnostic-log.d.ts +159 -0
  67. package/dist/lib/diagnostic-log.d.ts.map +1 -0
  68. package/dist/lib/diagnostic-log.js +186 -0
  69. package/dist/lib/diagnostic-log.js.map +1 -0
  70. package/dist/lib/package-version.d.ts +19 -0
  71. package/dist/lib/package-version.d.ts.map +1 -0
  72. package/dist/lib/package-version.js +38 -0
  73. package/dist/lib/package-version.js.map +1 -0
  74. package/dist/lib/redact.d.ts +153 -0
  75. package/dist/lib/redact.d.ts.map +1 -0
  76. package/dist/lib/redact.js +207 -0
  77. package/dist/lib/redact.js.map +1 -0
  78. package/dist/lib/text.d.ts +14 -0
  79. package/dist/lib/text.d.ts.map +1 -0
  80. package/dist/lib/text.js +21 -0
  81. package/dist/lib/text.js.map +1 -0
  82. package/dist/lib/wire-shape.d.ts +131 -0
  83. package/dist/lib/wire-shape.d.ts.map +1 -0
  84. package/dist/lib/wire-shape.js +376 -0
  85. package/dist/lib/wire-shape.js.map +1 -0
  86. package/dist/onepassword.d.ts +29 -0
  87. package/dist/onepassword.d.ts.map +1 -0
  88. package/dist/onepassword.js +112 -0
  89. package/dist/onepassword.js.map +1 -0
  90. package/dist/services/_shared/transport.d.ts +148 -0
  91. package/dist/services/_shared/transport.d.ts.map +1 -0
  92. package/dist/services/_shared/transport.js +102 -0
  93. package/dist/services/_shared/transport.js.map +1 -0
  94. package/dist/services/applications/index.d.ts +210 -0
  95. package/dist/services/applications/index.d.ts.map +1 -0
  96. package/dist/services/applications/index.js +240 -0
  97. package/dist/services/applications/index.js.map +1 -0
  98. package/dist/services/availability/index.d.ts +254 -0
  99. package/dist/services/availability/index.d.ts.map +1 -0
  100. package/dist/services/availability/index.js +310 -0
  101. package/dist/services/availability/index.js.map +1 -0
  102. package/dist/services/contracts/index.d.ts +132 -0
  103. package/dist/services/contracts/index.d.ts.map +1 -0
  104. package/dist/services/contracts/index.js +211 -0
  105. package/dist/services/contracts/index.js.map +1 -0
  106. package/dist/services/engagements/index.d.ts +504 -0
  107. package/dist/services/engagements/index.d.ts.map +1 -0
  108. package/dist/services/engagements/index.js +613 -0
  109. package/dist/services/engagements/index.js.map +1 -0
  110. package/dist/services/jobs/index.d.ts +490 -0
  111. package/dist/services/jobs/index.d.ts.map +1 -0
  112. package/dist/services/jobs/index.js +753 -0
  113. package/dist/services/jobs/index.js.map +1 -0
  114. package/dist/services/payments/index.d.ts +415 -0
  115. package/dist/services/payments/index.d.ts.map +1 -0
  116. package/dist/services/payments/index.js +636 -0
  117. package/dist/services/payments/index.js.map +1 -0
  118. package/dist/services/profile/__tests__/fixtures.d.ts +214 -0
  119. package/dist/services/profile/__tests__/fixtures.d.ts.map +1 -0
  120. package/dist/services/profile/__tests__/fixtures.js +176 -0
  121. package/dist/services/profile/__tests__/fixtures.js.map +1 -0
  122. package/dist/services/profile/basic/index.d.ts +390 -0
  123. package/dist/services/profile/basic/index.d.ts.map +1 -0
  124. package/dist/services/profile/basic/index.js +1007 -0
  125. package/dist/services/profile/basic/index.js.map +1 -0
  126. package/dist/services/profile/certifications/index.d.ts +74 -0
  127. package/dist/services/profile/certifications/index.d.ts.map +1 -0
  128. package/dist/services/profile/certifications/index.js +169 -0
  129. package/dist/services/profile/certifications/index.js.map +1 -0
  130. package/dist/services/profile/education/index.d.ts +73 -0
  131. package/dist/services/profile/education/index.d.ts.map +1 -0
  132. package/dist/services/profile/education/index.js +168 -0
  133. package/dist/services/profile/education/index.js.map +1 -0
  134. package/dist/services/profile/employment/index.d.ts +111 -0
  135. package/dist/services/profile/employment/index.d.ts.map +1 -0
  136. package/dist/services/profile/employment/index.js +202 -0
  137. package/dist/services/profile/employment/index.js.map +1 -0
  138. package/dist/services/profile/external/index.d.ts +219 -0
  139. package/dist/services/profile/external/index.d.ts.map +1 -0
  140. package/dist/services/profile/external/index.js +560 -0
  141. package/dist/services/profile/external/index.js.map +1 -0
  142. package/dist/services/profile/index.d.ts +24 -0
  143. package/dist/services/profile/index.d.ts.map +1 -0
  144. package/dist/services/profile/index.js +26 -0
  145. package/dist/services/profile/index.js.map +1 -0
  146. package/dist/services/profile/industries/index.d.ts +130 -0
  147. package/dist/services/profile/industries/index.d.ts.map +1 -0
  148. package/dist/services/profile/industries/index.js +292 -0
  149. package/dist/services/profile/industries/index.js.map +1 -0
  150. package/dist/services/profile/portfolio/index.d.ts +352 -0
  151. package/dist/services/profile/portfolio/index.d.ts.map +1 -0
  152. package/dist/services/profile/portfolio/index.js +833 -0
  153. package/dist/services/profile/portfolio/index.js.map +1 -0
  154. package/dist/services/profile/resume/index.d.ts +60 -0
  155. package/dist/services/profile/resume/index.d.ts.map +1 -0
  156. package/dist/services/profile/resume/index.js +212 -0
  157. package/dist/services/profile/resume/index.js.map +1 -0
  158. package/dist/services/profile/reviews/index.d.ts +137 -0
  159. package/dist/services/profile/reviews/index.d.ts.map +1 -0
  160. package/dist/services/profile/reviews/index.js +431 -0
  161. package/dist/services/profile/reviews/index.js.map +1 -0
  162. package/dist/services/profile/shared.d.ts +127 -0
  163. package/dist/services/profile/shared.d.ts.map +1 -0
  164. package/dist/services/profile/shared.js +155 -0
  165. package/dist/services/profile/shared.js.map +1 -0
  166. package/dist/services/profile/skills/index.d.ts +212 -0
  167. package/dist/services/profile/skills/index.d.ts.map +1 -0
  168. package/dist/services/profile/skills/index.js +461 -0
  169. package/dist/services/profile/skills/index.js.map +1 -0
  170. package/dist/services/profile/visas/index.d.ts +74 -0
  171. package/dist/services/profile/visas/index.d.ts.map +1 -0
  172. package/dist/services/profile/visas/index.js +306 -0
  173. package/dist/services/profile/visas/index.js.map +1 -0
  174. package/dist/services/timesheet/index.d.ts +326 -0
  175. package/dist/services/timesheet/index.d.ts.map +1 -0
  176. package/dist/services/timesheet/index.js +324 -0
  177. package/dist/services/timesheet/index.js.map +1 -0
  178. package/dist/services/translations.d.ts +79 -0
  179. package/dist/services/translations.d.ts.map +1 -0
  180. package/dist/services/translations.js +136 -0
  181. package/dist/services/translations.js.map +1 -0
  182. package/dist/transport-resilience.d.ts +136 -0
  183. package/dist/transport-resilience.d.ts.map +1 -0
  184. package/dist/transport-resilience.js +247 -0
  185. package/dist/transport-resilience.js.map +1 -0
  186. package/dist/transport.d.ts +408 -0
  187. package/dist/transport.d.ts.map +1 -0
  188. package/dist/transport.js +691 -0
  189. package/dist/transport.js.map +1 -0
  190. package/dist/types.d.ts +41 -0
  191. package/dist/types.d.ts.map +1 -0
  192. package/dist/types.js +18 -0
  193. package/dist/types.js.map +1 -0
  194. package/package.json +40 -12
  195. package/index.js +0 -7
@@ -0,0 +1,324 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ import { buildDryRunPreview } from "../../transport.js";
4
+ import { callGatewayShared } from "../_shared/transport.js";
5
+ export class TimesheetError extends Error {
6
+ code;
7
+ name = "TimesheetError";
8
+ constructor(code, message, options) {
9
+ super(message, options);
10
+ this.code = code;
11
+ }
12
+ }
13
+ // ---------------------------------------------------------------------
14
+ // Verbatim from `../research/graphql/gateway/operations/mobile/PendingTimesheets.graphql`.
15
+ const PENDING_TIMESHEETS_QUERY = `query PendingTimesheets { viewer { __typename id ...pendingTimesheets } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } } fragment pendingTimesheets on Viewer { __typename billingCycles(filters: { pendingTimesheetOnly: true } , pagination: { limit: 50 } ) { __typename nodes { __typename ...timesheetListFields } } }`;
16
+ // Verbatim from `../research/graphql/gateway/operations/mobile/Timesheets.graphql`.
17
+ const TIMESHEETS_QUERY = `query Timesheets($jobActivityItemId: ID!) { viewer { __typename id jobActivityItem(id: $jobActivityItemId) { __typename id engagement { __typename id billingCycles(filters: { onlyTimesheets: true } ) { __typename ids nodes { __typename ...timesheetListFields } } } } } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } }`;
18
+ // Verbatim from `../research/graphql/gateway/operations/mobile/TimesheetDetails.graphql`.
19
+ const TIMESHEET_DETAILS_QUERY = `query TimesheetDetails($id: ID!) { node(id: $id) { __typename ...timesheetDetailsFields } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } } fragment contactFieldsData on ContactFields { __typename communitySlackId email phoneNumber skype } fragment timeZoneFields on TimeZone { __typename location value } fragment recruiterData on Recruiter { __typename id fullName contactFields { __typename ...contactFieldsData } photo { __typename small } vacation { __typename id startDate endDate } timeZone { __typename ...timeZoneFields } } fragment pointOfContactData on PointsOfContact { __typename current { __typename ...recruiterData } handoff { __typename ...recruiterData } kind } fragment deliveryModelData on TalentEngagementDeliveryModel { __typename id identifier } fragment timesheetDetailsFields on BillingCycle { __typename ...timesheetListFields timesheetUrl actualAgreement { __typename applicationRate talentHourlyRate marketplaceMargin } engagement { __typename id expectedHours job { __typename id pointsOfContact { __typename ...pointOfContactData } engagementDeliveryModel { __typename ...deliveryModelData } } } timesheetComment timesheetRecords { __typename date duration isDayOff note } }`;
20
+ // Verbatim from `../research/graphql/gateway/operations/mobile/SubmitTimesheet.graphql`.
21
+ const SUBMIT_TIMESHEET_MUTATION = `mutation SubmitTimesheet($id: ID!) { submitTimesheet(billingCycleId: $id) { __typename billingCycle { __typename ...timesheetDetailsFields } ...mutationResultFields } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } } fragment contactFieldsData on ContactFields { __typename communitySlackId email phoneNumber skype } fragment timeZoneFields on TimeZone { __typename location value } fragment recruiterData on Recruiter { __typename id fullName contactFields { __typename ...contactFieldsData } photo { __typename small } vacation { __typename id startDate endDate } timeZone { __typename ...timeZoneFields } } fragment pointOfContactData on PointsOfContact { __typename current { __typename ...recruiterData } handoff { __typename ...recruiterData } kind } fragment deliveryModelData on TalentEngagementDeliveryModel { __typename id identifier } fragment timesheetDetailsFields on BillingCycle { __typename ...timesheetListFields timesheetUrl actualAgreement { __typename applicationRate talentHourlyRate marketplaceMargin } engagement { __typename id expectedHours job { __typename id pointsOfContact { __typename ...pointOfContactData } engagementDeliveryModel { __typename ...deliveryModelData } } } timesheetComment timesheetRecords { __typename date duration isDayOff note } } fragment mutationResultFields on MutationResult { __typename errors { __typename key message code } success }`;
22
+ /**
23
+ * Server-side GraphQL error messages that signal "the supplied id does
24
+ * not resolve to a known node" — remapped from `GRAPHQL_ERROR` to the
25
+ * domain-typed `NOT_FOUND` for UX clarity.
26
+ *
27
+ * Empirically observed (E2E 2026-05-12, `TimesheetDetails`, mobile-gateway):
28
+ *
29
+ * "Node id 'VjEtTm9uZXhpc3RlbnQtMA' resolves to an unknown type
30
+ * Nonexistent. Please check if there is no typo and schemas are up
31
+ * to date."
32
+ *
33
+ * This is the Relay-style global-id decode error — Toptal's gateway
34
+ * decodes `<Type>-<localId>` and rejects when the type prefix isn't a
35
+ * known schema node. Match the stable phrase `Node id ... resolves to
36
+ * an unknown type` (typo-tolerant via `.*?`); the historical
37
+ * `Record not found` literal is kept for defense-in-depth against an
38
+ * older message variant.
39
+ */
40
+ const NOT_FOUND_MESSAGE_PATTERN = /Record not found|Node id .*? resolves to an unknown type/i;
41
+ /**
42
+ * Thin per-service wrapper around {@link callGatewayShared} (issue
43
+ * #329). Pins the mobile-gateway surface and the {@link TimesheetError}
44
+ * domain class.
45
+ */
46
+ async function callGateway(token, operationName, query, variables, schema) {
47
+ return callGatewayShared("mobile-gateway", token, operationName, query, variables, TimesheetError, { schema });
48
+ }
49
+ /**
50
+ * Project a `TimesheetListWireItem` into the public list shape. Pure
51
+ * shape mirror — present so the list and resolve paths share a single
52
+ * mapper.
53
+ */
54
+ function projectListItem(wire) {
55
+ return {
56
+ id: wire.id,
57
+ startDate: wire.startDate,
58
+ endDate: wire.endDate,
59
+ hours: wire.hours,
60
+ minimumCommitment: wire.minimumCommitment,
61
+ timesheetOverdue: wire.timesheetOverdue,
62
+ timesheetSubmissionOpenDatetime: wire.timesheetSubmissionOpenDatetime,
63
+ timesheetSubmissionDeadlineDatetime: wire.timesheetSubmissionDeadlineDatetime,
64
+ timesheetSubmitted: wire.timesheetSubmitted,
65
+ engagement: wire.engagement,
66
+ };
67
+ }
68
+ function projectDetailItem(wire) {
69
+ return {
70
+ ...projectListItem(wire),
71
+ timesheetUrl: wire.timesheetUrl,
72
+ timesheetComment: wire.timesheetComment,
73
+ timesheetRecords: wire.timesheetRecords ?? [],
74
+ actualAgreement: wire.actualAgreement,
75
+ engagement: {
76
+ ...wire.engagement,
77
+ expectedHours: wire.engagement.expectedHours,
78
+ },
79
+ };
80
+ }
81
+ /**
82
+ * List the signed-in user's timesheet billing cycles.
83
+ *
84
+ * Default (no `engagement` option): uses `PendingTimesheets` to fetch
85
+ * viewer-wide cycles that still need submission. This is the
86
+ * "what needs my attention" view.
87
+ *
88
+ * With `engagement` option: uses `Timesheets($jobActivityItemId)` to
89
+ * fetch ALL cycles for that engagement (regardless of submission
90
+ * state). The argument is the public `JobActivityItem.id` exposed by
91
+ * `engagements list`.
92
+ *
93
+ * The returned array preserves server order; the CLI / MCP do not
94
+ * re-sort.
95
+ */
96
+ export async function list(token, opts = {}) {
97
+ if (opts.engagement === undefined) {
98
+ const data = await callGateway(token, "PendingTimesheets", PENDING_TIMESHEETS_QUERY, {});
99
+ if (data.viewer === null) {
100
+ throw new TimesheetError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
101
+ }
102
+ return (data.viewer.billingCycles?.nodes ?? []).map(projectListItem);
103
+ }
104
+ let data;
105
+ try {
106
+ data = await callGateway(token, "Timesheets", TIMESHEETS_QUERY, {
107
+ jobActivityItemId: opts.engagement,
108
+ });
109
+ }
110
+ catch (err) {
111
+ if (err instanceof TimesheetError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
112
+ throw new TimesheetError("NOT_FOUND", `No engagement found with id "${opts.engagement}" (or you don't have access to it).`, { cause: err });
113
+ }
114
+ throw err;
115
+ }
116
+ if (data.viewer === null) {
117
+ throw new TimesheetError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
118
+ }
119
+ if (data.viewer.jobActivityItem === null) {
120
+ throw new TimesheetError("NOT_FOUND", `No engagement found with id "${opts.engagement}" (or you don't have access to it).`);
121
+ }
122
+ if (data.viewer.jobActivityItem.engagement === null) {
123
+ throw new TimesheetError("NO_ENGAGEMENT", `Activity item "${opts.engagement}" exists but has no engagement (likely an application or interview).`);
124
+ }
125
+ return (data.viewer.jobActivityItem.engagement.billingCycles?.nodes ?? []).map(projectListItem);
126
+ }
127
+ /**
128
+ * Fetch a single timesheet's detail by `BillingCycle.id`.
129
+ *
130
+ * Uses the gateway's `node(id)` polymorphic root with the captured
131
+ * `timesheetDetailsFields` fragment. Throws
132
+ * `TimesheetError("NOT_FOUND")` when the id doesn't resolve (matches
133
+ * both the `Record not found` GraphQL error path AND the data-shape
134
+ * sentinel `data.node === null`).
135
+ *
136
+ * Throws `TimesheetError("UNKNOWN")` if the node resolves but isn't a
137
+ * `BillingCycle` — that would be a wire surprise (a different `node`
138
+ * type carrying our fragment) and warrants surfacing rather than
139
+ * silent type-coercion.
140
+ */
141
+ export async function show(token, id) {
142
+ let data;
143
+ try {
144
+ data = await callGateway(token, "TimesheetDetails", TIMESHEET_DETAILS_QUERY, { id });
145
+ }
146
+ catch (err) {
147
+ if (err instanceof TimesheetError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
148
+ throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to it).`, {
149
+ cause: err,
150
+ });
151
+ }
152
+ throw err;
153
+ }
154
+ if (data.node === null) {
155
+ throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to it).`);
156
+ }
157
+ return projectDetailItem(data.node);
158
+ }
159
+ /**
160
+ * Resolve the "current" pending timesheet — the billing cycle whose
161
+ * submission window contains `now` AND which is not yet submitted.
162
+ *
163
+ * - `kind: "found"`: exactly one cycle matches.
164
+ * - `kind: "none"`: zero cycles match (too early before next window,
165
+ * too late after every cycle's deadline, or no engagements have
166
+ * timesheets enabled).
167
+ * - `kind: "multiple"`: more than one cycle matches (parallel
168
+ * engagements with overlapping current windows).
169
+ *
170
+ * When `opts.engagement` is provided, the resolution scopes to that
171
+ * engagement (uses `Timesheets($jobActivityItemId)` + client-side
172
+ * filter to `timesheetSubmitted === false`). Otherwise uses
173
+ * `PendingTimesheets` (server-side filtered).
174
+ *
175
+ * The `now` option exists for deterministic testing; production code
176
+ * paths pass nothing and the helper uses `new Date()`.
177
+ */
178
+ export async function resolveCurrentCycle(token, opts = {}) {
179
+ const now = opts.now ?? new Date();
180
+ const candidates = await listPending(token, opts.engagement);
181
+ const inWindow = candidates.filter((c) => isInSubmissionWindow(c, now));
182
+ if (inWindow.length === 0)
183
+ return { kind: "none" };
184
+ if (inWindow.length === 1) {
185
+ const cycle = inWindow[0];
186
+ if (cycle === undefined)
187
+ return { kind: "none" };
188
+ return { kind: "found", cycle };
189
+ }
190
+ return { kind: "multiple", candidates: inWindow };
191
+ }
192
+ /**
193
+ * Internal: return the pending (`timesheetSubmitted === false`) cycles
194
+ * — viewer-wide if no engagement scope, scoped + client-filtered if
195
+ * scoped. Shared between {@link resolveCurrentCycle} and any future
196
+ * callers wanting the same pre-filtered list.
197
+ */
198
+ async function listPending(token, engagement) {
199
+ if (engagement === undefined) {
200
+ return list(token);
201
+ }
202
+ const all = await list(token, { engagement });
203
+ return all.filter((c) => !c.timesheetSubmitted);
204
+ }
205
+ /**
206
+ * Returns `true` when `now` falls within the cycle's submission window
207
+ * AND the cycle is not yet submitted.
208
+ *
209
+ * Submission window is `[timesheetSubmissionOpenDatetime,
210
+ * timesheetSubmissionDeadlineDatetime]` (inclusive). Cycles missing
211
+ * either bound are excluded — defensive: if the server hasn't decided
212
+ * when the window opens/closes, "now" is unambiguously not inside.
213
+ */
214
+ function isInSubmissionWindow(cycle, now) {
215
+ if (cycle.timesheetSubmitted)
216
+ return false;
217
+ if (cycle.timesheetSubmissionOpenDatetime === null)
218
+ return false;
219
+ if (cycle.timesheetSubmissionDeadlineDatetime === null)
220
+ return false;
221
+ const open = Date.parse(cycle.timesheetSubmissionOpenDatetime);
222
+ const deadline = Date.parse(cycle.timesheetSubmissionDeadlineDatetime);
223
+ if (Number.isNaN(open) || Number.isNaN(deadline))
224
+ return false;
225
+ const t = now.getTime();
226
+ return t >= open && t <= deadline;
227
+ }
228
+ /**
229
+ * Submit a timesheet for billing.
230
+ *
231
+ * **Destructive**: the submission is one-way at the wire level — once
232
+ * submitted, the timesheet enters Toptal's billing pipeline. Callers
233
+ * (CLI / MCP) are responsible for end-user confirmation.
234
+ *
235
+ * `id` is the BillingCycle.id from `list()` / `show()`. Returns the
236
+ * post-submission detail payload (with `timesheetSubmitted: true`)
237
+ * wrapped in `{ kind: "applied", result }` on the apply path.
238
+ *
239
+ * Dry-run path (`options.dryRun === true`): builds a
240
+ * {@link DryRunPreview} of the mutation WITHOUT invoking the
241
+ * gateway transport. Returns `{ kind: "preview", preview }`. The
242
+ * CLI's `--dry-run` flag flows through here so the destructive
243
+ * mutation is never sent in preview mode. See {@link DryRunOptions}
244
+ * for the placeholder-id semantics when the CLI is in auto-resolve
245
+ * mode.
246
+ *
247
+ * Throws (apply path only — dry-run never throws domain errors):
248
+ * - `TimesheetError("NOT_FOUND")` when the id doesn't resolve to a
249
+ * billing cycle the viewer can submit AND the server is willing to
250
+ * communicate that as a structured error (Relay-style global-id
251
+ * decode failure, matched via {@link NOT_FOUND_MESSAGE_PATTERN}).
252
+ * - `TimesheetError("GRAPHQL_ERROR")` when the server returns a
253
+ * top-level GraphQL error other than the Relay decode pattern.
254
+ * Empirically (E2E 2026-05-12), Toptal's `SubmitTimesheet` returns
255
+ * `"500: Internal Server Error"` for syntactically valid but
256
+ * non-existent BillingCycle ids — the wire does not pre-validate the
257
+ * id, so the 500 surfaces verbatim. The CLI presents it as
258
+ * `GRAPHQL_ERROR` (not `NOT_FOUND`) to avoid misleading the caller
259
+ * into thinking the id was definitively absent vs. the server
260
+ * genuinely failed.
261
+ * - `TimesheetError("MUTATION_ERROR")` when the server reports
262
+ * `success: false` on `MutationResult` (commonly: missing required
263
+ * hours, already submitted, deadline passed). The message carries
264
+ * the server-side error code+key+message tuples.
265
+ */
266
+ export async function submit(token, id, options = {}) {
267
+ if (options.dryRun === true) {
268
+ return {
269
+ kind: "preview",
270
+ preview: buildDryRunPreview({
271
+ surface: "mobile-gateway",
272
+ authToken: token,
273
+ body: {
274
+ operationName: "SubmitTimesheet",
275
+ query: SUBMIT_TIMESHEET_MUTATION,
276
+ variables: { id },
277
+ },
278
+ }),
279
+ };
280
+ }
281
+ let data;
282
+ try {
283
+ data = await callGateway(token, "SubmitTimesheet", SUBMIT_TIMESHEET_MUTATION, { id });
284
+ }
285
+ catch (err) {
286
+ // Remap Relay-style id-decode failures from GRAPHQL_ERROR to NOT_FOUND
287
+ // for consistent UX with `show()` and the engagement-scoped `list()`.
288
+ // The 500-on-bad-id case (no decode error, just a server crash) flows
289
+ // through verbatim as GRAPHQL_ERROR — see function docstring.
290
+ if (err instanceof TimesheetError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
291
+ throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to submit it).`, {
292
+ cause: err,
293
+ });
294
+ }
295
+ throw err;
296
+ }
297
+ const payload = data.submitTimesheet;
298
+ if (payload === null) {
299
+ throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to submit it).`);
300
+ }
301
+ if (!payload.success) {
302
+ throw new TimesheetError("MUTATION_ERROR", formatMutationErrors("SubmitTimesheet rejected", payload.errors));
303
+ }
304
+ if (payload.billingCycle === null) {
305
+ throw new TimesheetError("UNKNOWN", "SubmitTimesheet succeeded but the server returned no updated billingCycle payload.");
306
+ }
307
+ return { kind: "applied", result: projectDetailItem(payload.billingCycle) };
308
+ }
309
+ function formatMutationErrors(prefix, errors) {
310
+ if (errors == null || errors.length === 0) {
311
+ return `${prefix}: no error detail returned.`;
312
+ }
313
+ const parts = errors.map((e) => {
314
+ const fields = [];
315
+ if (e.code != null)
316
+ fields.push(`code=${e.code}`);
317
+ if (e.key != null)
318
+ fields.push(`key=${e.key}`);
319
+ const head = fields.length > 0 ? `[${fields.join(", ")}] ` : "";
320
+ return `${head}${e.message ?? "(no message)"}`;
321
+ });
322
+ return `${prefix}: ${parts.join("; ")}`;
323
+ }
324
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/services/timesheet/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Server-field ↔ CLI-flag translation tables. Centralizes the mapping
3
+ * between Toptal GraphQL field names (server-side, non-canonical) and the
4
+ * canonical CLI flag names that ttctl exposes to users.
5
+ *
6
+ * Naming policy (see issue #72): the canonical vocabulary is Toptal-faithful
7
+ * with the user-friendly CLI surface as the public API. When a Toptal field
8
+ * name reads poorly as a flag (`quote` for what users would call a
9
+ * "headline", `about` for what users would call a "bio"), this table
10
+ * translates between the two — letting `core` services speak the GraphQL
11
+ * vocabulary while the CLI / MCP surfaces speak the user-friendly one.
12
+ *
13
+ * Conventions:
14
+ *
15
+ * - Each table is a flat `Record<string, string>` from server field name
16
+ * (key) to CLI flag name (value).
17
+ * - Tables are `as const` so consumers can derive precise key/value union
18
+ * types when needed.
19
+ * - One table per sub-domain (basic, skills, …). Sub-domains beyond `basic`
20
+ * land in their respective issues (#70/#71/#73-#76); this file gets
21
+ * amended, not split, as those land.
22
+ *
23
+ * The CLI / MCP layer is the single consumer of these tables. Service code
24
+ * inside `core` continues to use the GraphQL field names directly — the
25
+ * boundary at which translation happens is the `presentation` boundary
26
+ * (CLI flag parsing, MCP tool input shaping), NOT the service boundary.
27
+ */
28
+ /**
29
+ * Server-field → CLI-flag mapping for the `basic` profile sub-domain.
30
+ *
31
+ * Covers every `basic` field whose user-facing CLI flag differs from its
32
+ * GraphQL name. Fields whose CLI flag matches their GraphQL name (e.g.,
33
+ * `email`, `fullName`) are NOT listed — `serverToCli` / `cliToServer`
34
+ * pass unmapped keys through unchanged. Each entry encodes a deliberate
35
+ * vocabulary choice — see the per-line rationale.
36
+ */
37
+ export declare const PROFILE_BASIC_FIELDS: {
38
+ readonly quote: "headline";
39
+ readonly about: "bio";
40
+ };
41
+ /**
42
+ * Server-field → CLI-flag mapping for the `skills` profile sub-domain.
43
+ *
44
+ * Currently empty: every field on `ProfileSkillSet` (`experience`, `rating`,
45
+ * `public`, `position`, `skill.{id,name}`) reads naturally as a CLI flag
46
+ * with no rename. The empty table is registered for two reasons:
47
+ *
48
+ * 1. **Structural symmetry** with `PROFILE_BASIC_FIELDS` — consumers can
49
+ * mechanically derive `serverToCli`/`cliToServer` calls per sub-domain
50
+ * regardless of whether any field actually renames, so future entries
51
+ * can be added in-place without changing call-sites.
52
+ * 2. **Discovery surface** — when a future field needs renaming
53
+ * (e.g., a server-side `experience` that's actually expressed in months
54
+ * while the CLI exposes `--experience-years`), this is the documented
55
+ * home for that mapping.
56
+ *
57
+ * The empty `as const` literal still produces a `Readonly<Record<string,
58
+ * string>>` per the `satisfies` clause, so `serverToCli(_, PROFILE_SKILL_FIELDS)`
59
+ * type-checks without the consumer having to special-case the empty case.
60
+ */
61
+ export declare const PROFILE_SKILL_FIELDS: {};
62
+ /**
63
+ * Translate a server-keyed object (GraphQL response shape) to a CLI-keyed
64
+ * object (user-facing flag shape). Keys present in the table are renamed;
65
+ * keys absent from the table pass through unchanged. Values are copied
66
+ * verbatim — translation is a key-rename pass, not a value transform.
67
+ *
68
+ * Returns a fresh object; the input is not mutated.
69
+ */
70
+ export declare function serverToCli<V>(serverObj: Readonly<Record<string, V>>, table: Readonly<Record<string, string>>): Record<string, V>;
71
+ /**
72
+ * Translate a CLI-keyed object (user-facing flag shape) to a server-keyed
73
+ * object (GraphQL input shape). Symmetric counterpart of `serverToCli`.
74
+ * Keys absent from the inverse mapping pass through unchanged.
75
+ *
76
+ * Returns a fresh object; the input is not mutated.
77
+ */
78
+ export declare function cliToServer<V>(cliObj: Readonly<Record<string, V>>, table: Readonly<Record<string, string>>): Record<string, V>;
79
+ //# sourceMappingURL=translations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../src/services/translations.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB;;;CAQoB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,IAEoB,CAAC;AAuCtD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EACtC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAOnB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAQnB"}
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ /**
4
+ * Server-field ↔ CLI-flag translation tables. Centralizes the mapping
5
+ * between Toptal GraphQL field names (server-side, non-canonical) and the
6
+ * canonical CLI flag names that ttctl exposes to users.
7
+ *
8
+ * Naming policy (see issue #72): the canonical vocabulary is Toptal-faithful
9
+ * with the user-friendly CLI surface as the public API. When a Toptal field
10
+ * name reads poorly as a flag (`quote` for what users would call a
11
+ * "headline", `about` for what users would call a "bio"), this table
12
+ * translates between the two — letting `core` services speak the GraphQL
13
+ * vocabulary while the CLI / MCP surfaces speak the user-friendly one.
14
+ *
15
+ * Conventions:
16
+ *
17
+ * - Each table is a flat `Record<string, string>` from server field name
18
+ * (key) to CLI flag name (value).
19
+ * - Tables are `as const` so consumers can derive precise key/value union
20
+ * types when needed.
21
+ * - One table per sub-domain (basic, skills, …). Sub-domains beyond `basic`
22
+ * land in their respective issues (#70/#71/#73-#76); this file gets
23
+ * amended, not split, as those land.
24
+ *
25
+ * The CLI / MCP layer is the single consumer of these tables. Service code
26
+ * inside `core` continues to use the GraphQL field names directly — the
27
+ * boundary at which translation happens is the `presentation` boundary
28
+ * (CLI flag parsing, MCP tool input shaping), NOT the service boundary.
29
+ */
30
+ /**
31
+ * Server-field → CLI-flag mapping for the `basic` profile sub-domain.
32
+ *
33
+ * Covers every `basic` field whose user-facing CLI flag differs from its
34
+ * GraphQL name. Fields whose CLI flag matches their GraphQL name (e.g.,
35
+ * `email`, `fullName`) are NOT listed — `serverToCli` / `cliToServer`
36
+ * pass unmapped keys through unchanged. Each entry encodes a deliberate
37
+ * vocabulary choice — see the per-line rationale.
38
+ */
39
+ export const PROFILE_BASIC_FIELDS = {
40
+ // GraphQL `Profile.quote` is the short tagline shown above the user's
41
+ // photo. "Quote" reads poorly as a CLI flag (`--quote` suggests pricing or
42
+ // an exact citation); the user-facing name is `headline`.
43
+ quote: "headline",
44
+ // GraphQL `Profile.about` is the long-form bio paragraph. The CLI flag
45
+ // calls this `bio` (shorter and idiomatic).
46
+ about: "bio",
47
+ };
48
+ /**
49
+ * Server-field → CLI-flag mapping for the `skills` profile sub-domain.
50
+ *
51
+ * Currently empty: every field on `ProfileSkillSet` (`experience`, `rating`,
52
+ * `public`, `position`, `skill.{id,name}`) reads naturally as a CLI flag
53
+ * with no rename. The empty table is registered for two reasons:
54
+ *
55
+ * 1. **Structural symmetry** with `PROFILE_BASIC_FIELDS` — consumers can
56
+ * mechanically derive `serverToCli`/`cliToServer` calls per sub-domain
57
+ * regardless of whether any field actually renames, so future entries
58
+ * can be added in-place without changing call-sites.
59
+ * 2. **Discovery surface** — when a future field needs renaming
60
+ * (e.g., a server-side `experience` that's actually expressed in months
61
+ * while the CLI exposes `--experience-years`), this is the documented
62
+ * home for that mapping.
63
+ *
64
+ * The empty `as const` literal still produces a `Readonly<Record<string,
65
+ * string>>` per the `satisfies` clause, so `serverToCli(_, PROFILE_SKILL_FIELDS)`
66
+ * type-checks without the consumer having to special-case the empty case.
67
+ */
68
+ export const PROFILE_SKILL_FIELDS = {
69
+ // Currently empty — see module docstring above for rationale.
70
+ };
71
+ /**
72
+ * Reverse the direction of a translation table — CLI-flag → server-field.
73
+ *
74
+ * Used by `cliToServer` to translate user-supplied option objects into the
75
+ * shape expected by GraphQL inputs. Computed once per import via the
76
+ * `INVERSE_*` constants below; do NOT call this at request time (it
77
+ * allocates a fresh object).
78
+ */
79
+ function invertTable(table) {
80
+ const inverse = {};
81
+ for (const [server, cli] of Object.entries(table)) {
82
+ inverse[cli] = server;
83
+ }
84
+ return inverse;
85
+ }
86
+ const INVERSE_PROFILE_BASIC_FIELDS = invertTable(PROFILE_BASIC_FIELDS);
87
+ const INVERSE_PROFILE_SKILL_FIELDS = invertTable(PROFILE_SKILL_FIELDS);
88
+ /**
89
+ * Pre-computed inverse tables (CLI-flag → server-field) keyed by their
90
+ * forward counterpart, so callers can pass either direction's table to a
91
+ * shared helper. Kept private — `cliToServer` resolves the right inverse
92
+ * via `getInverse()` rather than asking callers to pass it.
93
+ */
94
+ const INVERSES = new WeakMap([
95
+ [PROFILE_BASIC_FIELDS, INVERSE_PROFILE_BASIC_FIELDS],
96
+ [PROFILE_SKILL_FIELDS, INVERSE_PROFILE_SKILL_FIELDS],
97
+ ]);
98
+ function getInverse(table) {
99
+ // Tables registered above hit the WeakMap; ad-hoc tables (e.g., from a
100
+ // unit test) compute their inverse on the fly. The fast path is the
101
+ // expected one — service code always passes a registered table.
102
+ return INVERSES.get(table) ?? invertTable(table);
103
+ }
104
+ /**
105
+ * Translate a server-keyed object (GraphQL response shape) to a CLI-keyed
106
+ * object (user-facing flag shape). Keys present in the table are renamed;
107
+ * keys absent from the table pass through unchanged. Values are copied
108
+ * verbatim — translation is a key-rename pass, not a value transform.
109
+ *
110
+ * Returns a fresh object; the input is not mutated.
111
+ */
112
+ export function serverToCli(serverObj, table) {
113
+ const out = {};
114
+ for (const [key, value] of Object.entries(serverObj)) {
115
+ const cliKey = table[key] ?? key;
116
+ out[cliKey] = value;
117
+ }
118
+ return out;
119
+ }
120
+ /**
121
+ * Translate a CLI-keyed object (user-facing flag shape) to a server-keyed
122
+ * object (GraphQL input shape). Symmetric counterpart of `serverToCli`.
123
+ * Keys absent from the inverse mapping pass through unchanged.
124
+ *
125
+ * Returns a fresh object; the input is not mutated.
126
+ */
127
+ export function cliToServer(cliObj, table) {
128
+ const inverse = getInverse(table);
129
+ const out = {};
130
+ for (const [key, value] of Object.entries(cliObj)) {
131
+ const serverKey = inverse[key] ?? key;
132
+ out[serverKey] = value;
133
+ }
134
+ return out;
135
+ }
136
+ //# sourceMappingURL=translations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translations.js","sourceRoot":"","sources":["../../src/services/translations.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,oCAAoC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,sEAAsE;IACtE,2EAA2E;IAC3E,0DAA0D;IAC1D,KAAK,EAAE,UAAU;IACjB,uEAAuE;IACvE,4CAA4C;IAC5C,KAAK,EAAE,KAAK;CACuC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;AAClC,8DAA8D;CACX,CAAC;AAEtD;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,KAAuC;IAC1D,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;IACxB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,4BAA4B,GAAG,WAAW,CAAC,oBAAoB,CAAC,CAAC;AACvE,MAAM,4BAA4B,GAAG,WAAW,CAAC,oBAAoB,CAAC,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAqE;IAC/F,CAAC,oBAAoB,EAAE,4BAA4B,CAAC;IACpD,CAAC,oBAAoB,EAAE,4BAA4B,CAAC;CACrD,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,KAAuC;IACzD,uEAAuE;IACvE,oEAAoE;IACpE,gEAAgE;IAChE,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CACzB,SAAsC,EACtC,KAAuC;IAEvC,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;QACjC,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,MAAmC,EACnC,KAAuC;IAEvC,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;QACtC,GAAG,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;IACzB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}