@apifuse/provider-sdk 2.2.0-beta.52 → 2.2.0-beta.53

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.53
4
+
5
+ - Release candidate for main commit 40a916bac16bb2b2bbda7fc4a96132d10ca103bf.
6
+
3
7
  ## 2.2.0-beta.52
4
8
 
5
9
  - Release candidate for main commit 6e64ae0ad2e735b3de0c115f5ea29517efcf4ff2.
@@ -1,4 +1,4 @@
1
- export type OperationDeclarationRefusalReason = "no_safety" | "safety_conflict" | "locale_key_conflict" | "connection_mode_conflict" | "execution_conflict" | "approval_conflict" | "non_literal" | "unsupported_member" | "factory_composed_operations" | "source_syntax" | "codemod_syntax" | "missing_english_locale" | "operation_id_unresolved" | "examples_conflict" | "locale_todo_conflict";
1
+ export type OperationDeclarationRefusalReason = "no_safety" | "safety_conflict" | "locale_key_conflict" | "connection_mode_conflict" | "execution_conflict" | "approval_conflict" | "non_literal" | "unsupported_member" | "factory_composed_operations" | "source_syntax" | "codemod_syntax" | "missing_english_locale" | "operation_id_unresolved" | "examples_conflict" | "invalid_locale_key" | "locale_todo_conflict";
2
2
  export type OperationDeclarationRefusal = {
3
3
  readonly file: string;
4
4
  readonly operationKey: string;
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, extname, join, relative, resolve } from "node:path";
3
+ import { assertProviderLocaleKey } from "../i18n/keys.js";
4
+ import { operationIdToLocaleNamespace } from "../i18n/operation-locale-namespace.js";
3
5
  const ts = await loadTypeScript();
4
6
  async function loadTypeScript() {
5
7
  try {
@@ -81,6 +83,10 @@ export function migrateOperationDeclaration(sourceText, fileName, options = {})
81
83
  }
82
84
  if (refusals.length > 0)
83
85
  return { status: "refused", refusals };
86
+ const invalidLocaleKey = findInvalidLocaleTodo(todos, fileName);
87
+ if (invalidLocaleKey !== undefined) {
88
+ return { status: "refused", refusals: [invalidLocaleKey] };
89
+ }
84
90
  if (edits.length === 0) {
85
91
  return {
86
92
  status: "unchanged",
@@ -221,7 +227,20 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
221
227
  if (merged.insert !== undefined)
222
228
  addInsertion(insertions, "docs", merged.insert);
223
229
  }
224
- const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, constArrays, localeFiles);
230
+ const title = planTitleLocale(top.byName.get("title"), top.byName.get("titleKey"), docs.get("titleKey"), fileName, site, localeFiles);
231
+ if ("refusal" in title)
232
+ return title;
233
+ const localeNamespace = top.byName.get("inputExamples") === undefined
234
+ ? { namespace: site.operationKey }
235
+ : resolveOperationLocaleNamespace([
236
+ top.byName.get("titleKey"),
237
+ docs.get("titleKey"),
238
+ top.byName.get("descriptionKey"),
239
+ docs.get("descriptionKey"),
240
+ ], fileName, site);
241
+ if ("refusal" in localeNamespace)
242
+ return localeNamespace;
243
+ const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, constArrays, localeFiles, localeNamespace.namespace);
225
244
  if ("refusal" in examples)
226
245
  return examples;
227
246
  const edits = [...examples.edits];
@@ -245,7 +264,66 @@ function planOperationMigration(source, fileName, site, constObjects, constArray
245
264
  if (!edits.some((existing) => rangesOverlap(existing, edit)))
246
265
  edits.push(edit);
247
266
  }
