@palantir/pack.document-schema.type-gen 0.12.0 → 0.14.0

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 (101) hide show
  1. package/build/esm/commands/asset/assetUpdateSchemaHandler.js +3 -2
  2. package/build/esm/commands/asset/assetUpdateSchemaHandler.js.map +1 -1
  3. package/build/esm/commands/ir/__tests__/resolveIrInput.test.js +88 -0
  4. package/build/esm/commands/ir/__tests__/resolveIrInput.test.js.map +1 -0
  5. package/build/esm/commands/ir/irDeployHandler.js +4 -3
  6. package/build/esm/commands/ir/irDeployHandler.js.map +1 -1
  7. package/build/esm/commands/ir/irGenAssetHandler.js +2 -31
  8. package/build/esm/commands/ir/irGenAssetHandler.js.map +1 -1
  9. package/build/esm/commands/ir/irGenTypesHandler.js +6 -6
  10. package/build/esm/commands/ir/irGenTypesHandler.js.map +1 -1
  11. package/build/esm/commands/ir/registerIrCommands.js +1 -1
  12. package/build/esm/commands/ir/registerIrCommands.js.map +1 -1
  13. package/build/esm/commands/ir/resolveIrInput.js +50 -0
  14. package/build/esm/commands/ir/resolveIrInput.js.map +1 -0
  15. package/build/esm/commands/schema/registerSchemaCommands.js +1 -1
  16. package/build/esm/commands/schema/registerSchemaCommands.js.map +1 -1
  17. package/build/esm/commands/schema/schemaIrHandler.js +14 -4
  18. package/build/esm/commands/schema/schemaIrHandler.js.map +1 -1
  19. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/custom-discriminant.js +1 -1
  20. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/custom-discriminant.js.map +1 -1
  21. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/external-refs.js +1 -1
  22. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/external-refs.js.map +1 -1
  23. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/simple-record.js +1 -1
  24. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/simple-record.js.map +1 -1
  25. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/union-types.js +1 -1
  26. package/build/esm/utils/ir/__tests__/__snapshots__/generateModelsFromIr/union-types.js.map +1 -1
  27. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/array-and-map-types.js +1 -1
  28. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/array-and-map-types.js.map +1 -1
  29. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/boolean-fields.js +1 -1
  30. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/boolean-fields.js.map +1 -1
  31. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/custom-discriminant.js +1 -1
  32. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/custom-discriminant.js.map +1 -1
  33. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/datetime-fields.js +1 -1
  34. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/datetime-fields.js.map +1 -1
  35. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/simple-records.js +1 -1
  36. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/simple-records.js.map +1 -1
  37. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/union-before-records.js +1 -1
  38. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/union-before-records.js.map +1 -1
  39. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/union-types.js +1 -1
  40. package/build/esm/utils/ir/__tests__/__snapshots__/generateZodSchemasFromIr/union-types.js.map +1 -1
  41. package/build/esm/utils/schema/__tests__/__snapshots__/generateDocumentTypeFromSchema/with-name-and-description.js +1 -1
  42. package/build/esm/utils/schema/__tests__/__snapshots__/generateDocumentTypeFromSchema/with-name-and-description.js.map +1 -1
  43. package/build/esm/utils/schema/__tests__/__snapshots__/generateVersionsFromSchema/single-version.js.map +1 -1
  44. package/build/esm/utils/schema/__tests__/__snapshots__/generateVersionsFromSchema/three-version-chain.js.map +1 -1
  45. package/build/esm/utils/schema/__tests__/__snapshots__/generateVersionsFromSchema/two-version.js.map +1 -1
  46. package/build/esm/utils/schema/__tests__/fixtures.js +11 -0
  47. package/build/esm/utils/schema/__tests__/fixtures.js.map +1 -1
  48. package/build/esm/utils/schema/__tests__/generateInternalFromSchema.test.js +6 -1
  49. package/build/esm/utils/schema/__tests__/generateInternalFromSchema.test.js.map +1 -1
  50. package/build/esm/utils/schema/__tests__/provenanceScenarios.test.js +184 -0
  51. package/build/esm/utils/schema/__tests__/provenanceScenarios.test.js.map +1 -0
  52. package/build/esm/utils/schema/__tests__/resolveSchemaChain.test.js +135 -0
  53. package/build/esm/utils/schema/__tests__/resolveSchemaChain.test.js.map +1 -0
  54. package/build/esm/utils/schema/generateInternalFromSchema.js +55 -34
  55. package/build/esm/utils/schema/generateInternalFromSchema.js.map +1 -1
  56. package/build/esm/utils/schema/generateModelMetadataFromSchema.js +12 -10
  57. package/build/esm/utils/schema/generateModelMetadataFromSchema.js.map +1 -1
  58. package/build/esm/utils/schema/generateScopeFromSchema.js +15 -3
  59. package/build/esm/utils/schema/generateScopeFromSchema.js.map +1 -1
  60. package/build/esm/utils/schema/loadSchemaModule.js +2 -5
  61. package/build/esm/utils/schema/loadSchemaModule.js.map +1 -1
  62. package/build/esm/utils/schema/resolveSchemaChain.js +135 -29
  63. package/build/esm/utils/schema/resolveSchemaChain.js.map +1 -1
  64. package/build/esm/utils/steps/__tests__/__snapshots__/types/arrayFields.js.map +1 -1
  65. package/build/esm/utils/steps/__tests__/__snapshots__/types/customDiscriminant.js +1 -1
  66. package/build/esm/utils/steps/__tests__/__snapshots__/types/customDiscriminant.js.map +1 -1
  67. package/build/esm/utils/steps/__tests__/__snapshots__/types/generateTypeScriptInterfaces.js +1 -1
  68. package/build/esm/utils/steps/__tests__/__snapshots__/types/generateTypeScriptInterfaces.js.map +1 -1
  69. package/build/esm/utils/steps/__tests__/__snapshots__/types/refTypes.js.map +1 -1
  70. package/build/esm/utils/steps/__tests__/__snapshots__/types/simpleRecord.js.map +1 -1
  71. package/build/esm/utils/steps/__tests__/__snapshots__/types/simpleSteps.js +1 -1
  72. package/build/esm/utils/steps/__tests__/__snapshots__/types/simpleSteps.js.map +1 -1
  73. package/build/esm/utils/steps/convertStepsToIr.js +49 -24
  74. package/build/esm/utils/steps/convertStepsToIr.js.map +1 -1
  75. package/build/types/commands/asset/assetUpdateSchemaHandler.d.ts.map +1 -1
  76. package/build/types/commands/ir/__tests__/resolveIrInput.test.d.ts +2 -0
  77. package/build/types/commands/ir/__tests__/resolveIrInput.test.d.ts.map +1 -0
  78. package/build/types/commands/ir/irDeployHandler.d.ts.map +1 -1
  79. package/build/types/commands/ir/irGenAssetHandler.d.ts.map +1 -1
  80. package/build/types/commands/ir/irGenTypesHandler.d.ts.map +1 -1
  81. package/build/types/commands/ir/registerIrCommands.d.ts.map +1 -1
  82. package/build/types/commands/ir/resolveIrInput.d.ts +8 -0
  83. package/build/types/commands/ir/resolveIrInput.d.ts.map +1 -0
  84. package/build/types/commands/schema/registerSchemaCommands.d.ts.map +1 -1
  85. package/build/types/commands/schema/schemaIrHandler.d.ts +4 -3
  86. package/build/types/commands/schema/schemaIrHandler.d.ts.map +1 -1
  87. package/build/types/utils/schema/__tests__/fixtures.d.ts +1 -0
  88. package/build/types/utils/schema/__tests__/fixtures.d.ts.map +1 -1
  89. package/build/types/utils/schema/__tests__/provenanceScenarios.test.d.ts +2 -0
  90. package/build/types/utils/schema/__tests__/provenanceScenarios.test.d.ts.map +1 -0
  91. package/build/types/utils/schema/__tests__/resolveSchemaChain.test.d.ts +2 -0
  92. package/build/types/utils/schema/__tests__/resolveSchemaChain.test.d.ts.map +1 -0
  93. package/build/types/utils/schema/generateInternalFromSchema.d.ts.map +1 -1
  94. package/build/types/utils/schema/generateModelMetadataFromSchema.d.ts.map +1 -1
  95. package/build/types/utils/schema/generateScopeFromSchema.d.ts.map +1 -1
  96. package/build/types/utils/schema/loadSchemaModule.d.ts.map +1 -1
  97. package/build/types/utils/schema/resolveSchemaChain.d.ts +25 -5
  98. package/build/types/utils/schema/resolveSchemaChain.d.ts.map +1 -1
  99. package/build/types/utils/steps/convertStepsToIr.d.ts +19 -4
  100. package/build/types/utils/steps/convertStepsToIr.d.ts.map +1 -1
  101. package/package.json +4 -4
