soustack 0.3.0 → 0.4.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 (148) hide show
  1. package/README.md +44 -27
  2. package/dist/cli/index.js +5225 -992
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/index.d.mts +163 -91
  5. package/dist/index.d.ts +163 -91
  6. package/dist/index.js +5077 -1007
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5076 -1007
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/{scrape.d.mts → scrape/index.d.mts} +88 -74
  11. package/dist/{scrape.d.ts → scrape/index.d.ts} +88 -74
  12. package/dist/{scrape.js → scrape/index.js} +255 -124
  13. package/dist/scrape/index.js.map +1 -0
  14. package/dist/{scrape.mjs → scrape/index.mjs} +255 -124
  15. package/dist/scrape/index.mjs.map +1 -0
  16. package/package.json +21 -9
  17. package/spec/.sync-meta.json +149 -0
  18. package/spec/SOUSTACK_SPEC_VERSION +1 -0
  19. package/spec/defs/common.schema.json +46 -0
  20. package/spec/defs/duration.schema.json +33 -0
  21. package/spec/defs/entities.schema.json +111 -0
  22. package/spec/defs/ingredientQuantified.schema.json +9 -0
  23. package/spec/defs/quantity.schema.json +16 -0
  24. package/spec/defs/scalingRule.schema.json +127 -0
  25. package/spec/defs/temperature.schema.json +63 -0
  26. package/spec/fixtures/content/illustrated-step.valid.json +24 -0
  27. package/spec/fixtures/invalid/equipment-unknown-reference.invalid.json +38 -0
  28. package/spec/fixtures/invalid/mise-en-place-unknown-equipment.invalid.json +37 -0
  29. package/spec/fixtures/invalid/mise-en-place-unknown-input.invalid.json +41 -0
  30. package/spec/fixtures/invalid/storage-leftovers-missing-method.invalid.json +31 -0
  31. package/spec/fixtures/invalid/storage-leftovers-wrong-type.invalid.json +23 -0
  32. package/spec/fixtures/level/base-full.valid.json +162 -0
  33. package/spec/fixtures/level/base-missing-yield.invalid.json +12 -0
  34. package/spec/fixtures/level/lite-min.valid.json +14 -0
  35. package/spec/fixtures/profile/profile-base.valid.json +20 -0
  36. package/spec/fixtures/profile/profile-equipped.valid.json +28 -0
  37. package/spec/fixtures/profile/profile-illustrated.valid.json +28 -0
  38. package/spec/fixtures/profile/profile-lite.valid.json +13 -0
  39. package/spec/fixtures/profile/profile-prepped.valid.json +31 -0
  40. package/spec/fixtures/profile/profile-scalable-missing-scaling.invalid.json +29 -0
  41. package/spec/fixtures/profile/profile-scalable.valid.json +49 -0
  42. package/spec/fixtures/profile/profile-timed-missing-structured.invalid.json +30 -0
  43. package/spec/fixtures/scaling/bakers-percent-missing-ref.invalid.json +41 -0
  44. package/spec/fixtures/scaling/bakers-percent.valid.json +51 -0
  45. package/spec/fixtures/scaling/discrete-range.invalid.json +36 -0
  46. package/spec/fixtures/scaling/missing-quantified.invalid.json +40 -0
  47. package/spec/fixtures/scaling/reject-bakersPercentage.invalid.json +50 -0
  48. package/spec/fixtures/stacks/compute-missing-timed.invalid.json +32 -0
  49. package/spec/fixtures/stacks/dietary-no-signal.invalid.json +16 -0
  50. package/spec/fixtures/stacks/illustrated-empty.invalid.json +13 -0
  51. package/spec/fixtures/stacks/quantified-string.invalid.json +22 -0
  52. package/spec/fixtures/stacks/referenced-missing-input.invalid.json +32 -0
  53. package/spec/fixtures/stacks/storage-min.valid.json +20 -0
  54. package/spec/fixtures/stacks/storage-no-duration.invalid.json +16 -0
  55. package/spec/fixtures/stacks/timed-implies-structured.valid.json +50 -0
  56. package/spec/fixtures/stacks/timed-range.invalid.json +33 -0
  57. package/spec/fixtures/valid/equipment-scaling-rules.valid.json +76 -0
  58. package/spec/fixtures/valid/equipment-strings.valid.json +31 -0
  59. package/spec/fixtures/valid/equipment-structured-uses.valid.json +47 -0
  60. package/spec/fixtures/valid/mise-en-place-basic.valid.json +31 -0
  61. package/spec/fixtures/valid/mise-en-place-referenced-equipment.valid.json +51 -0
  62. package/spec/fixtures/valid/prep-ingredient-strings.valid.json +48 -0
  63. package/spec/fixtures/valid/prep-ingredient-structured.valid.json +45 -0
  64. package/spec/fixtures/valid/profile-equipped.valid.json +29 -0
  65. package/spec/fixtures/valid/profile-prepped.valid.json +32 -0
  66. package/spec/fixtures/valid/quantified-nested-ingredient-sections.valid.json +61 -0
  67. package/spec/fixtures/valid/referenced-scaling.valid.json +67 -0
  68. package/spec/fixtures/valid/storage-leftovers-simple.valid.json +27 -0
  69. package/spec/fixtures/valid/storage-leftovers-structured.valid.json +43 -0
  70. package/spec/fixtures/valid/structured-nested-step-sections.valid.json +84 -0
  71. package/spec/schemas/stacks-registry.schema.json +108 -0
  72. package/spec/soustack.schema.json +2379 -0
  73. package/spec/stacks/compute.schema.json +7 -0
  74. package/spec/stacks/compute@1.md +22 -0
  75. package/spec/stacks/dietary.schema.json +45 -0
  76. package/spec/stacks/dietary@1.md +24 -0
  77. package/spec/stacks/equipment.schema.json +98 -0
  78. package/spec/stacks/equipment@1.md +244 -0
  79. package/spec/stacks/illustrated.schema.json +54 -0
  80. package/spec/stacks/illustrated@1.md +24 -0
  81. package/spec/stacks/prep.schema.json +76 -0
  82. package/spec/stacks/prep@1.md +276 -0
  83. package/spec/stacks/quantified.schema.json +74 -0
  84. package/spec/stacks/quantified@1.md +24 -0
  85. package/spec/stacks/referenced.schema.json +96 -0
  86. package/spec/stacks/referenced@1.md +23 -0
  87. package/spec/stacks/registry.json +112 -0
  88. package/spec/stacks/scaling.schema.json +99 -0
  89. package/spec/stacks/scaling@1.md +238 -0
  90. package/spec/stacks/storage.schema.json +132 -0
  91. package/spec/stacks/storage@1.md +256 -0
  92. package/spec/stacks/structured.schema.json +48 -0
  93. package/spec/stacks/structured@1.md +24 -0
  94. package/spec/stacks/substitutions.schema.json +43 -0
  95. package/spec/stacks/substitutions@1.md +24 -0
  96. package/spec/stacks/techniques.schema.json +28 -0
  97. package/spec/stacks/techniques@1.md +23 -0
  98. package/spec/stacks/timed.schema.json +60 -0
  99. package/spec/stacks/timed@1.md +23 -0
  100. package/src/defs/common.schema.json +46 -0
  101. package/src/defs/duration.schema.json +33 -0
  102. package/src/defs/entities.schema.json +111 -0
  103. package/src/defs/ingredientQuantified.schema.json +9 -0
  104. package/src/defs/quantity.schema.json +16 -0
  105. package/src/defs/scalingRule.schema.json +127 -0
  106. package/src/defs/temperature.schema.json +63 -0
  107. package/src/profiles/base.schema.json +2 -2
  108. package/src/profiles/equipped.schema.json +10 -0
  109. package/src/profiles/illustrated.schema.json +4 -4
  110. package/src/profiles/lite.schema.json +10 -0
  111. package/src/profiles/prepped.schema.json +10 -0
  112. package/src/profiles/scalable.schema.json +6 -6
  113. package/src/profiles/timed.schema.json +10 -0
  114. package/src/schema.json +2271 -248
  115. package/src/schemas/stacks-registry.schema.json +108 -0
  116. package/src/soustack.schema.json +2271 -248
  117. package/src/stacks/compute.schema.json +7 -0
  118. package/src/stacks/compute@1.md +22 -0
  119. package/src/stacks/dietary.schema.json +45 -0
  120. package/src/stacks/dietary@1.md +24 -0
  121. package/src/stacks/equipment.schema.json +98 -0
  122. package/src/stacks/equipment@1.md +244 -0
  123. package/src/stacks/illustrated.schema.json +54 -0
  124. package/src/stacks/illustrated@1.md +24 -0
  125. package/src/stacks/prep.schema.json +76 -0
  126. package/src/stacks/prep@1.md +276 -0
  127. package/src/stacks/quantified.schema.json +74 -0
  128. package/src/stacks/quantified@1.md +24 -0
  129. package/src/stacks/referenced.schema.json +96 -0
  130. package/src/stacks/referenced@1.md +23 -0
  131. package/src/stacks/registry.json +112 -0
  132. package/src/stacks/scaling.schema.json +99 -0
  133. package/src/stacks/scaling@1.md +238 -0
  134. package/src/stacks/storage.schema.json +132 -0
  135. package/src/stacks/storage@1.md +256 -0
  136. package/src/stacks/structured.schema.json +48 -0
  137. package/src/stacks/structured@1.md +24 -0
  138. package/src/stacks/substitutions.schema.json +43 -0
  139. package/src/stacks/substitutions@1.md +24 -0
  140. package/src/stacks/techniques.schema.json +28 -0
  141. package/src/stacks/techniques@1.md +23 -0
  142. package/src/stacks/timed.schema.json +60 -0
  143. package/src/stacks/timed@1.md +23 -0
  144. package/dist/scrape.js.map +0 -1
  145. package/dist/scrape.mjs.map +0 -1
  146. package/src/profiles/cookable.schema.json +0 -18
  147. package/src/profiles/quantified.schema.json +0 -43
  148. package/src/profiles/schedulable.schema.json +0 -43
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Soustack Recipe Schema v0.3.0
2
+ * Soustack Recipe Schema v0.0.2
3
3
  * A portable, scalable, interoperable recipe format.
