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