@@ -0,0 +1,184 @@
1
+ /*
2
+ * Copyright 2026 Palantir Technologies, Inc. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as P from "@palantir/pack.schema";
18
+ import { defineRecord, defineSchema, defineSchemaUpdate, nextSchema } from "@palantir/pack.schema";
19
+ import { describe, expect, it } from "vitest";
20
+ import { generateInternalFromChain } from "../generateInternalFromSchema.js";
21
+ import { resolveSchemaChain, stripDeprecatedFieldsForSdk } from "../resolveSchemaChain.js";
22
+ function entry(chain, v) {
23
+ return chain.find(c => c.version === v);
24
+ }
25
+ function recordFields(e, key) {
26
+ const m = e.ir.models[key];
27
+ if (m.type !== "record") throw new Error(`${key} not a record`);
28
+ return Object.fromEntries(m.record.fields.map(f => [f.key, f]));
29
+ }
30
+ function unionMeta(e, key) {
31
+ const m = e.ir.models[key];
32
+ if (m.type !== "union") throw new Error(`${key} not a union`);
33
+ return m.union.metadata;
34
+ }
35
+ describe("provenance — scenarios beyond the demo", () => {
36
+ const v1 = defineSchema({
37
+ Box: defineRecord("Box", {
38
+ docs: "",
39
+ fields: {
40
+ a: P.Double
41
+ }
42
+ }),
43
+ OldUnion: P.defineUnion("OldUnion", {
44
+ docs: "",
45
+ variants: {
46
+ box: defineRecord("UVariant", {
47
+ docs: "",
48
+ fields: {
49
+ z: P.Double
50
+ }
51
+ })
52
+ }
53
+ })
54
+ });
55
+ const v2update = defineSchemaUpdate("v2", s => {
56
+ const Box = s.Box.addField("b", P.Optional(P.String)).build();
57
+ const Circle = defineRecord("Circle", {
58
+ docs: "",
59
+ fields: {
60
+ r: P.Double
61
+ }
62
+ });
63
+ const NewUnion = P.defineUnion("NewUnion", {
64
+ docs: "",
65
+ variants: {
66
+ c: Circle
67
+ }
68
+ });
69
+ return {
70
+ Box,
71
+ Circle,
72
+ NewUnion
73
+ };
74
+ });
75
+ const v2 = nextSchema(v1).addSchemaUpdate(v2update).build();
76
+ const v3update = defineSchemaUpdate("v3", s => ({
77
+ Box: s.Box.addField("c", P.Optional(P.Double)).build()
78
+ }));
79
+ const v3 = nextSchema(v2).addSchemaUpdate(v3update).build();
80
+ it("union defined in v1 stays addedInVersion 1 in every version", () => {
81
+ const {
82
+ chain
83
+ } = resolveSchemaChain(v3);
84
+ for (const v of [1, 2, 3]) {
85
+ expect(unionMeta(entry(chain, v), "OldUnion").addedInVersion).toBe(1);
86
+ }
87
+ });
88
+ it("union added in v2 gets addedInVersion 2", () => {
89
+ const {
90
+ chain
91
+ } = resolveSchemaChain(v3);
92
+ expect(entry(chain, 1).ir.models.NewUnion).toBeUndefined();
93
+ expect(unionMeta(entry(chain, 2), "NewUnion").addedInVersion).toBe(2);
94
+ expect(unionMeta(entry(chain, 3), "NewUnion").addedInVersion).toBe(2);
95
+ });
96
+ it("record added in v2 gets its fields stamped addedInVersion 2", () => {
97
+ const {
98
+ chain
99
+ } = resolveSchemaChain(v3);
100
+ expect(entry(chain, 1).ir.models.Circle).toBeUndefined();
101
+ expect(recordFields(entry(chain, 2), "Circle").r.metadata.addedInVersion).toBe(2);
102
+ expect(recordFields(entry(chain, 3), "Circle").r.metadata.addedInVersion).toBe(2);
103
+ });
104
+ it("field added in v3 to a v1 record gets addedInVersion 3; older fields keep theirs", () => {
105
+ const {
106
+ chain
107
+ } = resolveSchemaChain(v3);
108
+ const f = recordFields(entry(chain, 3), "Box");
109
+ expect(f.b.metadata.addedInVersion).toBe(2);
110
+ expect(f.c.metadata.addedInVersion).toBe(3);
111
+ });
112
+ it("hand-built schema provenance is derived from first appearance in the chain", () => {
113
+ const hand = {
114
+ type: "versioned",
115
+ version: 2,
116
+ previous: {
117
+ type: "initial",
118
+ version: 1,
119
+ models: v1.models
120
+ },
121
+ models: {
122
+ Box: defineRecord("Box", {
123
+ docs: "",
124
+ fields: {
125
+ a: P.Double,
126
+ b: P.Double
127
+ }
128
+ })
129
+ }
130
+ };
131
+ const {
132
+ chain
133
+ } = resolveSchemaChain(hand);
134
+ expect(recordFields(entry(chain, 2), "Box").a.metadata.addedInVersion).toBe(1);
135
+ expect(recordFields(entry(chain, 2), "Box").b.metadata.addedInVersion).toBe(2);
136
+ expect(recordFields(entry(chain, 1), "Box").a.metadata.addedInVersion).toBe(1);
137
+ });
138
+ it("upgrade lens still records removedFields + derivedFrom for a deprecated field", () => {
139
+ const b1 = defineSchema({
140
+ Box: defineRecord("Box", {
141
+ docs: "",
142
+ fields: {
143
+ a: P.Double,
144
+ color: P.Optional(P.String)
145
+ }
146
+ })
147
+ });
148
+ const split = defineSchemaUpdate("split", s => ({
149
+ Box: s.Box.addField("fillColor", P.Optional(P.String), {
150
+ derivedFrom: ["color"]
151
+ }).deprecateField("color").build()
152
+ }));
153
+ const b2 = nextSchema(b1).addSchemaUpdate(split).build();
154
+ const resolved = resolveSchemaChain(b2);
155
+ const stripped = {
156
+ ...resolved,
157
+ chain: stripDeprecatedFieldsForSdk(resolved.chain)
158
+ };
159
+ const internal = generateInternalFromChain(stripped);
160
+ expect(internal.upgrades).toContain("removedFields: [\"color\"]");
161
+ expect(internal.upgrades).toContain("derivedFrom: [\"color\"]");
162
+ });
163
+ it("deprecation with and without message both serialize (spread fix #5)", () => {
164
+ const b1 = defineSchema({
165
+ Box: defineRecord("Box", {
166
+ docs: "",
167
+ fields: {
168
+ x: P.Optional(P.String),
169
+ y: P.Optional(P.String)
170
+ }
171
+ })
172
+ });
173
+ const dep = defineSchemaUpdate("dep", s => ({
174
+ Box: s.Box.deprecateField("x", "gone").deprecateField("y").build()
175
+ }));
176
+ const b2 = nextSchema(b1).addSchemaUpdate(dep).build();
177
+ const f = recordFields(entry(resolveSchemaChain(b2).chain, 2), "Box");
178
+ expect(f.x.metadata.deprecatedFromVersion).toBe(2);
179
+ expect(f.x.metadata.deprecatedMessage).toBe("gone");
180
+ expect(f.y.metadata.deprecatedFromVersion).toBe(2);
181
+ expect(f.y.metadata.deprecatedMessage ?? undefined).toBeUndefined();
182
+ });
183
+ });
184
+ //# sourceMappingURL=provenanceScenarios.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provenanceScenarios.test.js","names":["P","defineRecord","defineSchema","defineSchemaUpdate","nextSchema","describe","expect","it","generateInternalFromChain","resolveSchemaChain","stripDeprecatedFieldsForSdk","entry","chain","v","find","c","version","recordFields","e","key","m","ir","models","type","Error","Object","fromEntries","record","fields","map","f","unionMeta","union","metadata","v1","Box","docs","a","Double","OldUnion","defineUnion","variants","box","z","v2update","s","addField","Optional","String","build","Circle","r","NewUnion","v2","addSchemaUpdate","v3update","v3","addedInVersion","toBe","toBeUndefined","b","hand","previous","b1","color","split","derivedFrom","deprecateField","b2","resolved","stripped","internal","upgrades","toContain","x","y","dep","deprecatedFromVersion","deprecatedMessage","undefined"],"sources":["provenanceScenarios.test.ts"],"sourcesContent":["/*\n * Copyright 2026 Palantir Technologies, Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as P from \"@palantir/pack.schema\";\nimport { defineRecord, defineSchema, defineSchemaUpdate, nextSchema } from \"@palantir/pack.schema\";\nimport { describe, expect, it } from \"vitest\";\nimport type {\n IFieldDef,\n IModelDef,\n} from \"../../../lib/pack-docschema-api/pack-docschema-ir/index.js\";\nimport { generateInternalFromChain } from \"../generateInternalFromSchema.js\";\nimport {\n resolveSchemaChain,\n stripDeprecatedFieldsForSdk,\n type VersionedIrEntry,\n} from \"../resolveSchemaChain.js\";\n\nfunction entry(chain: VersionedIrEntry[], v: number): VersionedIrEntry {\n return chain.find(c => c.version === v)!;\n}\nfunction recordFields(e: VersionedIrEntry, key: string): Record<string, IFieldDef> {\n const m: IModelDef = e.ir.models[key]!;\n if (m.type !== \"record\") throw new Error(`${key} not a record`);\n return Object.fromEntries(m.record.fields.map(f => [f.key, f]));\n}\nfunction unionMeta(e: VersionedIrEntry, key: string) {\n const m: IModelDef = e.ir.models[key]!;\n if (m.type !== \"union\") throw new Error(`${key} not a union`);\n return m.union.metadata;\n}\n\ndescribe(\"provenance — scenarios beyond the demo\", () => {\n const v1 = defineSchema({\n Box: defineRecord(\"Box\", { docs: \"\", fields: { a: P.Double } }),\n OldUnion: P.defineUnion(\"OldUnion\", {\n docs: \"\",\n variants: { box: defineRecord(\"UVariant\", { docs: \"\", fields: { z: P.Double } }) },\n }),\n });\n\n const v2update = defineSchemaUpdate(\"v2\", (s: P.SchemaBuilder<typeof v1.models>) => {\n const Box = s.Box.addField(\"b\", P.Optional(P.String)).build();\n const Circle = defineRecord(\"Circle\", { docs: \"\", fields: { r: P.Double } });\n const NewUnion = P.defineUnion(\"NewUnion\", {\n docs: \"\",\n variants: { c: Circle },\n });\n return { Box, Circle, NewUnion };\n });\n const v2 = nextSchema(v1).addSchemaUpdate(v2update).build();\n\n const v3update = defineSchemaUpdate(\"v3\", (s: P.SchemaBuilder<typeof v2.models>) => ({\n Box: s.Box.addField(\"c\", P.Optional(P.Double)).build(),\n }));\n const v3 = nextSchema(v2).addSchemaUpdate(v3update).build();\n\n it(\"union defined in v1 stays addedInVersion 1 in every version\", () => {\n const { chain } = resolveSchemaChain(v3);\n for (const v of [1, 2, 3]) {\n expect(unionMeta(entry(chain, v), \"OldUnion\").addedInVersion).toBe(1);\n }\n });\n\n it(\"union added in v2 gets addedInVersion 2\", () => {\n const { chain } = resolveSchemaChain(v3);\n expect(entry(chain, 1).ir.models.NewUnion).toBeUndefined();\n expect(unionMeta(entry(chain, 2), \"NewUnion\").addedInVersion).toBe(2);\n expect(unionMeta(entry(chain, 3), \"NewUnion\").addedInVersion).toBe(2);\n });\n\n it(\"record added in v2 gets its fields stamped addedInVersion 2\", () => {\n const { chain } = resolveSchemaChain(v3);\n expect(entry(chain, 1).ir.models.Circle).toBeUndefined();\n expect(recordFields(entry(chain, 2), \"Circle\").r!.metadata.addedInVersion).toBe(2);\n expect(recordFields(entry(chain, 3), \"Circle\").r!.metadata.addedInVersion).toBe(2);\n });\n\n it(\"field added in v3 to a v1 record gets addedInVersion 3; older fields keep theirs\", () => {\n const { chain } = resolveSchemaChain(v3);\n const f = recordFields(entry(chain, 3), \"Box\");\n expect(f.b!.metadata.addedInVersion).toBe(2);\n expect(f.c!.metadata.addedInVersion).toBe(3);\n });\n\n it(\"hand-built schema provenance is derived from first appearance in the chain\", () => {\n const hand: P.SchemaDefinition = {\n type: \"versioned\",\n version: 2,\n previous: { type: \"initial\", version: 1, models: v1.models },\n models: { Box: defineRecord(\"Box\", { docs: \"\", fields: { a: P.Double, b: P.Double } }) },\n };\n const { chain } = resolveSchemaChain(hand);\n expect(recordFields(entry(chain, 2), \"Box\").a!.metadata.addedInVersion).toBe(1);\n expect(recordFields(entry(chain, 2), \"Box\").b!.metadata.addedInVersion).toBe(2);\n expect(recordFields(entry(chain, 1), \"Box\").a!.metadata.addedInVersion).toBe(1);\n });\n\n it(\"upgrade lens still records removedFields + derivedFrom for a deprecated field\", () => {\n const b1 = defineSchema({\n Box: defineRecord(\"Box\", { docs: \"\", fields: { a: P.Double, color: P.Optional(P.String) } }),\n });\n const split = defineSchemaUpdate(\"split\", (s: P.SchemaBuilder<typeof b1.models>) => ({\n Box: s.Box\n .addField(\"fillColor\", P.Optional(P.String), { derivedFrom: [\"color\"] })\n .deprecateField(\"color\")\n .build(),\n }));\n const b2 = nextSchema(b1).addSchemaUpdate(split).build();\n\n const resolved = resolveSchemaChain(b2);\n const stripped = { ...resolved, chain: stripDeprecatedFieldsForSdk(resolved.chain) };\n const internal = generateInternalFromChain(stripped);\n expect(internal.upgrades).toContain(\"removedFields: [\\\"color\\\"]\");\n expect(internal.upgrades).toContain(\"derivedFrom: [\\\"color\\\"]\");\n });\n\n it(\"deprecation with and without message both serialize (spread fix #5)\", () => {\n const b1 = defineSchema({\n Box: defineRecord(\"Box\", {\n docs: \"\",\n fields: { x: P.Optional(P.String), y: P.Optional(P.String) },\n }),\n });\n const dep = defineSchemaUpdate(\"dep\", (s: P.SchemaBuilder<typeof b1.models>) => ({\n Box: s.Box.deprecateField(\"x\", \"gone\").deprecateField(\"y\").build(),\n }));\n const b2 = nextSchema(b1).addSchemaUpdate(dep).build();\n const f = recordFields(entry(resolveSchemaChain(b2).chain, 2), \"Box\");\n expect(f.x!.metadata.deprecatedFromVersion).toBe(2);\n expect(f.x!.metadata.deprecatedMessage).toBe(\"gone\");\n expect(f.y!.metadata.deprecatedFromVersion).toBe(2);\n expect(f.y!.metadata.deprecatedMessage ?? undefined).toBeUndefined();\n });\n});\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,135 @@
1
+ /*
2
+ * Copyright 2026 Palantir Technologies, Inc. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as P from "@palantir/pack.schema";
18
+ import { defineRecord, defineSchema, defineSchemaUpdate, nextSchema } from "@palantir/pack.schema";
19
+ import { describe, expect, it } from "vitest";
20
+ import { resolveSchemaChain, stripDeprecatedFieldsForSdk } from "../resolveSchemaChain.js";
21
+
22
+ /** Index a record model's fields by key for a given chain entry. */
23
+ function fieldsByKey(entry, modelKey) {
24
+ const model = entry.ir.models[modelKey];
25
+ if (model.type !== "record") throw new Error(`${modelKey} is not a record`);
26
+ return Object.fromEntries(model.record.fields.map(f => [f.key, f]));
27
+ }
28
+ describe("resolveSchemaChain provenance", () => {
29
+ const v1 = defineSchema({
30
+ ShapeBox: defineRecord("ShapeBox", {
31
+ docs: "A box",
32
+ fields: {
33
+ left: P.Double,
34
+ color: P.Optional(P.String)
35
+ }
36
+ })
37
+ });
38
+ const colorSplit = defineSchemaUpdate("colorSplit", schema => ({
39
+ ShapeBox: schema.ShapeBox.addField("fillColor", P.Optional(P.String), {
40
+ derivedFrom: ["color"]
41
+ }).addField("strokeColor", P.Optional(P.String), {
42
+ derivedFrom: ["color"]
43
+ }).deprecateField("color", "use fillColor/strokeColor").build()
44
+ }));
45
+ const v2 = nextSchema(v1).addSchemaUpdate(colorSplit).build();
46
+ it("derives addedInVersion from first appearance in the chain", () => {
47
+ const {
48
+ chain
49
+ } = resolveSchemaChain(v2);
50
+ const v2Fields = fieldsByKey(chain.find(c => c.version === 2), "ShapeBox");
51
+ expect(v2Fields.left.metadata.addedInVersion).toBe(1);
52
+ expect(v2Fields.fillColor.metadata.addedInVersion).toBe(2);
53
+ expect(v2Fields.strokeColor.metadata.addedInVersion).toBe(2);
54
+ });
55
+ it("caps the chain at maxVersion", () => {
56
+ const {
57
+ chain,
58
+ latestVersion,
59
+ minVersion
60
+ } = resolveSchemaChain(v2, 1, {}, 1);
61
+ expect(chain.map(entry => entry.version)).toEqual([1]);
62
+ expect(latestVersion).toBe(1);
63
+ expect(minVersion).toBe(1);
64
+ });
65
+ it("rejects a maxVersion that is not in the chain", () => {
66
+ expect(() => resolveSchemaChain(v2, undefined, {}, 3)).toThrow("maxVersion 3 is not in the schema chain");
67
+ });
68
+ it("keeps a deprecated field present and stamps deprecatedFromVersion + message", () => {
69
+ const {
70
+ chain
71
+ } = resolveSchemaChain(v2);
72
+ const v2Fields = fieldsByKey(chain.find(c => c.version === 2), "ShapeBox");
73
+ expect(v2Fields.color).toBeDefined();
74
+ expect(v2Fields.color.metadata.addedInVersion).toBe(1);
75
+ expect(v2Fields.color.metadata.deprecatedFromVersion).toBe(2);
76
+ expect(v2Fields.color.metadata.deprecatedMessage).toBe("use fillColor/strokeColor");
77
+ });
78
+ it("does not mark the field deprecated in versions before the deprecation", () => {
79
+ const {
80
+ chain
81
+ } = resolveSchemaChain(v2);
82
+ const v1Fields = fieldsByKey(chain.find(c => c.version === 1), "ShapeBox");
83
+ expect(v1Fields.color).toBeDefined();
84
+ expect(v1Fields.color.metadata.deprecatedFromVersion ?? undefined).toBeUndefined();
85
+ expect(v1Fields.fillColor).toBeUndefined();
86
+ });
87
+ it("stripDeprecatedFieldsForSdk drops deprecated fields from v>=deprecation only", () => {
88
+ const {
89
+ chain
90
+ } = resolveSchemaChain(v2);
91
+ const stripped = stripDeprecatedFieldsForSdk(chain);
92
+ const v1Fields = fieldsByKey(stripped.find(c => c.version === 1), "ShapeBox");
93
+ expect(v1Fields.color).toBeDefined();
94
+ const v2Fields = fieldsByKey(stripped.find(c => c.version === 2), "ShapeBox");
95
+ expect(v2Fields.color).toBeUndefined();
96
+ expect(v2Fields.fillColor).toBeDefined();
97
+ expect(fieldsByKey(chain.find(c => c.version === 2), "ShapeBox").color).toBeDefined();
98
+ });
99
+ });
100
+ describe("resolveSchemaChain — union provenance", () => {
101
+ const v1 = defineSchema({
102
+ Box: defineRecord("Box", {
103
+ docs: "",
104
+ fields: {
105
+ a: P.Double
106
+ }
107
+ }),
108
+ NodeShape: P.defineUnion("NodeShape", {
109
+ docs: "shape",
110
+ variants: {
111
+ box: defineRecord("Inner", {
112
+ docs: "",
113
+ fields: {
114
+ z: P.Double
115
+ }
116
+ })
117
+ }
118
+ })
119
+ });
120
+ const addB = defineSchemaUpdate("addB", schema => ({
121
+ Box: schema.Box.addField("b", P.Optional(P.String)).build()
122
+ }));
123
+ const v2 = nextSchema(v1).addSchemaUpdate(addB).build();
124
+ it("a v1-defined union stays addedInVersion 1 in every version", () => {
125
+ const {
126
+ chain
127
+ } = resolveSchemaChain(v2);
128
+ for (const version of [1, 2]) {
129
+ const union = chain.find(c => c.version === version).ir.models.NodeShape;
130
+ if (union.type !== "union") throw new Error("NodeShape should be a union");
131
+ expect(union.union.metadata.addedInVersion).toBe(1);
132
+ }
133
+ });
134
+ });
135
+ //# sourceMappingURL=resolveSchemaChain.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveSchemaChain.test.js","names":["P","defineRecord","defineSchema","defineSchemaUpdate","nextSchema","describe","expect","it","resolveSchemaChain","stripDeprecatedFieldsForSdk","fieldsByKey","entry","modelKey","model","ir","models","type","Error","Object","fromEntries","record","fields","map","f","key","v1","ShapeBox","docs","left","Double","color","Optional","String","colorSplit","schema","addField","derivedFrom","deprecateField","build","v2","addSchemaUpdate","chain","v2Fields","find","c","version","metadata","addedInVersion","toBe","fillColor","strokeColor","latestVersion","minVersion","toEqual","undefined","toThrow","toBeDefined","deprecatedFromVersion","deprecatedMessage","v1Fields","toBeUndefined","stripped","Box","a","NodeShape","defineUnion","variants","box","z","addB","union"],"sources":["resolveSchemaChain.test.ts"],"sourcesContent":["/*\n * Copyright 2026 Palantir Technologies, Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as P from \"@palantir/pack.schema\";\nimport { defineRecord, defineSchema, defineSchemaUpdate, nextSchema } from \"@palantir/pack.schema\";\nimport { describe, expect, it } from \"vitest\";\nimport type {\n IFieldDef,\n IModelDef,\n} from \"../../../lib/pack-docschema-api/pack-docschema-ir/index.js\";\nimport {\n resolveSchemaChain,\n stripDeprecatedFieldsForSdk,\n type VersionedIrEntry,\n} from \"../resolveSchemaChain.js\";\n\n/** Index a record model's fields by key for a given chain entry. */\nfunction fieldsByKey(entry: VersionedIrEntry, modelKey: string): Record<string, IFieldDef> {\n const model: IModelDef = entry.ir.models[modelKey]!;\n if (model.type !== \"record\") throw new Error(`${modelKey} is not a record`);\n return Object.fromEntries(model.record.fields.map(f => [f.key, f]));\n}\n\ndescribe(\"resolveSchemaChain provenance\", () => {\n const v1 = defineSchema({\n ShapeBox: defineRecord(\"ShapeBox\", {\n docs: \"A box\",\n fields: { left: P.Double, color: P.Optional(P.String) },\n }),\n });\n\n const colorSplit = defineSchemaUpdate(\n \"colorSplit\",\n (schema: P.SchemaBuilder<typeof v1.models>) => ({\n ShapeBox: schema.ShapeBox\n .addField(\"fillColor\", P.Optional(P.String), { derivedFrom: [\"color\"] })\n .addField(\"strokeColor\", P.Optional(P.String), { derivedFrom: [\"color\"] })\n .deprecateField(\"color\", \"use fillColor/strokeColor\")\n .build(),\n }),\n );\n\n const v2 = nextSchema(v1).addSchemaUpdate(colorSplit).build();\n\n it(\"derives addedInVersion from first appearance in the chain\", () => {\n const { chain } = resolveSchemaChain(v2);\n const v2Fields = fieldsByKey(chain.find(c => c.version === 2)!, \"ShapeBox\");\n\n expect(v2Fields.left!.metadata.addedInVersion).toBe(1);\n expect(v2Fields.fillColor!.metadata.addedInVersion).toBe(2);\n expect(v2Fields.strokeColor!.metadata.addedInVersion).toBe(2);\n });\n\n it(\"caps the chain at maxVersion\", () => {\n const { chain, latestVersion, minVersion } = resolveSchemaChain(v2, 1, {}, 1);\n\n expect(chain.map(entry => entry.version)).toEqual([1]);\n expect(latestVersion).toBe(1);\n expect(minVersion).toBe(1);\n });\n\n it(\"rejects a maxVersion that is not in the chain\", () => {\n expect(() => resolveSchemaChain(v2, undefined, {}, 3)).toThrow(\n \"maxVersion 3 is not in the schema chain\",\n );\n });\n\n it(\"keeps a deprecated field present and stamps deprecatedFromVersion + message\", () => {\n const { chain } = resolveSchemaChain(v2);\n const v2Fields = fieldsByKey(chain.find(c => c.version === 2)!, \"ShapeBox\");\n\n expect(v2Fields.color).toBeDefined();\n expect(v2Fields.color!.metadata.addedInVersion).toBe(1);\n expect(v2Fields.color!.metadata.deprecatedFromVersion).toBe(2);\n expect(v2Fields.color!.metadata.deprecatedMessage).toBe(\"use fillColor/strokeColor\");\n });\n\n it(\"does not mark the field deprecated in versions before the deprecation\", () => {\n const { chain } = resolveSchemaChain(v2);\n const v1Fields = fieldsByKey(chain.find(c => c.version === 1)!, \"ShapeBox\");\n\n expect(v1Fields.color).toBeDefined();\n expect(v1Fields.color!.metadata.deprecatedFromVersion ?? undefined).toBeUndefined();\n expect(v1Fields.fillColor).toBeUndefined();\n });\n\n it(\"stripDeprecatedFieldsForSdk drops deprecated fields from v>=deprecation only\", () => {\n const { chain } = resolveSchemaChain(v2);\n const stripped = stripDeprecatedFieldsForSdk(chain);\n\n const v1Fields = fieldsByKey(stripped.find(c => c.version === 1)!, \"ShapeBox\");\n expect(v1Fields.color).toBeDefined();\n\n const v2Fields = fieldsByKey(stripped.find(c => c.version === 2)!, \"ShapeBox\");\n expect(v2Fields.color).toBeUndefined();\n expect(v2Fields.fillColor).toBeDefined();\n\n expect(fieldsByKey(chain.find(c => c.version === 2)!, \"ShapeBox\").color).toBeDefined();\n });\n});\n\ndescribe(\"resolveSchemaChain — union provenance\", () => {\n const v1 = defineSchema({\n Box: defineRecord(\"Box\", { docs: \"\", fields: { a: P.Double } }),\n NodeShape: P.defineUnion(\"NodeShape\", {\n docs: \"shape\",\n variants: { box: defineRecord(\"Inner\", { docs: \"\", fields: { z: P.Double } }) },\n }),\n });\n const addB = defineSchemaUpdate(\n \"addB\",\n (schema: P.SchemaBuilder<typeof v1.models>) => ({\n Box: schema.Box.addField(\"b\", P.Optional(P.String)).build(),\n }),\n );\n const v2 = nextSchema(v1).addSchemaUpdate(addB).build();\n\n it(\"a v1-defined union stays addedInVersion 1 in every version\", () => {\n const { chain } = resolveSchemaChain(v2);\n for (const version of [1, 2]) {\n const union = chain.find(c => c.version === version)!.ir.models.NodeShape!;\n if (union.type !== \"union\") throw new Error(\"NodeShape should be a union\");\n expect(union.union.metadata.addedInVersion).toBe(1);\n }\n });\n});\n"],"mappings":"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","ignoreList":[]}
@@ -16,12 +16,18 @@
16
16
 
