@distilled.cloud/core 0.30.2 → 1.0.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 (132) hide show
  1. package/lib/api.d.ts +165 -0
  2. package/lib/api.d.ts.map +1 -0
  3. package/lib/api.js +178 -0
  4. package/lib/api.js.map +1 -0
  5. package/lib/codegen/cli.d.ts +29 -0
  6. package/lib/codegen/cli.d.ts.map +1 -0
  7. package/lib/codegen/cli.js +165 -0
  8. package/lib/codegen/cli.js.map +1 -0
  9. package/lib/codegen/emit.d.ts +129 -0
  10. package/lib/codegen/emit.d.ts.map +1 -0
  11. package/lib/codegen/emit.js +105 -0
  12. package/lib/codegen/emit.js.map +1 -0
  13. package/lib/codegen/format.d.ts +23 -0
  14. package/lib/codegen/format.d.ts.map +1 -0
  15. package/lib/codegen/format.js +28 -0
  16. package/lib/codegen/format.js.map +1 -0
  17. package/lib/codegen/generator.d.ts +334 -0
  18. package/lib/codegen/generator.d.ts.map +1 -0
  19. package/lib/codegen/generator.js +691 -0
  20. package/lib/codegen/generator.js.map +1 -0
  21. package/lib/codegen/graph.d.ts +36 -0
  22. package/lib/codegen/graph.d.ts.map +1 -0
  23. package/lib/codegen/graph.js +136 -0
  24. package/lib/codegen/graph.js.map +1 -0
  25. package/lib/codegen/members.d.ts +25 -0
  26. package/lib/codegen/members.d.ts.map +1 -0
  27. package/lib/codegen/members.js +55 -0
  28. package/lib/codegen/members.js.map +1 -0
  29. package/lib/codegen/naming.d.ts +29 -0
  30. package/lib/codegen/naming.d.ts.map +1 -0
  31. package/lib/codegen/naming.js +74 -0
  32. package/lib/codegen/naming.js.map +1 -0
  33. package/lib/codegen/openapi-cli.d.ts +38 -0
  34. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  35. package/lib/codegen/openapi-cli.js +107 -0
  36. package/lib/codegen/openapi-cli.js.map +1 -0
  37. package/lib/codegen/openapi.d.ts +115 -0
  38. package/lib/codegen/openapi.d.ts.map +1 -0
  39. package/lib/codegen/openapi.js +1220 -0
  40. package/lib/codegen/openapi.js.map +1 -0
  41. package/lib/codegen/operations.d.ts +24 -0
  42. package/lib/codegen/operations.d.ts.map +1 -0
  43. package/lib/codegen/operations.js +56 -0
  44. package/lib/codegen/operations.js.map +1 -0
  45. package/lib/codegen/pagination.d.ts +39 -0
  46. package/lib/codegen/pagination.d.ts.map +1 -0
  47. package/lib/codegen/pagination.js +33 -0
  48. package/lib/codegen/pagination.js.map +1 -0
  49. package/lib/codegen/prelude.d.ts +15 -0
  50. package/lib/codegen/prelude.d.ts.map +1 -0
  51. package/lib/codegen/prelude.js +60 -0
  52. package/lib/codegen/prelude.js.map +1 -0
  53. package/lib/error-category.d.ts +28 -0
  54. package/lib/error-category.d.ts.map +1 -0
  55. package/lib/error-category.js +46 -0
  56. package/lib/error-category.js.map +1 -0
  57. package/lib/errors.d.ts +1 -0
  58. package/lib/errors.d.ts.map +1 -1
  59. package/lib/errors.js +1 -0
  60. package/lib/errors.js.map +1 -1
  61. package/lib/json-patch.d.ts +25 -32
  62. package/lib/json-patch.d.ts.map +1 -1
  63. package/lib/json-patch.js +23 -95
  64. package/lib/json-patch.js.map +1 -1
  65. package/lib/pagination.d.ts +37 -51
  66. package/lib/pagination.d.ts.map +1 -1
  67. package/lib/pagination.js +72 -90
  68. package/lib/pagination.js.map +1 -1
  69. package/lib/protocol-http.d.ts +74 -0
  70. package/lib/protocol-http.d.ts.map +1 -0
  71. package/lib/protocol-http.js +554 -0
  72. package/lib/protocol-http.js.map +1 -0
  73. package/lib/protocol-rest.d.ts +124 -0
  74. package/lib/protocol-rest.d.ts.map +1 -0
  75. package/lib/protocol-rest.js +242 -0
  76. package/lib/protocol-rest.js.map +1 -0
  77. package/lib/retry.d.ts +8 -2
  78. package/lib/retry.d.ts.map +1 -1
  79. package/lib/retry.js +21 -15
  80. package/lib/retry.js.map +1 -1
  81. package/lib/schema.d.ts +7 -8
  82. package/lib/schema.d.ts.map +1 -1
  83. package/lib/schema.js +7 -8
  84. package/lib/schema.js.map +1 -1
  85. package/lib/trait.d.ts +150 -0
  86. package/lib/trait.d.ts.map +1 -0
  87. package/lib/trait.js +107 -0
  88. package/lib/trait.js.map +1 -0
  89. package/package.json +18 -75
  90. package/src/api.ts +446 -0
  91. package/src/codegen/cli.ts +268 -0
  92. package/src/codegen/emit.ts +207 -0
  93. package/src/codegen/format.ts +47 -0
  94. package/src/codegen/generator.ts +1153 -0
  95. package/src/codegen/graph.ts +151 -0
  96. package/src/codegen/members.ts +71 -0
  97. package/src/codegen/naming.ts +86 -0
  98. package/src/codegen/openapi-cli.ts +166 -0
  99. package/src/codegen/openapi.ts +1450 -0
  100. package/src/codegen/operations.ts +76 -0
  101. package/src/codegen/pagination.ts +71 -0
  102. package/src/codegen/prelude.ts +70 -0
  103. package/src/error-category.ts +84 -0
  104. package/src/errors.ts +2 -0
  105. package/src/json-patch.ts +26 -110
  106. package/src/pagination.ts +86 -142
  107. package/src/protocol-http.ts +699 -0
  108. package/src/protocol-rest.ts +367 -0
  109. package/src/retry.ts +20 -21
  110. package/src/schema.ts +7 -8
  111. package/src/trait.ts +238 -0
  112. package/README.md +0 -30
  113. package/lib/client.d.ts +0 -167
  114. package/lib/client.d.ts.map +0 -1
  115. package/lib/client.js +0 -659
  116. package/lib/client.js.map +0 -1
  117. package/lib/schemas.d.ts +0 -60
  118. package/lib/schemas.d.ts.map +0 -1
  119. package/lib/schemas.js +0 -79
  120. package/lib/schemas.js.map +0 -1
  121. package/lib/sensitive.d.ts +0 -71
  122. package/lib/sensitive.d.ts.map +0 -1
  123. package/lib/sensitive.js +0 -96
  124. package/lib/sensitive.js.map +0 -1
  125. package/lib/traits.d.ts +0 -421
  126. package/lib/traits.d.ts.map +0 -1
  127. package/lib/traits.js +0 -737
  128. package/lib/traits.js.map +0 -1
  129. package/src/client.ts +0 -1177
  130. package/src/schemas.ts +0 -128
  131. package/src/sensitive.ts +0 -119
  132. package/src/traits.ts +0 -996
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Operation-level smithy helpers for the SDK generators (dev-time only).
3
+ */
4
+ import { local } from "./naming.ts";
5
+ import type { ShapeMap } from "./graph.ts";
6
+
7
+ export interface OpEntry {
8
+ id: string;
9
+ def: any;
10
+ }
11
+
12
+ /** All operation shapes, sorted by local name. */
13
+ export const collectOperations = (shapes: ShapeMap): OpEntry[] => {
14
+ const operations: OpEntry[] = [];
15
+ for (const [id, def] of Object.entries(shapes)) {
16
+ if (def.type === "operation") operations.push({ id, def });
17
+ }
18
+ operations.sort((a, b) => local(a.id).localeCompare(local(b.id)));
19
+ return operations;
20
+ };
21
+
22
+ /** The model's shape namespace (from the first operation, else any shape). */
23
+ export const modelNamespace = (
24
+ operations: readonly OpEntry[],
25
+ shapes: ShapeMap,
26
+ fallback: string,
27
+ ): string =>
28
+ operations.length
29
+ ? operations[0].id.split("#")[0]
30
+ : (Object.keys(shapes)[0]?.split("#")[0] ?? fallback);
31
+
32
+ /**
33
+ * Resolve an operation's input/output targets, synthesizing an empty named
34
+ * structure for `smithy.api#Unit` so every operation has a shape that can
35
+ * carry operation-level traits (mutates `shapes`).
36
+ */
37
+ export const ensureNamedIo = (
38
+ shapes: ShapeMap,
39
+ op: OpEntry,
40
+ ns: string,
41
+ ): { input: string; output: string } => {
42
+ const opName = local(op.id);
43
+ let input = op.def.input?.target ?? "smithy.api#Unit";
44
+ let output = op.def.output?.target ?? "smithy.api#Unit";
45
+ if (input === "smithy.api#Unit") {
46
+ input = `${ns}#${opName}Request`;
47
+ shapes[input] = { type: "structure", members: {} };
48
+ }
49
+ if (output === "smithy.api#Unit") {
50
+ output = `${ns}#${opName}Response`;
51
+ shapes[output] = { type: "structure", members: {} };
52
+ }
53
+ return { input, output };
54
+ };
55
+
56
+ /**
57
+ * Every error shape id referenced by the operations' `errors` lists —
58
+ * deduplicated, existing in the model, sorted by local name.
59
+ */
60
+ export const collectOpErrorIds = (
61
+ operations: readonly OpEntry[],
62
+ shapes: ShapeMap,
63
+ ): string[] => {
64
+ const errorIds: string[] = [];
65
+ const seen = new Set<string>();
66
+ for (const op of operations) {
67
+ for (const e of op.def.errors ?? []) {
68
+ if (!seen.has(e.target) && shapes[e.target]) {
69
+ seen.add(e.target);
70
+ errorIds.push(e.target);
71
+ }
72
+ }
73
+ }
74
+ errorIds.sort((a, b) => local(a).localeCompare(local(b)));
75
+ return errorIds;
76
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared pagination-trait handling for the SDK generators (dev-time only).
3
+ *
4
+ * Both SDKs source `smithy.api#paginated`; AWS merges operation-level
5
+ * config over service-level defaults, cloudflare validates the trait
6
+ * against the operation's actual members (degrading to a plain op when it
7
+ * can't work). Both behaviors live here.
8
+ */
9
+ import type { PaginatedTrait } from "../pagination.ts";
10
+
11
+ /**
12
+ * Merge an operation-level paginated trait over service-level defaults
13
+ * (Smithy semantics: operations may specify partial pagination and inherit
14
+ * the rest from the service).
15
+ */
16
+ export const mergePaginated = (
17
+ op: Partial<PaginatedTrait> | undefined,
18
+ service: Partial<PaginatedTrait> | undefined,
19
+ ): PaginatedTrait | undefined => {
20
+ if (!op) return undefined;
21
+ const merged = {
22
+ mode: op.mode ?? service?.mode,
23
+ inputToken: op.inputToken ?? service?.inputToken,
24
+ outputToken: op.outputToken ?? service?.outputToken,
25
+ items: op.items ?? service?.items,
26
+ pageSize: op.pageSize ?? service?.pageSize,
27
+ };
28
+ return Object.fromEntries(
29
+ Object.entries(merged).filter(([, v]) => v !== undefined),
30
+ ) as PaginatedTrait;
31
+ };
32
+
33
+ export interface ValidatePaginatedOptions {
34
+ readonly trait: Partial<PaginatedTrait>;
35
+ /** TS-facing member names of the operation input shape. */
36
+ readonly inputNames: ReadonlySet<string>;
37
+ /** TS-facing member names of the operation output shape. */
38
+ readonly outputNames: ReadonlySet<string>;
39
+ /** Items path fallback when the trait omits `items` (e.g. `"result"`). */
40
+ readonly itemsFallback?: string;
41
+ /** Output names accepted even when not modeled (e.g. `"resultInfo"`). */
42
+ readonly syntheticOutputs?: ReadonlySet<string>;
43
+ }
44
+
45
+ export interface ValidatedPagination {
46
+ readonly ok: boolean;
47
+ /** Root member of the items path (`"result.items"` → `"result"`). */
48
+ readonly itemsRoot: string;
49
+ }
50
+
51
+ /**
52
+ * A paginated op must actually carry its page/cursor token on the input
53
+ * and its items member on the output — otherwise `.pages()` would loop or
54
+ * yield nothing. Returns the items-path root for emitters that promote it
55
+ * to required.
56
+ */
57
+ export const validatePaginated = (
58
+ o: ValidatePaginatedOptions,
59
+ ): ValidatedPagination => {
60
+ const itemsRoot = String(o.trait.items ?? o.itemsFallback ?? "").split(
61
+ ".",
62
+ )[0];
63
+ const tokenOk =
64
+ o.trait.mode === "single" ||
65
+ (typeof o.trait.inputToken === "string" &&
66
+ o.inputNames.has(o.trait.inputToken));
67
+ const itemsOk =
68
+ o.outputNames.has(itemsRoot) ||
69
+ (o.syntheticOutputs?.has(itemsRoot) ?? false);
70
+ return { ok: tokenOk && itemsOk, itemsRoot };
71
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Smithy prelude scalar baselines + reference resolvers for the SDK
3
+ * generators (dev-time only).
4
+ *
5
+ * The JSON flavor maps timestamps/blobs to plain strings — the baseline for
6
+ * SDKs whose wire format is JSON-over-docs (cloudflare). Generators with
7
+ * richer models (AWS) emit real timestamp/blob schemas themselves and only
8
+ * fall back here for untyped scalars.
9
+ */
10
+ import { isPrelude, local } from "./naming.ts";
11
+ import { isForwardRef } from "./graph.ts";
12
+
13
+ /** Prelude shape → schema expression (JSON-wire flavor). */
14
+ export const JSON_PRELUDE: Record<string, string> = {
15
+ String: "S.String",
16
+ Boolean: "S.Boolean",
17
+ Double: "S.Number",
18
+ Float: "S.Number",
19
+ Integer: "S.Number",
20
+ Long: "S.Number",
21
+ BigInteger: "S.Number",
22
+ BigDecimal: "S.Number",
23
+ Timestamp: "S.String",
24
+ Blob: "S.String",
25
+ Document: "S.Unknown",
26
+ Unit: "S.Struct({})",
27
+ };
28
+
29
+ /** TypeScript type for each prelude shape, mirroring {@link JSON_PRELUDE}. */
30
+ export const TS_JSON_PRELUDE: Record<string, string> = {
31
+ String: "string",
32
+ Boolean: "boolean",
33
+ Double: "number",
34
+ Float: "number",
35
+ Integer: "number",
36
+ Long: "number",
37
+ BigInteger: "number",
38
+ BigDecimal: "number",
39
+ Timestamp: "string",
40
+ Blob: "string",
41
+ Document: "unknown",
42
+ Unit: "{}",
43
+ };
44
+
45
+ /**
46
+ * Schema reference resolver: prelude shapes map through the prelude table,
47
+ * forward references (including into cycles) wrap in `S.suspend`.
48
+ */
49
+ export const makeSchemaRef =
50
+ (
51
+ prelude: Record<string, string>,
52
+ indexOf: ReadonlyMap<string, number>,
53
+ ): ((target: string, selfIdx: number) => string) =>
54
+ (target, selfIdx) => {
55
+ if (isPrelude(target)) return prelude[local(target)] ?? "S.Unknown";
56
+ const name = local(target);
57
+ if (isForwardRef(indexOf, target, selfIdx)) {
58
+ return `S.suspend(() => ${name})`;
59
+ }
60
+ return name;
61
+ };
62
+
63
+ /**
64
+ * TypeScript type reference resolver. Named shapes all get an emitted
65
+ * interface / type alias, so forward references are always fine.
66
+ */
67
+ export const makeTsRef =
68
+ (tsPrelude: Record<string, string>): ((target: string) => string) =>
69
+ (target) =>
70
+ isPrelude(target) ? (tsPrelude[local(target)] ?? "unknown") : local(target);
@@ -0,0 +1,84 @@
1
+ import { Effect, Predicate } from "effect";
2
+
3
+ export const categoriesKey = "@distilled/meta/error-categories";
4
+
5
+ export const withCategory =
6
+ <Categories extends Array<PropertyKey>>(...categories: Categories) =>
7
+ <Args extends Array<any>, Ret, C extends { new (...args: Args): Ret }>(
8
+ C: C,
9
+ ): C & {
10
+ new (...args: Args): Ret & {
11
+ [categoriesKey]: { [Cat in Categories[number]]: true };
12
+ };
13
+ } => {
14
+ // @ts-expect-error
15
+ const Mixed = class extends C {};
16
+
17
+ for (const category of categories) {
18
+ if (!(categoriesKey in Mixed.prototype)) {
19
+ // @ts-expect-error
20
+ Mixed.prototype[categoriesKey] = {};
21
+ }
22
+ // @ts-expect-error
23
+ Mixed.prototype[categoriesKey][category] = true;
24
+ }
25
+
26
+ return Mixed as any;
27
+ };
28
+
29
+ export type AllKeys<E> = E extends { [categoriesKey]: infer Q }
30
+ ? keyof Q
31
+ : never;
32
+
33
+ export type ExtractAll<E, Cats extends PropertyKey> = Cats extends any
34
+ ? Extract<E, { [categoriesKey]: { [K in Cats]: any } }>
35
+ : never;
36
+
37
+ /**
38
+ * Runtime check: does `value` carry the given category?
39
+ * Works on instances tagged via `withCategory(...)`.
40
+ */
41
+ export const hasCategory =
42
+ <const Cat extends PropertyKey>(category: Cat) =>
43
+ (
44
+ value: unknown,
45
+ ): value is { readonly [categoriesKey]: { readonly [K in Cat]: true } } => {
46
+ if (!Predicate.isObject(value)) return false;
47
+ if (!Predicate.hasProperty(categoriesKey)(value)) return false;
48
+ return category in (value as any)[categoriesKey];
49
+ };
50
+
51
+ /** True if `value` has any of the given categories. */
52
+ export const hasAnyCategory =
53
+ (...categories: ReadonlyArray<PropertyKey>) =>
54
+ (value: unknown): boolean => {
55
+ if (!Predicate.isObject(value)) return false;
56
+ if (!Predicate.hasProperty(categoriesKey)(value)) return false;
57
+ const cats = (value as any)[categoriesKey];
58
+ for (const c of categories) if (c in cats) return true;
59
+ return false;
60
+ };
61
+
62
+ export const catchCategory =
63
+ <E, const Categories extends Array<AllKeys<E>>, A2, E2, R2>(
64
+ ...args: [
65
+ ...Categories,
66
+ f: (err: ExtractAll<E, Categories[number]>) => Effect.Effect<A2, E2, R2>,
67
+ ]
68
+ ) =>
69
+ <A, R>(
70
+ effect: Effect.Effect<A, E, R>,
71
+ ): Effect.Effect<
72
+ A | A2,
73
+ E2 | Exclude<E, ExtractAll<E, Categories[number]>>,
74
+ R | R2
75
+ > => {
76
+ const f = args.pop()! as (
77
+ err: ExtractAll<E, Categories[number]>,
78
+ ) => Effect.Effect<A2, E2, R2>;
79
+ const categories = args as Array<PropertyKey>;
80
+ const matches = hasAnyCategory(...categories);
81
+ return Effect.catchIf(effect, matches, (e) =>
82
+ f(e as ExtractAll<E, Categories[number]>),
83
+ ) as any;
84
+ };
package/src/errors.ts CHANGED
@@ -228,3 +228,5 @@ export const DEFAULT_ERRORS = [
228
228
  ] as const;
229
229
 
230
230
  export type DefaultErrors = InstanceType<(typeof DEFAULT_ERRORS)[number]>;
231
+
232
+ export const RETRYABLE = "@distilled/errors/retryable" as const;
package/src/json-patch.ts CHANGED
@@ -1,20 +1,14 @@
1
1
  /**
2
2
  * JSON Patch (RFC 6902) Implementation
3
3
  *
4
- * Provides a unified spec patching system for all SDKs.
5
- * Patches are applied to OpenAPI/Discovery/Smithy specs before code generation
6
- * to add error types, fix nullable fields, mark sensitive data, etc.
4
+ * Provides a unified spec patching system for all SDKs. Patches are applied to
5
+ * generated intermediary specs (Smithy models, OpenAPI docs, …) before code
6
+ * generation to add typed error shapes, rename operations, mark nullable
7
+ * fields, etc. Ported from the distilled repo's `core/json-patch`.
7
8
  *
8
- * @example
9
- * ```ts
10
- * import { applyAllPatches } from "@distilled.cloud/core/json-patch";
11
- *
12
- * const spec = JSON.parse(fs.readFileSync("openapi.json", "utf-8"));
13
- * const { applied, errors } = applyAllPatches(spec, "./patches");
14
- * ```
9
+ * Pure functions only — callers load patch files themselves (the generators
10
+ * use Effect's FileSystem) and hand the parsed operations to `applyPatch`.
15
11
  */
16
- import * as fs from "fs";
17
- import * as path from "path";
18
12
 
19
13
  // ============================================================================
20
14
  // Types
@@ -30,7 +24,7 @@ export interface JsonPatchOperation {
30
24
  export type JsonPatch = JsonPatchOperation[];
31
25
 
32
26
  export interface PatchFile {
33
- description: string;
27
+ description?: string;
34
28
  patches: JsonPatch;
35
29
  }
36
30
 
@@ -38,9 +32,7 @@ export interface PatchFile {
38
32
  // JSON Pointer (RFC 6901)
39
33
  // ============================================================================
40
34
 
41
- /**
42
- * Parse a JSON Pointer (RFC 6901) path into segments.
43
- */
35
+ /** Parse a JSON Pointer (RFC 6901) path into segments. */
44
36
  export function parseJsonPointer(pointer: string): string[] {
45
37
  if (pointer === "") return [];
46
38
  if (!pointer.startsWith("/")) {
@@ -52,9 +44,7 @@ export function parseJsonPointer(pointer: string): string[] {
52
44
  .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
53
45
  }
54
46
 
55
- /**
56
- * Get a value at a JSON Pointer path.
57
- */
47
+ /** Get a value at a JSON Pointer path. */
58
48
  export function getValueAtPath(obj: unknown, pointer: string): unknown {
59
49
  const segments = parseJsonPointer(pointer);
60
50
  let current: unknown = obj;
@@ -74,9 +64,7 @@ export function getValueAtPath(obj: unknown, pointer: string): unknown {
74
64
  return current;
75
65
  }
76
66
 
77
- /**
78
- * Set a value at a JSON Pointer path.
79
- */
67
+ /** Set a value at a JSON Pointer path. */
80
68
  export function setValueAtPath(
81
69
  obj: unknown,
82
70
  pointer: string,
@@ -95,8 +83,7 @@ export function setValueAtPath(
95
83
  throw new Error(`Cannot traverse path ${pointer}: not an object`);
96
84
  }
97
85
  if (Array.isArray(current)) {
98
- const index = parseInt(segment, 10);
99
- current = current[index];
86
+ current = current[parseInt(segment, 10)];
100
87
  } else {
101
88
  current = (current as Record<string, unknown>)[segment];
102
89
  }
@@ -110,21 +97,17 @@ export function setValueAtPath(
110
97
  }
111
98
 
112
99
  if (Array.isArray(current)) {
113
- const index =
114
- lastSegment === "-" ? current.length : parseInt(lastSegment, 10);
115
100
  if (lastSegment === "-") {
116
101
  current.push(value);
117
102
  } else {
118
- current[index] = value;
103
+ current[parseInt(lastSegment, 10)] = value;
119
104
  }
120
105
  } else {
121
106
  (current as Record<string, unknown>)[lastSegment] = value;
122
107
  }
123
108
  }
124
109
 
125
- /**
126
- * Remove a value at a JSON Pointer path.
127
- */
110
+ /** Remove a value at a JSON Pointer path. */
128
111
  export function removeValueAtPath(obj: unknown, pointer: string): void {
129
112
  const segments = parseJsonPointer(pointer);
130
113
  if (segments.length === 0) {
@@ -163,9 +146,7 @@ export function removeValueAtPath(obj: unknown, pointer: string): void {
163
146
  // Patch Operations
164
147
  // ============================================================================
165
148
 
166
- /**
167
- * Apply a single JSON Patch operation.
168
- */
149
+ /** Apply a single JSON Patch operation (mutates `obj` in place). */
169
150
  export function applyOperation(
170
151
  obj: unknown,
171
152
  operation: JsonPatchOperation,
@@ -185,6 +166,11 @@ export function applyOperation(
185
166
  case "move": {
186
167
  if (!operation.from) throw new Error("move operation requires 'from'");
187
168
  const moveValue = getValueAtPath(obj, operation.from);
169
+ if (moveValue === undefined) {
170
+ throw new Error(
171
+ `Cannot move from path ${operation.from}: not an object`,
172
+ );
173
+ }
188
174
  removeValueAtPath(obj, operation.from);
189
175
  setValueAtPath(obj, operation.path, moveValue);
190
176
  break;
@@ -213,9 +199,7 @@ export function applyOperation(
213
199
  }
214
200
  }
215
201
 
216
- /**
217
- * Apply a JSON Patch to an object (mutates in place).
218
- */
202
+ /** Apply a JSON Patch to an object (mutates in place). */
219
203
  export function applyPatch(obj: unknown, patch: JsonPatch): void {
220
204
  for (const operation of patch) {
221
205
  applyOperation(obj, operation);
@@ -224,83 +208,15 @@ export function applyPatch(obj: unknown, patch: JsonPatch): void {
224
208
 
225
209
  /**
226
210
  * Whether a per-operation failure is caused by the target location being
227
- * absent from the spec (i.e. vendor spec drift — the operation/schema the
228
- * patch targets was renamed or removed upstream) as opposed to a malformed
229
- * patch. Stale targets are skipped with a warning rather than aborting the
230
- * whole generate; a patch that only adds an error response to an operation
231
- * that no longer exists is harmless to drop.
211
+ * absent from the spec (i.e. spec drift — the operation/shape the patch
212
+ * targets was renamed or removed upstream) as opposed to a malformed patch.
213
+ * Stale targets should be skipped with a warning rather than aborting the
214
+ * whole generate; a patch that only adds an error to an operation that no
215
+ * longer exists is harmless to drop.
232
216
  */
233
- function isStaleTargetError(message: string): boolean {
217
+ export function isStaleTargetError(message: string): boolean {
234
218
  return (
235
219
  message.includes("not an object") ||
236
220
  message.includes("parent is not an object")
237
221
  );
238
222
  }
239
-
240
- /**
241
- * Load and apply all patches from a directory.
242
- * Finds all *.patch.json files and applies them.
243
- *
244
- * Each operation within a patch file is applied independently. Operations
245
- * whose target path no longer exists in the spec (vendor spec drift) are
246
- * collected as `skipped` warnings and do not abort generation. Only genuine
247
- * failures (malformed JSON, invalid pointers, failed `test` ops) are reported
248
- * as `errors`.
249
- */
250
- export function applyAllPatches(
251
- spec: unknown,
252
- patchDir: string,
253
- ): { applied: string[]; skipped: string[]; errors: string[] } {
254
- const applied: string[] = [];
255
- const skipped: string[] = [];
256
- const errors: string[] = [];
257
-
258
- if (!fs.existsSync(patchDir)) {
259
- return { applied, skipped, errors };
260
- }
261
-
262
- // Find all .patch.json files
263
- const files = fs
264
- .readdirSync(patchDir)
265
- .filter((f) => f.endsWith(".patch.json"))
266
- .sort(); // Sort for deterministic application order
267
-
268
- for (const file of files) {
269
- const filePath = path.join(patchDir, file);
270
- let patchFile: PatchFile;
271
- try {
272
- const content = fs.readFileSync(filePath, "utf-8");
273
- patchFile = JSON.parse(content);
274
- } catch (error) {
275
- errors.push(
276
- `${file}: ${error instanceof Error ? error.message : String(error)}`,
277
- );
278
- continue;
279
- }
280
-
281
- let appliedAny = false;
282
- for (const operation of patchFile.patches) {
283
- try {
284
- applyOperation(spec, operation);
285
- appliedAny = true;
286
- } catch (error) {
287
- const message = error instanceof Error ? error.message : String(error);
288
- if (isStaleTargetError(message)) {
289
- skipped.push(
290
- `${file} [${operation.op} ${operation.path}]: ${message}`,
291
- );
292
- } else {
293
- errors.push(
294
- `${file} [${operation.op} ${operation.path}]: ${message}`,
295
- );
296
- }
297
- }
298
- }
299
-
300
- if (appliedAny) {
301
- applied.push(`${file}: ${patchFile.description}`);
302
- }
303
- }
304
-
305
- return { applied, skipped, errors };
306
- }