248
- return { edits, localeTodos: examples.localeTodos };
267
+ return { edits, localeTodos: [...title.localeTodos, ...examples.localeTodos] };
268
+ }
269
+ function planTitleLocale(title, flatTitleKey, nestedTitleKey, fileName, site, localeFiles) {
270
+ if (title === undefined)
271
+ return { localeTodos: [] };
272
+ const originalProse = literalString(title.initializer);
273
+ if (originalProse === undefined) {
274
+ return nonLiteral(fileName, site.operationKey, "title must be a string literal so its authored prose can be preserved in the English locale catalog.");
275
+ }
276
+ if (!site.operationIdProven) {
277
+ return {
278
+ refusal: refusal(fileName, site.operationKey, "operation_id_unresolved", "title requires an exact operation id proven from a static operations map."),
279
+ };
280
+ }
281
+ if (!localeFiles.includes("locales/en.json")) {
282
+ return {
283
+ refusal: refusal(fileName, site.operationKey, "missing_english_locale", "title cannot be migrated because locales/en.json does not exist."),
284
+ };
285
+ }
286
+ let selectedTitleKey = flatTitleKey ?? nestedTitleKey;
287
+ if (flatTitleKey !== undefined && nestedTitleKey !== undefined) {
288
+ const same = equivalentLiteral(flatTitleKey.initializer, nestedTitleKey.initializer);
289
+ if (same === undefined) {
290
+ return nonLiteral(fileName, site.operationKey, "titleKey must be literal when both top-level and nested declarations exist.");
291
+ }
292
+ if (!same) {
293
+ return {
294
+ refusal: refusal(fileName, site.operationKey, "locale_key_conflict", "Top-level titleKey conflicts with nested titleKey."),
295
+ };
296
+ }
297
+ selectedTitleKey = flatTitleKey;
298
+ }
299
+ const explicitTitleKey = selectedTitleKey === undefined ? undefined : literalString(selectedTitleKey.initializer);
300
+ if (selectedTitleKey !== undefined && explicitTitleKey === undefined) {
301
+ return nonLiteral(fileName, site.operationKey, "titleKey must be a string literal so the title locale destination is provable.");
302
+ }
303
+ let selectedTitleLocaleKey;
304
+ if (explicitTitleKey !== undefined) {
305
+ selectedTitleLocaleKey = explicitTitleKey;
306
+ }
307
+ else {
308
+ try {
309
+ selectedTitleLocaleKey = `operations.${operationIdToLocaleNamespace(site.operationKey)}.title`;
310
+ }
311
+ catch (error) {
312
+ return {
313
+ refusal: invalidLocaleKeyRefusal(fileName, site.operationKey, `operations.${site.operationKey}.title`, error),
314
+ };
315
+ }
316
+ }
317
+ return {
318
+ localeTodos: [
319
+ {
320
+ localeFile: "locales/en.json",
321
+ operationKey: site.operationKey,
322
+ key: selectedTitleLocaleKey,
323
+ originalProse,
324
+ },
325
+ ],
326
+ };
249
327
  }
250
328
  function resolveRiskClass(top, annotations, toolRouter, fileName, operationKey) {
251
329
  const topRisk = top.get("riskClass");
@@ -381,7 +459,7 @@ function mergeFlatAndNested(field, flat, nested, fileName, operationKey, source,
381
459
  }
382
460
  return {};
383
461
  }
