@kosdev-code/kos-codegen-core 0.1.0-next.861 → 0.1.0-next.864

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/index.mjs CHANGED
@@ -1066,7 +1066,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1066
1066
  }
1067
1067
  literal.addPropertyAssignment({
1068
1068
  name: spec.key,
1069
- initializer: spec.initializer
1069
+ initializer: spec.initializer,
1070
+ leadingTrivia: spec.leadingTrivia
1070
1071
  });
1071
1072
  return { key: spec.key, created: true };
1072
1073
  }
@@ -1084,7 +1085,8 @@ function ensureConstCatalogEntry(sourceFile, spec) {
1084
1085
  }
1085
1086
  literal.addPropertyAssignment({
1086
1087
  name: spec.key,
1087
- initializer: spec.initializer
1088
+ initializer: spec.initializer,
1089
+ leadingTrivia: spec.leadingTrivia
1088
1090
  });
1089
1091
  return { key: spec.key, created: true };
1090
1092
  }
@@ -1093,7 +1095,8 @@ function ensureExportedTypeAlias(sourceFile, spec) {
1093
1095
  sourceFile.addTypeAlias({
1094
1096
  name: spec.name,
1095
1097
  type: spec.type,
1096
- isExported: true
1098
+ isExported: true,
1099
+ docs: spec.docs ? [spec.docs] : void 0
1097
1100
  });
1098
1101
  return true;
1099
1102
  }
@@ -1123,6 +1126,7 @@ function addDecoratedMethod(cls, spec) {
1123
1126
  name: spec.name,
1124
1127
  isAsync: spec.isAsync,
1125
1128
  returnType: spec.returnType,
1129
+ docs: spec.docs ? [spec.docs] : void 0,
1126
1130
  parameters: spec.parameters?.map((p) => {
1127
1131
  return { name: p.name, type: p.type, hasQuestionToken: p.optional };
1128
1132
  }),
@@ -1130,6 +1134,7 @@ function addDecoratedMethod(cls, spec) {
1130
1134
  decorators: [
1131
1135
  {
1132
1136
  name: spec.decoratorName,
1137
+ typeArguments: spec.decoratorTypeArgs,
1133
1138
  arguments: spec.decoratorArgsText ? [spec.decoratorArgsText] : []
1134
1139
  }
1135
1140
  ]
@@ -1212,17 +1217,28 @@ function listServiceCatalog(codegenFs, query, projects) {
1212
1217
  app,
1213
1218
  version,
1214
1219
  serviceModulePath: posix,
1215
- operations: readOperations(codegenFs, openapiPath)
1220
+ operations: readOperations(codegenFs, openapiPath) ?? []
1216
1221
  });
1217
1222
  }
1218
1223
  return entries.sort(
1219
1224
  (a, b) => a.app.localeCompare(b.app) || a.version.localeCompare(b.version)
1220
1225
  );
1221
1226
  }