4
4
  */
5
5
  interface SoustackRecipe {
@@ -7,18 +7,10 @@ interface SoustackRecipe {
7
7
  '@type'?: 'Recipe';
8
8
  /** Optional $schema pointer for profile-aware validation */
9
9
  $schema?: string;
10
- /** Optional declared validation profile */
11
- profile?: string;
12
- /** Enabled module identifiers (e.g., "nutrition@1") */
13
- modules?: string[];
14
- /** Attribution module payload */
15
- attribution?: AttributionModule;
16
- /** Taxonomy module payload */
17
- taxonomy?: TaxonomyModule;
18
- /** Media module payload */
19
- media?: MediaModule;
20
- /** Times module payload */
21
- times?: TimesModule;
10
+ /** Optional declared validation profile (vNext only) */
11
+ profile?: "base" | "equipped" | "illustrated" | "lite" | "prepped" | "scalable" | "timed";
12
+ /** Stack declarations as a map: Record<stackName, versionNumber> */
13
+ stacks?: Record<string, number>;
22
14
  /** Unique identifier (slug or UUID) */
23
15
  id?: string;
24
16
  /** Optional display title */
@@ -35,6 +27,10 @@ interface SoustackRecipe {
35
27
  /** Additional tags for filtering */
36
28
  tags?: string[];
37
29
  /** URL(s) to recipe image(s) */
30
+ images?: string[];
31
+ /** URL(s) to recipe video(s) */
32
+ videos?: string[];
33
+ /** Legacy image field preserved for compatibility */
38
34
  image?: string | string[];
39
35
  /** ISO 8601 date string */
40
36
  dateAdded?: string;
@@ -66,19 +62,18 @@ interface Yield {
66
62
  description?: string;
67
63
  }
68
64
  /**
69
- * Time can be structured (machine-readable) or simple (strings).
70
- * Structured time takes precedence if both exist.
65
+ * Time uses DurationMinutes format (vNext).
66
+ * Required total field with minutes.
71
67
  */
72
- type Time = StructuredTime | SimpleTime;
73
- interface StructuredTime {
74
- prep?: number;
75
- active?: number;
76
- passive?: number;
77
- total?: number;
78
- }
79
- interface SimpleTime {
80
- prepTime?: string;
81
- cookTime?: string;
68
+ interface Time {
69
+ total: DurationMinutes;
70
+ metadata?: Record<string, unknown>;
71
+ [k: `x-${string}`]: unknown;
72
+ }
73
+ interface DurationMinutes {
74
+ minutes: number;
75
+ metadata?: Record<string, unknown>;
76
+ [k: `x-${string}`]: unknown;
82
77
  }
83
78
  interface Equipment {
84
79
  id?: string;
@@ -88,87 +83,119 @@ interface Equipment {
88
83
  capacity?: Quantity;
89
84
  scalingLimit?: number;
90
85
  alternatives?: string[];
86
+ count?: number;
87
+ countScaling?: EquipmentCountScaling;
88
+ upgrades?: EquipmentUpgradeRule[];
91
89
  }
92
90
  interface Quantity {
93
91
  amount: number;
94
92
  /** Unit string (e.g. "g", "cup") or null for count-based items (e.g. "2 eggs") */
95
93
  unit: string | null;
96
94
  }
95
+ type EquipmentCountScaling = 'fixed' | 'linear' | EquipmentThresholdScaling;
96
+ interface EquipmentThresholdScaling {
97
+ mode: 'threshold';
98
+ steps: EquipmentThresholdStep[];
99
+ }
100
+ interface EquipmentThresholdStep {
101
+ maxFactor: number;
102
+ count: number;
103
+ }
104
+ interface EquipmentUpgradeRule {
105
+ minFactor: number;
106
+ use: string;
107
+ }
97
108
  type IngredientItem = string | Ingredient | IngredientSubsection;
98
109
  interface IngredientSubsection {
99
- subsection: string;
100
- items: (string | Ingredient)[];
110
+ section: string;
111
+ ingredients: IngredientItem[];
101
112
  }
102
113
  interface Ingredient {
103
114
  id?: string;
104
- /** Full human-readable text (e.g. "2 cups flour") */
105
- item: string;
115
+ /** Ingredient name (required in vNext) */
116
+ name: string;
106
117
  quantity?: Quantity;
107
- name?: string;
108
- aisle?: string;
109
- /** Required prep state (e.g. "diced") */
110
- prep?: string;
111
- prepAction?: string;
112
- prepTime?: number;
118
+ /** Required prep state (e.g. "diced") or array of prep items */
119
+ prep?: string | string[];
113
120
  /** ID of equipment where this ingredient goes */
114
121
  destination?: string;
115
122
  scaling?: Scaling;
116
- critical?: boolean;
117
123
  optional?: boolean;
118
124
  notes?: string;
125
+ temperature?: Temperature;
126
+ metadata?: Record<string, unknown>;
127
+ [k: `x-${string}`]: unknown;
119
128
  }
120
129
  /**
121
- * Intelligent Scaling Logic
130
+ * Intelligent Scaling Logic (vNext format)
122
131
  * Defines how an ingredient behaves when the recipe yield changes.
123
132
  */
124
- type Scaling = ScalingLinear | ScalingDiscrete | ScalingProportional | ScalingFixed | ScalingBakersPercentage;
133
+ type Scaling = ScalingLinear | ScalingDiscrete | ScalingProportional | ScalingFixed | ScalingToTaste | ScalingBakersPercentage;
125
134
  interface ScalingBase {
126
135
  min?: number;
127
136
  max?: number;
128
137
  }
129
138
  interface ScalingLinear extends ScalingBase {
130
- type: "linear";
139
+ mode: "linear";
131
140
  }
132
141
  interface ScalingDiscrete extends ScalingBase {
133
- type: "discrete";
134
- roundTo?: number;
142
+ mode: "discrete";
143
+ step?: number;
144
+ rounding?: "nearest" | "ceil" | "floor";
135
145
  }
136
146
  interface ScalingProportional extends ScalingBase {
137
- type: "proportional";
147
+ mode: "proportional";
138
148
  factor?: number;
139
149
  }
140
150
  interface ScalingFixed extends ScalingBase {
141
- type: "fixed";
151
+ mode: "fixed";
152
+ }
153
+ interface ScalingToTaste {
154
+ mode: "toTaste";
142
155
  }
143
- interface ScalingBakersPercentage extends ScalingBase {
144
- type: 'bakers_percentage';
156
+ interface ScalingBakersPercentage {
157
+ mode: "bakersPercent";
158
+ /** The percentage relative to the reference (e.g. 2 for 2%) */
159
+ percent: number;
145
160
  /** The ID of the flour/base ingredient this is relative to */
146
- referenceId: string;
147
- /** The percentage relative to the reference (e.g. 0.02 for 2%) */
148
- factor?: number;
161
+ of: string;
149
162
  }
150
163
  type InstructionItem = string | Instruction | InstructionSubsection;
151
164
  interface InstructionSubsection {
152
- subsection: string;
153
- items: (string | Instruction)[];
165
+ section: string;
166
+ steps: InstructionItem[];
154
167
  }
155
168
  interface SoustackInstruction {
156
169
  id?: string;
157
170
  text: string;
158
- destination?: string;
159
171
  /** IDs of steps that must complete before this one starts */
160
172
  dependsOn?: string[];
161
173
  /** IDs of ingredients used in this step */
162
174
  inputs?: string[];
175
+ /** IDs of techniques used in this step */
176
+ techniqueIds?: string[];
177
+ /** IDs of equipment used in this step */
178
+ usesEquipment?: string[];
163
179
  timing?: StepTiming;
164
- /** Optional image URL for this instruction */
165
- image?: string;
180
+ temperature?: Temperature;
181
+ images?: string[];
182
+ videos?: string[];
183
+ metadata?: Record<string, unknown>;
184
+ [k: `x-${string}`]: unknown;
166
185
  }
167
186
  type Instruction = SoustackInstruction;
168
187
  interface StepTiming {
169
- duration: number | string;
170
- type: "active" | "passive";
171
- scaling?: "linear" | "fixed" | "sqrt";
188
+ activity: "active" | "passive";
189
+ duration?: DurationMinutes | DurationRange;
190
+ completionCue?: string;
191
+ metadata?: Record<string, unknown>;
192
+ [k: `x-${string}`]: unknown;
193
+ }
194
+ interface DurationRange {
195
+ minMinutes: number;
196
+ maxMinutes: number;
197
+ metadata?: Record<string, unknown>;
198
+ [k: `x-${string}`]: unknown;
172
199
  }
173
200
  interface Storage {
174
201
  roomTemp?: StorageMethod;
@@ -207,24 +234,11 @@ interface NutritionFacts {
207
234
  calories?: number;
208
235
  protein_g?: number;
209
236
  }
210
- interface AttributionModule {
211
- url?: string;
212
- author?: string;
213
- datePublished?: string;
214
- }
215
- interface TaxonomyModule {
216
- keywords?: string[];
217
- category?: string;
218
- cuisine?: string;
219
- }
220
- interface MediaModule {
221
- images?: string[];
222
- videos?: string[];
223
- }
224
- interface TimesModule {
225
- prepMinutes?: number;
226
- cookMinutes?: number;
227
- totalMinutes?: number;
237
+ interface Temperature {
238
+ value: number;
239
+ unit: "celsius" | "fahrenheit";
240
+ metadata?: Record<string, unknown>;
241
+ [k: `x-${string}`]: unknown;
228
242
  }
229
243
 
230
244
  interface HowToStep {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Soustack Recipe Schema v0.3.0
2
+ * Soustack Recipe Schema v0.0.2
3
3
  * A portable, scalable, interoperable recipe format.
4
4
  */
5
5
  interface SoustackRecipe {
@@ -7,18 +7,10 @@ interface SoustackRecipe {
7
7
  '@type'?: 'Recipe';
8
8
  /** Optional $schema pointer for profile-aware validation */
9
9
  $schema?: string;
10
- /** Optional declared validation profile */
11
- profile?: string;
12
- /** Enabled module identifiers (e.g., "nutrition@1") */
13
- modules?: string[];
14
- /** Attribution module payload */
15
- attribution?: AttributionModule;
16
- /** Taxonomy module payload */
17
- taxonomy?: TaxonomyModule;
18
- /** Media module payload */
19
- media?: MediaModule;
20
- /** Times module payload */
21
- times?: TimesModule;
10
+ /** Optional declared validation profile (vNext only) */
11
+ profile?: "base" | "equipped" | "illustrated" | "lite" | "prepped" | "scalable" | "timed";
12
+ /** Stack declarations as a map: Record<stackName, versionNumber> */
13
+ stacks?: Record<string, number>;
22
14
  /** Unique identifier (slug or UUID) */
23
15
  id?: string;
24
16
  /** Optional display title */
@@ -35,6 +27,10 @@ interface SoustackRecipe {
35
27
  /** Additional tags for filtering */
36
28
  tags?: string[];
37
29
  /** URL(s) to recipe image(s) */
30
+ images?: string[];
31
+ /** URL(s) to recipe video(s) */
32
+ videos?: string[];
33
+ /** Legacy image field preserved for compatibility */
38
34
  image?: string | string[];
39
35
  /** ISO 8601 date string */
40
36
  dateAdded?: string;
@@ -66,19 +62,18 @@ interface Yield {
66
62
  description?: string;
67
63
  }
68
64
  /**
69
- * Time can be structured (machine-readable) or simple (strings).
70
- * Structured time takes precedence if both exist.
65
+ * Time uses DurationMinutes format (vNext).
66
+ * Required total field with minutes.
71
67
  */
72
- type Time = StructuredTime | SimpleTime;
73
- interface StructuredTime {
74
- prep?: number;
75
- active?: number;
76
- passive?: number;
77
- total?: number;
78
- }
79
- interface SimpleTime {
80
- prepTime?: string;
81
- cookTime?: string;
68
+ interface Time {
69
+ total: DurationMinutes;
70
+ metadata?: Record<string, unknown>;
71
+ [k: `x-${string}`]: unknown;
72
+ }
73
+ interface DurationMinutes {
74
+ minutes: number;
75
+ metadata?: Record<string, unknown>;
76
+ [k: `x-${string}`]: unknown;
82
77
  }
83
78
  interface Equipment {
84
79
  id?: string;
@@ -88,87 +83,119 @@ interface Equipment {
88
83
  capacity?: Quantity;
89
84
  scalingLimit?: number;
90
85
  alternatives?: string[];
86
+ count?: number;
87
+ countScaling?: EquipmentCountScaling;
88
+ upgrades?: EquipmentUpgradeRule[];
91
89
  }
92
90
  interface Quantity {
93
91
  amount: number;
94
92
  /** Unit string (e.g. "g", "cup") or null for count-based items (e.g. "2 eggs") */
95
93
  unit: string | null;
96
94
  }
95
+ type EquipmentCountScaling = 'fixed' | 'linear' | EquipmentThresholdScaling;
96
+ interface EquipmentThresholdScaling {
97
+ mode: 'threshold';
98
+ steps: EquipmentThresholdStep[];
99
+ }
100
+ interface EquipmentThresholdStep {
101
+ maxFactor: number;
102
+ count: number;
103
+ }
104
+ interface EquipmentUpgradeRule {
105
+ minFactor: number;
106
+ use: string;
107
+ }
97
108
  type IngredientItem = string | Ingredient | IngredientSubsection;
98
109
  interface IngredientSubsection {
99
- subsection: string;
100
- items: (string | Ingredient)[];
110
+ section: string;
111
+ ingredients: IngredientItem[];
101
112
  }
102
113
  interface Ingredient {
103
114
  id?: string;
104
- /** Full human-readable text (e.g. "2 cups flour") */
105
- item: string;
115
+ /** Ingredient name (required in vNext) */
116
+ name: string;
106
117
  quantity?: Quantity;
107
- name?: string;
108
- aisle?: string;
109
- /** Required prep state (e.g. "diced") */
110
- prep?: string;
111
- prepAction?: string;
112
- prepTime?: number;
118
+ /** Required prep state (e.g. "diced") or array of prep items */
119
+ prep?: string | string[];
113
120
  /** ID of equipment where this ingredient goes */
114
121
  destination?: string;
115
122
  scaling?: Scaling;
116
- critical?: boolean;
117
123
  optional?: boolean;
118
124
  notes?: string;
125
+ temperature?: Temperature;
126
+ metadata?: Record<string, unknown>;
127
+ [k: `x-${string}`]: unknown;
119
128
  }
120
129
  /**
121
- * Intelligent Scaling Logic
130
+ * Intelligent Scaling Logic (vNext format)
122
131
  * Defines how an ingredient behaves when the recipe yield changes.
123
132
  */
124
- type Scaling = ScalingLinear | ScalingDiscrete | ScalingProportional | ScalingFixed | ScalingBakersPercentage;
133
+ type Scaling = ScalingLinear | ScalingDiscrete | ScalingProportional | ScalingFixed | ScalingToTaste | ScalingBakersPercentage;
125
134
  interface ScalingBase {
126
135
  min?: number;
127
136
  max?: number;
128
137
  }
129
138
  interface ScalingLinear extends ScalingBase {
130
- type: "linear";
139
+ mode: "linear";
131
140
  }
132
141
  interface ScalingDiscrete extends ScalingBase {
133
- type: "discrete";
134
- roundTo?: number;
142
+ mode: "discrete";
143
+ step?: number;
144
+ rounding?: "nearest" | "ceil" | "floor";
135
145
  }
136
146
  interface ScalingProportional extends ScalingBase {
137
- type: "proportional";
147
+ mode: "proportional";
138
148
  factor?: number;
139
149
  }
140
150
  interface ScalingFixed extends ScalingBase {
141
- type: "fixed";
151
+ mode: "fixed";
152
+ }
153
+ interface ScalingToTaste {
154
+ mode: "toTaste";
142
155
  }
143
- interface ScalingBakersPercentage extends ScalingBase {
144
- type: 'bakers_percentage';
156
+ interface ScalingBakersPercentage {
157
+ mode: "bakersPercent";
158
+ /** The percentage relative to the reference (e.g. 2 for 2%) */
159
+ percent: number;
145
160
  /** The ID of the flour/base ingredient this is relative to */
146
- referenceId: string;
147
- /** The percentage relative to the reference (e.g. 0.02 for 2%) */
148
- factor?: number;
161
+ of: string;
149
162
  }
150
163
  type InstructionItem = string | Instruction | InstructionSubsection;
151
164
  interface InstructionSubsection {
152
- subsection: string;
153
- items: (string | Instruction)[];
165
+ section: string;
166
+ steps: InstructionItem[];
154
167
  }
155
168
  interface SoustackInstruction {
156
169
  id?: string;
157
170
  text: string;
158
- destination?: string;
159
171
  /** IDs of steps that must complete before this one starts */
160
172
  dependsOn?: string[];
161
173
  /** IDs of ingredients used in this step */
162
174
  inputs?: string[];
175
+ /** IDs of techniques used in this step */
176
+ techniqueIds?: string[];
177
+ /** IDs of equipment used in this step */
178
+ usesEquipment?: string[];
163
179
  timing?: StepTiming;
164
- /** Optional image URL for this instruction */
165
- image?: string;
180
+ temperature?: Temperature;
181
+ images?: string[];
182
+ videos?: string[];
183
+ metadata?: Record<string, unknown>;
184
+ [k: `x-${string}`]: unknown;
166
185
  }
167
186
  type Instruction = SoustackInstruction;
168
187
  interface StepTiming {
169
- duration: number | string;
170
- type: "active" | "passive";
171
- scaling?: "linear" | "fixed" | "sqrt";
188
+ activity: "active" | "passive";
189
+ duration?: DurationMinutes | DurationRange;
190
+ completionCue?: string;
191
+ metadata?: Record<string, unknown>;
192
+ [k: `x-${string}`]: unknown;
193
+ }
194
+ interface DurationRange {
195
+ minMinutes: number;
196
+ maxMinutes: number;
197
+ metadata?: Record<string, unknown>;
198
+ [k: `x-${string}`]: unknown;
172
199
  }
173
200
  interface Storage {
174
201
  roomTemp?: StorageMethod;
@@ -207,24 +234,11 @@ interface NutritionFacts {
207
234
  calories?: number;
208
235
  protein_g?: number;
209
236
  }
210
- interface AttributionModule {
211
- url?: string;
212
- author?: string;
213
- datePublished?: string;
214
- }
215
- interface TaxonomyModule {
216
- keywords?: string[];
217
- category?: string;
218
- cuisine?: string;
219
- }
220
- interface MediaModule {
221
- images?: string[];
222
- videos?: string[];
223
- }
224
- interface TimesModule {
225
- prepMinutes?: number;
226
- cookMinutes?: number;
227
- totalMinutes?: number;
237
+ interface Temperature {
238
+ value: number;
239
+ unit: "celsius" | "fahrenheit";
240
+ metadata?: Record<string, unknown>;
241
+ [k: `x-${string}`]: unknown;
228
242
  }
229
243
 
230
244
  interface HowToStep {