17
17
  import { GENERATED_FILE_HEADER } from "../generatedFileHeader.js";
18
18
  import { convertFieldTypeToDescriptor, convertFieldTypeToInternalTypeScript, convertFieldTypeToInternalZod } from "../ir/irFieldHelpers.js";
19
- import { chainHasDerivedFields } from "./resolveSchemaChain.js";
19
+ import { chainNeedsUpgradeFns } from "./resolveSchemaChain.js";
20
20
 
21
21
  /** Collected info about all fields for a model across all versions. */
22
22
 
23
23
  /**
24
24
  * Represents an upgrade step for a model between version transitions.
25
+ *
26
+ * `fields` only contains entries that require lens treatment — derived fields
27
+ * (`derivedFrom.length > 0`) and required additive fields (`derivedFrom.length
28
+ * === 0 && !isOptional`). Optional additive fields are absent because
29
+ * `undefined` is a legal value at the current version and the lens has no
30
+ * work to do.
25
31
  */
26
32
 
27
33
  /**
@@ -30,6 +36,9 @@ import { chainHasDerivedFields } from "./resolveSchemaChain.js";
30
36
  function sortedEntries(map) {
31
37
  return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
32
38
  }
39
+ function getAddedInVersion(fieldInfo) {
40
+ return Math.min(...fieldInfo.presentInVersions);
41
+ }
33
42
 
34
43
  /**
35
44
  * Collect all record model info across all versions and compute upgrade steps.
@@ -87,25 +96,25 @@ function collectRecordModels(chain) {
87
96
  const prevFields = new Set(prevModelDef.record.fields.map(f => f.key));
88
97
  const currFields = new Set(currModelDef.record.fields.map(f => f.key));
89
98
 
90
- // Fields added in this version
99
+ // Fields added in this version that require lens treatment. Optional
100
+ // additive fields are skipped — `undefined` is a legal v_current value
101
+ // for them, so the lens has nothing to invent.
91
102
  const addedFields = new Map();
92
103
  const recordUpgrades = currEntry.migrations?.[modelKey];
93
- for (const fieldKey of currFields) {
94
- if (!prevFields.has(fieldKey)) {
95
- const annotation = recordUpgrades?.[fieldKey];
96
- if (annotation != null) {
97
- addedFields.set(fieldKey, {
98
- derivedFrom: [...annotation.derivedFrom]
99
- });
100
- } else {
101
- addedFields.set(fieldKey, {
102
- derivedFrom: []
103
- });
104
- }
105
- }
104
+ for (const field of currModelDef.record.fields) {
105
+ const fieldKey = field.key;
106
+ if (prevFields.has(fieldKey)) continue;
107
+ const isOptional = field.isOptional === true;
108
+ const annotation = recordUpgrades?.[fieldKey];
109
+ const derivedFrom = annotation != null ? [...annotation.derivedFrom] : [];
110
+ if (derivedFrom.length === 0 && isOptional) continue;
111
+ addedFields.set(fieldKey, {
112
+ derivedFrom,
113
+ isOptional
114
+ });
106
115
  }
107
116
 
108
- // Fields removed in this version
117
+ // Fields absent from this version relative to the previous one (i.e. deprecated fields).
109
118
  const removedFields = [];
110
119
  for (const fieldKey of prevFields) {
111
120
  if (!currFields.has(fieldKey)) {
@@ -224,7 +233,7 @@ function generateUpgrades(allRecordModels, latestIr) {
224
233
  output += ` allFields: {\n`;
225
234
  for (const [fieldName, fieldInfo] of sortedEntries(model.allFields)) {
226
235
  const typeDescriptor = convertFieldTypeToDescriptor(fieldInfo.fieldType, fieldInfo.isOptional);
227
- output += ` ${fieldName}: { type: ${typeDescriptor} },\n`;
236
+ output += ` ${fieldName}: { type: ${typeDescriptor}, addedInVersion: ${getAddedInVersion(fieldInfo)} },\n`;
228
237
  }
229
238
  output += ` },\n`;
230
239
 
@@ -237,9 +246,6 @@ function generateUpgrades(allRecordModels, latestIr) {
237
246
  for (const [fieldName, fieldMeta] of step.fields) {
238
247
  output += ` ${fieldName}: {\n`;
239
248
  output += ` derivedFrom: [${fieldMeta.derivedFrom.map(d => `"${d}"`).join(", ")}],\n`;
240
- if (fieldMeta.default !== undefined) {
241
- output += ` default: ${JSON.stringify(fieldMeta.default)},\n`;
242
- }
243
249
  output += ` },\n`;
244
250
  }
245
251
  output += ` },\n`;
@@ -292,16 +298,21 @@ function generateInternalSchemaContent(allRecordModels) {
292
298
  * Generate _internal/upgradeFns.ts content: a typed `DocumentUpgradeFns`
293
299
  * interface enumerating every upgrade function the app must supply at boot.
294
300
  *
295
- * For every `(modelName, v${version}, fieldName)` triple where `derivedFrom` is
296
- * non-empty, emit a property typed
297
- * `(oldFields: Pick<<Model>__v<prior>, derivedFrom[number]>) => <Model>__v<current>[fieldName]`.
298
- * Additive fields (empty `derivedFrom`) are intentionally absent — they are
299
- * handled by the structural `default` in `_internal/upgrades.ts`.
301
+ * For each `(modelName, v${version}, fieldName)` triple in a step's `fields`,
302
+ * emit one property:
303
+ * - Derived (`derivedFrom.length > 0`):
304
+ * `(oldFields: Pick<<Model>__v<prior>, derivedFrom[number]>) => <Model>__v<current>[fieldName]`.
305
+ * - Required additive (`derivedFrom.length === 0`):
306
+ * `() => <Model>__v<current>[fieldName]`.
307
+ *
308
+ * Optional additive fields are filtered out at step-collection time and don't
309
+ * appear here — `undefined` is a legal v<current> value for them, so the app
310
+ * has no obligation to invent one.
300
311
  *
301
312
  * The interface is consumed by the generated `DocumentModel` factory in
302
- * `models.ts`, which requires it as a parameter when the schema has any
303
- * derived fields. Schemas without derived fields emit an empty interface
304
- * (still exported for type-uniformity in downstream code).
313
+ * `models.ts`, which requires it as a parameter when the schema needs any
314
+ * upgrade functions. Schemas with no required fields added past v1 emit an
315
+ * empty interface (still exported for type-uniformity in downstream code).
305
316
  */