384
- function planExamples(inputExamples, existingExamples, fileName, site, source, constArrays, localeFiles) {
462
+ function planExamples(inputExamples, existingExamples, fileName, site, source, constArrays, localeFiles, localeNamespace) {
385
463
  if (inputExamples === undefined)
386
464
  return { edits: [], localeTodos: [] };
387
465
  if (existingExamples !== undefined) {
@@ -440,7 +518,7 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
440
518
  if (scenarioProse === undefined) {
441
519
  return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].scenario must be a string literal.`);
442
520
  }
443
- const scenarioKey = `operations.${site.operationKey}.examples.${index}.scenario`;
521
+ const scenarioKey = `operations.${localeNamespace}.examples.${index}.scenario`;
444
522
  edits.push(replaceExampleLocaleMember(scenario, "scenarioKey", scenarioKey, source));
445
523
  for (const localeFile of localeFiles) {
446
524
  todos.push({
@@ -456,7 +534,7 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
456
534
  if (rationaleProse === undefined) {
457
535
  return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].rationale must be a string literal.`);
458
536
  }
459
- const rationaleKey = `operations.${site.operationKey}.examples.${index}.rationale`;
537
+ const rationaleKey = `operations.${localeNamespace}.examples.${index}.rationale`;
460
538
  edits.push(replaceExampleLocaleMember(rationale, "rationaleKey", rationaleKey, source));
461
539
  for (const localeFile of localeFiles) {
462
540
  todos.push({
@@ -470,6 +548,34 @@ function planExamples(inputExamples, existingExamples, fileName, site, source, c
470
548
  }
471
549
  return { edits, localeTodos: todos };
472
550
  }
551
+ function resolveOperationLocaleNamespace(members, fileName, site) {
552
+ const authoredNamespaces = new Set();
553
+ for (const member of members) {
554
+ const localeKey = literalString(member?.initializer);
555
+ if (localeKey === undefined)
556
+ continue;
557
+ const segments = localeKey.split(".");
558
+ if (segments[0] === "operations" && segments[1] !== undefined) {
559
+ authoredNamespaces.add(segments[1]);
560
+ }
561
+ }
562
+ if (authoredNamespaces.size > 1) {
563
+ return {
564
+ refusal: refusal(fileName, site.operationKey, "locale_key_conflict", `Operation titleKey and descriptionKey declarations use different locale namespaces: ${[...authoredNamespaces].join(", ")}.`),
565
+ };
566
+ }
567
+ const authored = authoredNamespaces.values().next().value;
568
+ if (authored !== undefined)
569
+ return { namespace: authored };
570
+ try {
571
+ return { namespace: operationIdToLocaleNamespace(site.operationKey) };
572
+ }
573
+ catch (error) {
574
+ return {
575
+ refusal: invalidLocaleKeyRefusal(fileName, site.operationKey, `operations.${site.operationKey}.examples`, error),
576
+ };
577
+ }
578
+ }
473
579
  function replaceExampleLocaleMember(member, newName, localeKey, source) {
474
580
  return {
475
581
  start: member.property.getStart(source),
@@ -1049,6 +1155,21 @@ function firstSyntaxError(source) {
1049
1155
  function refusal(file, operationKey, reason, detail) {
1050
1156
  return { file, operationKey, reason, detail };
1051
1157
  }
1158
+ function findInvalidLocaleTodo(todos, fileName) {
1159
+ for (const todo of todos) {
1160
+ try {
1161
+ assertProviderLocaleKey(todo.key);
1162
+ }
1163
+ catch (error) {
1164
+ return invalidLocaleKeyRefusal(fileName, todo.operationKey, todo.key, error);
1165
+ }
1166
+ }
1167
+ return undefined;
1168
+ }
1169
+ function invalidLocaleKeyRefusal(fileName, operationKey, localeKey, error) {
1170
+ const validatorDetail = error instanceof Error ? error.message : String(error);
1171
+ return refusal(fileName, operationKey, "invalid_locale_key", `Refusing to write invalid provider locale key ${JSON.stringify(localeKey)}: ${validatorDetail}`);
1172
+ }
1052
1173
  function nonLiteral(fileName, operationKey, detail) {
1053
1174
  return {
1054
1175
  refusal: refusal(fileName, operationKey, "non_literal", detail),
@@ -0,0 +1,2 @@
1
+ /** Canonical locale-catalog namespace for a URL-safe provider operation id. */
2
+ export declare function operationIdToLocaleNamespace(operationId: string): string;
@@ -0,0 +1,7 @@
1
+ import { assertProviderLocaleKey } from "./keys.js";
2
+ /** Canonical locale-catalog namespace for a URL-safe provider operation id. */
3
+ export function operationIdToLocaleNamespace(operationId) {
4
+ const namespace = operationId.replace(/[-_]([a-z0-9])/g, (_separator, character) => character.toUpperCase());
5
+ assertProviderLocaleKey(`operations.${namespace}.description`);
6
+ return namespace;
7
+ }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.52",
2
+ "version": "2.2.0-beta.53",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -0,0 +1,17 @@
1
+ const listHospitalsOperation = defineOperation<ProviderContext>()({
2
+ annotations: { readOnly: true },
3
+ inputExamples: [
4
+ {
5
+ scenario: "List nearby hospitals",
6
+ input: { latitude: 37.5665, longitude: 126.978 },
7
+ rationale: "Exercises a kebab-case operation id.",
8
+ },
9
+ ],
10
+ input: InputSchema,
11
+ output: OutputSchema,
12
+ handler,
13
+ });
14
+
15
+ export default buildProvider({
16
+ operations: { "list-hospitals": listHospitalsOperation },
17
+ });
@@ -0,0 +1,21 @@
1
+ const listRecentEarthquakesOperation = defineOperation<ProviderContext>()({
2
+ annotations: { readOnly: true },
3
+ docs: {
4
+ titleKey: "operations.listRecentEarthquakes.title",
5
+ descriptionKey: "operations.listRecentEarthquakes.description",
6
+ },
7
+ inputExamples: [
8
+ {
9
+ scenario: "List recent earthquakes",
10
+ input: { limit: 10 },
11
+ rationale: "Exercises a snake-case operation id.",
12
+ },
13
+ ],
14
+ input: InputSchema,
15
+ output: OutputSchema,
16
+ handler,
17
+ });
18
+
19
+ export default buildProvider({
20
+ operations: { list_recent_earthquakes: listRecentEarthquakesOperation },
21
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "operations": {
3
+ "listRecentEarthquakes": {
4
+ "title": "Recent earthquakes",
5
+ "description": "Lists recent earthquakes."
6
+ }
7
+ }
8
+ }
@@ -3,6 +3,9 @@ import { dirname, extname, join, relative, resolve } from "node:path";
3
3
 
4
4
  import type TS from "typescript";
5
5
 
6
+ import { assertProviderLocaleKey } from "../i18n/keys.js";
7
+ import { operationIdToLocaleNamespace } from "../i18n/operation-locale-namespace.js";
8
+
6
9
  const ts: typeof import("typescript") = await loadTypeScript();
7
10
 
8
11
  async function loadTypeScript(): Promise<typeof import("typescript")> {
@@ -67,6 +70,7 @@ export type OperationDeclarationRefusalReason =
67
70
  | "missing_english_locale"
68
71
  | "operation_id_unresolved"
69
72
  | "examples_conflict"
73
+ | "invalid_locale_key"
70
74
  | "locale_todo_conflict";
71
75
 
72
76
  export type OperationDeclarationRefusal = {
@@ -180,6 +184,10 @@ export function migrateOperationDeclaration(
180
184
  todos.push(...plan.localeTodos);
181
185
  }
182
186
  if (refusals.length > 0) return { status: "refused", refusals };
187
+ const invalidLocaleKey = findInvalidLocaleTodo(todos, fileName);
188
+ if (invalidLocaleKey !== undefined) {
189
+ return { status: "refused", refusals: [invalidLocaleKey] };
190
+ }
183
191
 
184
192
  if (edits.length === 0) {
185
193
  return {
@@ -400,6 +408,30 @@ function planOperationMigration(
400
408
  if (merged.insert !== undefined) addInsertion(insertions, "docs", merged.insert);
401
409
  }
402
410
 
411
+ const title = planTitleLocale(
412
+ top.byName.get("title"),
413
+ top.byName.get("titleKey"),
414
+ docs.get("titleKey"),
415
+ fileName,
416
+ site,
417
+ localeFiles,
418
+ );
419
+ if ("refusal" in title) return title;
420
+ const localeNamespace =
421
+ top.byName.get("inputExamples") === undefined
422
+ ? { namespace: site.operationKey }
423
+ : resolveOperationLocaleNamespace(
424
+ [
425
+ top.byName.get("titleKey"),
426
+ docs.get("titleKey"),
427
+ top.byName.get("descriptionKey"),
428
+ docs.get("descriptionKey"),
429
+ ],
430
+ fileName,
431
+ site,
432
+ );
433
+ if ("refusal" in localeNamespace) return localeNamespace;
434
+
403
435
  const examples = planExamples(
404
436
  top.byName.get("inputExamples"),
405
437
  top.byName.get("examples"),
@@ -408,6 +440,7 @@ function planOperationMigration(
408
440
  source,
409
441
  constArrays,
410
442
  localeFiles,
443
+ localeNamespace.namespace,
411
444
  );
412
445
  if ("refusal" in examples) return examples;
413
446
 
@@ -431,7 +464,107 @@ function planOperationMigration(
431
464
  if (!edits.some((existing) => rangesOverlap(existing, edit))) edits.push(edit);
432
465
  }
433
466
 
434
- return { edits, localeTodos: examples.localeTodos };
467
+ return { edits, localeTodos: [...title.localeTodos, ...examples.localeTodos] };
468
+ }
469
+
470
+ function planTitleLocale(
471
+ title: ResolvedMember | undefined,
472
+ flatTitleKey: ResolvedMember | undefined,
473
+ nestedTitleKey: ResolvedMember | undefined,
474
+ fileName: string,
475
+ site: OperationSite,
476
+ localeFiles: readonly string[],
477
+ ):
478
+ | { readonly localeTodos: readonly LocaleTodo[] }
479
+ | { readonly refusal: OperationDeclarationRefusal } {
480
+ if (title === undefined) return { localeTodos: [] };
481
+ const originalProse = literalString(title.initializer);
482
+ if (originalProse === undefined) {
483
+ return nonLiteral(
484
+ fileName,
485
+ site.operationKey,
486
+ "title must be a string literal so its authored prose can be preserved in the English locale catalog.",
487
+ );
488
+ }
489
+ if (!site.operationIdProven) {
490
+ return {
491
+ refusal: refusal(
492
+ fileName,
493
+ site.operationKey,
494
+ "operation_id_unresolved",
495
+ "title requires an exact operation id proven from a static operations map.",
496
+ ),
497
+ };
498
+ }
499
+ if (!localeFiles.includes("locales/en.json")) {
500
+ return {
501
+ refusal: refusal(
502
+ fileName,
503
+ site.operationKey,
504
+ "missing_english_locale",
505
+ "title cannot be migrated because locales/en.json does not exist.",
506
+ ),
507
+ };
508
+ }
509
+
510
+ let selectedTitleKey = flatTitleKey ?? nestedTitleKey;
511
+ if (flatTitleKey !== undefined && nestedTitleKey !== undefined) {
512
+ const same = equivalentLiteral(flatTitleKey.initializer, nestedTitleKey.initializer);
513
+ if (same === undefined) {
514
+ return nonLiteral(
515
+ fileName,
516
+ site.operationKey,
517
+ "titleKey must be literal when both top-level and nested declarations exist.",
518
+ );
519
+ }
520
+ if (!same) {
521
+ return {
522
+ refusal: refusal(
523
+ fileName,
524
+ site.operationKey,
525
+ "locale_key_conflict",
526
+ "Top-level titleKey conflicts with nested titleKey.",
527
+ ),
528
+ };
529
+ }
530
+ selectedTitleKey = flatTitleKey;
531
+ }
532
+ const explicitTitleKey =
533
+ selectedTitleKey === undefined ? undefined : literalString(selectedTitleKey.initializer);
534
+ if (selectedTitleKey !== undefined && explicitTitleKey === undefined) {
535
+ return nonLiteral(
536
+ fileName,
537
+ site.operationKey,
538
+ "titleKey must be a string literal so the title locale destination is provable.",
539
+ );
540
+ }
541
+ let selectedTitleLocaleKey: string;
542
+ if (explicitTitleKey !== undefined) {
543
+ selectedTitleLocaleKey = explicitTitleKey;
544
+ } else {
545
+ try {
546
+ selectedTitleLocaleKey = `operations.${operationIdToLocaleNamespace(site.operationKey)}.title`;
547
+ } catch (error) {
548
+ return {
549
+ refusal: invalidLocaleKeyRefusal(
550
+ fileName,
551
+ site.operationKey,
552
+ `operations.${site.operationKey}.title`,
553
+ error,
554
+ ),
555
+ };
556
+ }
557
+ }
558
+ return {
559
+ localeTodos: [
560
+ {
561
+ localeFile: "locales/en.json",
562
+ operationKey: site.operationKey,
563
+ key: selectedTitleLocaleKey,
564
+ originalProse,
565
+ },
566
+ ],
567
+ };
435
568
  }
436
569
 
437
570
  function resolveRiskClass(
@@ -664,6 +797,7 @@ function planExamples(
664
797
  source: TS.SourceFile,
665
798
  constArrays: ReadonlyMap<string, TS.ArrayLiteralExpression>,
666
799
  localeFiles: readonly string[],
800
+ localeNamespace: string,
667
801
  ):
668
802
  | { readonly edits: readonly TextEdit[]; readonly localeTodos: readonly LocaleTodo[] }
669
803
  | { readonly refusal: OperationDeclarationRefusal } {
@@ -760,7 +894,7 @@ function planExamples(
760
894
  `inputExamples[${index}].scenario must be a string literal.`,
761
895
  );
762
896
  }
763
- const scenarioKey = `operations.${site.operationKey}.examples.${index}.scenario`;
897
+ const scenarioKey = `operations.${localeNamespace}.examples.${index}.scenario`;
764
898
  edits.push(replaceExampleLocaleMember(scenario, "scenarioKey", scenarioKey, source));
765
899
  for (const localeFile of localeFiles) {
766
900
  todos.push({
@@ -781,7 +915,7 @@ function planExamples(
781
915
  `inputExamples[${index}].rationale must be a string literal.`,
782
916
  );
783
917
  }
784
- const rationaleKey = `operations.${site.operationKey}.examples.${index}.rationale`;
918
+ const rationaleKey = `operations.${localeNamespace}.examples.${index}.rationale`;
785
919
  edits.push(replaceExampleLocaleMember(rationale, "rationaleKey", rationaleKey, source));
786
920
  for (const localeFile of localeFiles) {
787
921
  todos.push({
@@ -796,6 +930,47 @@ function planExamples(
796
930
  return { edits, localeTodos: todos };
797
931
  }
798
932
 
933
+ function resolveOperationLocaleNamespace(
934
+ members: readonly (ResolvedMember | undefined)[],
935
+ fileName: string,
936
+ site: OperationSite,
937
+ ): { readonly namespace: string } | { readonly refusal: OperationDeclarationRefusal } {
938
+ const authoredNamespaces = new Set<string>();
939
+ for (const member of members) {
940
+ const localeKey = literalString(member?.initializer);
941
+ if (localeKey === undefined) continue;
942
+ const segments = localeKey.split(".");
943
+ if (segments[0] === "operations" && segments[1] !== undefined) {
944
+ authoredNamespaces.add(segments[1]);
945
+ }
946
+ }
947
+ if (authoredNamespaces.size > 1) {
948
+ return {
949
+ refusal: refusal(
950
+ fileName,
951
+ site.operationKey,
952
+ "locale_key_conflict",
953
+ `Operation titleKey and descriptionKey declarations use different locale namespaces: ${[...authoredNamespaces].join(", ")}.`,
954
+ ),
955
+ };
956
+ }
957
+ const authored = authoredNamespaces.values().next().value;
958
+ if (authored !== undefined) return { namespace: authored };
959
+
960
+ try {
961
+ return { namespace: operationIdToLocaleNamespace(site.operationKey) };
962
+ } catch (error) {
963
+ return {
964
+ refusal: invalidLocaleKeyRefusal(
965
+ fileName,
966
+ site.operationKey,
967
+ `operations.${site.operationKey}.examples`,
968
+ error,
969
+ ),
970
+ };
971
+ }
972
+ }
973
+
799
974
  function replaceExampleLocaleMember(
800
975
  member: ResolvedMember,
801
976
  newName: string,
@@ -984,9 +1159,7 @@ function isProviderOperationsProperty(
984
1159
  // (a) The initializer (direct or via same-file const) mentions
985
1160
  // defineOperation / defineStreamOperation — the strongest signal.
986
1161
  const target = ts.isIdentifier(unwrapExpression(node.initializer) ?? node.initializer)
987
- ? constObjects.get(
988
- (unwrapExpression(node.initializer) as TS.Identifier).text,
989
- )
1162
+ ? constObjects.get((unwrapExpression(node.initializer) as TS.Identifier).text)
990
1163
  : undefined;
991
1164
  const initializerText = (target ?? node.initializer).getText();
992
1165
  if (/\bdefine(?:Stream)?Operation\b/.test(initializerText)) return true;
@@ -1513,6 +1686,35 @@ function refusal(
1513
1686
  return { file, operationKey, reason, detail };
1514
1687
  }
1515
1688
 
1689
+ function findInvalidLocaleTodo(
1690
+ todos: readonly LocaleTodo[],
1691
+ fileName: string,
1692
+ ): OperationDeclarationRefusal | undefined {
1693
+ for (const todo of todos) {
1694
+ try {
1695
+ assertProviderLocaleKey(todo.key);
1696
+ } catch (error) {
1697
+ return invalidLocaleKeyRefusal(fileName, todo.operationKey, todo.key, error);
1698
+ }
1699
+ }
1700
+ return undefined;
1701
+ }
1702
+
1703
+ function invalidLocaleKeyRefusal(
1704
+ fileName: string,
1705
+ operationKey: string,
1706
+ localeKey: string,
1707
+ error: unknown,
1708
+ ): OperationDeclarationRefusal {
1709
+ const validatorDetail = error instanceof Error ? error.message : String(error);
1710
+ return refusal(
1711
+ fileName,
1712
+ operationKey,
1713
+ "invalid_locale_key",
1714
+ `Refusing to write invalid provider locale key ${JSON.stringify(localeKey)}: ${validatorDetail}`,
1715
+ );
1716
+ }
1717
+
1516
1718
  function nonLiteral(
1517
1719
  fileName: string,
1518
1720
  operationKey: string,
@@ -0,0 +1,10 @@
1
+ import { assertProviderLocaleKey } from "./keys.js";
2
+
3
+ /** Canonical locale-catalog namespace for a URL-safe provider operation id. */
4
+ export function operationIdToLocaleNamespace(operationId: string): string {
5
+ const namespace = operationId.replace(/[-_]([a-z0-9])/g, (_separator, character: string) =>
6
+ character.toUpperCase(),
7
+ );
8
+ assertProviderLocaleKey(`operations.${namespace}.description`);
9
+ return namespace;
10
+ }