1227
+ function readServiceModuleOperations(codegenFs, serviceModuleFile) {
1228
+ const moduleFile = serviceModuleFile.endsWith(".ts") ? serviceModuleFile : `${serviceModuleFile}.ts`;
1229
+ const posix = moduleFile.split(path.sep).join("/");
1230
+ const openapiPath = posix.replace(/service\.ts$/, "openapi.d.ts");
1231
+ for (const candidate of [openapiPath, posix]) {
1232
+ if (!codegenFs.exists(candidate)) continue;
1233
+ const operations = readOperations(codegenFs, candidate);
1234
+ if (operations) return operations;
1235
+ }
1236
+ return null;
1237
+ }
1222
1238
  function readOperations(codegenFs, openapiPath) {
1223
1239
  return readSourceFile(codegenFs, openapiPath, (sf) => {
1224
1240
  const paths = sf.getInterface("paths");
1225
- if (!paths) return [];
1241
+ if (!paths) return null;
1226
1242
  const operations = [];
1227
1243
  for (const pathProp of paths.getProperties()) {
1228
1244
  const servicePath = unquote(pathProp.getName());
@@ -1744,7 +1760,7 @@ function servicesFilePathFor(modelFilePath, modelBase) {
1744
1760
  `${modelBase}-services.ts`
1745
1761
  );
1746
1762
  }
1747
- function header(modelBase) {
1763
+ function header$1(modelBase) {
1748
1764
  return `/**
1749
1765
  * Service layer for the ${modelBase} model: the endpoints it calls and the
1750
1766
  * types derived from them. Standalone service functions for callers outside a
@@ -1755,7 +1771,7 @@ function header(modelBase) {
1755
1771
  function ensureServicesModule(codegenFs, modelFilePath, modelBase) {
1756
1772
  const servicesFilePath = servicesFilePathFor(modelFilePath, modelBase);
1757
1773
  if (!codegenFs.exists(servicesFilePath)) {
1758
- codegenFs.write(servicesFilePath, header(modelBase));
1774
+ codegenFs.write(servicesFilePath, header$1(modelBase));
1759
1775
  }
1760
1776
  const barrelPath = path.join(path.dirname(servicesFilePath), "index.ts");
1761
1777
  if (!codegenFs.exists(barrelPath)) {
@@ -2515,6 +2531,154 @@ function addComputedToModel(codegenFs, options, projects) {
2515
2531
  });
2516
2532
  return { modelFilePath };
2517
2533
  }
2534
+ function mocksFilePathFor(modelFilePath, modelBase) {
2535
+ return path.join(
2536
+ path.dirname(modelFilePath),
2537
+ "mocks",
2538
+ `${modelBase}-mocks.ts`
2539
+ );
2540
+ }
2541
+ function mockRegisterFunctionName(modelBase) {
2542
+ return `register${pascalCase(modelBase)}Mocks`;
2543
+ }
2544
+ function toMockRoutePattern(servicePath) {
2545
+ return servicePath.replace(/\{([^}]+)\}/g, ":$1");
2546
+ }
2547
+ function mockRouteCall(method, pattern, body) {
2548
+ const shorthand = {
2549
+ get: "get",
2550
+ post: "post",
2551
+ put: "put",
2552
+ delete: "del"
2553
+ };
2554
+ const fn = shorthand[method];
2555
+ return fn ? `KosMock.${fn}(${JSON.stringify(pattern)}, ${body});` : `KosMock.route(${JSON.stringify(method.toUpperCase())}, ${JSON.stringify(
2556
+ pattern
2557
+ )}, ${body});`;
2558
+ }
2559
+ function header(modelBase, registerFn) {
2560
+ return `/**
2561
+ * KosMock routes for the ${modelBase} model's PROVISIONAL endpoints — the ones
2562
+ * this project's generated OpenAPI types do not declare.
2563
+ *
2564
+ * NOTHING IMPORTS THIS FILE. Call \`${registerFn}()\` from the app's dev entry,
2565
+ * gated so it cannot run in a production build (\`import.meta.env.DEV\`, a
2566
+ * dev-only entry module, or behind your own switch). Registering a route enables
2567
+ * KosMock, and a mock that shipped would shadow the real endpoint once it exists.
2568
+ *
2569
+ * Unmatched requests still pass through to the real device (KosMock's hybrid
2570
+ * default), so these routes shadow only the paths named below. Every mocked
2571
+ * response carries the \`kos-mocked: true\` wire header, so mock-fed data is
2572
+ * identifiable in devtools.
2573
+ *
2574
+ * DELETE THIS FILE once every endpoint below is in the generated types.
2575
+ */
2576
+ `;
2577
+ }
2578
+ function ensureMockRoute(codegenFs, options) {
2579
+ const mocksFilePath = mocksFilePathFor(
2580
+ options.modelFilePath,
2581
+ options.modelBase
2582
+ );
2583
+ const registerFunction = mockRegisterFunctionName(options.modelBase);
2584
+ const routePattern = toMockRoutePattern(options.servicePath);
2585
+ const sampleName = `SAMPLE_${constantCase(dashCase(options.endpointKey))}`;
2586
+ const samplePlaceholder = !options.sampleText;
2587
+ if (!codegenFs.exists(mocksFilePath)) {
2588
+ codegenFs.write(mocksFilePath, header(options.modelBase, registerFunction));
2589
+ }
2590
+ const sdkSpecifier = options.sdkModuleSpecifier.startsWith(".") ? `../${options.sdkModuleSpecifier}` : options.sdkModuleSpecifier;
2591
+ let routeCreated = false;
2592
+ transformSourceFile(codegenFs, mocksFilePath, (sf) => {
2593
+ ensureNamedImport(sf, sdkSpecifier, [{ name: "KosMock" }]);
2594
+ ensureNamedImport(sf, "../services", [
2595
+ { name: options.rawAlias, isTypeOnly: true }
2596
+ ]);
2597
+ ensureSample(sf, {
2598
+ name: sampleName,
2599
+ type: options.rawAlias,
2600
+ servicePath: options.servicePath,
2601
+ method: options.method,
2602
+ sampleText: options.sampleText
2603
+ });
2604
+ const statement = mockRouteCall(
2605
+ options.method,
2606
+ routePattern,
2607
+ `{ data: ${sampleName} }`
2608
+ );
2609
+ routeCreated = ensureRegisteredRoute(sf, {
2610
+ registerFunction,
2611
+ modelBase: options.modelBase,
2612
+ statement,
2613
+ // Matching on the pattern literal alone would collide across methods.
2614
+ marker: `${options.method.toUpperCase()} ${routePattern}`,
2615
+ matches: (existing) => existing.includes(JSON.stringify(routePattern)) && existing.includes(shorthandOrMethod(options.method))
2616
+ });
2617
+ });
2618
+ return {
2619
+ mocksFilePath,
2620
+ registerFunction,
2621
+ routePattern,
2622
+ sampleName,
2623
+ routeCreated,
2624
+ samplePlaceholder
2625
+ };
2626
+ }
2627
+ function shorthandOrMethod(method) {
2628
+ const shorthand = {
2629
+ get: "KosMock.get(",
2630
+ post: "KosMock.post(",
2631
+ put: "KosMock.put(",
2632
+ delete: "KosMock.del("
2633
+ };
2634
+ return shorthand[method] ?? `"${method.toUpperCase()}"`;
2635
+ }
2636
+ function ensureSample(sf, spec) {
2637
+ if (sf.getVariableDeclaration(spec.name)) return;
2638
+ const lines = [
2639
+ `Sample payload for \`${spec.method.toUpperCase()} ${spec.servicePath}\` — the`,
2640
+ "UNWRAPPED `data` payload, i.e. exactly what the transform receives."
2641
+ ];
2642
+ if (!spec.sampleText) {
2643
+ lines.push(
2644
+ "",
2645
+ "TODO: replace the placeholder with something the backend would actually",
2646
+ "serve. Until then the route answers with nothing and the transform is",
2647
+ "never exercised."
2648
+ );
2649
+ }
2650
+ const docs = lines.join("\n");
2651
+ sf.addVariableStatement({
2652
+ isExported: true,
2653
+ declarationKind: VariableDeclarationKind.Const,
2654
+ docs: [docs],
2655
+ declarations: [
2656
+ {
2657
+ name: spec.name,
2658
+ type: spec.type,
2659
+ initializer: spec.sampleText ?? `null as unknown as ${spec.type}`
2660
+ }
2661
+ ]
2662
+ });
2663
+ }
2664
+ function ensureRegisteredRoute(sf, spec) {
2665
+ let fn = sf.getFunction(spec.registerFunction);
2666
+ if (!fn) {
2667
+ fn = sf.addFunction({
2668
+ name: spec.registerFunction,
2669
+ isExported: true,
2670
+ returnType: "void",
2671
+ docs: [
2672
+ `Arm the ${spec.modelBase} model's provisional endpoints. Call this from a dev-only entry point — never from code that ships.`
2673
+ ]
2674
+ });
2675
+ }
2676
+ const already = fn.getStatements().some((statement) => spec.matches(statement.getText()));
2677
+ if (already) return false;
2678
+ fn.addStatements(`// ${spec.marker}
2679
+ ${spec.statement}`);
2680
+ return true;
2681
+ }
2518
2682
  const SERVICE_MODULE_RE = /\/utils\/services\/.*\/service\.ts$/;
2519
2683
  function findServiceModules(codegenFs, sourceRoot) {
2520
2684
  return codegenFs.listFiles(sourceRoot).filter((f) => SERVICE_MODULE_RE.test(f.split(path.sep).join("/")));
@@ -2539,10 +2703,23 @@ function assertServiceModuleAcceptsTransform(codegenFs, serviceModuleFile, model
2539
2703
  `${file} predates the transform-aware service layer (EndpointCtx takes ${typeParams} type parameter). Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh the helper layer in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
2540
2704
  );
2541
2705
  }
2706
+ function assertServiceModuleAcceptsProvisional(codegenFs, serviceModuleFile, modelProject) {
2707
+ const file = `${serviceModuleFile}.ts`;
2708
+ if (!codegenFs.exists(file)) return;
2709
+ const declared = readSourceFile(
2710
+ codegenFs,
2711
+ file,
2712
+ (sf) => Boolean(sf.getFunction("provisionalServiceRequest"))
2713
+ );
2714
+ if (declared) return;
2715
+ throw new Error(
2716
+ `${file} has no \`provisionalServiceRequest\` — the helper layer predates provisional endpoints. Run \`kosui api:generate --project ${modelProject} --helpers-only\` to refresh it in place (no spec fetch, openapi.d.ts untouched), then add the service request.`
2717
+ );
2718
+ }
2542
2719
  function sameEndpoint(existing, candidate) {
2543
2720
  const args = (text) => {
2544
2721
  const match = text.match(
2545
- /^endpoint\s*\(\s*(['"])(.*?)\1\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2722
+ /^endpoint\s*\(\s*(['"])(.*?)\1(?:\s+as\s+ApiPath)?\s*,\s*(['"])(.*?)\3\s*,?\s*\)$/
2546
2723
  );
2547
2724
  return match ? [match[2], match[4].toLowerCase()] : null;
2548
2725
  };
@@ -2550,6 +2727,104 @@ function sameEndpoint(existing, candidate) {
2550
2727
  const b = args(candidate.trim());
2551
2728
  return !!a && !!b && a[0] === b[0] && a[1] === b[1];
2552
2729
  }
2730
+ function editDistance(a, b) {
2731
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
2732
+ for (let i = 1; i <= a.length; i++) {
2733
+ const current = [i];
2734
+ for (let j = 1; j <= b.length; j++) {
2735
+ current[j] = Math.min(
2736
+ previous[j] + 1,
2737
+ current[j - 1] + 1,
2738
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2739
+ );
2740
+ }
2741
+ previous = current;
2742
+ }
2743
+ return previous[b.length];
2744
+ }
2745
+ const NEAREST_SHOWN = 5;
2746
+ function provisionalEntryComment(servicePath, method, catalogName) {
2747
+ return `/**
2748
+ * PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is NOT in this project's
2749
+ * generated OpenAPI types. \`as ApiPath\` is standing in for a \`paths\` entry that
2750
+ * does not exist, so nothing about this endpoint is checked against a spec and
2751
+ * its response cannot be derived from one. Do not read this as a real endpoint.
2752
+ *
2753
+ * WHEN THE ENDPOINT LANDS:
2754
+ * 1. regenerate this project's API types (\`kosui api:generate\`)
2755
+ * 2. delete \` as ApiPath\` from the entry below
2756
+ * 3. point the endpoint's \`…Raw\` alias at
2757
+ * \`EndpointResponse<typeof ${catalogName}.…>\`
2758
+ * 4. swap \`provisionalServiceRequest\` for \`serviceRequest\` in the model
2759
+ * 5. delete the model's \`mocks/\` module
2760
+ * 6. KEEP the transform — it is real code and survives all of the above
2761
+ */
2762
+ `;
2763
+ }
2764
+ function provisionalMethodDocs(servicePath, method) {
2765
+ return [
2766
+ `PROVISIONAL — \`${method.toUpperCase()} ${servicePath}\` is not in the`,
2767
+ "generated OpenAPI types, so this request is served by a KosMock route, not",
2768
+ "by the device. The feature it backs is NOT complete: report the missing",
2769
+ "endpoint rather than treating this as finished.",
2770
+ "",
2771
+ "See the endpoint's entry in `./services` for what to undo when it lands."
2772
+ ].join("\n");
2773
+ }
2774
+ function rawTypeDocs(servicePath, method, rawType) {
2775
+ const shape = rawType ? [
2776
+ "Stated by hand and unverified: there is no spec entry to check it",
2777
+ "against."
2778
+ ] : [
2779
+ "TODO: state the shape the backend is expected to serve. `unknown`",
2780
+ "compiles, but leaves the response boundary undescribed and the",
2781
+ "transform with nothing to narrow."
2782
+ ];
2783
+ return [
2784
+ `Raw wire shape of \`${method.toUpperCase()} ${servicePath}\` — the UNWRAPPED`,
2785
+ "`data` payload, since the client strips the `{status, data}` envelope",
2786
+ "before the transform runs.",
2787
+ "",
2788
+ ...shape,
2789
+ "",
2790
+ "Replace with `EndpointResponse<…>` once the endpoint is in the generated",
2791
+ "types."
2792
+ ].join("\n");
2793
+ }
2794
+ function removalSteps(spec) {
2795
+ return [
2796
+ `Regenerate the API types for ${spec.modelProject} (generate_api_types), and confirm ${spec.method.toUpperCase()} ${spec.servicePath} is now in them.`,
2797
+ `In ${spec.servicesFilePath}: delete \` as ApiPath\` from \`${spec.catalogName}.${spec.endpointKey}\`.`,
2798
+ `In ${spec.servicesFilePath}: point the endpoint's \`…Raw\` alias at \`EndpointResponse<typeof ${spec.catalogName}.${spec.endpointKey}>\` and drop the hand-written shape.`,
2799
+ `In the model: swap \`provisionalServiceRequest\` for \`serviceRequest\` and drop its explicit type arguments — the response type comes from the spec again.`,
2800
+ spec.mocksFilePath ? `Delete ${spec.mocksFilePath} (and the call to its register function in the app) once no provisional endpoint is left in it.` : `Delete the model's mocks module once no provisional endpoint is left in it.`,
2801
+ `KEEP \`${spec.mapperName}\` and the data type it produces. The transform is real code; it is what let the model read the final shape all along.`
2802
+ ];
2803
+ }
2804
+ function untypedEndpointError(spec) {
2805
+ const methodsForPath = spec.operations.filter((op) => op.path === spec.servicePath).map((op) => op.method);
2806
+ const pathTypedForOtherMethods = methodsForPath.length > 0;
2807
+ const nearest = [...new Set(spec.operations.map((op) => op.path))].sort(
2808
+ (a, b) => editDistance(a, spec.servicePath) - editDistance(b, spec.servicePath)
2809
+ ).slice(0, NEAREST_SHOWN);
2810
+ const waysForward = [
2811
+ `The endpoint EXISTS on the device but this project has not pulled types for it: re-run api:generate (generate_api_types) for ${spec.modelProject}, then add the request unchanged. Confirm on the device first — kos-device search_services / describe_endpoint — since the live OpenAPI is what says whether it exists.`,
2812
+ `The endpoint DOES NOT EXIST yet: pass mock:"auto" (the default) or mock:"always" to emit the provisional, mock-backed form — real decorator, lifecycle, error-first handler and transform, with a KosMock route standing in for the wire response.`,
2813
+ `What is NOT supported: falling back to resolveServiceUrl / ServiceFactory.build / getAll to make the UI render. That compiles and looks finished, which is worse than the hole. A missing endpoint is a blocker to report, not a licence to route around the generated service layer.`
2814
+ ];
2815
+ const message = pathTypedForOtherMethods ? `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is typed, but only for: ${methodsForPath.map((m) => m.toUpperCase()).join(", ")}.` : `${spec.method.toUpperCase()} ${spec.servicePath} is not in the generated OpenAPI types for ${spec.modelProject} — the path is absent entirely. Nothing was written.`;
2816
+ const error = new Error(message);
2817
+ error.details = {
2818
+ servicePath: spec.servicePath,
2819
+ method: spec.method,
2820
+ pathTypedForOtherMethods,
2821
+ methodsForPath,
2822
+ otherEndpointsInThisApi: nearest,
2823
+ warning: "These are OTHER endpoints in this API, listed only so they can be ruled out — they are almost certainly NOT what you want. Do not substitute one for the requested path because the strings look similar.",
2824
+ waysForward
2825
+ };
2826
+ return error;
2827
+ }
2553
2828
  function addServiceRequestToModel(codegenFs, options, projects) {
2554
2829
  const logger = getCodegenLogger();
2555
2830
  const { modelFilePath, sourceRoot } = resolveModelFilePath(
@@ -2611,34 +2886,86 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2611
2886
  const mode = options.mode ?? (options.lifecycle ? "lifecycle" : "method");
2612
2887
  const lifecycle = options.lifecycle || "LOAD";
2613
2888
  const catalogName = `${pascalCase(modelBase)}Endpoints`;
2889
+ const mockMode = options.mock ?? "auto";
2890
+ const operations = readServiceModuleOperations(codegenFs, serviceModuleFile);
2891
+ const pathValidated = operations !== null;
2892
+ const operationTyped = operations === null || operations.some(
2893
+ (op) => op.path === options.servicePath && op.method === method
2894
+ );
2895
+ if (!operationTyped && mockMode === "never") {
2896
+ throw untypedEndpointError({
2897
+ servicePath: options.servicePath,
2898
+ method,
2899
+ modelProject: options.modelProject,
2900
+ operations: operations ?? []
2901
+ });
2902
+ }
2903
+ const provisional = !operationTyped;
2904
+ const writeMock = provisional || mockMode === "always";
2905
+ if (provisional) {
2906
+ assertServiceModuleAcceptsProvisional(
2907
+ codegenFs,
2908
+ serviceModuleFile,
2909
+ options.modelProject
2910
+ );
2911
+ }
2614
2912
  logger.info(
2615
- `Adding ${mode}-driven @kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2913
+ `Adding ${mode}-driven ${provisional ? "PROVISIONAL " : ""}@kosServiceRequest "${options.methodName}" (${method} ${options.servicePath}) to ${options.modelName}`
2616
2914
  );
2915
+ if (provisional) {
2916
+ logger.warn(
2917
+ `${method.toUpperCase()} ${options.servicePath} is not in ${options.modelProject}'s generated OpenAPI types — emitting the provisional, mock-backed form. The feature is NOT complete until the endpoint lands.`
2918
+ );
2919
+ }
2617
2920
  ensureServicesModule(codegenFs, modelFilePath, modelBase);
2618
2921
  let endpointKey = options.methodName;
2619
2922
  let endpointCreated = true;
2620
2923
  let dataAlias = "";
2621
2924
  let mapperName = "";
2622
2925
  let ctxAlias = "";
2926
+ let rawAlias = "";
2623
2927
  transformSourceFile(codegenFs, servicesFilePath, (sf) => {
2624
2928
  ensureNamedImport(sf, serviceImportFromServices, [{ name: "endpoint" }]);
2929
+ if (provisional) {
2930
+ ensureNamedImport(sf, serviceImportFromServices, [
2931
+ { name: "ApiPath", isTypeOnly: true }
2932
+ ]);
2933
+ }
2934
+ const pathText = provisional ? `${JSON.stringify(options.servicePath)} as ApiPath` : JSON.stringify(options.servicePath);
2625
2935
  const entry = ensureConstCatalogEntry(sf, {
2626
2936
  catalogName,
2627
2937
  key: options.methodName,
2628
- initializer: `endpoint(${JSON.stringify(
2629
- options.servicePath
2630
- )}, ${JSON.stringify(method)})`,
2631
- equals: sameEndpoint
2938
+ initializer: `endpoint(${pathText}, ${JSON.stringify(method)})`,
2939
+ equals: sameEndpoint,
2940
+ leadingTrivia: provisional ? provisionalEntryComment(options.servicePath, method, catalogName) : void 0
2632
2941
  });
2633
2942
  endpointKey = entry.key;
2634
2943
  endpointCreated = entry.created;
2635
2944
  dataAlias = `${pascalCase(endpointKey)}Data`;
2636
2945
  mapperName = `to${pascalCase(endpointKey)}Data`;
2637
2946
  ctxAlias = `${pascalCase(endpointKey)}Ctx`;
2638
- const rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2639
- ensureNamedImport(sf, serviceImportFromServices, [
2640
- { name: "EndpointResponse", isTypeOnly: true }
2641
- ]);
2947
+ rawAlias = `${pascalCase(endpointKey)}Raw`;
2948
+ let rawType;
2949
+ if (provisional) {
2950
+ ensureExportedTypeAlias(sf, {
2951
+ name: rawAlias,
2952
+ type: options.rawType || "unknown",
2953
+ docs: rawTypeDocs(options.servicePath, method, options.rawType)
2954
+ });
2955
+ rawType = rawAlias;
2956
+ } else {
2957
+ rawType = `EndpointResponse<typeof ${catalogName}.${endpointKey}>`;
2958
+ ensureNamedImport(sf, serviceImportFromServices, [
2959
+ { name: "EndpointResponse", isTypeOnly: true }
2960
+ ]);
2961
+ if (writeMock) {
2962
+ ensureExportedTypeAlias(sf, {
2963
+ name: rawAlias,
2964
+ type: rawType,
2965
+ docs: `Raw wire shape of \`${method.toUpperCase()} ${options.servicePath}\`, as the spec declares it — what a mock for this endpoint must serve.`
2966
+ });
2967
+ }
2968
+ }
2642
2969
  ensureExportedTypeAlias(sf, { name: dataAlias, type: rawType });
2643
2970
  ensureExportedMapper(sf, {
2644
2971
  name: mapperName,
@@ -2659,20 +2986,25 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2659
2986
  const className = getModelClass(sf).getName();
2660
2987
  if (!className) throw new Error("Model class has no name.");
2661
2988
  const typeParams = getModelClass(sf).getTypeParameters().map((tp) => tp.getText());
2662
- ensureNamedImport(sf, serviceImportFromModel, [{ name: "serviceRequest" }]);
2989
+ const decoratorName = provisional ? "provisionalServiceRequest" : "serviceRequest";
2990
+ const decoratorTypeArgs = provisional ? [rawAlias, dataAlias] : void 0;
2991
+ ensureNamedImport(sf, serviceImportFromModel, [{ name: decoratorName }]);
2663
2992
  if (mode === "lifecycle") {
2664
2993
  ensureNamedImport(sf, "./services", [
2665
2994
  { name: catalogName },
2666
2995
  { name: mapperName },
2667
- { name: dataAlias, isTypeOnly: true }
2996
+ { name: dataAlias, isTypeOnly: true },
2997
+ ...provisional ? [{ name: rawAlias, isTypeOnly: true }] : []
2668
2998
  ]);
2669
2999
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2670
3000
  { name: "DependencyLifecycle" }
2671
3001
  ]);
2672
3002
  addDecoratedMethod(getModelClass(sf), {
2673
3003
  name: options.methodName,
2674
- decoratorName: "serviceRequest",
3004
+ decoratorName,
3005
+ decoratorTypeArgs,
2675
3006
  decoratorArgsText: `${catalogName}.${endpointKey}, { lifecycle: DependencyLifecycle.${lifecycle}, transform: ${mapperName} }`,
3007
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2676
3008
  // The manager invokes phase handlers error-first, passing (null, data)
2677
3009
  // on success. The `error` half is only ever reached because
2678
3010
  // `serviceRequest` supplies an errorHandler — the bare decorator
@@ -2689,7 +3021,11 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2689
3021
  ensureNamedImport(sf, "./services", [
2690
3022
  { name: catalogName },
2691
3023
  { name: mapperName },
2692
- { name: ctxAlias, isTypeOnly: true }
3024
+ { name: ctxAlias, isTypeOnly: true },
3025
+ ...provisional ? [
3026
+ { name: rawAlias, isTypeOnly: true },
3027
+ { name: dataAlias, isTypeOnly: true }
3028
+ ] : []
2693
3029
  ]);
2694
3030
  ensureNamedImport(sf, resolveSdkModuleSpecifier(sf), [
2695
3031
  { name: "executeServiceRequest" },
@@ -2704,8 +3040,10 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2704
3040
  addClassDecorator(getModelClass(sf), "kosLoggerAware", { argsText: "" });
2705
3041
  addDecoratedMethod(getModelClass(sf), {
2706
3042
  name: options.methodName,
2707
- decoratorName: "serviceRequest",
3043
+ decoratorName,
3044
+ decoratorTypeArgs,
2708
3045
  decoratorArgsText: `${catalogName}.${endpointKey}, { transform: ${mapperName} }`,
3046
+ docs: provisional ? provisionalMethodDocs(options.servicePath, method) : void 0,
2709
3047
  // Optional: the framework appends the context, callers never pass it.
2710
3048
  parameters: [{ name: "$ctx", type: ctxAlias, optional: true }],
2711
3049
  isAsync: true,
@@ -2717,13 +3055,43 @@ function addServiceRequestToModel(codegenFs, options, projects) {
2717
3055
  ].join("\n")
2718
3056
  });
2719
3057
  });
3058
+ const mock = writeMock ? ensureMockRoute(codegenFs, {
3059
+ modelFilePath,
3060
+ modelBase,
3061
+ sdkModuleSpecifier: readSourceFile(
3062
+ codegenFs,
3063
+ modelFilePath,
3064
+ resolveSdkModuleSpecifier
3065
+ ),
3066
+ rawAlias,
3067
+ servicePath: options.servicePath,
3068
+ method,
3069
+ endpointKey,
3070
+ sampleText: options.sample
3071
+ }) : void 0;
2720
3072
  return {
2721
3073
  modelFilePath,
2722
3074
  servicesFilePath,
2723
3075
  serviceModule: serviceImportFromModel,
2724
3076
  mode,
2725
3077
  endpointKey,
2726
- endpointCreated
3078
+ endpointCreated,
3079
+ pathValidated,
3080
+ provisional,
3081
+ mocksFilePath: mock?.mocksFilePath,
3082
+ mockRegisterFunction: mock?.registerFunction,
3083
+ mockRoute: mock && `${method.toUpperCase()} ${mock.routePattern}`,
3084
+ mockSamplePlaceholder: mock?.samplePlaceholder,
3085
+ removalSteps: provisional ? removalSteps({
3086
+ modelProject: options.modelProject,
3087
+ servicePath: options.servicePath,
3088
+ method,
3089
+ catalogName,
3090
+ endpointKey,
3091
+ servicesFilePath,
3092
+ mocksFilePath: mock?.mocksFilePath,
3093
+ mapperName
3094
+ }) : void 0
2727
3095
  };
2728
3096
  }
2729
3097
  function modelElementType(typeText) {