soustack 0.2.3 → 0.4.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.
- package/README.md +128 -18
- package/dist/cli/index.js +1706 -665
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +172 -28
- package/dist/index.d.ts +172 -28
- package/dist/index.js +2028 -662
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2022 -662
- package/dist/index.mjs.map +1 -1
- package/dist/{scrape.d.mts → scrape/index.d.mts} +38 -10
- package/dist/{scrape.d.ts → scrape/index.d.ts} +38 -10
- package/dist/{scrape.js → scrape/index.js} +268 -62
- package/dist/scrape/index.js.map +1 -0
- package/dist/{scrape.mjs → scrape/index.mjs} +268 -62
- package/dist/scrape/index.mjs.map +1 -0
- package/package.json +15 -9
- package/src/profiles/base.schema.json +2 -2
- package/src/profiles/cookable.schema.json +4 -4
- package/src/profiles/illustrated.schema.json +4 -4
- package/src/profiles/quantified.schema.json +4 -4
- package/src/profiles/scalable.schema.json +6 -6
- package/src/profiles/schedulable.schema.json +4 -4
- package/src/schema.json +15 -3
- package/src/soustack.schema.json +15 -3
- package/dist/scrape.js.map +0 -1
- package/dist/scrape.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
|
|
4
4
|
// src/parsers/duration.ts
|
|
@@ -95,17 +95,17 @@ function scaleRecipe(recipe, options = {}) {
|
|
|
95
95
|
const orderedIngredients = [];
|
|
96
96
|
collectIngredients(scaled.ingredients || [], orderedIngredients);
|
|
97
97
|
orderedIngredients.filter((ing) => {
|
|
98
|
-
var
|
|
99
|
-
return (((
|
|
98
|
+
var _a;
|
|
99
|
+
return (((_a = ing.scaling) == null ? void 0 : _a.type) || "linear") !== "bakers_percentage";
|
|
100
100
|
}).forEach((ing) => {
|
|
101
101
|
const key = getIngredientKey(ing);
|
|
102
102
|
scaledAmounts.set(key, calculateIndependentIngredient(ing, multiplier));
|
|
103
103
|
});
|
|
104
104
|
orderedIngredients.filter((ing) => {
|
|
105
|
-
var
|
|
106
|
-
return ((
|
|
105
|
+
var _a;
|
|
106
|
+
return ((_a = ing.scaling) == null ? void 0 : _a.type) === "bakers_percentage";
|
|
107
107
|
}).forEach((ing) => {
|
|
108
|
-
var
|
|
108
|
+
var _a, _b;
|
|
109
109
|
const key = getIngredientKey(ing);
|
|
110
110
|
const scaling = ing.scaling;
|
|
111
111
|
if (!(scaling == null ? void 0 : scaling.referenceId)) {
|
|
@@ -115,9 +115,9 @@ function scaleRecipe(recipe, options = {}) {
|
|
|
115
115
|
if (referenceAmount === void 0) {
|
|
116
116
|
throw new Error(`Reference ingredient "${scaling.referenceId}" not found for baker's percentage item "${key}"`);
|
|
117
117
|
}
|
|
118
|
-
const baseAmount = ((
|
|
118
|
+
const baseAmount = ((_a = ing.quantity) == null ? void 0 : _a.amount) || 0;
|
|
119
119
|
const referenceBase = baseAmounts.get(scaling.referenceId);
|
|
120
|
-
const factor = (
|
|
120
|
+
const factor = (_b = scaling.factor) != null ? _b : referenceBase ? baseAmount / referenceBase : void 0;
|
|
121
121
|
if (factor === void 0) {
|
|
122
122
|
throw new Error(`Unable to determine factor for baker's percentage ingredient "${key}"`);
|
|
123
123
|
}
|
|
@@ -137,19 +137,19 @@ function scaleRecipe(recipe, options = {}) {
|
|
|
137
137
|
return scaled;
|
|
138
138
|
}
|
|
139
139
|
function resolveMultiplier(recipe, options) {
|
|
140
|
-
var
|
|
140
|
+
var _a, _b;
|
|
141
141
|
if (options.multiplier && options.multiplier > 0) {
|
|
142
142
|
return options.multiplier;
|
|
143
143
|
}
|
|
144
|
-
if ((
|
|
145
|
-
const base = ((
|
|
144
|
+
if ((_a = options.targetYield) == null ? void 0 : _a.amount) {
|
|
145
|
+
const base = ((_b = recipe.yield) == null ? void 0 : _b.amount) || 1;
|
|
146
146
|
return options.targetYield.amount / base;
|
|
147
147
|
}
|
|
148
148
|
return 1;
|
|
149
149
|
}
|
|
150
150
|
function applyYieldScaling(recipe, options, multiplier) {
|
|
151
|
-
var
|
|
152
|
-
const baseAmount = (
|
|
151
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
152
|
+
const baseAmount = (_b = (_a = recipe.yield) == null ? void 0 : _a.amount) != null ? _b : 1;
|
|
153
153
|
const targetAmount = (_d = (_c = options.targetYield) == null ? void 0 : _c.amount) != null ? _d : baseAmount * multiplier;
|
|
154
154
|
const unit = (_g = (_e = options.targetYield) == null ? void 0 : _e.unit) != null ? _g : (_f = recipe.yield) == null ? void 0 : _f.unit;
|
|
155
155
|
if (!recipe.yield && !options.targetYield) return;
|
|
@@ -162,9 +162,9 @@ function getIngredientKey(ing) {
|
|
|
162
162
|
return ing.id || ing.item;
|
|
163
163
|
}
|
|
164
164
|
function calculateIndependentIngredient(ing, multiplier) {
|
|
165
|
-
var
|
|
166
|
-
const baseAmount = ((
|
|
167
|
-
const type = ((
|
|
165
|
+
var _a, _b, _c, _d, _e, _f;
|
|
166
|
+
const baseAmount = ((_a = ing.quantity) == null ? void 0 : _a.amount) || 0;
|
|
167
|
+
const type = ((_b = ing.scaling) == null ? void 0 : _b.type) || "linear";
|
|
168
168
|
switch (type) {
|
|
169
169
|
case "fixed":
|
|
170
170
|
return baseAmount;
|
|
@@ -194,12 +194,12 @@ function collectIngredients(items, bucket) {
|
|
|
194
194
|
}
|
|
195
195
|
function collectBaseIngredientAmounts(items, map = /* @__PURE__ */ new Map()) {
|
|
196
196
|
items.forEach((item) => {
|
|
197
|
-
var
|
|
197
|
+
var _a, _b;
|
|
198
198
|
if (typeof item === "string") return;
|
|
199
199
|
if ("subsection" in item) {
|
|
200
200
|
collectBaseIngredientAmounts(item.items, map);
|
|
201
201
|
} else {
|
|
202
|
-
map.set(getIngredientKey(item), (
|
|
202
|
+
map.set(getIngredientKey(item), (_b = (_a = item.quantity) == null ? void 0 : _a.amount) != null ? _b : 0);
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
return map;
|
|
@@ -240,11 +240,321 @@ function toDurationMinutes(duration) {
|
|
|
240
240
|
return 0;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
// src/
|
|
244
|
-
|
|
243
|
+
// src/normalize.ts
|
|
244
|
+
function normalizeRecipe(input) {
|
|
245
|
+
if (!input || typeof input !== "object") {
|
|
246
|
+
throw new Error("Recipe input must be an object");
|
|
247
|
+
}
|
|
248
|
+
const recipe = JSON.parse(JSON.stringify(input));
|
|
249
|
+
const warnings = [];
|
|
250
|
+
const legacyField = ["mod", "ules"].join("");
|
|
251
|
+
if (legacyField in recipe) {
|
|
252
|
+
throw new Error("The legacy field is no longer supported. Use `stacks` instead.");
|
|
253
|
+
}
|
|
254
|
+
normalizeStacks(recipe, warnings);
|
|
255
|
+
if (!recipe.stacks) {
|
|
256
|
+
recipe.stacks = {};
|
|
257
|
+
}
|
|
258
|
+
if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
|
|
259
|
+
recipe.recipeVersion = recipe.version;
|
|
260
|
+
warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
|
|
261
|
+
}
|
|
262
|
+
normalizeTime(recipe);
|
|
263
|
+
return {
|
|
264
|
+
recipe,
|
|
265
|
+
warnings
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function normalizeStacks(recipe, warnings) {
|
|
269
|
+
let stacks = {};
|
|
270
|
+
if (recipe.stacks && typeof recipe.stacks === "object" && !Array.isArray(recipe.stacks)) {
|
|
271
|
+
for (const [key, value] of Object.entries(recipe.stacks)) {
|
|
272
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
|
|
273
|
+
stacks[key] = value;
|
|
274
|
+
} else {
|
|
275
|
+
warnings.push(`Invalid stack version for '${key}': expected positive integer, got ${value}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(recipe.stacks)) {
|
|
280
|
+
const stackIdentifiers = recipe.stacks.filter((s) => typeof s === "string");
|
|
281
|
+
for (const identifier of stackIdentifiers) {
|
|
282
|
+
const parsed = parseStackIdentifier(identifier);
|
|
283
|
+
if (parsed) {
|
|
284
|
+
const { name, version } = parsed;
|
|
285
|
+
if (!stacks[name] || stacks[name] < version) {
|
|
286
|
+
stacks[name] = version;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
warnings.push(`Invalid stack identifier '${identifier}': expected format 'name@version' (e.g., 'scaling@1')`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
recipe.stacks = stacks;
|
|
294
|
+
}
|
|
295
|
+
function parseStackIdentifier(identifier) {
|
|
296
|
+
if (typeof identifier !== "string" || !identifier.trim()) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const match = identifier.trim().match(/^([a-z0-9_-]+)@(\d+)$/i);
|
|
300
|
+
if (!match) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const [, name, versionStr] = match;
|
|
304
|
+
const version = parseInt(versionStr, 10);
|
|
305
|
+
if (isNaN(version) || version < 1) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return { name, version };
|
|
309
|
+
}
|
|
310
|
+
function normalizeTime(recipe) {
|
|
311
|
+
const time = recipe == null ? void 0 : recipe.time;
|
|
312
|
+
if (!time || typeof time !== "object" || Array.isArray(time)) return;
|
|
313
|
+
const structuredKeys = [
|
|
314
|
+
"prep",
|
|
315
|
+
"active",
|
|
316
|
+
"passive",
|
|
317
|
+
"total"
|
|
318
|
+
];
|
|
319
|
+
structuredKeys.forEach((key) => {
|
|
320
|
+
const value = time[key];
|
|
321
|
+
if (typeof value === "number") return;
|
|
322
|
+
const parsed = parseDuration(value);
|
|
323
|
+
if (parsed !== null) {
|
|
324
|
+
time[key] = parsed;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/conformance/index.ts
|
|
330
|
+
function validateConformance(recipe) {
|
|
331
|
+
const issues = [];
|
|
332
|
+
issues.push(...checkDAGValidity(recipe));
|
|
333
|
+
if (hasSchedulableProfile(recipe)) {
|
|
334
|
+
issues.push(...checkTimingSchedulability(recipe));
|
|
335
|
+
}
|
|
336
|
+
issues.push(...checkScalingSanity(recipe));
|
|
337
|
+
const ok = issues.filter((i) => i.severity === "error").length === 0;
|
|
338
|
+
return { ok, issues };
|
|
339
|
+
}
|
|
340
|
+
function hasSchedulableProfile(recipe) {
|
|
341
|
+
const schema = recipe.$schema;
|
|
342
|
+
if (typeof schema === "string") {
|
|
343
|
+
return schema.includes("schedulable") || schema === "http://soustack.org/schema/v0.3.0/profiles/schedulable";
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
function checkDAGValidity(recipe) {
|
|
348
|
+
const issues = [];
|
|
349
|
+
const instructions = recipe.instructions;
|
|
350
|
+
if (!Array.isArray(instructions)) {
|
|
351
|
+
return issues;
|
|
352
|
+
}
|
|
353
|
+
const instructionIds = /* @__PURE__ */ new Set();
|
|
354
|
+
const dependencyRefs = [];
|
|
355
|
+
const collect = (items, basePath) => {
|
|
356
|
+
items.forEach((item, index) => {
|
|
357
|
+
const currentPath = `${basePath}/${index}`;
|
|
358
|
+
if (isInstructionSubsection(item)) {
|
|
359
|
+
if (Array.isArray(item.items)) {
|
|
360
|
+
collect(item.items, `${currentPath}/items`);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (isInstruction(item)) {
|
|
365
|
+
const id = typeof item.id === "string" ? item.id : void 0;
|
|
366
|
+
if (id) {
|
|
367
|
+
instructionIds.add(id);
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(item.dependsOn)) {
|
|
370
|
+
item.dependsOn.forEach((depId, depIndex) => {
|
|
371
|
+
if (typeof depId === "string") {
|
|
372
|
+
dependencyRefs.push({
|
|
373
|
+
fromId: id,
|
|
374
|
+
toId: depId,
|
|
375
|
+
path: `${currentPath}/dependsOn/${depIndex}`
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
collect(instructions, "/instructions");
|
|
384
|
+
dependencyRefs.forEach((ref) => {
|
|
385
|
+
if (!instructionIds.has(ref.toId)) {
|
|
386
|
+
issues.push({
|
|
387
|
+
code: "DAG_MISSING_NODE",
|
|
388
|
+
path: ref.path,
|
|
389
|
+
message: `Instruction dependency references missing step id '${ref.toId}'.`,
|
|
390
|
+
severity: "error"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
395
|
+
dependencyRefs.forEach((ref) => {
|
|
396
|
+
var _a;
|
|
397
|
+
if (ref.fromId && instructionIds.has(ref.fromId) && instructionIds.has(ref.toId)) {
|
|
398
|
+
const list = (_a = adjacency.get(ref.fromId)) != null ? _a : [];
|
|
399
|
+
list.push({ toId: ref.toId, path: ref.path });
|
|
400
|
+
adjacency.set(ref.fromId, list);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
404
|
+
const visited = /* @__PURE__ */ new Set();
|
|
405
|
+
const detectCycles = (nodeId) => {
|
|
406
|
+
var _a;
|
|
407
|
+
if (visiting.has(nodeId)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (visited.has(nodeId)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
visiting.add(nodeId);
|
|
414
|
+
const neighbors = (_a = adjacency.get(nodeId)) != null ? _a : [];
|
|
415
|
+
neighbors.forEach((edge) => {
|
|
416
|
+
if (visiting.has(edge.toId)) {
|
|
417
|
+
issues.push({
|
|
418
|
+
code: "DAG_CYCLE",
|
|
419
|
+
path: edge.path,
|
|
420
|
+
message: `Circular dependency detected involving step id '${edge.toId}'.`,
|
|
421
|
+
severity: "error"
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
detectCycles(edge.toId);
|
|
426
|
+
});
|
|
427
|
+
visiting.delete(nodeId);
|
|
428
|
+
visited.add(nodeId);
|
|
429
|
+
};
|
|
430
|
+
instructionIds.forEach((id) => detectCycles(id));
|
|
431
|
+
return issues;
|
|
432
|
+
}
|
|
433
|
+
function checkTimingSchedulability(recipe) {
|
|
434
|
+
const issues = [];
|
|
435
|
+
const instructions = recipe.instructions;
|
|
436
|
+
if (!Array.isArray(instructions)) {
|
|
437
|
+
return issues;
|
|
438
|
+
}
|
|
439
|
+
const checkInstruction = (item, path) => {
|
|
440
|
+
if (isInstructionSubsection(item)) {
|
|
441
|
+
if (Array.isArray(item.items)) {
|
|
442
|
+
item.items.forEach((subItem, index) => {
|
|
443
|
+
checkInstruction(subItem, `${path}/items/${index}`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (isInstruction(item)) {
|
|
449
|
+
if (!item.id) {
|
|
450
|
+
issues.push({
|
|
451
|
+
code: "SCHEDULABLE_MISSING_ID",
|
|
452
|
+
path,
|
|
453
|
+
message: "Schedulable profile requires all instructions to have an id.",
|
|
454
|
+
severity: "error"
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (!item.timing) {
|
|
458
|
+
issues.push({
|
|
459
|
+
code: "SCHEDULABLE_MISSING_TIMING",
|
|
460
|
+
path,
|
|
461
|
+
message: "Schedulable profile requires all instructions to have timing information.",
|
|
462
|
+
severity: "error"
|
|
463
|
+
});
|
|
464
|
+
} else if (!item.timing.duration) {
|
|
465
|
+
issues.push({
|
|
466
|
+
code: "SCHEDULABLE_MISSING_DURATION",
|
|
467
|
+
path: `${path}/timing`,
|
|
468
|
+
message: "Schedulable profile requires timing.duration for all instructions.",
|
|
469
|
+
severity: "error"
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
instructions.forEach((item, index) => {
|
|
475
|
+
checkInstruction(item, `/instructions/${index}`);
|
|
476
|
+
});
|
|
477
|
+
return issues;
|
|
478
|
+
}
|
|
479
|
+
function checkScalingSanity(recipe) {
|
|
480
|
+
const issues = [];
|
|
481
|
+
const ingredients = recipe.ingredients;
|
|
482
|
+
if (!Array.isArray(ingredients)) {
|
|
483
|
+
return issues;
|
|
484
|
+
}
|
|
485
|
+
const ingredientIds = /* @__PURE__ */ new Set();
|
|
486
|
+
const collectIngredientIds = (items, basePath) => {
|
|
487
|
+
items.forEach((item, index) => {
|
|
488
|
+
if (isIngredientSubsection(item)) {
|
|
489
|
+
if (Array.isArray(item.items)) {
|
|
490
|
+
collectIngredientIds(item.items);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (isIngredient(item)) {
|
|
495
|
+
if (typeof item.id === "string") {
|
|
496
|
+
ingredientIds.add(item.id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
collectIngredientIds(ingredients);
|
|
502
|
+
const checkIngredient = (item, path) => {
|
|
503
|
+
if (isIngredientSubsection(item)) {
|
|
504
|
+
if (Array.isArray(item.items)) {
|
|
505
|
+
item.items.forEach((subItem, index) => {
|
|
506
|
+
checkIngredient(subItem, `${path}/items/${index}`);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (isIngredient(item)) {
|
|
512
|
+
const scaling = item.scaling;
|
|
513
|
+
if (scaling && typeof scaling === "object" && "type" in scaling && scaling.type === "bakers_percentage") {
|
|
514
|
+
const bakersScaling = scaling;
|
|
515
|
+
if (bakersScaling.referenceId) {
|
|
516
|
+
if (!ingredientIds.has(bakersScaling.referenceId)) {
|
|
517
|
+
issues.push({
|
|
518
|
+
code: "SCALING_INVALID_REFERENCE",
|
|
519
|
+
path: `${path}/scaling/referenceId`,
|
|
520
|
+
message: `Baker's percentage references missing ingredient id '${bakersScaling.referenceId}'.`,
|
|
521
|
+
severity: "error"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
issues.push({
|
|
526
|
+
code: "SCALING_MISSING_REFERENCE",
|
|
527
|
+
path: `${path}/scaling`,
|
|
528
|
+
message: "Baker's percentage scaling requires a referenceId.",
|
|
529
|
+
severity: "error"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
ingredients.forEach((item, index) => {
|
|
536
|
+
checkIngredient(item, `/ingredients/${index}`);
|
|
537
|
+
});
|
|
538
|
+
return issues;
|
|
539
|
+
}
|
|
540
|
+
function isInstruction(item) {
|
|
541
|
+
return item && typeof item === "object" && !Array.isArray(item) && "text" in item;
|
|
542
|
+
}
|
|
543
|
+
function isInstructionSubsection(item) {
|
|
544
|
+
return item && typeof item === "object" && !Array.isArray(item) && "items" in item && "subsection" in item;
|
|
545
|
+
}
|
|
546
|
+
function isIngredient(item) {
|
|
547
|
+
return item && typeof item === "object" && !Array.isArray(item) && "item" in item;
|
|
548
|
+
}
|
|
549
|
+
function isIngredientSubsection(item) {
|
|
550
|
+
return item && typeof item === "object" && !Array.isArray(item) && "items" in item && "subsection" in item;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/soustack.schema.json
|
|
554
|
+
var soustack_schema_default = {
|
|
245
555
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
246
|
-
$id: "http://soustack.org/schema/v0.
|
|
247
|
-
title: "Soustack Recipe Schema v0.
|
|
556
|
+
$id: "http://soustack.org/schema/v0.3.0",
|
|
557
|
+
title: "Soustack Recipe Schema v0.3.0",
|
|
248
558
|
description: "A portable, scalable, interoperable recipe format.",
|
|
249
559
|
type: "object",
|
|
250
560
|
required: ["name", "ingredients", "instructions"],
|
|
@@ -392,7 +702,10 @@ var schema_default = {
|
|
|
392
702
|
required: ["amount"],
|
|
393
703
|
properties: {
|
|
394
704
|
amount: { type: "number" },
|
|
395
|
-
unit: {
|
|
705
|
+
unit: {
|
|
706
|
+
type: ["string", "null"],
|
|
707
|
+
description: "Display-friendly unit text; implementations may normalize or canonicalize units separately."
|
|
708
|
+
}
|
|
396
709
|
}
|
|
397
710
|
},
|
|
398
711
|
scaling: {
|
|
@@ -427,7 +740,16 @@ var schema_default = {
|
|
|
427
740
|
aisle: { type: "string" },
|
|
428
741
|
prep: { type: "string" },
|
|
429
742
|
prepAction: { type: "string" },
|
|
743
|
+
prepActions: {
|
|
744
|
+
type: "array",
|
|
745
|
+
items: { type: "string" },
|
|
746
|
+
description: "Structured prep verbs (e.g., peel, dice) for mise en place workflows."
|
|
747
|
+
},
|
|
430
748
|
prepTime: { type: "number" },
|
|
749
|
+
form: {
|
|
750
|
+
type: "string",
|
|
751
|
+
description: "State of the ingredient as used (packed, sifted, melted, room_temperature, etc.)."
|
|
752
|
+
},
|
|
431
753
|
destination: { type: "string" },
|
|
432
754
|
scaling: { $ref: "#/definitions/scaling" },
|
|
433
755
|
critical: { type: "boolean" },
|
|
@@ -586,548 +908,823 @@ var schema_default = {
|
|
|
586
908
|
}
|
|
587
909
|
};
|
|
588
910
|
|
|
589
|
-
// src/
|
|
590
|
-
var
|
|
911
|
+
// src/schemas/recipe/base.schema.json
|
|
912
|
+
var base_schema_default = {
|
|
591
913
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
592
|
-
$id: "http://soustack.org/schema/
|
|
593
|
-
title: "Soustack Recipe Schema
|
|
594
|
-
description: "
|
|
914
|
+
$id: "http://soustack.org/schema/recipe/base.schema.json",
|
|
915
|
+
title: "Soustack Recipe Base Schema",
|
|
916
|
+
description: "Base document shape for Soustack recipe documents. Profiles and stacks build on this baseline.",
|
|
595
917
|
type: "object",
|
|
596
|
-
|
|
597
|
-
additionalProperties: false,
|
|
598
|
-
patternProperties: {
|
|
599
|
-
"^x-": {}
|
|
600
|
-
},
|
|
918
|
+
additionalProperties: true,
|
|
601
919
|
properties: {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
description: "Optional schema hint for tooling compatibility"
|
|
606
|
-
},
|
|
607
|
-
id: {
|
|
608
|
-
type: "string",
|
|
609
|
-
description: "Unique identifier (slug or UUID)"
|
|
610
|
-
},
|
|
611
|
-
name: {
|
|
612
|
-
type: "string",
|
|
613
|
-
description: "The title of the recipe"
|
|
920
|
+
"@type": {
|
|
921
|
+
const: "Recipe",
|
|
922
|
+
description: "Document marker for Soustack recipes"
|
|
614
923
|
},
|
|
615
|
-
|
|
924
|
+
profile: {
|
|
616
925
|
type: "string",
|
|
617
|
-
description: "
|
|
926
|
+
description: "Profile identifier applied to this recipe"
|
|
618
927
|
},
|
|
619
|
-
|
|
620
|
-
type: "
|
|
621
|
-
|
|
622
|
-
|
|
928
|
+
stacks: {
|
|
929
|
+
type: "object",
|
|
930
|
+
description: "Stack declarations as a map: Record<stackName, versionNumber>",
|
|
931
|
+
additionalProperties: {
|
|
932
|
+
type: "integer",
|
|
933
|
+
minimum: 1
|
|
934
|
+
}
|
|
623
935
|
},
|
|
624
|
-
|
|
936
|
+
name: {
|
|
625
937
|
type: "string",
|
|
626
|
-
|
|
627
|
-
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
628
|
-
},
|
|
629
|
-
description: {
|
|
630
|
-
type: "string"
|
|
938
|
+
description: "Human-readable recipe name"
|
|
631
939
|
},
|
|
632
|
-
|
|
633
|
-
type: "
|
|
634
|
-
|
|
940
|
+
ingredients: {
|
|
941
|
+
type: "array",
|
|
942
|
+
description: "Ingredients payload; content is validated by profiles/stacks"
|
|
635
943
|
},
|
|
636
|
-
|
|
944
|
+
instructions: {
|
|
637
945
|
type: "array",
|
|
638
|
-
|
|
946
|
+
description: "Instruction payload; content is validated by profiles/stacks"
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
required: ["@type"]
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/schemas/recipe/profiles/minimal.schema.json
|
|
953
|
+
var minimal_schema_default = {
|
|
954
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
955
|
+
$id: "http://soustack.org/schema/recipe/profiles/minimal.schema.json",
|
|
956
|
+
title: "Soustack Recipe Minimal Profile",
|
|
957
|
+
description: "Minimal profile that ensures the basic Recipe structure is present while allowing stacks to extend it.",
|
|
958
|
+
allOf: [
|
|
959
|
+
{
|
|
960
|
+
$ref: "http://soustack.org/schema/recipe/base.schema.json"
|
|
639
961
|
},
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
{
|
|
962
|
+
{
|
|
963
|
+
type: "object",
|
|
964
|
+
properties: {
|
|
965
|
+
profile: {
|
|
966
|
+
const: "minimal"
|
|
967
|
+
},
|
|
968
|
+
stacks: {
|
|
969
|
+
type: "object",
|
|
970
|
+
additionalProperties: {
|
|
971
|
+
type: "integer",
|
|
972
|
+
minimum: 1
|
|
973
|
+
},
|
|
974
|
+
properties: {
|
|
975
|
+
attribution: { type: "integer", minimum: 1 },
|
|
976
|
+
taxonomy: { type: "integer", minimum: 1 },
|
|
977
|
+
media: { type: "integer", minimum: 1 },
|
|
978
|
+
nutrition: { type: "integer", minimum: 1 },
|
|
979
|
+
times: { type: "integer", minimum: 1 }
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
name: {
|
|
644
983
|
type: "string",
|
|
645
|
-
|
|
984
|
+
minLength: 1
|
|
646
985
|
},
|
|
647
|
-
{
|
|
986
|
+
ingredients: {
|
|
648
987
|
type: "array",
|
|
649
|
-
minItems: 1
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
988
|
+
minItems: 1
|
|
989
|
+
},
|
|
990
|
+
instructions: {
|
|
991
|
+
type: "array",
|
|
992
|
+
minItems: 1
|
|
654
993
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
994
|
+
},
|
|
995
|
+
required: [
|
|
996
|
+
"profile",
|
|
997
|
+
"name",
|
|
998
|
+
"ingredients",
|
|
999
|
+
"instructions"
|
|
1000
|
+
],
|
|
1001
|
+
additionalProperties: true
|
|
1002
|
+
}
|
|
1003
|
+
]
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// src/schemas/recipe/profiles/core.schema.json
|
|
1007
|
+
var core_schema_default = {
|
|
1008
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1009
|
+
$id: "http://soustack.org/schema/recipe/profiles/core.schema.json",
|
|
1010
|
+
title: "Soustack Recipe Core Profile",
|
|
1011
|
+
description: "Core profile that builds on the minimal profile and is intended to be combined with recipe stacks.",
|
|
1012
|
+
allOf: [
|
|
1013
|
+
{ $ref: "http://soustack.org/schema/recipe/base.schema.json" },
|
|
1014
|
+
{
|
|
667
1015
|
type: "object",
|
|
668
1016
|
properties: {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
$ref: "#/definitions/yield"
|
|
677
|
-
},
|
|
678
|
-
time: {
|
|
679
|
-
$ref: "#/definitions/time"
|
|
680
|
-
},
|
|
681
|
-
equipment: {
|
|
682
|
-
type: "array",
|
|
683
|
-
items: { $ref: "#/definitions/equipment" }
|
|
684
|
-
},
|
|
685
|
-
ingredients: {
|
|
686
|
-
type: "array",
|
|
687
|
-
items: {
|
|
688
|
-
anyOf: [
|
|
689
|
-
{ type: "string" },
|
|
690
|
-
{ $ref: "#/definitions/ingredient" },
|
|
691
|
-
{ $ref: "#/definitions/ingredientSubsection" }
|
|
692
|
-
]
|
|
693
|
-
}
|
|
694
|
-
},
|
|
695
|
-
instructions: {
|
|
696
|
-
type: "array",
|
|
697
|
-
items: {
|
|
698
|
-
anyOf: [
|
|
699
|
-
{ type: "string" },
|
|
700
|
-
{ $ref: "#/definitions/instruction" },
|
|
701
|
-
{ $ref: "#/definitions/instructionSubsection" }
|
|
702
|
-
]
|
|
703
|
-
}
|
|
704
|
-
},
|
|
705
|
-
storage: {
|
|
706
|
-
$ref: "#/definitions/storage"
|
|
707
|
-
},
|
|
708
|
-
substitutions: {
|
|
709
|
-
type: "array",
|
|
710
|
-
items: { $ref: "#/definitions/substitution" }
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
definitions: {
|
|
714
|
-
yield: {
|
|
715
|
-
type: "object",
|
|
716
|
-
required: ["amount", "unit"],
|
|
717
|
-
properties: {
|
|
718
|
-
amount: { type: "number" },
|
|
719
|
-
unit: { type: "string" },
|
|
720
|
-
servings: { type: "number" },
|
|
721
|
-
description: { type: "string" }
|
|
722
|
-
}
|
|
723
|
-
},
|
|
724
|
-
time: {
|
|
725
|
-
type: "object",
|
|
726
|
-
properties: {
|
|
727
|
-
prep: { type: "number" },
|
|
728
|
-
active: { type: "number" },
|
|
729
|
-
passive: { type: "number" },
|
|
730
|
-
total: { type: "number" },
|
|
731
|
-
prepTime: { type: "string", format: "duration" },
|
|
732
|
-
cookTime: { type: "string", format: "duration" }
|
|
733
|
-
},
|
|
734
|
-
minProperties: 1
|
|
735
|
-
},
|
|
736
|
-
quantity: {
|
|
737
|
-
type: "object",
|
|
738
|
-
required: ["amount"],
|
|
739
|
-
properties: {
|
|
740
|
-
amount: { type: "number" },
|
|
741
|
-
unit: { type: ["string", "null"] }
|
|
742
|
-
}
|
|
743
|
-
},
|
|
744
|
-
scaling: {
|
|
745
|
-
type: "object",
|
|
746
|
-
required: ["type"],
|
|
747
|
-
properties: {
|
|
748
|
-
type: {
|
|
749
|
-
type: "string",
|
|
750
|
-
enum: ["linear", "discrete", "proportional", "fixed", "bakers_percentage"]
|
|
1017
|
+
profile: { const: "core" },
|
|
1018
|
+
stacks: {
|
|
1019
|
+
type: "object",
|
|
1020
|
+
additionalProperties: {
|
|
1021
|
+
type: "integer",
|
|
1022
|
+
minimum: 1
|
|
1023
|
+
}
|
|
751
1024
|
},
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
min: { type: "number" },
|
|
756
|
-
max: { type: "number" }
|
|
757
|
-
},
|
|
758
|
-
if: {
|
|
759
|
-
properties: { type: { const: "bakers_percentage" } }
|
|
1025
|
+
name: { type: "string", minLength: 1 },
|
|
1026
|
+
ingredients: { type: "array", minItems: 1 },
|
|
1027
|
+
instructions: { type: "array", minItems: 1 }
|
|
760
1028
|
},
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1029
|
+
required: ["profile", "name", "ingredients", "instructions"],
|
|
1030
|
+
additionalProperties: true
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// spec/profiles/base.schema.json
|
|
1036
|
+
var base_schema_default2 = {
|
|
1037
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1038
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/base",
|
|
1039
|
+
title: "Soustack Base Profile Schema",
|
|
1040
|
+
description: "Wrapper schema that exposes the unmodified Soustack base schema.",
|
|
1041
|
+
allOf: [
|
|
1042
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" }
|
|
1043
|
+
]
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// spec/profiles/cookable.schema.json
|
|
1047
|
+
var cookable_schema_default = {
|
|
1048
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1049
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/cookable",
|
|
1050
|
+
title: "Soustack Cookable Profile Schema",
|
|
1051
|
+
description: "Extends the base schema to require structured yield + time metadata and non-empty ingredient/instruction lists.",
|
|
1052
|
+
allOf: [
|
|
1053
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1054
|
+
{
|
|
1055
|
+
required: ["yield", "time", "ingredients", "instructions"],
|
|
787
1056
|
properties: {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
}
|
|
1057
|
+
yield: { $ref: "http://soustack.org/schema/v0.3.0#/definitions/yield" },
|
|
1058
|
+
time: { $ref: "http://soustack.org/schema/v0.3.0#/definitions/time" },
|
|
1059
|
+
ingredients: { type: "array", minItems: 1 },
|
|
1060
|
+
instructions: { type: "array", minItems: 1 }
|
|
793
1061
|
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1062
|
+
}
|
|
1063
|
+
]
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// spec/profiles/illustrated.schema.json
|
|
1067
|
+
var illustrated_schema_default = {
|
|
1068
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1069
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/illustrated",
|
|
1070
|
+
title: "Soustack Illustrated Profile Schema",
|
|
1071
|
+
description: "Extends the base schema to guarantee at least one illustrative image.",
|
|
1072
|
+
allOf: [
|
|
1073
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1074
|
+
{
|
|
1075
|
+
anyOf: [
|
|
1076
|
+
{ required: ["image"] },
|
|
1077
|
+
{
|
|
1078
|
+
properties: {
|
|
1079
|
+
instructions: {
|
|
1080
|
+
type: "array",
|
|
1081
|
+
contains: {
|
|
1082
|
+
anyOf: [
|
|
1083
|
+
{ $ref: "#/definitions/imageInstruction" },
|
|
1084
|
+
{ $ref: "#/definitions/instructionSubsectionWithImage" }
|
|
1085
|
+
]
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
808
1089
|
}
|
|
809
|
-
|
|
1090
|
+
]
|
|
1091
|
+
}
|
|
1092
|
+
],
|
|
1093
|
+
definitions: {
|
|
1094
|
+
imageInstruction: {
|
|
1095
|
+
allOf: [
|
|
1096
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instruction" },
|
|
1097
|
+
{ required: ["image"] }
|
|
1098
|
+
]
|
|
810
1099
|
},
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
id: { type: "string" },
|
|
816
|
-
text: { type: "string" },
|
|
817
|
-
image: {
|
|
818
|
-
type: "string",
|
|
819
|
-
format: "uri",
|
|
820
|
-
description: "Optional image that illustrates this instruction"
|
|
821
|
-
},
|
|
822
|
-
destination: { type: "string" },
|
|
823
|
-
dependsOn: {
|
|
824
|
-
type: "array",
|
|
825
|
-
items: { type: "string" }
|
|
826
|
-
},
|
|
827
|
-
inputs: {
|
|
828
|
-
type: "array",
|
|
829
|
-
items: { type: "string" }
|
|
830
|
-
},
|
|
831
|
-
timing: {
|
|
832
|
-
type: "object",
|
|
833
|
-
required: ["duration", "type"],
|
|
1100
|
+
instructionSubsectionWithImage: {
|
|
1101
|
+
allOf: [
|
|
1102
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instructionSubsection" },
|
|
1103
|
+
{
|
|
834
1104
|
properties: {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
],
|
|
840
|
-
description: "Minutes as a number or ISO8601 duration string"
|
|
841
|
-
},
|
|
842
|
-
type: { type: "string", enum: ["active", "passive"] },
|
|
843
|
-
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
1105
|
+
items: {
|
|
1106
|
+
type: "array",
|
|
1107
|
+
contains: { $ref: "#/definitions/imageInstruction" }
|
|
1108
|
+
}
|
|
844
1109
|
}
|
|
845
1110
|
}
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1111
|
+
]
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// spec/profiles/quantified.schema.json
|
|
1117
|
+
var quantified_schema_default = {
|
|
1118
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1119
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/quantified",
|
|
1120
|
+
title: "Soustack Quantified Profile Schema",
|
|
1121
|
+
description: "Extends the base schema to require quantified ingredient entries.",
|
|
1122
|
+
allOf: [
|
|
1123
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1124
|
+
{
|
|
851
1125
|
properties: {
|
|
852
|
-
|
|
853
|
-
items: {
|
|
1126
|
+
ingredients: {
|
|
854
1127
|
type: "array",
|
|
855
1128
|
items: {
|
|
856
1129
|
anyOf: [
|
|
857
|
-
{
|
|
858
|
-
{ $ref: "#/definitions/
|
|
1130
|
+
{ $ref: "#/definitions/quantifiedIngredient" },
|
|
1131
|
+
{ $ref: "#/definitions/quantifiedIngredientSubsection" }
|
|
859
1132
|
]
|
|
860
1133
|
}
|
|
861
1134
|
}
|
|
862
1135
|
}
|
|
1136
|
+
}
|
|
1137
|
+
],
|
|
1138
|
+
definitions: {
|
|
1139
|
+
quantifiedIngredient: {
|
|
1140
|
+
allOf: [
|
|
1141
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredient" },
|
|
1142
|
+
{ required: ["item", "quantity"] }
|
|
1143
|
+
]
|
|
863
1144
|
},
|
|
864
|
-
|
|
865
|
-
|
|
1145
|
+
quantifiedIngredientSubsection: {
|
|
1146
|
+
allOf: [
|
|
1147
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredientSubsection" },
|
|
1148
|
+
{
|
|
1149
|
+
properties: {
|
|
1150
|
+
items: {
|
|
1151
|
+
type: "array",
|
|
1152
|
+
items: { $ref: "#/definitions/quantifiedIngredient" }
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
]
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// spec/profiles/scalable.schema.json
|
|
1162
|
+
var scalable_schema_default = {
|
|
1163
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1164
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/scalable",
|
|
1165
|
+
title: "Soustack Scalable Profile Schema",
|
|
1166
|
+
description: "Extends the base schema to guarantee quantified ingredients plus a structured yield for deterministic scaling.",
|
|
1167
|
+
allOf: [
|
|
1168
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1169
|
+
{
|
|
1170
|
+
required: ["yield", "ingredients"],
|
|
866
1171
|
properties: {
|
|
867
|
-
|
|
868
|
-
refrigerated: { $ref: "#/definitions/storageMethod" },
|
|
869
|
-
frozen: {
|
|
1172
|
+
yield: {
|
|
870
1173
|
allOf: [
|
|
871
|
-
{ $ref: "#/definitions/
|
|
872
|
-
{
|
|
873
|
-
type: "object",
|
|
874
|
-
properties: { thawing: { type: "string" } }
|
|
875
|
-
}
|
|
1174
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/yield" },
|
|
1175
|
+
{ properties: { amount: { type: "number", exclusiveMinimum: 0 } } }
|
|
876
1176
|
]
|
|
877
1177
|
},
|
|
878
|
-
|
|
879
|
-
makeAhead: {
|
|
1178
|
+
ingredients: {
|
|
880
1179
|
type: "array",
|
|
1180
|
+
minItems: 1,
|
|
881
1181
|
items: {
|
|
882
|
-
|
|
883
|
-
{ $ref: "#/definitions/
|
|
884
|
-
{
|
|
1182
|
+
anyOf: [
|
|
1183
|
+
{ $ref: "#/definitions/scalableIngredient" },
|
|
1184
|
+
{ $ref: "#/definitions/scalableIngredientSubsection" }
|
|
1185
|
+
]
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
],
|
|
1191
|
+
definitions: {
|
|
1192
|
+
scalableIngredient: {
|
|
1193
|
+
allOf: [
|
|
1194
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredient" },
|
|
1195
|
+
{ required: ["item", "quantity"] },
|
|
1196
|
+
{
|
|
1197
|
+
properties: {
|
|
1198
|
+
quantity: {
|
|
1199
|
+
allOf: [
|
|
1200
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/quantity" },
|
|
1201
|
+
{ properties: { amount: { type: "number", exclusiveMinimum: 0 } } }
|
|
1202
|
+
]
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
if: {
|
|
1208
|
+
properties: {
|
|
1209
|
+
scaling: {
|
|
885
1210
|
type: "object",
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
component: { type: "string" },
|
|
889
|
-
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
890
|
-
}
|
|
1211
|
+
properties: { type: { const: "bakers_percentage" } },
|
|
1212
|
+
required: ["type"]
|
|
891
1213
|
}
|
|
1214
|
+
},
|
|
1215
|
+
required: ["scaling"]
|
|
1216
|
+
},
|
|
1217
|
+
then: { required: ["id"] }
|
|
1218
|
+
}
|
|
1219
|
+
]
|
|
1220
|
+
},
|
|
1221
|
+
scalableIngredientSubsection: {
|
|
1222
|
+
allOf: [
|
|
1223
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredientSubsection" },
|
|
1224
|
+
{
|
|
1225
|
+
properties: {
|
|
1226
|
+
items: {
|
|
1227
|
+
type: "array",
|
|
1228
|
+
minItems: 1,
|
|
1229
|
+
items: { $ref: "#/definitions/scalableIngredient" }
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// spec/profiles/schedulable.schema.json
|
|
1239
|
+
var schedulable_schema_default = {
|
|
1240
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1241
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/schedulable",
|
|
1242
|
+
title: "Soustack Schedulable Profile Schema",
|
|
1243
|
+
description: "Extends the base schema to ensure every instruction is fully scheduled.",
|
|
1244
|
+
allOf: [
|
|
1245
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1246
|
+
{
|
|
1247
|
+
properties: {
|
|
1248
|
+
instructions: {
|
|
1249
|
+
type: "array",
|
|
1250
|
+
items: {
|
|
1251
|
+
anyOf: [
|
|
1252
|
+
{ $ref: "#/definitions/schedulableInstruction" },
|
|
1253
|
+
{ $ref: "#/definitions/schedulableInstructionSubsection" }
|
|
892
1254
|
]
|
|
893
1255
|
}
|
|
894
1256
|
}
|
|
895
1257
|
}
|
|
1258
|
+
}
|
|
1259
|
+
],
|
|
1260
|
+
definitions: {
|
|
1261
|
+
schedulableInstruction: {
|
|
1262
|
+
allOf: [
|
|
1263
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instruction" },
|
|
1264
|
+
{ required: ["id", "timing"] }
|
|
1265
|
+
]
|
|
896
1266
|
},
|
|
897
|
-
|
|
1267
|
+
schedulableInstructionSubsection: {
|
|
1268
|
+
allOf: [
|
|
1269
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instructionSubsection" },
|
|
1270
|
+
{
|
|
1271
|
+
properties: {
|
|
1272
|
+
items: {
|
|
1273
|
+
type: "array",
|
|
1274
|
+
items: { $ref: "#/definitions/schedulableInstruction" }
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
]
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/schemas/recipe/stacks/attribution/1.schema.json
|
|
1284
|
+
var schema_default = {
|
|
1285
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1286
|
+
$id: "https://soustack.org/schemas/recipe/stacks/attribution/1.schema.json",
|
|
1287
|
+
title: "Soustack Recipe Stack: attribution v1",
|
|
1288
|
+
description: "Schema for the attribution stack. Ensures namespace data is present when the stack is enabled and vice versa.",
|
|
1289
|
+
type: "object",
|
|
1290
|
+
properties: {
|
|
1291
|
+
stacks: {
|
|
898
1292
|
type: "object",
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
method: { type: "string" },
|
|
903
|
-
notes: { type: "string" }
|
|
1293
|
+
additionalProperties: {
|
|
1294
|
+
type: "integer",
|
|
1295
|
+
minimum: 1
|
|
904
1296
|
}
|
|
905
1297
|
},
|
|
906
|
-
|
|
1298
|
+
attribution: {
|
|
907
1299
|
type: "object",
|
|
908
|
-
required: ["ingredient"],
|
|
909
1300
|
properties: {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1301
|
+
url: { type: "string" },
|
|
1302
|
+
author: { type: "string" },
|
|
1303
|
+
datePublished: { type: "string" }
|
|
1304
|
+
},
|
|
1305
|
+
additionalProperties: false
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
allOf: [
|
|
1309
|
+
{
|
|
1310
|
+
if: {
|
|
1311
|
+
properties: {
|
|
1312
|
+
stacks: {
|
|
916
1313
|
type: "object",
|
|
917
|
-
required: ["name", "ratio"],
|
|
918
1314
|
properties: {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1315
|
+
attribution: { const: 1 }
|
|
1316
|
+
},
|
|
1317
|
+
required: ["attribution"]
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1321
|
+
then: {
|
|
1322
|
+
required: ["attribution"]
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
if: {
|
|
1327
|
+
required: ["attribution"]
|
|
1328
|
+
},
|
|
1329
|
+
then: {
|
|
1330
|
+
required: ["stacks"],
|
|
1331
|
+
properties: {
|
|
1332
|
+
stacks: {
|
|
1333
|
+
type: "object",
|
|
1334
|
+
properties: {
|
|
1335
|
+
attribution: { const: 1 }
|
|
1336
|
+
},
|
|
1337
|
+
required: ["attribution"]
|
|
928
1338
|
}
|
|
929
1339
|
}
|
|
930
1340
|
}
|
|
931
1341
|
}
|
|
932
|
-
|
|
1342
|
+
],
|
|
1343
|
+
additionalProperties: true
|
|
933
1344
|
};
|
|
934
1345
|
|
|
935
|
-
// src/
|
|
936
|
-
var
|
|
1346
|
+
// src/schemas/recipe/stacks/media/1.schema.json
|
|
1347
|
+
var schema_default2 = {
|
|
937
1348
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
938
|
-
$id: "
|
|
939
|
-
title: "Soustack
|
|
940
|
-
description: "
|
|
1349
|
+
$id: "https://soustack.org/schemas/recipe/stacks/media/1.schema.json",
|
|
1350
|
+
title: "Soustack Recipe Stack: media v1",
|
|
1351
|
+
description: "Schema for the media stack. Guards media blocks based on stack activation and ensures declarations accompany payloads.",
|
|
1352
|
+
type: "object",
|
|
1353
|
+
properties: {
|
|
1354
|
+
stacks: {
|
|
1355
|
+
type: "object",
|
|
1356
|
+
additionalProperties: {
|
|
1357
|
+
type: "integer",
|
|
1358
|
+
minimum: 1
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
media: {
|
|
1362
|
+
type: "object",
|
|
1363
|
+
properties: {
|
|
1364
|
+
images: { type: "array", items: { type: "string" } },
|
|
1365
|
+
videos: { type: "array", items: { type: "string" } }
|
|
1366
|
+
},
|
|
1367
|
+
additionalProperties: false
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
941
1370
|
allOf: [
|
|
942
|
-
{
|
|
943
|
-
|
|
1371
|
+
{
|
|
1372
|
+
if: {
|
|
1373
|
+
properties: {
|
|
1374
|
+
stacks: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: {
|
|
1377
|
+
media: { const: 1 }
|
|
1378
|
+
},
|
|
1379
|
+
required: ["media"]
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
then: {
|
|
1384
|
+
required: ["media"]
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
if: {
|
|
1389
|
+
required: ["media"]
|
|
1390
|
+
},
|
|
1391
|
+
then: {
|
|
1392
|
+
required: ["stacks"],
|
|
1393
|
+
properties: {
|
|
1394
|
+
stacks: {
|
|
1395
|
+
type: "object",
|
|
1396
|
+
properties: {
|
|
1397
|
+
media: { const: 1 }
|
|
1398
|
+
},
|
|
1399
|
+
required: ["media"]
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
],
|
|
1405
|
+
additionalProperties: true
|
|
944
1406
|
};
|
|
945
1407
|
|
|
946
|
-
// src/
|
|
947
|
-
var
|
|
1408
|
+
// src/schemas/recipe/stacks/nutrition/1.schema.json
|
|
1409
|
+
var schema_default3 = {
|
|
948
1410
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
949
|
-
$id: "
|
|
950
|
-
title: "Soustack
|
|
951
|
-
description: "
|
|
1411
|
+
$id: "https://soustack.org/schemas/recipe/stacks/nutrition/1.schema.json",
|
|
1412
|
+
title: "Soustack Recipe Stack: nutrition v1",
|
|
1413
|
+
description: "Schema for the nutrition stack. Keeps nutrition data aligned with stack declarations and vice versa.",
|
|
1414
|
+
type: "object",
|
|
1415
|
+
properties: {
|
|
1416
|
+
stacks: {
|
|
1417
|
+
type: "object",
|
|
1418
|
+
additionalProperties: {
|
|
1419
|
+
type: "integer",
|
|
1420
|
+
minimum: 1
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
nutrition: {
|
|
1424
|
+
type: "object",
|
|
1425
|
+
properties: {
|
|
1426
|
+
calories: { type: "number" },
|
|
1427
|
+
protein_g: { type: "number" }
|
|
1428
|
+
},
|
|
1429
|
+
additionalProperties: false
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
952
1432
|
allOf: [
|
|
953
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
954
1433
|
{
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1434
|
+
if: {
|
|
1435
|
+
properties: {
|
|
1436
|
+
stacks: {
|
|
1437
|
+
type: "object",
|
|
1438
|
+
properties: {
|
|
1439
|
+
nutrition: { const: 1 }
|
|
1440
|
+
},
|
|
1441
|
+
required: ["nutrition"]
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
then: {
|
|
1446
|
+
required: ["nutrition"]
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
if: {
|
|
1451
|
+
required: ["nutrition"]
|
|
1452
|
+
},
|
|
1453
|
+
then: {
|
|
1454
|
+
required: ["stacks"],
|
|
1455
|
+
properties: {
|
|
1456
|
+
stacks: {
|
|
1457
|
+
type: "object",
|
|
1458
|
+
properties: {
|
|
1459
|
+
nutrition: { const: 1 }
|
|
1460
|
+
},
|
|
1461
|
+
required: ["nutrition"]
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
961
1464
|
}
|
|
962
1465
|
}
|
|
963
|
-
]
|
|
1466
|
+
],
|
|
1467
|
+
additionalProperties: true
|
|
964
1468
|
};
|
|
965
1469
|
|
|
966
|
-
// src/
|
|
967
|
-
var
|
|
1470
|
+
// src/schemas/recipe/stacks/schedule/1.schema.json
|
|
1471
|
+
var schema_default4 = {
|
|
968
1472
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
969
|
-
$id: "
|
|
970
|
-
title: "Soustack
|
|
971
|
-
description: "
|
|
1473
|
+
$id: "https://soustack.org/schemas/recipe/stacks/schedule/1.schema.json",
|
|
1474
|
+
title: "Soustack Recipe Stack: schedule v1",
|
|
1475
|
+
description: "Schema for the schedule stack. Enforces bidirectional stack gating and restricts usage to the core profile.",
|
|
1476
|
+
type: "object",
|
|
1477
|
+
properties: {
|
|
1478
|
+
profile: { type: "string" },
|
|
1479
|
+
stacks: {
|
|
1480
|
+
type: "object",
|
|
1481
|
+
additionalProperties: {
|
|
1482
|
+
type: "integer",
|
|
1483
|
+
minimum: 1
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
schedule: {
|
|
1487
|
+
type: "object",
|
|
1488
|
+
properties: {
|
|
1489
|
+
tasks: { type: "array" }
|
|
1490
|
+
},
|
|
1491
|
+
additionalProperties: false
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
972
1494
|
allOf: [
|
|
973
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
974
1495
|
{
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
{
|
|
981
|
-
|
|
982
|
-
]
|
|
1496
|
+
if: {
|
|
1497
|
+
properties: {
|
|
1498
|
+
stacks: {
|
|
1499
|
+
type: "object",
|
|
1500
|
+
properties: {
|
|
1501
|
+
schedule: { const: 1 }
|
|
1502
|
+
},
|
|
1503
|
+
required: ["schedule"]
|
|
983
1504
|
}
|
|
984
1505
|
}
|
|
1506
|
+
},
|
|
1507
|
+
then: {
|
|
1508
|
+
required: ["schedule", "profile"],
|
|
1509
|
+
properties: {
|
|
1510
|
+
profile: { const: "core" }
|
|
1511
|
+
}
|
|
985
1512
|
}
|
|
986
|
-
}
|
|
987
|
-
],
|
|
988
|
-
definitions: {
|
|
989
|
-
quantifiedIngredient: {
|
|
990
|
-
allOf: [
|
|
991
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/ingredient" },
|
|
992
|
-
{ required: ["item", "quantity"] }
|
|
993
|
-
]
|
|
994
1513
|
},
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1514
|
+
{
|
|
1515
|
+
if: {
|
|
1516
|
+
required: ["schedule"]
|
|
1517
|
+
},
|
|
1518
|
+
then: {
|
|
1519
|
+
required: ["stacks", "profile"],
|
|
1520
|
+
properties: {
|
|
1521
|
+
stacks: {
|
|
1522
|
+
type: "object",
|
|
1523
|
+
properties: {
|
|
1524
|
+
schedule: { const: 1 }
|
|
1525
|
+
},
|
|
1526
|
+
required: ["schedule"]
|
|
1527
|
+
},
|
|
1528
|
+
profile: { const: "core" }
|
|
1005
1529
|
}
|
|
1006
|
-
|
|
1530
|
+
}
|
|
1007
1531
|
}
|
|
1008
|
-
|
|
1532
|
+
],
|
|
1533
|
+
additionalProperties: true
|
|
1009
1534
|
};
|
|
1010
1535
|
|
|
1011
|
-
// src/
|
|
1012
|
-
var
|
|
1536
|
+
// src/schemas/recipe/stacks/taxonomy/1.schema.json
|
|
1537
|
+
var schema_default5 = {
|
|
1013
1538
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1014
|
-
$id: "
|
|
1015
|
-
title: "Soustack
|
|
1016
|
-
description: "
|
|
1539
|
+
$id: "https://soustack.org/schemas/recipe/stacks/taxonomy/1.schema.json",
|
|
1540
|
+
title: "Soustack Recipe Stack: taxonomy v1",
|
|
1541
|
+
description: "Schema for the taxonomy stack. Enforces keyword and categorization data when enabled and ensures stack declaration accompanies the namespace block.",
|
|
1542
|
+
type: "object",
|
|
1543
|
+
properties: {
|
|
1544
|
+
stacks: {
|
|
1545
|
+
type: "object",
|
|
1546
|
+
additionalProperties: {
|
|
1547
|
+
type: "integer",
|
|
1548
|
+
minimum: 1
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
taxonomy: {
|
|
1552
|
+
type: "object",
|
|
1553
|
+
properties: {
|
|
1554
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
1555
|
+
category: { type: "string" },
|
|
1556
|
+
cuisine: { type: "string" }
|
|
1557
|
+
},
|
|
1558
|
+
additionalProperties: false
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1017
1561
|
allOf: [
|
|
1018
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
1019
1562
|
{
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
{ $ref: "#/definitions/imageInstruction" },
|
|
1029
|
-
{ $ref: "#/definitions/instructionSubsectionWithImage" }
|
|
1030
|
-
]
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1563
|
+
if: {
|
|
1564
|
+
properties: {
|
|
1565
|
+
stacks: {
|
|
1566
|
+
type: "object",
|
|
1567
|
+
properties: {
|
|
1568
|
+
taxonomy: { const: 1 }
|
|
1569
|
+
},
|
|
1570
|
+
required: ["taxonomy"]
|
|
1033
1571
|
}
|
|
1034
1572
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
imageInstruction: {
|
|
1040
|
-
allOf: [
|
|
1041
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/instruction" },
|
|
1042
|
-
{ required: ["image"] }
|
|
1043
|
-
]
|
|
1573
|
+
},
|
|
1574
|
+
then: {
|
|
1575
|
+
required: ["taxonomy"]
|
|
1576
|
+
}
|
|
1044
1577
|
},
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1578
|
+
{
|
|
1579
|
+
if: {
|
|
1580
|
+
required: ["taxonomy"]
|
|
1581
|
+
},
|
|
1582
|
+
then: {
|
|
1583
|
+
required: ["stacks"],
|
|
1584
|
+
properties: {
|
|
1585
|
+
stacks: {
|
|
1586
|
+
type: "object",
|
|
1587
|
+
properties: {
|
|
1588
|
+
taxonomy: { const: 1 }
|
|
1589
|
+
},
|
|
1590
|
+
required: ["taxonomy"]
|
|
1054
1591
|
}
|
|
1055
1592
|
}
|
|
1056
|
-
|
|
1593
|
+
}
|
|
1057
1594
|
}
|
|
1058
|
-
|
|
1595
|
+
],
|
|
1596
|
+
additionalProperties: true
|
|
1059
1597
|
};
|
|
1060
1598
|
|
|
1061
|
-
// src/
|
|
1062
|
-
var
|
|
1599
|
+
// src/schemas/recipe/stacks/times/1.schema.json
|
|
1600
|
+
var schema_default6 = {
|
|
1063
1601
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1064
|
-
$id: "
|
|
1065
|
-
title: "Soustack
|
|
1066
|
-
description: "
|
|
1602
|
+
$id: "https://soustack.org/schemas/recipe/stacks/times/1.schema.json",
|
|
1603
|
+
title: "Soustack Recipe Stack: times v1",
|
|
1604
|
+
description: "Schema for the times stack. Maintains alignment between stack declarations and timing payloads.",
|
|
1605
|
+
type: "object",
|
|
1606
|
+
properties: {
|
|
1607
|
+
stacks: {
|
|
1608
|
+
type: "object",
|
|
1609
|
+
additionalProperties: {
|
|
1610
|
+
type: "integer",
|
|
1611
|
+
minimum: 1
|
|
1612
|
+
}
|
|
1613
|
+
},
|
|
1614
|
+
times: {
|
|
1615
|
+
type: "object",
|
|
1616
|
+
properties: {
|
|
1617
|
+
prepMinutes: { type: "number" },
|
|
1618
|
+
cookMinutes: { type: "number" },
|
|
1619
|
+
totalMinutes: { type: "number" }
|
|
1620
|
+
},
|
|
1621
|
+
additionalProperties: false
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1067
1624
|
allOf: [
|
|
1068
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
1069
1625
|
{
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
{
|
|
1076
|
-
|
|
1077
|
-
]
|
|
1626
|
+
if: {
|
|
1627
|
+
properties: {
|
|
1628
|
+
stacks: {
|
|
1629
|
+
type: "object",
|
|
1630
|
+
properties: {
|
|
1631
|
+
times: { const: 1 }
|
|
1632
|
+
},
|
|
1633
|
+
required: ["times"]
|
|
1078
1634
|
}
|
|
1079
1635
|
}
|
|
1636
|
+
},
|
|
1637
|
+
then: {
|
|
1638
|
+
required: ["times"]
|
|
1080
1639
|
}
|
|
1081
|
-
}
|
|
1082
|
-
],
|
|
1083
|
-
definitions: {
|
|
1084
|
-
schedulableInstruction: {
|
|
1085
|
-
allOf: [
|
|
1086
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/instruction" },
|
|
1087
|
-
{ required: ["id", "timing"] }
|
|
1088
|
-
]
|
|
1089
1640
|
},
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1641
|
+
{
|
|
1642
|
+
if: {
|
|
1643
|
+
required: ["times"]
|
|
1644
|
+
},
|
|
1645
|
+
then: {
|
|
1646
|
+
required: ["stacks"],
|
|
1647
|
+
properties: {
|
|
1648
|
+
stacks: {
|
|
1649
|
+
type: "object",
|
|
1650
|
+
properties: {
|
|
1651
|
+
times: { const: 1 }
|
|
1652
|
+
},
|
|
1653
|
+
required: ["times"]
|
|
1099
1654
|
}
|
|
1100
1655
|
}
|
|
1101
|
-
|
|
1656
|
+
}
|
|
1102
1657
|
}
|
|
1103
|
-
|
|
1658
|
+
],
|
|
1659
|
+
additionalProperties: true
|
|
1104
1660
|
};
|
|
1105
1661
|
|
|
1106
1662
|
// src/validator.ts
|
|
1107
|
-
var
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
quantified: quantified_schema_default,
|
|
1112
|
-
illustrated: illustrated_schema_default,
|
|
1113
|
-
schedulable: schedulable_schema_default
|
|
1114
|
-
};
|
|
1663
|
+
var LEGACY_ROOT_SCHEMA_ID = "http://soustack.org/schema/v0.3.0";
|
|
1664
|
+
var DEFAULT_ROOT_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
|
|
1665
|
+
var BASE_SCHEMA_ID = "http://soustack.org/schema/recipe/base.schema.json";
|
|
1666
|
+
var PROFILE_SCHEMA_PREFIX = "http://soustack.org/schema/recipe/profiles/";
|
|
1115
1667
|
var validationContexts = /* @__PURE__ */ new Map();
|
|
1668
|
+
function loadAllSchemas(ajv) {
|
|
1669
|
+
const schemas = [
|
|
1670
|
+
soustack_schema_default,
|
|
1671
|
+
base_schema_default,
|
|
1672
|
+
minimal_schema_default,
|
|
1673
|
+
core_schema_default,
|
|
1674
|
+
base_schema_default2,
|
|
1675
|
+
cookable_schema_default,
|
|
1676
|
+
illustrated_schema_default,
|
|
1677
|
+
quantified_schema_default,
|
|
1678
|
+
scalable_schema_default,
|
|
1679
|
+
schedulable_schema_default,
|
|
1680
|
+
schema_default,
|
|
1681
|
+
schema_default2,
|
|
1682
|
+
schema_default3,
|
|
1683
|
+
schema_default4,
|
|
1684
|
+
schema_default5,
|
|
1685
|
+
schema_default6
|
|
1686
|
+
];
|
|
1687
|
+
for (const schema of schemas) {
|
|
1688
|
+
if (schema && typeof schema === "object" && "$id" in schema) {
|
|
1689
|
+
const schemaWithId = schema;
|
|
1690
|
+
if (schemaWithId.$id) {
|
|
1691
|
+
ajv.addSchema(schemaWithId, schemaWithId.$id);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
ajv.addSchema(
|
|
1696
|
+
{
|
|
1697
|
+
$id: DEFAULT_ROOT_SCHEMA_ID,
|
|
1698
|
+
allOf: [
|
|
1699
|
+
{ $ref: LEGACY_ROOT_SCHEMA_ID },
|
|
1700
|
+
{
|
|
1701
|
+
type: "object",
|
|
1702
|
+
properties: {
|
|
1703
|
+
$schema: { const: DEFAULT_ROOT_SCHEMA_ID }
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
},
|
|
1708
|
+
DEFAULT_ROOT_SCHEMA_ID
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1116
1711
|
function createContext(collectAllErrors) {
|
|
1117
|
-
const ajv = new
|
|
1712
|
+
const ajv = new Ajv2020({
|
|
1713
|
+
strict: false,
|
|
1714
|
+
allErrors: collectAllErrors,
|
|
1715
|
+
validateSchema: false
|
|
1716
|
+
// Don't validate schemas themselves
|
|
1717
|
+
});
|
|
1118
1718
|
addFormats(ajv);
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1719
|
+
loadAllSchemas(ajv);
|
|
1720
|
+
const rootValidator = ajv.getSchema(DEFAULT_ROOT_SCHEMA_ID) || ajv.getSchema(LEGACY_ROOT_SCHEMA_ID);
|
|
1721
|
+
const baseValidator = ajv.getSchema(BASE_SCHEMA_ID);
|
|
1722
|
+
return {
|
|
1723
|
+
ajv,
|
|
1724
|
+
rootValidator: rootValidator || void 0,
|
|
1725
|
+
baseValidator: baseValidator || void 0,
|
|
1726
|
+
validators: /* @__PURE__ */ new Map()
|
|
1126
1727
|
};
|
|
1127
|
-
addSchemaIfNew(schema_default);
|
|
1128
|
-
addSchemaIfNew(soustack_schema_default);
|
|
1129
|
-
Object.values(profileSchemas).forEach(addSchemaIfNew);
|
|
1130
|
-
return { ajv, validators: {} };
|
|
1131
1728
|
}
|
|
1132
1729
|
function getContext(collectAllErrors) {
|
|
1133
1730
|
if (!validationContexts.has(collectAllErrors)) {
|
|
@@ -1141,200 +1738,227 @@ function cloneRecipe(recipe) {
|
|
|
1141
1738
|
}
|
|
1142
1739
|
return JSON.parse(JSON.stringify(recipe));
|
|
1143
1740
|
}
|
|
1144
|
-
function detectProfileFromSchema(schemaRef) {
|
|
1145
|
-
if (!schemaRef) return void 0;
|
|
1146
|
-
const match = schemaRef.match(/\/profiles\/([a-z]+)\.schema\.json$/i);
|
|
1147
|
-
if (match) {
|
|
1148
|
-
const profile = match[1].toLowerCase();
|
|
1149
|
-
if (profile in profileSchemas) return profile;
|
|
1150
|
-
}
|
|
1151
|
-
return void 0;
|
|
1152
|
-
}
|
|
1153
|
-
function getValidator(profile, context) {
|
|
1154
|
-
if (!profileSchemas[profile]) {
|
|
1155
|
-
throw new Error(`Unknown Soustack profile: ${profile}`);
|
|
1156
|
-
}
|
|
1157
|
-
if (!context.validators[profile]) {
|
|
1158
|
-
context.validators[profile] = context.ajv.compile(profileSchemas[profile]);
|
|
1159
|
-
}
|
|
1160
|
-
return context.validators[profile];
|
|
1161
|
-
}
|
|
1162
|
-
function normalizeRecipe(recipe) {
|
|
1163
|
-
const normalized = cloneRecipe(recipe);
|
|
1164
|
-
const warnings = [];
|
|
1165
|
-
normalizeTime(normalized);
|
|
1166
|
-
if (normalized && typeof normalized === "object" && "version" in normalized && !normalized.recipeVersion && typeof normalized.version === "string") {
|
|
1167
|
-
normalized.recipeVersion = normalized.version;
|
|
1168
|
-
warnings.push({ path: "/version", message: "'version' is deprecated; mapped to 'recipeVersion'." });
|
|
1169
|
-
}
|
|
1170
|
-
return { normalized, warnings };
|
|
1171
|
-
}
|
|
1172
|
-
function normalizeTime(recipe) {
|
|
1173
|
-
const time = recipe == null ? void 0 : recipe.time;
|
|
1174
|
-
if (!time || typeof time !== "object" || Array.isArray(time)) return;
|
|
1175
|
-
const structuredKeys = [
|
|
1176
|
-
"prep",
|
|
1177
|
-
"active",
|
|
1178
|
-
"passive",
|
|
1179
|
-
"total"
|
|
1180
|
-
];
|
|
1181
|
-
structuredKeys.forEach((key) => {
|
|
1182
|
-
const value = time[key];
|
|
1183
|
-
if (typeof value === "number") return;
|
|
1184
|
-
const parsed = parseDuration(value);
|
|
1185
|
-
if (parsed !== null) {
|
|
1186
|
-
time[key] = parsed;
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
var _a, _b;
|
|
1191
|
-
var allowedTopLevelProps = /* @__PURE__ */ new Set([
|
|
1192
|
-
...Object.keys((_b = (_a = soustack_schema_default) == null ? void 0 : _a.properties) != null ? _b : {}),
|
|
1193
|
-
"metadata",
|
|
1194
|
-
"$schema"
|
|
1195
|
-
]);
|
|
1196
|
-
function detectUnknownTopLevelKeys(recipe) {
|
|
1197
|
-
if (!recipe || typeof recipe !== "object") return [];
|
|
1198
|
-
const disallowedKeys = Object.keys(recipe).filter(
|
|
1199
|
-
(key) => !allowedTopLevelProps.has(key) && !key.startsWith("x-")
|
|
1200
|
-
);
|
|
1201
|
-
return disallowedKeys.map((key) => ({
|
|
1202
|
-
path: `/${key}`,
|
|
1203
|
-
keyword: "additionalProperties",
|
|
1204
|
-
message: `Unknown top-level property '${key}' is not allowed by the Soustack spec`
|
|
1205
|
-
}));
|
|
1206
|
-
}
|
|
1207
1741
|
function formatAjvError(error) {
|
|
1208
|
-
var
|
|
1742
|
+
var _a;
|
|
1209
1743
|
let path = error.instancePath || "/";
|
|
1210
|
-
if (error.keyword === "additionalProperties" && ((
|
|
1744
|
+
if (error.keyword === "additionalProperties" && ((_a = error.params) == null ? void 0 : _a.additionalProperty)) {
|
|
1211
1745
|
const extra = error.params.additionalProperty;
|
|
1212
1746
|
path = `${error.instancePath || ""}/${extra}`.replace(/\/+/g, "/") || "/";
|
|
1213
1747
|
}
|
|
1214
1748
|
return {
|
|
1215
1749
|
path,
|
|
1216
1750
|
keyword: error.keyword,
|
|
1217
|
-
message: error.message || "Validation error"
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
function
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1751
|
+
message: error.message || "Validation error"
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function isSoustackSchemaId(schemaId) {
|
|
1755
|
+
return schemaId.startsWith("http://soustack.org/schema") || schemaId.startsWith("https://soustack.org/schema") || schemaId.startsWith("https://soustack.spec/") || schemaId.startsWith("https://soustack.org/schemas/");
|
|
1756
|
+
}
|
|
1757
|
+
function inferStacksFromPayload(recipe) {
|
|
1758
|
+
const inferred = {};
|
|
1759
|
+
const payloadToStack = {
|
|
1760
|
+
attribution: "attribution",
|
|
1761
|
+
taxonomy: "taxonomy",
|
|
1762
|
+
media: "media",
|
|
1763
|
+
times: "times",
|
|
1764
|
+
nutrition: "nutrition",
|
|
1765
|
+
schedule: "schedule"
|
|
1766
|
+
};
|
|
1767
|
+
for (const [field, stackName] of Object.entries(payloadToStack)) {
|
|
1768
|
+
if (recipe && typeof recipe === "object" && field in recipe && recipe[field] !== void 0) {
|
|
1769
|
+
inferred[stackName] = 1;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return inferred;
|
|
1773
|
+
}
|
|
1774
|
+
function getComposedValidator(profile, stacks, context) {
|
|
1775
|
+
const stackIdentifiers = Object.entries(stacks).map(([name, version]) => `${name}@${version}`).sort();
|
|
1776
|
+
const cacheKey = `${profile}::${stackIdentifiers.join(",")}`;
|
|
1777
|
+
const cached = context.validators.get(cacheKey);
|
|
1778
|
+
if (cached) return cached;
|
|
1779
|
+
const allOf = [{ $ref: BASE_SCHEMA_ID }];
|
|
1780
|
+
if (!context.ajv.getSchema(BASE_SCHEMA_ID)) {
|
|
1781
|
+
throw new Error(`Base schema not loaded: ${BASE_SCHEMA_ID}. Ensure schemas are loaded before creating validators.`);
|
|
1782
|
+
}
|
|
1783
|
+
const profileSchemaId = `${PROFILE_SCHEMA_PREFIX}${profile}.schema.json`;
|
|
1784
|
+
if (!context.ajv.getSchema(profileSchemaId)) {
|
|
1785
|
+
throw new Error(`Profile schema not loaded: ${profileSchemaId}`);
|
|
1786
|
+
}
|
|
1787
|
+
allOf.push({ $ref: profileSchemaId });
|
|
1788
|
+
for (const [name, version] of Object.entries(stacks)) {
|
|
1789
|
+
if (typeof version === "number" && version >= 1) {
|
|
1790
|
+
const stackSchemaId = `https://soustack.org/schemas/recipe/stacks/${name}/${version}.schema.json`;
|
|
1791
|
+
if (!context.ajv.getSchema(stackSchemaId)) {
|
|
1792
|
+
throw new Error(`Stack schema not loaded: ${stackSchemaId}`);
|
|
1258
1793
|
}
|
|
1259
|
-
|
|
1794
|
+
allOf.push({ $ref: stackSchemaId });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
const composedSchema = {
|
|
1798
|
+
$id: `urn:soustack:composed:${cacheKey}`,
|
|
1799
|
+
allOf
|
|
1260
1800
|
};
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1801
|
+
const validateFn = context.ajv.compile(composedSchema);
|
|
1802
|
+
context.validators.set(cacheKey, validateFn);
|
|
1803
|
+
return validateFn;
|
|
1804
|
+
}
|
|
1805
|
+
function validateRecipeSchemaNormalized(normalizedInput, inputHasStacks, collectAllErrors, schemaOverride) {
|
|
1806
|
+
const normalized = cloneRecipe(normalizedInput);
|
|
1807
|
+
const context = getContext(collectAllErrors);
|
|
1808
|
+
const schemaId = typeof schemaOverride === "string" ? schemaOverride : typeof normalized.$schema === "string" ? normalized.$schema : void 0;
|
|
1809
|
+
const hasSchemaOverride = typeof schemaOverride === "string";
|
|
1810
|
+
const isSoustackSchema = schemaId ? isSoustackSchemaId(schemaId) : false;
|
|
1811
|
+
if (schemaId && isSoustackSchema) {
|
|
1812
|
+
const schemaValidator = context.ajv.getSchema(schemaId);
|
|
1813
|
+
if (!schemaValidator) {
|
|
1814
|
+
return {
|
|
1815
|
+
ok: false,
|
|
1816
|
+
errors: [
|
|
1817
|
+
{
|
|
1818
|
+
path: "/$schema",
|
|
1819
|
+
message: `Unknown schema: ${schemaId}`
|
|
1820
|
+
}
|
|
1821
|
+
]
|
|
1822
|
+
};
|
|
1269
1823
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
var _a2;
|
|
1274
|
-
if (ref.fromId && instructionIds.has(ref.fromId) && instructionIds.has(ref.toId)) {
|
|
1275
|
-
const list = (_a2 = adjacency.get(ref.fromId)) != null ? _a2 : [];
|
|
1276
|
-
list.push({ toId: ref.toId, path: ref.path });
|
|
1277
|
-
adjacency.set(ref.fromId, list);
|
|
1824
|
+
const schemaInput = cloneRecipe(normalized);
|
|
1825
|
+
if (hasSchemaOverride && "$schema" in schemaInput && schemaInput.$schema !== schemaId) {
|
|
1826
|
+
delete schemaInput.$schema;
|
|
1278
1827
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1828
|
+
const isLegacySchema = schemaId.startsWith(LEGACY_ROOT_SCHEMA_ID);
|
|
1829
|
+
const shouldRemoveStacks = (isLegacySchema || schemaId === DEFAULT_ROOT_SCHEMA_ID) && !inputHasStacks;
|
|
1830
|
+
if (isLegacySchema && "@type" in schemaInput) {
|
|
1831
|
+
delete schemaInput["@type"];
|
|
1832
|
+
}
|
|
1833
|
+
if (shouldRemoveStacks && "stacks" in schemaInput) {
|
|
1834
|
+
delete schemaInput.stacks;
|
|
1835
|
+
}
|
|
1836
|
+
const schemaValid = schemaValidator(schemaInput);
|
|
1837
|
+
const schemaErrors = schemaValidator.errors || [];
|
|
1838
|
+
return {
|
|
1839
|
+
ok: !!schemaValid,
|
|
1840
|
+
errors: schemaErrors.map(formatAjvError)
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
const hasProfile = normalized.profile && typeof normalized.profile === "string";
|
|
1844
|
+
let declaredStacks = {};
|
|
1845
|
+
if (normalized.stacks && typeof normalized.stacks === "object" && !Array.isArray(normalized.stacks)) {
|
|
1846
|
+
for (const [name, version] of Object.entries(normalized.stacks)) {
|
|
1847
|
+
if (typeof version === "number" && version >= 1) {
|
|
1848
|
+
declaredStacks[name] = version;
|
|
1295
1849
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const inferredStacks = inferStacksFromPayload(normalized);
|
|
1853
|
+
const allStacks = { ...declaredStacks };
|
|
1854
|
+
for (const [name, version] of Object.entries(inferredStacks)) {
|
|
1855
|
+
if (!allStacks[name] || allStacks[name] < version) {
|
|
1856
|
+
allStacks[name] = version;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
let isValid;
|
|
1860
|
+
let errors = [];
|
|
1861
|
+
const profile = hasProfile ? normalized.profile.toLowerCase() : "core";
|
|
1862
|
+
const profileSchemaId = `${PROFILE_SCHEMA_PREFIX}${profile}.schema.json`;
|
|
1863
|
+
if (!context.ajv.getSchema(profileSchemaId)) {
|
|
1864
|
+
return {
|
|
1865
|
+
ok: false,
|
|
1866
|
+
errors: [
|
|
1867
|
+
{
|
|
1868
|
+
path: "/profile",
|
|
1869
|
+
message: `Profile schema not loaded: ${profileSchemaId}`
|
|
1870
|
+
}
|
|
1871
|
+
]
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
{
|
|
1875
|
+
const validationCopy = cloneRecipe(normalized);
|
|
1876
|
+
if (!validationCopy.stacks || typeof validationCopy.stacks !== "object" || Array.isArray(validationCopy.stacks)) {
|
|
1877
|
+
validationCopy.stacks = declaredStacks;
|
|
1878
|
+
}
|
|
1879
|
+
if (!validationCopy.profile) {
|
|
1880
|
+
validationCopy.profile = profile;
|
|
1881
|
+
}
|
|
1882
|
+
const validator = getComposedValidator(profile, allStacks, context);
|
|
1883
|
+
isValid = validator(validationCopy);
|
|
1884
|
+
errors = validator.errors || [];
|
|
1885
|
+
if (isValid && context.rootValidator) {
|
|
1886
|
+
const rootCheckCopy = cloneRecipe(normalized);
|
|
1887
|
+
if ("@type" in rootCheckCopy) {
|
|
1888
|
+
delete rootCheckCopy["@type"];
|
|
1889
|
+
}
|
|
1890
|
+
if ("stacks" in rootCheckCopy) {
|
|
1891
|
+
delete rootCheckCopy.stacks;
|
|
1892
|
+
}
|
|
1893
|
+
if ("profile" in rootCheckCopy) {
|
|
1894
|
+
delete rootCheckCopy.profile;
|
|
1895
|
+
}
|
|
1896
|
+
const stackPayloadFields = ["attribution", "taxonomy", "media", "times", "nutrition", "schedule"];
|
|
1897
|
+
for (const field of stackPayloadFields) {
|
|
1898
|
+
if (field in rootCheckCopy) {
|
|
1899
|
+
delete rootCheckCopy[field];
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const rootValid = context.rootValidator(rootCheckCopy);
|
|
1903
|
+
if (!rootValid && context.rootValidator.errors) {
|
|
1904
|
+
const unknownKeyErrors = context.rootValidator.errors.filter(
|
|
1905
|
+
(e) => e.keyword === "additionalProperties" && (e.instancePath === "" || e.instancePath === "/")
|
|
1906
|
+
);
|
|
1907
|
+
const schemaConstErrors = context.rootValidator.errors.filter(
|
|
1908
|
+
(e) => e.keyword === "const" && e.instancePath === "/$schema"
|
|
1909
|
+
);
|
|
1910
|
+
const relevantErrors = [...unknownKeyErrors, ...schemaConstErrors];
|
|
1911
|
+
if (relevantErrors.length > 0) {
|
|
1912
|
+
errors.push(...relevantErrors);
|
|
1913
|
+
isValid = false;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
ok: isValid,
|
|
1920
|
+
errors: errors.map(formatAjvError)
|
|
1300
1921
|
};
|
|
1301
|
-
instructionIds.forEach((id) => detectCycles(id));
|
|
1302
|
-
return errors;
|
|
1303
1922
|
}
|
|
1304
1923
|
function validateRecipe(input, options = {}) {
|
|
1305
|
-
var
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1924
|
+
var _a, _b;
|
|
1925
|
+
const { recipe: normalized, warnings } = normalizeRecipe(input);
|
|
1926
|
+
if (options.profile) {
|
|
1927
|
+
normalized.profile = options.profile;
|
|
1928
|
+
}
|
|
1929
|
+
const inputHasStacks = !!input && typeof input === "object" && !Array.isArray(input) && "stacks" in input;
|
|
1930
|
+
const { ok: schemaOk, errors: schemaErrors } = validateRecipeSchemaNormalized(
|
|
1931
|
+
normalized,
|
|
1932
|
+
inputHasStacks,
|
|
1933
|
+
(_a = options.collectAllErrors) != null ? _a : true,
|
|
1934
|
+
options.schema
|
|
1935
|
+
);
|
|
1936
|
+
const mode = (_b = options.mode) != null ? _b : "full";
|
|
1937
|
+
let conformanceIssues = [];
|
|
1938
|
+
let conformanceOk = true;
|
|
1939
|
+
if (mode === "full") {
|
|
1940
|
+
if (schemaOk) {
|
|
1941
|
+
const conformanceResult = validateConformance(normalized);
|
|
1942
|
+
conformanceIssues = conformanceResult.issues;
|
|
1943
|
+
conformanceOk = conformanceResult.ok;
|
|
1944
|
+
} else {
|
|
1945
|
+
conformanceOk = false;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const ok = schemaOk && (mode === "schema" ? true : conformanceOk);
|
|
1949
|
+
const normalizedRecipe = ok || options.includeNormalized ? normalized : void 0;
|
|
1315
1950
|
return {
|
|
1316
|
-
|
|
1317
|
-
|
|
1951
|
+
ok,
|
|
1952
|
+
schemaErrors,
|
|
1953
|
+
conformanceIssues,
|
|
1318
1954
|
warnings,
|
|
1319
|
-
|
|
1955
|
+
normalizedRecipe
|
|
1320
1956
|
};
|
|
1321
1957
|
}
|
|
1322
1958
|
function detectProfiles(recipe) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const normalizedRecipe = (_a2 = result.normalized) != null ? _a2 : recipe;
|
|
1327
|
-
const profiles = ["base"];
|
|
1328
|
-
const context = getContext(false);
|
|
1329
|
-
Object.keys(profileSchemas).forEach((profile) => {
|
|
1330
|
-
if (profile === "base") return;
|
|
1331
|
-
if (!profileSchemas[profile]) return;
|
|
1332
|
-
const errors = runAjvValidation(normalizedRecipe, profile, context);
|
|
1333
|
-
if (errors.length === 0) {
|
|
1334
|
-
profiles.push(profile);
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
return profiles;
|
|
1959
|
+
const result = validateRecipe(recipe, { collectAllErrors: false });
|
|
1960
|
+
if (!result.ok) return [];
|
|
1961
|
+
return ["core"];
|
|
1338
1962
|
}
|
|
1339
1963
|
|
|
1340
1964
|
// src/converters/yield.ts
|
|
@@ -1377,12 +2001,12 @@ function parseYield(value) {
|
|
|
1377
2001
|
return void 0;
|
|
1378
2002
|
}
|
|
1379
2003
|
function formatYield(yieldValue) {
|
|
1380
|
-
var
|
|
2004
|
+
var _a;
|
|
1381
2005
|
if (!yieldValue) return void 0;
|
|
1382
2006
|
if (!yieldValue.amount && !yieldValue.unit) {
|
|
1383
2007
|
return void 0;
|
|
1384
2008
|
}
|
|
1385
|
-
const amount = (
|
|
2009
|
+
const amount = (_a = yieldValue.amount) != null ? _a : "";
|
|
1386
2010
|
const unit = yieldValue.unit ? ` ${yieldValue.unit}` : "";
|
|
1387
2011
|
return `${amount}${unit}`.trim() || yieldValue.description;
|
|
1388
2012
|
}
|
|
@@ -1423,7 +2047,7 @@ function extractUrl(value) {
|
|
|
1423
2047
|
|
|
1424
2048
|
// src/fromSchemaOrg.ts
|
|
1425
2049
|
function fromSchemaOrg(input) {
|
|
1426
|
-
var
|
|
2050
|
+
var _a;
|
|
1427
2051
|
const recipeNode = extractRecipeNode(input);
|
|
1428
2052
|
if (!recipeNode) {
|
|
1429
2053
|
return null;
|
|
@@ -1435,22 +2059,42 @@ function fromSchemaOrg(input) {
|
|
|
1435
2059
|
const tags = collectTags(recipeNode.recipeCuisine, recipeNode.keywords);
|
|
1436
2060
|
const category = extractFirst(recipeNode.recipeCategory);
|
|
1437
2061
|
const source = convertSource(recipeNode);
|
|
1438
|
-
const
|
|
1439
|
-
|
|
2062
|
+
const dateModified = recipeNode.dateModified || void 0;
|
|
2063
|
+
const nutrition = convertNutrition(recipeNode.nutrition);
|
|
2064
|
+
const attribution = convertAttribution(recipeNode);
|
|
2065
|
+
const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
|
|
2066
|
+
const media = convertMedia(recipeNode.image, recipeNode.video);
|
|
2067
|
+
const times = convertTimes(time);
|
|
2068
|
+
const stacks = {};
|
|
2069
|
+
if (attribution) stacks.attribution = 1;
|
|
2070
|
+
if (taxonomy) stacks.taxonomy = 1;
|
|
2071
|
+
if (media) stacks.media = 1;
|
|
2072
|
+
if (nutrition) stacks.nutrition = 1;
|
|
2073
|
+
if (times) stacks.times = 1;
|
|
2074
|
+
const rawRecipe = {
|
|
2075
|
+
"@type": "Recipe",
|
|
2076
|
+
profile: "minimal",
|
|
2077
|
+
stacks,
|
|
1440
2078
|
name: recipeNode.name.trim(),
|
|
1441
|
-
description: ((
|
|
2079
|
+
description: ((_a = recipeNode.description) == null ? void 0 : _a.trim()) || void 0,
|
|
1442
2080
|
image: normalizeImage(recipeNode.image),
|
|
1443
2081
|
category,
|
|
1444
2082
|
tags: tags.length ? tags : void 0,
|
|
1445
2083
|
source,
|
|
1446
2084
|
dateAdded: recipeNode.datePublished || void 0,
|
|
1447
|
-
dateModified: recipeNode.dateModified || void 0,
|
|
1448
2085
|
yield: recipeYield,
|
|
1449
2086
|
time,
|
|
1450
2087
|
ingredients,
|
|
1451
2088
|
instructions,
|
|
1452
|
-
|
|
2089
|
+
...dateModified ? { dateModified } : {},
|
|
2090
|
+
...nutrition ? { nutrition } : {},
|
|
2091
|
+
...attribution ? { attribution } : {},
|
|
2092
|
+
...taxonomy ? { taxonomy } : {},
|
|
2093
|
+
...media ? { media } : {},
|
|
2094
|
+
...times ? { times } : {}
|
|
1453
2095
|
};
|
|
2096
|
+
const { recipe } = normalizeRecipe(rawRecipe);
|
|
2097
|
+
return recipe;
|
|
1454
2098
|
}
|
|
1455
2099
|
function extractRecipeNode(input) {
|
|
1456
2100
|
if (!input) return null;
|
|
@@ -1497,7 +2141,7 @@ function convertIngredients(value) {
|
|
|
1497
2141
|
return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
|
|
1498
2142
|
}
|
|
1499
2143
|
function convertInstructions(value) {
|
|
1500
|
-
var
|
|
2144
|
+
var _a;
|
|
1501
2145
|
if (!value) return [];
|
|
1502
2146
|
const normalized = Array.isArray(value) ? value : [value];
|
|
1503
2147
|
const result = [];
|
|
@@ -1514,7 +2158,7 @@ function convertInstructions(value) {
|
|
|
1514
2158
|
const subsectionItems = extractSectionItems(entry.itemListElement);
|
|
1515
2159
|
if (subsectionItems.length) {
|
|
1516
2160
|
result.push({
|
|
1517
|
-
subsection: ((
|
|
2161
|
+
subsection: ((_a = entry.name) == null ? void 0 : _a.trim()) || "Section",
|
|
1518
2162
|
items: subsectionItems
|
|
1519
2163
|
});
|
|
1520
2164
|
}
|
|
@@ -1598,9 +2242,9 @@ function isHowToSection(value) {
|
|
|
1598
2242
|
return Boolean(value) && typeof value === "object" && value["@type"] === "HowToSection" && Array.isArray(value.itemListElement);
|
|
1599
2243
|
}
|
|
1600
2244
|
function convertTime(recipe) {
|
|
1601
|
-
var
|
|
1602
|
-
const prep = smartParseDuration((
|
|
1603
|
-
const cook = smartParseDuration((
|
|
2245
|
+
var _a, _b, _c;
|
|
2246
|
+
const prep = smartParseDuration((_a = recipe.prepTime) != null ? _a : "");
|
|
2247
|
+
const cook = smartParseDuration((_b = recipe.cookTime) != null ? _b : "");
|
|
1604
2248
|
const total = smartParseDuration((_c = recipe.totalTime) != null ? _c : "");
|
|
1605
2249
|
const structured = {};
|
|
1606
2250
|
if (prep !== null && prep !== void 0) structured.prep = prep;
|
|
@@ -1634,10 +2278,10 @@ function extractFirst(value) {
|
|
|
1634
2278
|
return arr.length ? arr[0] : void 0;
|
|
1635
2279
|
}
|
|
1636
2280
|
function convertSource(recipe) {
|
|
1637
|
-
var
|
|
2281
|
+
var _a;
|
|
1638
2282
|
const author = extractEntityName(recipe.author);
|
|
1639
2283
|
const publisher = extractEntityName(recipe.publisher);
|
|
1640
|
-
const url = (
|
|
2284
|
+
const url = (_a = recipe.url || recipe.mainEntityOfPage) == null ? void 0 : _a.trim();
|
|
1641
2285
|
const source = {};
|
|
1642
2286
|
if (author) source.author = author;
|
|
1643
2287
|
if (publisher) source.name = publisher;
|
|
@@ -1665,17 +2309,174 @@ function extractEntityName(value) {
|
|
|
1665
2309
|
}
|
|
1666
2310
|
return void 0;
|
|
1667
2311
|
}
|
|
2312
|
+
function convertAttribution(recipe) {
|
|
2313
|
+
var _a, _b;
|
|
2314
|
+
const attribution = {};
|
|
2315
|
+
const url = (_a = recipe.url || recipe.mainEntityOfPage) == null ? void 0 : _a.trim();
|
|
2316
|
+
const author = extractEntityName(recipe.author);
|
|
2317
|
+
const datePublished = (_b = recipe.datePublished) == null ? void 0 : _b.trim();
|
|
2318
|
+
if (url) attribution.url = url;
|
|
2319
|
+
if (author) attribution.author = author;
|
|
2320
|
+
if (datePublished) attribution.datePublished = datePublished;
|
|
2321
|
+
return Object.keys(attribution).length ? attribution : void 0;
|
|
2322
|
+
}
|
|
2323
|
+
function convertTaxonomy(keywords, category, cuisine) {
|
|
2324
|
+
const taxonomy = {};
|
|
2325
|
+
if (keywords.length) taxonomy.keywords = keywords;
|
|
2326
|
+
if (category) taxonomy.category = category;
|
|
2327
|
+
if (cuisine) taxonomy.cuisine = cuisine;
|
|
2328
|
+
return Object.keys(taxonomy).length ? taxonomy : void 0;
|
|
2329
|
+
}
|
|
2330
|
+
function normalizeMediaList(value) {
|
|
2331
|
+
if (!value) return [];
|
|
2332
|
+
if (typeof value === "string") return [value.trim()].filter(Boolean);
|
|
2333
|
+
if (Array.isArray(value)) {
|
|
2334
|
+
return value.map((item) => typeof item === "string" ? item.trim() : extractMediaUrl(item)).filter((entry) => Boolean(entry == null ? void 0 : entry.length));
|
|
2335
|
+
}
|
|
2336
|
+
const url = extractMediaUrl(value);
|
|
2337
|
+
return url ? [url] : [];
|
|
2338
|
+
}
|
|
2339
|
+
function extractMediaUrl(value) {
|
|
2340
|
+
if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
|
|
2341
|
+
const trimmed = value.url.trim();
|
|
2342
|
+
return trimmed || void 0;
|
|
2343
|
+
}
|
|
2344
|
+
return void 0;
|
|
2345
|
+
}
|
|
2346
|
+
function convertMedia(image, video) {
|
|
2347
|
+
const normalizedImage = normalizeImage(image);
|
|
2348
|
+
const images = normalizedImage ? Array.isArray(normalizedImage) ? normalizedImage : [normalizedImage] : [];
|
|
2349
|
+
const videos = normalizeMediaList(video);
|
|
2350
|
+
const media = {};
|
|
2351
|
+
if (images.length) media.images = images;
|
|
2352
|
+
if (videos.length) media.videos = videos;
|
|
2353
|
+
return Object.keys(media).length ? media : void 0;
|
|
2354
|
+
}
|
|
2355
|
+
function convertTimes(time) {
|
|
2356
|
+
if (!time) return void 0;
|
|
2357
|
+
const times = {};
|
|
2358
|
+
if (typeof time.prep === "number") times.prepMinutes = time.prep;
|
|
2359
|
+
if (typeof time.active === "number") times.cookMinutes = time.active;
|
|
2360
|
+
if (typeof time.total === "number") times.totalMinutes = time.total;
|
|
2361
|
+
return Object.keys(times).length ? times : void 0;
|
|
2362
|
+
}
|
|
2363
|
+
function convertNutrition(nutrition) {
|
|
2364
|
+
if (!nutrition || typeof nutrition !== "object") {
|
|
2365
|
+
return void 0;
|
|
2366
|
+
}
|
|
2367
|
+
const result = {};
|
|
2368
|
+
let hasData = false;
|
|
2369
|
+
if ("calories" in nutrition) {
|
|
2370
|
+
const calories = nutrition.calories;
|
|
2371
|
+
if (typeof calories === "number") {
|
|
2372
|
+
result.calories = calories;
|
|
2373
|
+
hasData = true;
|
|
2374
|
+
} else if (typeof calories === "string") {
|
|
2375
|
+
const parsed = parseFloat(calories.replace(/[^\d.-]/g, ""));
|
|
2376
|
+
if (!isNaN(parsed)) {
|
|
2377
|
+
result.calories = parsed;
|
|
2378
|
+
hasData = true;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if ("proteinContent" in nutrition || "protein_g" in nutrition) {
|
|
2383
|
+
const protein = nutrition.proteinContent || nutrition.protein_g;
|
|
2384
|
+
if (typeof protein === "number") {
|
|
2385
|
+
result.protein_g = protein;
|
|
2386
|
+
hasData = true;
|
|
2387
|
+
} else if (typeof protein === "string") {
|
|
2388
|
+
const parsed = parseFloat(protein.replace(/[^\d.-]/g, ""));
|
|
2389
|
+
if (!isNaN(parsed)) {
|
|
2390
|
+
result.protein_g = parsed;
|
|
2391
|
+
hasData = true;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return hasData ? result : void 0;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// src/schemas/registry/stacks.json
|
|
2399
|
+
var stacks_default = {
|
|
2400
|
+
stacks: [
|
|
2401
|
+
{
|
|
2402
|
+
id: "attribution",
|
|
2403
|
+
versions: [1],
|
|
2404
|
+
latest: 1,
|
|
2405
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/attribution",
|
|
2406
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/attribution",
|
|
2407
|
+
schemaOrgMappable: true,
|
|
2408
|
+
schemaOrgConfidence: "medium",
|
|
2409
|
+
minProfile: "minimal",
|
|
2410
|
+
allowedOnMinimal: true
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
id: "taxonomy",
|
|
2414
|
+
versions: [1],
|
|
2415
|
+
latest: 1,
|
|
2416
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/taxonomy",
|
|
2417
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/taxonomy",
|
|
2418
|
+
schemaOrgMappable: true,
|
|
2419
|
+
schemaOrgConfidence: "high",
|
|
2420
|
+
minProfile: "minimal",
|
|
2421
|
+
allowedOnMinimal: true
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
id: "media",
|
|
2425
|
+
versions: [1],
|
|
2426
|
+
latest: 1,
|
|
2427
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/media",
|
|
2428
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/media",
|
|
2429
|
+
schemaOrgMappable: true,
|
|
2430
|
+
schemaOrgConfidence: "medium",
|
|
2431
|
+
minProfile: "minimal",
|
|
2432
|
+
allowedOnMinimal: true
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
id: "nutrition",
|
|
2436
|
+
versions: [1],
|
|
2437
|
+
latest: 1,
|
|
2438
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/nutrition",
|
|
2439
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/nutrition",
|
|
2440
|
+
schemaOrgMappable: false,
|
|
2441
|
+
schemaOrgConfidence: "low",
|
|
2442
|
+
minProfile: "minimal",
|
|
2443
|
+
allowedOnMinimal: true
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
id: "times",
|
|
2447
|
+
versions: [1],
|
|
2448
|
+
latest: 1,
|
|
2449
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/times",
|
|
2450
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/times",
|
|
2451
|
+
schemaOrgMappable: true,
|
|
2452
|
+
schemaOrgConfidence: "medium",
|
|
2453
|
+
minProfile: "minimal",
|
|
2454
|
+
allowedOnMinimal: true
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
id: "schedule",
|
|
2458
|
+
versions: [1],
|
|
2459
|
+
latest: 1,
|
|
2460
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/schedule",
|
|
2461
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/schedule",
|
|
2462
|
+
schemaOrgMappable: false,
|
|
2463
|
+
schemaOrgConfidence: "low",
|
|
2464
|
+
minProfile: "core",
|
|
2465
|
+
allowedOnMinimal: false
|
|
2466
|
+
}
|
|
2467
|
+
]
|
|
2468
|
+
};
|
|
1668
2469
|
|
|
1669
2470
|
// src/converters/toSchemaOrg.ts
|
|
1670
2471
|
function convertBasicMetadata(recipe) {
|
|
1671
|
-
var
|
|
2472
|
+
var _a;
|
|
1672
2473
|
return cleanOutput({
|
|
1673
2474
|
"@context": "https://schema.org",
|
|
1674
2475
|
"@type": "Recipe",
|
|
1675
2476
|
name: recipe.name,
|
|
1676
2477
|
description: recipe.description,
|
|
1677
2478
|
image: recipe.image,
|
|
1678
|
-
url: (
|
|
2479
|
+
url: (_a = recipe.source) == null ? void 0 : _a.url,
|
|
1679
2480
|
datePublished: recipe.dateAdded,
|
|
1680
2481
|
dateModified: recipe.dateModified
|
|
1681
2482
|
});
|
|
@@ -1683,7 +2484,7 @@ function convertBasicMetadata(recipe) {
|
|
|
1683
2484
|
function convertIngredients2(ingredients = []) {
|
|
1684
2485
|
const result = [];
|
|
1685
2486
|
ingredients.forEach((ingredient) => {
|
|
1686
|
-
var
|
|
2487
|
+
var _a;
|
|
1687
2488
|
if (!ingredient) {
|
|
1688
2489
|
return;
|
|
1689
2490
|
}
|
|
@@ -1713,7 +2514,7 @@ function convertIngredients2(ingredients = []) {
|
|
|
1713
2514
|
});
|
|
1714
2515
|
return;
|
|
1715
2516
|
}
|
|
1716
|
-
const value = (
|
|
2517
|
+
const value = (_a = ingredient.item) == null ? void 0 : _a.trim();
|
|
1717
2518
|
if (value) {
|
|
1718
2519
|
result.push(value);
|
|
1719
2520
|
}
|
|
@@ -1748,13 +2549,13 @@ function convertInstruction(entry) {
|
|
|
1748
2549
|
return createHowToStep(String(entry));
|
|
1749
2550
|
}
|
|
1750
2551
|
function createHowToStep(entry) {
|
|
1751
|
-
var
|
|
2552
|
+
var _a;
|
|
1752
2553
|
if (!entry) return null;
|
|
1753
2554
|
if (typeof entry === "string") {
|
|
1754
2555
|
const trimmed2 = entry.trim();
|
|
1755
2556
|
return trimmed2 || null;
|
|
1756
2557
|
}
|
|
1757
|
-
const trimmed = (
|
|
2558
|
+
const trimmed = (_a = entry.text) == null ? void 0 : _a.trim();
|
|
1758
2559
|
if (!trimmed) {
|
|
1759
2560
|
return null;
|
|
1760
2561
|
}
|
|
@@ -1806,6 +2607,22 @@ function convertTime2(time) {
|
|
|
1806
2607
|
}
|
|
1807
2608
|
return result;
|
|
1808
2609
|
}
|
|
2610
|
+
function convertTimesModule(times) {
|
|
2611
|
+
if (!times) {
|
|
2612
|
+
return {};
|
|
2613
|
+
}
|
|
2614
|
+
const result = {};
|
|
2615
|
+
if (times.prepMinutes !== void 0) {
|
|
2616
|
+
result.prepTime = formatDuration(times.prepMinutes);
|
|
2617
|
+
}
|
|
2618
|
+
if (times.cookMinutes !== void 0) {
|
|
2619
|
+
result.cookTime = formatDuration(times.cookMinutes);
|
|
2620
|
+
}
|
|
2621
|
+
if (times.totalMinutes !== void 0) {
|
|
2622
|
+
result.totalTime = formatDuration(times.totalMinutes);
|
|
2623
|
+
}
|
|
2624
|
+
return result;
|
|
2625
|
+
}
|
|
1809
2626
|
function convertYield(yld) {
|
|
1810
2627
|
if (!yld) {
|
|
1811
2628
|
return void 0;
|
|
@@ -1844,33 +2661,65 @@ function convertCategoryTags(category, tags) {
|
|
|
1844
2661
|
}
|
|
1845
2662
|
return result;
|
|
1846
2663
|
}
|
|
1847
|
-
function
|
|
2664
|
+
function convertNutrition2(nutrition) {
|
|
1848
2665
|
if (!nutrition) {
|
|
1849
2666
|
return void 0;
|
|
1850
2667
|
}
|
|
1851
|
-
|
|
1852
|
-
...nutrition,
|
|
2668
|
+
const result = {
|
|
1853
2669
|
"@type": "NutritionInformation"
|
|
1854
2670
|
};
|
|
2671
|
+
if (nutrition.calories !== void 0) {
|
|
2672
|
+
if (typeof nutrition.calories === "number") {
|
|
2673
|
+
result.calories = `${nutrition.calories} calories`;
|
|
2674
|
+
} else {
|
|
2675
|
+
result.calories = nutrition.calories;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
Object.keys(nutrition).forEach((key) => {
|
|
2679
|
+
if (key !== "calories" && key !== "@type") {
|
|
2680
|
+
result[key] = nutrition[key];
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
return result;
|
|
1855
2684
|
}
|
|
1856
2685
|
function cleanOutput(obj) {
|
|
1857
2686
|
return Object.fromEntries(
|
|
1858
2687
|
Object.entries(obj).filter(([, value]) => value !== void 0)
|
|
1859
2688
|
);
|
|
1860
2689
|
}
|
|
2690
|
+
function getSchemaOrgMappableStacks(stacks = {}) {
|
|
2691
|
+
const mappableStackIds = /* @__PURE__ */ new Set();
|
|
2692
|
+
const mappableFromRegistry = stacks_default.stacks.filter((stack) => stack.schemaOrgMappable).map((stack) => `${stack.id}@${stack.latest}`);
|
|
2693
|
+
for (const [name, version] of Object.entries(stacks)) {
|
|
2694
|
+
const stackId = `${name}@${version}`;
|
|
2695
|
+
if (mappableFromRegistry.includes(stackId)) {
|
|
2696
|
+
mappableStackIds.add(stackId);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return mappableStackIds;
|
|
2700
|
+
}
|
|
1861
2701
|
function toSchemaOrg(recipe) {
|
|
1862
2702
|
const base = convertBasicMetadata(recipe);
|
|
1863
2703
|
const ingredients = convertIngredients2(recipe.ingredients);
|
|
1864
2704
|
const instructions = convertInstructions2(recipe.instructions);
|
|
1865
|
-
const
|
|
2705
|
+
const recipeStacks = recipe.stacks && typeof recipe.stacks === "object" && !Array.isArray(recipe.stacks) ? recipe.stacks : {};
|
|
2706
|
+
const mappableStacks = getSchemaOrgMappableStacks(recipeStacks);
|
|
2707
|
+
const hasMappableNutrition = mappableStacks.has("nutrition@1");
|
|
2708
|
+
const nutrition = hasMappableNutrition ? convertNutrition2(recipe.nutrition) : void 0;
|
|
2709
|
+
const hasMappableTimes = mappableStacks.has("times@1");
|
|
2710
|
+
const timeData = hasMappableTimes ? recipe.times ? convertTimesModule(recipe.times) : convertTime2(recipe.time) : {};
|
|
2711
|
+
const hasMappableAttribution = mappableStacks.has("attribution@1");
|
|
2712
|
+
const attributionData = hasMappableAttribution ? convertAuthor(recipe.source) : {};
|
|
2713
|
+
const hasMappableTaxonomy = mappableStacks.has("taxonomy@1");
|
|
2714
|
+
const taxonomyData = hasMappableTaxonomy ? convertCategoryTags(recipe.category, recipe.tags) : {};
|
|
1866
2715
|
return cleanOutput({
|
|
1867
2716
|
...base,
|
|
1868
2717
|
recipeIngredient: ingredients.length ? ingredients : void 0,
|
|
1869
2718
|
recipeInstructions: instructions.length ? instructions : void 0,
|
|
1870
2719
|
recipeYield: convertYield(recipe.yield),
|
|
1871
|
-
...
|
|
1872
|
-
...
|
|
1873
|
-
...
|
|
2720
|
+
...timeData,
|
|
2721
|
+
...attributionData,
|
|
2722
|
+
...taxonomyData,
|
|
1874
2723
|
nutrition
|
|
1875
2724
|
});
|
|
1876
2725
|
}
|
|
@@ -1889,8 +2738,6 @@ function isRecipeNode(value) {
|
|
|
1889
2738
|
return false;
|
|
1890
2739
|
}
|
|
1891
2740
|
const type = value["@type"];
|
|
1892
|
-
fetch("http://127.0.0.1:7243/ingest/7225c3b5-9ac2-4c94-b561-807ca9003b66", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/extractors/utils.ts:14", message: "isRecipeNode check", data: { type, typeLower: typeof type === "string" ? type.toLowerCase() : Array.isArray(type) ? type.map((t) => typeof t === "string" ? t.toLowerCase() : t) : void 0, isMatch: typeof type === "string" ? RECIPE_TYPES.has(type.toLowerCase()) : Array.isArray(type) ? type.some((e) => typeof e === "string" && RECIPE_TYPES.has(e.toLowerCase())) : false }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "A" }) }).catch(() => {
|
|
1893
|
-
});
|
|
1894
2741
|
if (typeof type === "string") {
|
|
1895
2742
|
return RECIPE_TYPES.has(type.toLowerCase());
|
|
1896
2743
|
}
|
|
@@ -1928,7 +2775,7 @@ function extractRecipeBrowser(html) {
|
|
|
1928
2775
|
return { recipe: null, source: null };
|
|
1929
2776
|
}
|
|
1930
2777
|
function extractJsonLdBrowser(html) {
|
|
1931
|
-
var
|
|
2778
|
+
var _a;
|
|
1932
2779
|
if (typeof globalThis.DOMParser === "undefined") {
|
|
1933
2780
|
return null;
|
|
1934
2781
|
}
|
|
@@ -1943,7 +2790,7 @@ function extractJsonLdBrowser(html) {
|
|
|
1943
2790
|
if (!parsed) return;
|
|
1944
2791
|
collectCandidates(parsed, candidates);
|
|
1945
2792
|
});
|
|
1946
|
-
return (
|
|
2793
|
+
return (_a = candidates[0]) != null ? _a : null;
|
|
1947
2794
|
}
|
|
1948
2795
|
function extractMicrodataBrowser(html) {
|
|
1949
2796
|
if (typeof globalThis.DOMParser === "undefined") {
|
|
@@ -1976,8 +2823,8 @@ function extractMicrodataBrowser(html) {
|
|
|
1976
2823
|
}
|
|
1977
2824
|
const instructions = [];
|
|
1978
2825
|
recipeEl.querySelectorAll('[itemprop="recipeInstructions"]').forEach((el) => {
|
|
1979
|
-
var
|
|
1980
|
-
const text = normalizeText(el.getAttribute("content")) || normalizeText(((
|
|
2826
|
+
var _a;
|
|
2827
|
+
const text = normalizeText(el.getAttribute("content")) || normalizeText(((_a = el.querySelector('[itemprop="text"]')) == null ? void 0 : _a.textContent) || void 0) || normalizeText(el.textContent || void 0);
|
|
1981
2828
|
if (text) instructions.push(text);
|
|
1982
2829
|
});
|
|
1983
2830
|
if (instructions.length) {
|
|
@@ -2019,8 +2866,521 @@ function extractSchemaOrgRecipeFromHTML(html) {
|
|
|
2019
2866
|
}
|
|
2020
2867
|
|
|
2021
2868
|
// src/specVersion.ts
|
|
2022
|
-
var SOUSTACK_SPEC_VERSION = "0.
|
|
2869
|
+
var SOUSTACK_SPEC_VERSION = "0.3.0";
|
|
2870
|
+
|
|
2871
|
+
// src/conversion/units.ts
|
|
2872
|
+
var MASS_UNITS = {
|
|
2873
|
+
g: {
|
|
2874
|
+
dimension: "mass",
|
|
2875
|
+
toMetricBase: 1,
|
|
2876
|
+
metricBaseUnit: "g",
|
|
2877
|
+
isMetric: true
|
|
2878
|
+
},
|
|
2879
|
+
kg: {
|
|
2880
|
+
dimension: "mass",
|
|
2881
|
+
toMetricBase: 1e3,
|
|
2882
|
+
metricBaseUnit: "g",
|
|
2883
|
+
isMetric: true
|
|
2884
|
+
},
|
|
2885
|
+
oz: {
|
|
2886
|
+
dimension: "mass",
|
|
2887
|
+
toMetricBase: 28.349523125,
|
|
2888
|
+
metricBaseUnit: "g",
|
|
2889
|
+
isMetric: false
|
|
2890
|
+
},
|
|
2891
|
+
lb: {
|
|
2892
|
+
dimension: "mass",
|
|
2893
|
+
toMetricBase: 453.59237,
|
|
2894
|
+
metricBaseUnit: "g",
|
|
2895
|
+
isMetric: false
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
var VOLUME_UNITS = {
|
|
2899
|
+
ml: {
|
|
2900
|
+
dimension: "volume",
|
|
2901
|
+
toMetricBase: 1,
|
|
2902
|
+
metricBaseUnit: "ml",
|
|
2903
|
+
isMetric: true
|
|
2904
|
+
},
|
|
2905
|
+
l: {
|
|
2906
|
+
dimension: "volume",
|
|
2907
|
+
toMetricBase: 1e3,
|
|
2908
|
+
metricBaseUnit: "ml",
|
|
2909
|
+
isMetric: true
|
|
2910
|
+
},
|
|
2911
|
+
tsp: {
|
|
2912
|
+
dimension: "volume",
|
|
2913
|
+
toMetricBase: 4.92892159375,
|
|
2914
|
+
metricBaseUnit: "ml",
|
|
2915
|
+
isMetric: false
|
|
2916
|
+
},
|
|
2917
|
+
tbsp: {
|
|
2918
|
+
dimension: "volume",
|
|
2919
|
+
toMetricBase: 14.78676478125,
|
|
2920
|
+
metricBaseUnit: "ml",
|
|
2921
|
+
isMetric: false
|
|
2922
|
+
},
|
|
2923
|
+
fl_oz: {
|
|
2924
|
+
dimension: "volume",
|
|
2925
|
+
toMetricBase: 29.5735295625,
|
|
2926
|
+
metricBaseUnit: "ml",
|
|
2927
|
+
isMetric: false
|
|
2928
|
+
},
|
|
2929
|
+
cup: {
|
|
2930
|
+
dimension: "volume",
|
|
2931
|
+
toMetricBase: 236.5882365,
|
|
2932
|
+
metricBaseUnit: "ml",
|
|
2933
|
+
isMetric: false
|
|
2934
|
+
},
|
|
2935
|
+
pint: {
|
|
2936
|
+
dimension: "volume",
|
|
2937
|
+
toMetricBase: 473.176473,
|
|
2938
|
+
metricBaseUnit: "ml",
|
|
2939
|
+
isMetric: false
|
|
2940
|
+
},
|
|
2941
|
+
quart: {
|
|
2942
|
+
dimension: "volume",
|
|
2943
|
+
toMetricBase: 946.352946,
|
|
2944
|
+
metricBaseUnit: "ml",
|
|
2945
|
+
isMetric: false
|
|
2946
|
+
},
|
|
2947
|
+
gallon: {
|
|
2948
|
+
dimension: "volume",
|
|
2949
|
+
toMetricBase: 3785.411784,
|
|
2950
|
+
metricBaseUnit: "ml",
|
|
2951
|
+
isMetric: false
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
var COUNT_UNITS = {
|
|
2955
|
+
clove: {
|
|
2956
|
+
dimension: "count",
|
|
2957
|
+
toMetricBase: 1,
|
|
2958
|
+
metricBaseUnit: "count",
|
|
2959
|
+
isMetric: true
|
|
2960
|
+
},
|
|
2961
|
+
sprig: {
|
|
2962
|
+
dimension: "count",
|
|
2963
|
+
toMetricBase: 1,
|
|
2964
|
+
metricBaseUnit: "count",
|
|
2965
|
+
isMetric: true
|
|
2966
|
+
},
|
|
2967
|
+
leaf: {
|
|
2968
|
+
dimension: "count",
|
|
2969
|
+
toMetricBase: 1,
|
|
2970
|
+
metricBaseUnit: "count",
|
|
2971
|
+
isMetric: true
|
|
2972
|
+
},
|
|
2973
|
+
pinch: {
|
|
2974
|
+
dimension: "count",
|
|
2975
|
+
toMetricBase: 1,
|
|
2976
|
+
metricBaseUnit: "count",
|
|
2977
|
+
isMetric: true
|
|
2978
|
+
},
|
|
2979
|
+
bottle: {
|
|
2980
|
+
dimension: "count",
|
|
2981
|
+
toMetricBase: 1,
|
|
2982
|
+
metricBaseUnit: "count",
|
|
2983
|
+
isMetric: true
|
|
2984
|
+
},
|
|
2985
|
+
count: {
|
|
2986
|
+
dimension: "count",
|
|
2987
|
+
toMetricBase: 1,
|
|
2988
|
+
metricBaseUnit: "count",
|
|
2989
|
+
isMetric: true
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
var UNIT_DEFINITIONS = {
|
|
2993
|
+
...MASS_UNITS,
|
|
2994
|
+
...VOLUME_UNITS,
|
|
2995
|
+
...COUNT_UNITS
|
|
2996
|
+
};
|
|
2997
|
+
function normalizeUnitToken(unit) {
|
|
2998
|
+
var _a;
|
|
2999
|
+
if (!unit) {
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
const token = unit.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
3003
|
+
const canonical = (_a = UNIT_SYNONYMS[token]) != null ? _a : token;
|
|
3004
|
+
return canonical in UNIT_DEFINITIONS ? canonical : null;
|
|
3005
|
+
}
|
|
3006
|
+
var UNIT_SYNONYMS = {
|
|
3007
|
+
teaspoons: "tsp",
|
|
3008
|
+
teaspoon: "tsp",
|
|
3009
|
+
tsps: "tsp",
|
|
3010
|
+
tbsp: "tbsp",
|
|
3011
|
+
tbsps: "tbsp",
|
|
3012
|
+
tablespoon: "tbsp",
|
|
3013
|
+
tablespoons: "tbsp",
|
|
3014
|
+
cup: "cup",
|
|
3015
|
+
cups: "cup",
|
|
3016
|
+
pint: "pint",
|
|
3017
|
+
pints: "pint",
|
|
3018
|
+
quart: "quart",
|
|
3019
|
+
quarts: "quart",
|
|
3020
|
+
gallon: "gallon",
|
|
3021
|
+
gallons: "gallon",
|
|
3022
|
+
ml: "ml",
|
|
3023
|
+
milliliter: "ml",
|
|
3024
|
+
milliliters: "ml",
|
|
3025
|
+
millilitre: "ml",
|
|
3026
|
+
millilitres: "ml",
|
|
3027
|
+
l: "l",
|
|
3028
|
+
liter: "l",
|
|
3029
|
+
liters: "l",
|
|
3030
|
+
litre: "l",
|
|
3031
|
+
litres: "l",
|
|
3032
|
+
fl_oz: "fl_oz",
|
|
3033
|
+
"fl.oz": "fl_oz",
|
|
3034
|
+
"fl.oz.": "fl_oz",
|
|
3035
|
+
"fl_oz.": "fl_oz",
|
|
3036
|
+
"fl oz": "fl_oz",
|
|
3037
|
+
"fl oz.": "fl_oz",
|
|
3038
|
+
fluid_ounce: "fl_oz",
|
|
3039
|
+
fluid_ounces: "fl_oz",
|
|
3040
|
+
oz: "oz",
|
|
3041
|
+
ounce: "oz",
|
|
3042
|
+
ounces: "oz",
|
|
3043
|
+
lb: "lb",
|
|
3044
|
+
lbs: "lb",
|
|
3045
|
+
pound: "lb",
|
|
3046
|
+
pounds: "lb",
|
|
3047
|
+
g: "g",
|
|
3048
|
+
gram: "g",
|
|
3049
|
+
grams: "g",
|
|
3050
|
+
kg: "kg",
|
|
3051
|
+
kilogram: "kg",
|
|
3052
|
+
kilograms: "kg",
|
|
3053
|
+
clove: "clove",
|
|
3054
|
+
cloves: "clove",
|
|
3055
|
+
sprig: "sprig",
|
|
3056
|
+
sprigs: "sprig",
|
|
3057
|
+
leaf: "leaf",
|
|
3058
|
+
leaves: "leaf",
|
|
3059
|
+
pinch: "pinch",
|
|
3060
|
+
pinches: "pinch",
|
|
3061
|
+
bottle: "bottle",
|
|
3062
|
+
bottles: "bottle",
|
|
3063
|
+
count: "count",
|
|
3064
|
+
counts: "count"
|
|
3065
|
+
};
|
|
3066
|
+
function convertToMetricBase(quantity, unit) {
|
|
3067
|
+
const definition = UNIT_DEFINITIONS[unit];
|
|
3068
|
+
const quantityInMetricBase = quantity * definition.toMetricBase;
|
|
3069
|
+
return {
|
|
3070
|
+
quantity: quantityInMetricBase,
|
|
3071
|
+
baseUnit: definition.metricBaseUnit,
|
|
3072
|
+
definition
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/conversion/convertLineItem.ts
|
|
3077
|
+
var UnknownUnitError = class extends Error {
|
|
3078
|
+
constructor(unit) {
|
|
3079
|
+
super(`Unknown unit "${unit}".`);
|
|
3080
|
+
this.unit = unit;
|
|
3081
|
+
this.name = "UnknownUnitError";
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
var UnsupportedConversionError = class extends Error {
|
|
3085
|
+
constructor(unit, mode) {
|
|
3086
|
+
super(`Cannot convert unit "${unit}" in ${mode} mode.`);
|
|
3087
|
+
this.unit = unit;
|
|
3088
|
+
this.mode = mode;
|
|
3089
|
+
this.name = "UnsupportedConversionError";
|
|
3090
|
+
}
|
|
3091
|
+
};
|
|
3092
|
+
var MissingEquivalencyError = class extends Error {
|
|
3093
|
+
constructor(ingredient, unit) {
|
|
3094
|
+
super(
|
|
3095
|
+
`No volume to mass equivalency for "${ingredient}" (${unit}).`
|
|
3096
|
+
);
|
|
3097
|
+
this.ingredient = ingredient;
|
|
3098
|
+
this.unit = unit;
|
|
3099
|
+
this.name = "MissingEquivalencyError";
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
var VOLUME_TO_MASS_EQUIV_G_PER_UNIT = {
|
|
3103
|
+
flour: {
|
|
3104
|
+
cup: 120
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
var DEFAULT_ROUND_MODE = "sane";
|
|
3108
|
+
function convertLineItemToMetric(item, mode, opts) {
|
|
3109
|
+
var _a, _b, _c, _d;
|
|
3110
|
+
const roundMode = (_a = opts == null ? void 0 : opts.round) != null ? _a : DEFAULT_ROUND_MODE;
|
|
3111
|
+
const normalizedUnit = normalizeUnitToken(item.unit);
|
|
3112
|
+
if (!normalizedUnit) {
|
|
3113
|
+
if (!item.unit || item.unit.trim() === "") {
|
|
3114
|
+
return item;
|
|
3115
|
+
}
|
|
3116
|
+
throw new UnknownUnitError(item.unit);
|
|
3117
|
+
}
|
|
3118
|
+
const definition = UNIT_DEFINITIONS[normalizedUnit];
|
|
3119
|
+
if (definition.dimension === "count") {
|
|
3120
|
+
return item;
|
|
3121
|
+
}
|
|
3122
|
+
if (mode === "volume") {
|
|
3123
|
+
if (definition.dimension !== "volume") {
|
|
3124
|
+
throw new UnsupportedConversionError((_b = item.unit) != null ? _b : "", mode);
|
|
3125
|
+
}
|
|
3126
|
+
const { quantity, unit } = finalizeMetricVolume(
|
|
3127
|
+
convertToMetricBase(item.quantity, normalizedUnit).quantity,
|
|
3128
|
+
roundMode
|
|
3129
|
+
);
|
|
3130
|
+
return {
|
|
3131
|
+
...item,
|
|
3132
|
+
quantity,
|
|
3133
|
+
unit
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
if (definition.dimension === "mass") {
|
|
3137
|
+
const { quantity, unit } = finalizeMetricMass(
|
|
3138
|
+
convertToMetricBase(item.quantity, normalizedUnit).quantity,
|
|
3139
|
+
roundMode
|
|
3140
|
+
);
|
|
3141
|
+
return {
|
|
3142
|
+
...item,
|
|
3143
|
+
quantity,
|
|
3144
|
+
unit
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
if (definition.dimension !== "volume") {
|
|
3148
|
+
throw new UnsupportedConversionError((_c = item.unit) != null ? _c : "", mode);
|
|
3149
|
+
}
|
|
3150
|
+
const gramsPerUnit = lookupEquivalency(
|
|
3151
|
+
item.ingredient,
|
|
3152
|
+
normalizedUnit
|
|
3153
|
+
);
|
|
3154
|
+
if (!gramsPerUnit) {
|
|
3155
|
+
throw new MissingEquivalencyError(item.ingredient, (_d = item.unit) != null ? _d : "");
|
|
3156
|
+
}
|
|
3157
|
+
const grams = item.quantity * gramsPerUnit;
|
|
3158
|
+
const massResult = finalizeMetricMass(grams, roundMode);
|
|
3159
|
+
return {
|
|
3160
|
+
...item,
|
|
3161
|
+
quantity: massResult.quantity,
|
|
3162
|
+
unit: massResult.unit,
|
|
3163
|
+
notes: `Converted using ${gramsPerUnit}g per ${normalizedUnit} for ${item.ingredient}.`
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
function finalizeMetricVolume(milliliters, roundMode) {
|
|
3167
|
+
if (roundMode === "none") {
|
|
3168
|
+
return milliliters >= 1e3 ? { quantity: milliliters / 1e3, unit: "l" } : { quantity: milliliters, unit: "ml" };
|
|
3169
|
+
}
|
|
3170
|
+
const roundedMl = roundMilliliters(milliliters);
|
|
3171
|
+
if (roundedMl >= 1e3) {
|
|
3172
|
+
const liters = roundedMl / 1e3;
|
|
3173
|
+
return {
|
|
3174
|
+
quantity: roundLargeMetric(liters),
|
|
3175
|
+
unit: "l"
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
return { quantity: roundedMl, unit: "ml" };
|
|
3179
|
+
}
|
|
3180
|
+
function finalizeMetricMass(grams, roundMode) {
|
|
3181
|
+
if (roundMode === "none") {
|
|
3182
|
+
return grams >= 1e3 ? { quantity: grams / 1e3, unit: "kg" } : { quantity: grams, unit: "g" };
|
|
3183
|
+
}
|
|
3184
|
+
const roundedGrams = roundGrams(grams);
|
|
3185
|
+
if (roundedGrams >= 1e3) {
|
|
3186
|
+
const kilograms = roundedGrams / 1e3;
|
|
3187
|
+
return {
|
|
3188
|
+
quantity: roundLargeMetric(kilograms),
|
|
3189
|
+
unit: "kg"
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
return { quantity: roundedGrams, unit: "g" };
|
|
3193
|
+
}
|
|
3194
|
+
function roundGrams(value) {
|
|
3195
|
+
if (value < 1e3) {
|
|
3196
|
+
return Math.round(value);
|
|
3197
|
+
}
|
|
3198
|
+
return Math.round(value / 5) * 5;
|
|
3199
|
+
}
|
|
3200
|
+
function roundMilliliters(value) {
|
|
3201
|
+
if (value < 1e3) {
|
|
3202
|
+
return Math.round(value);
|
|
3203
|
+
}
|
|
3204
|
+
return Math.round(value / 10) * 10;
|
|
3205
|
+
}
|
|
3206
|
+
function roundLargeMetric(value) {
|
|
3207
|
+
return Math.round(value * 100) / 100;
|
|
3208
|
+
}
|
|
3209
|
+
function lookupEquivalency(ingredient, unit) {
|
|
3210
|
+
var _a;
|
|
3211
|
+
const key = ingredient.trim().toLowerCase();
|
|
3212
|
+
return (_a = VOLUME_TO_MASS_EQUIV_G_PER_UNIT[key]) == null ? void 0 : _a[unit];
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// src/mise-en-place/index.ts
|
|
3216
|
+
function miseEnPlace(ingredients) {
|
|
3217
|
+
const list = Array.isArray(ingredients) ? ingredients : [];
|
|
3218
|
+
const prepGroups = /* @__PURE__ */ new Map();
|
|
3219
|
+
const stateGroups = /* @__PURE__ */ new Map();
|
|
3220
|
+
let measureTask;
|
|
3221
|
+
let otherTask;
|
|
3222
|
+
const ungrouped = [];
|
|
3223
|
+
for (const ingredient of list) {
|
|
3224
|
+
if (!ingredient || typeof ingredient !== "object") continue;
|
|
3225
|
+
const label = deriveIngredientLabel(ingredient);
|
|
3226
|
+
const quantity = normalizeQuantity(ingredient.quantity);
|
|
3227
|
+
const baseNotes = toDisplayString(ingredient.notes);
|
|
3228
|
+
const prepNotes = toDisplayString(ingredient.prep);
|
|
3229
|
+
const isOptional = typeof ingredient.optional === "boolean" ? ingredient.optional : void 0;
|
|
3230
|
+
const buildItem = (extraNotes) => {
|
|
3231
|
+
const item = {
|
|
3232
|
+
ingredient: label
|
|
3233
|
+
};
|
|
3234
|
+
if (quantity) {
|
|
3235
|
+
item.quantity = { ...quantity };
|
|
3236
|
+
}
|
|
3237
|
+
if (typeof isOptional === "boolean") {
|
|
3238
|
+
item.optional = isOptional;
|
|
3239
|
+
}
|
|
3240
|
+
const notes = combineNotes(extraNotes, baseNotes);
|
|
3241
|
+
if (notes) {
|
|
3242
|
+
item.notes = notes;
|
|
3243
|
+
}
|
|
3244
|
+
return item;
|
|
3245
|
+
};
|
|
3246
|
+
let addedToTask = false;
|
|
3247
|
+
let hasPrepGrouping = false;
|
|
3248
|
+
const prepActionKeys = extractNormalizedList(ingredient.prepActions);
|
|
3249
|
+
if (prepActionKeys.length > 0) {
|
|
3250
|
+
hasPrepGrouping = true;
|
|
3251
|
+
for (const actionKey of prepActionKeys) {
|
|
3252
|
+
const task = ensureGroup(prepGroups, actionKey, () => ({
|
|
3253
|
+
category: "prep",
|
|
3254
|
+
action: actionKey,
|
|
3255
|
+
items: []
|
|
3256
|
+
}));
|
|
3257
|
+
task.items.push(buildItem());
|
|
3258
|
+
addedToTask = true;
|
|
3259
|
+
}
|
|
3260
|
+
} else {
|
|
3261
|
+
const singleActionKey = normalizeKey(ingredient.prepAction);
|
|
3262
|
+
if (singleActionKey) {
|
|
3263
|
+
hasPrepGrouping = true;
|
|
3264
|
+
const task = ensureGroup(prepGroups, singleActionKey, () => ({
|
|
3265
|
+
category: "prep",
|
|
3266
|
+
action: singleActionKey,
|
|
3267
|
+
items: []
|
|
3268
|
+
}));
|
|
3269
|
+
task.items.push(buildItem());
|
|
3270
|
+
addedToTask = true;
|
|
3271
|
+
} else if (prepNotes) {
|
|
3272
|
+
otherTask = otherTask != null ? otherTask : { category: "other", items: [] };
|
|
3273
|
+
otherTask.items.push(buildItem(prepNotes));
|
|
3274
|
+
addedToTask = true;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
const formKey = normalizeKey(ingredient.form);
|
|
3278
|
+
const hasStateGrouping = Boolean(formKey);
|
|
3279
|
+
if (formKey) {
|
|
3280
|
+
const task = ensureGroup(stateGroups, formKey, () => ({
|
|
3281
|
+
category: "state",
|
|
3282
|
+
form: formKey,
|
|
3283
|
+
items: []
|
|
3284
|
+
}));
|
|
3285
|
+
task.items.push(buildItem());
|
|
3286
|
+
addedToTask = true;
|
|
3287
|
+
}
|
|
3288
|
+
const shouldMeasure = Boolean(quantity) && !hasPrepGrouping && !hasStateGrouping;
|
|
3289
|
+
if (shouldMeasure) {
|
|
3290
|
+
measureTask = measureTask != null ? measureTask : { category: "measure", items: [] };
|
|
3291
|
+
measureTask.items.push(buildItem());
|
|
3292
|
+
addedToTask = true;
|
|
3293
|
+
}
|
|
3294
|
+
if (!addedToTask) {
|
|
3295
|
+
ungrouped.push(ingredient);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
const tasks = [
|
|
3299
|
+
...Array.from(prepGroups.values()).sort((a, b) => localeCompare(a.action, b.action)),
|
|
3300
|
+
...Array.from(stateGroups.values()).sort((a, b) => localeCompare(a.form, b.form))
|
|
3301
|
+
];
|
|
3302
|
+
if (measureTask) {
|
|
3303
|
+
tasks.push(measureTask);
|
|
3304
|
+
}
|
|
3305
|
+
if (otherTask) {
|
|
3306
|
+
tasks.push(otherTask);
|
|
3307
|
+
}
|
|
3308
|
+
return { tasks, ungrouped };
|
|
3309
|
+
}
|
|
3310
|
+
function deriveIngredientLabel(ingredient) {
|
|
3311
|
+
var _a, _b, _c;
|
|
3312
|
+
return (_c = (_b = (_a = toDisplayString(ingredient.name)) != null ? _a : toDisplayString(ingredient.item)) != null ? _b : toDisplayString(ingredient.id)) != null ? _c : "ingredient";
|
|
3313
|
+
}
|
|
3314
|
+
function extractNormalizedList(values) {
|
|
3315
|
+
if (!Array.isArray(values)) {
|
|
3316
|
+
return [];
|
|
3317
|
+
}
|
|
3318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3319
|
+
const result = [];
|
|
3320
|
+
for (const value of values) {
|
|
3321
|
+
const key = normalizeKey(value);
|
|
3322
|
+
if (key && !seen.has(key)) {
|
|
3323
|
+
seen.add(key);
|
|
3324
|
+
result.push(key);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
return result;
|
|
3328
|
+
}
|
|
3329
|
+
function normalizeKey(value) {
|
|
3330
|
+
if (typeof value !== "string") {
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
const trimmed = value.trim().toLowerCase();
|
|
3334
|
+
return trimmed || null;
|
|
3335
|
+
}
|
|
3336
|
+
function toDisplayString(value) {
|
|
3337
|
+
if (typeof value !== "string") {
|
|
3338
|
+
return void 0;
|
|
3339
|
+
}
|
|
3340
|
+
const trimmed = value.trim();
|
|
3341
|
+
return trimmed || void 0;
|
|
3342
|
+
}
|
|
3343
|
+
function combineNotes(...notes) {
|
|
3344
|
+
const cleaned = notes.map((note) => toDisplayString(note != null ? note : void 0)).filter(Boolean);
|
|
3345
|
+
if (cleaned.length === 0) {
|
|
3346
|
+
return void 0;
|
|
3347
|
+
}
|
|
3348
|
+
return cleaned.join(" | ");
|
|
3349
|
+
}
|
|
3350
|
+
function normalizeQuantity(quantity) {
|
|
3351
|
+
if (!quantity || typeof quantity !== "object") {
|
|
3352
|
+
return void 0;
|
|
3353
|
+
}
|
|
3354
|
+
const amount = quantity.amount;
|
|
3355
|
+
if (typeof amount !== "number" || Number.isNaN(amount)) {
|
|
3356
|
+
return void 0;
|
|
3357
|
+
}
|
|
3358
|
+
const normalized = { amount };
|
|
3359
|
+
if ("unit" in quantity) {
|
|
3360
|
+
const unit = quantity.unit;
|
|
3361
|
+
if (typeof unit === "string") {
|
|
3362
|
+
const trimmed = unit.trim();
|
|
3363
|
+
if (trimmed) {
|
|
3364
|
+
normalized.unit = trimmed;
|
|
3365
|
+
}
|
|
3366
|
+
} else if (unit === null) {
|
|
3367
|
+
normalized.unit = null;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
return normalized;
|
|
3371
|
+
}
|
|
3372
|
+
function ensureGroup(map, key, factory) {
|
|
3373
|
+
let task = map.get(key);
|
|
3374
|
+
if (!task) {
|
|
3375
|
+
task = factory();
|
|
3376
|
+
map.set(key, task);
|
|
3377
|
+
}
|
|
3378
|
+
return task;
|
|
3379
|
+
}
|
|
3380
|
+
function localeCompare(left, right) {
|
|
3381
|
+
return (left != null ? left : "").localeCompare(right != null ? right : "");
|
|
3382
|
+
}
|
|
2023
3383
|
|
|
2024
|
-
export { SOUSTACK_SPEC_VERSION, detectProfiles, extractSchemaOrgRecipeFromHTML, fromSchemaOrg, scaleRecipe, toSchemaOrg, validateRecipe };
|
|
3384
|
+
export { MissingEquivalencyError, SOUSTACK_SPEC_VERSION, UnknownUnitError, UnsupportedConversionError, convertLineItemToMetric, detectProfiles, extractSchemaOrgRecipeFromHTML, fromSchemaOrg, miseEnPlace, normalizeRecipe, scaleRecipe, toSchemaOrg, validateRecipe };
|
|
2025
3385
|
//# sourceMappingURL=index.mjs.map
|
|
2026
3386
|
//# sourceMappingURL=index.mjs.map
|