306
317
  function generateUpgradeFns(chain, allRecordModels) {
307
318
  const perModel = new Map();
@@ -313,7 +324,6 @@ function generateUpgradeFns(chain, allRecordModels) {
313
324
  const priorVersion = chain[currentIdx - 1].version;
314
325
  const fields = [];
315
326
  for (const [fieldName, fieldMeta] of step.fields) {
316
- if (fieldMeta.derivedFrom.length === 0) continue;
317
327
  fields.push({
318
328
  fieldName,
319
329
  derivedFrom: [...fieldMeta.derivedFrom]
@@ -330,7 +340,10 @@ function generateUpgradeFns(chain, allRecordModels) {
330
340
  priorVersion,
331
341
  fields
332
342
  });
333
- neededTypes.add(`${modelKey}__v${priorVersion}`);
343
+ // Prior-version type only needed when at least one field derives from it.
344
+ if (fields.some(f => f.derivedFrom.length > 0)) {
345
+ neededTypes.add(`${modelKey}__v${priorVersion}`);
346
+ }
334
347
  neededTypes.add(`${modelKey}__v${step.addedInVersion}`);
335
348
  }
336
349
  }
@@ -343,7 +356,10 @@ function generateUpgradeFns(chain, allRecordModels) {
343
356
  output += `/**\n`;
344
357
  output += ` * Typed upgrade functions the application must supply at boot. Coverage is\n`;
345
358
  output += ` * exhaustive: missing or extra entries fail type-check at the \`DocumentModel(...)\`\n`;
346
- output += ` * call site. Additive fields (empty \`derivedFrom\`) are intentionally absent.\n`;
359
+ output += ` * call site. Derived fields receive their declared source fields; required\n`;
360
+ output += ` * additive fields receive no arguments. Optional additive fields are absent\n`;
361
+ output += ` * — \`undefined\` is a legal value at the new version, so the runtime lens\n`;
362
+ output += ` * leaves them alone.\n`;
347
363
  output += ` */\n`;
348
364
  if (perModel.size === 0) {
349
365
  output += `// eslint-disable-next-line @typescript-eslint/no-empty-object-type\n`;
@@ -355,8 +371,13 @@ function generateUpgradeFns(chain, allRecordModels) {
355
371
  for (const step of steps) {
356
372
  output += ` v${step.currentVersion}: {\n`;
357
373
  for (const f of step.fields) {
358
- const pickKeys = f.derivedFrom.map(d => `"${d}"`).join(" | ");
359
- output += ` ${f.fieldName}: (oldFields: Pick<${modelKey}__v${step.priorVersion}, ${pickKeys}>) => ${modelKey}__v${step.currentVersion}["${f.fieldName}"];\n`;
374
+ const returnType = `${modelKey}__v${step.currentVersion}["${f.fieldName}"]`;
375
+ if (f.derivedFrom.length === 0) {
376
+ output += ` ${f.fieldName}: () => ${returnType};\n`;
377
+ } else {
378
+ const pickKeys = f.derivedFrom.map(d => `"${d}"`).join(" | ");
379
+ output += ` ${f.fieldName}: (oldFields: Pick<${modelKey}__v${step.priorVersion}, ${pickKeys}>) => ${returnType};\n`;
380
+ }
360
381
  }
361
382
  output += ` };\n`;
362
383
  }
@@ -377,7 +398,7 @@ export function generateInternalFromChain(resolved) {
377
398
  } = resolved;
378
399
  const latestIr = chain[chain.length - 1].ir;
379
400
  const allRecordModels = collectRecordModels(chain);
380
- const internalTypes = generateInternalTypes(allRecordModels) + (chainHasDerivedFields(chain) ? generatePerVersionInternalTypes(chain) : "");
401
+ const internalTypes = generateInternalTypes(allRecordModels) + (chainNeedsUpgradeFns(chain) ? generatePerVersionInternalTypes(chain) : "");
381
402
  const upgrades = generateUpgrades(allRecordModels, latestIr);
382
403
  const internalSchema = generateInternalSchemaContent(allRecordModels);
383
404
  const internalUpgradeFns = generateUpgradeFns(chain, allRecordModels);