soustack 0.3.0 → 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 +41 -24
- package/dist/cli/index.js +1703 -607
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +65 -19
- package/dist/index.d.ts +65 -19
- package/dist/index.js +1490 -587
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1489 -587
- package/dist/index.mjs.map +1 -1
- package/dist/{scrape.d.mts → scrape/index.d.mts} +8 -6
- package/dist/{scrape.d.ts → scrape/index.d.ts} +8 -6
- package/dist/{scrape.js → scrape/index.js} +170 -66
- package/dist/scrape/index.js.map +1 -0
- package/dist/{scrape.mjs → scrape/index.mjs} +170 -66
- package/dist/scrape/index.mjs.map +1 -0
- package/package.json +9 -6
- package/dist/scrape.js.map +0 -1
- package/dist/scrape.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
|
|
4
4
|
// src/parsers/duration.ts
|
|
@@ -95,17 +95,17 @@ function scaleRecipe(recipe, options = {}) {
|
|
|
95
95
|
const orderedIngredients = [];
|
|
96
96
|
collectIngredients(scaled.ingredients || [], orderedIngredients);
|
|
97
97
|
orderedIngredients.filter((ing) => {
|
|
98
|
-
var
|
|
99
|
-
return (((
|
|
98
|
+
var _a;
|
|
99
|
+
return (((_a = ing.scaling) == null ? void 0 : _a.type) || "linear") !== "bakers_percentage";
|
|
100
100
|
}).forEach((ing) => {
|
|
101
101
|
const key = getIngredientKey(ing);
|
|
102
102
|
scaledAmounts.set(key, calculateIndependentIngredient(ing, multiplier));
|
|
103
103
|
});
|
|
104
104
|
orderedIngredients.filter((ing) => {
|
|
105
|
-
var
|
|
106
|
-
return ((
|
|
105
|
+
var _a;
|
|
106
|
+
return ((_a = ing.scaling) == null ? void 0 : _a.type) === "bakers_percentage";
|
|
107
107
|
}).forEach((ing) => {
|
|
108
|
-
var
|
|
108
|
+
var _a, _b;
|
|
109
109
|
const key = getIngredientKey(ing);
|
|
110
110
|
const scaling = ing.scaling;
|
|
111
111
|
if (!(scaling == null ? void 0 : scaling.referenceId)) {
|
|
@@ -115,9 +115,9 @@ function scaleRecipe(recipe, options = {}) {
|
|
|
115
115
|
if (referenceAmount === void 0) {
|
|
116
116
|
throw new Error(`Reference ingredient "${scaling.referenceId}" not found for baker's percentage item "${key}"`);
|
|
117
117
|
}
|
|
118
|
-
const baseAmount = ((
|
|
118
|
+
const baseAmount = ((_a = ing.quantity) == null ? void 0 : _a.amount) || 0;
|
|
119
119
|
const referenceBase = baseAmounts.get(scaling.referenceId);
|
|
120
|
-
const factor = (
|
|
120
|
+
const factor = (_b = scaling.factor) != null ? _b : referenceBase ? baseAmount / referenceBase : void 0;
|
|
121
121
|
if (factor === void 0) {
|
|
122
122
|
throw new Error(`Unable to determine factor for baker's percentage ingredient "${key}"`);
|
|
123
123
|
}
|
|
@@ -137,19 +137,19 @@ function scaleRecipe(recipe, options = {}) {
|
|
|
137
137
|
return scaled;
|
|
138
138
|
}
|
|
139
139
|
function resolveMultiplier(recipe, options) {
|
|
140
|
-
var
|
|
140
|
+
var _a, _b;
|
|
141
141
|
if (options.multiplier && options.multiplier > 0) {
|
|
142
142
|
return options.multiplier;
|
|
143
143
|
}
|
|
144
|
-
if ((
|
|
145
|
-
const base = ((
|
|
144
|
+
if ((_a = options.targetYield) == null ? void 0 : _a.amount) {
|
|
145
|
+
const base = ((_b = recipe.yield) == null ? void 0 : _b.amount) || 1;
|
|
146
146
|
return options.targetYield.amount / base;
|
|
147
147
|
}
|
|
148
148
|
return 1;
|
|
149
149
|
}
|
|
150
150
|
function applyYieldScaling(recipe, options, multiplier) {
|
|
151
|
-
var
|
|
152
|
-
const baseAmount = (
|
|
151
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
152
|
+
const baseAmount = (_b = (_a = recipe.yield) == null ? void 0 : _a.amount) != null ? _b : 1;
|
|
153
153
|
const targetAmount = (_d = (_c = options.targetYield) == null ? void 0 : _c.amount) != null ? _d : baseAmount * multiplier;
|
|
154
154
|
const unit = (_g = (_e = options.targetYield) == null ? void 0 : _e.unit) != null ? _g : (_f = recipe.yield) == null ? void 0 : _f.unit;
|
|
155
155
|
if (!recipe.yield && !options.targetYield) return;
|
|
@@ -162,9 +162,9 @@ function getIngredientKey(ing) {
|
|
|
162
162
|
return ing.id || ing.item;
|
|
163
163
|
}
|
|
164
164
|
function calculateIndependentIngredient(ing, multiplier) {
|
|
165
|
-
var
|
|
166
|
-
const baseAmount = ((
|
|
167
|
-
const type = ((
|
|
165
|
+
var _a, _b, _c, _d, _e, _f;
|
|
166
|
+
const baseAmount = ((_a = ing.quantity) == null ? void 0 : _a.amount) || 0;
|
|
167
|
+
const type = ((_b = ing.scaling) == null ? void 0 : _b.type) || "linear";
|
|
168
168
|
switch (type) {
|
|
169
169
|
case "fixed":
|
|
170
170
|
return baseAmount;
|
|
@@ -194,12 +194,12 @@ function collectIngredients(items, bucket) {
|
|
|
194
194
|
}
|
|
195
195
|
function collectBaseIngredientAmounts(items, map = /* @__PURE__ */ new Map()) {
|
|
196
196
|
items.forEach((item) => {
|
|
197
|
-
var
|
|
197
|
+
var _a, _b;
|
|
198
198
|
if (typeof item === "string") return;
|
|
199
199
|
if ("subsection" in item) {
|
|
200
200
|
collectBaseIngredientAmounts(item.items, map);
|
|
201
201
|
} else {
|
|
202
|
-
map.set(getIngredientKey(item), (
|
|
202
|
+
map.set(getIngredientKey(item), (_b = (_a = item.quantity) == null ? void 0 : _a.amount) != null ? _b : 0);
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
return map;
|
|
@@ -240,12 +240,680 @@ function toDurationMinutes(duration) {
|
|
|
240
240
|
return 0;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// src/normalize.ts
|
|
244
|
+
function normalizeRecipe(input) {
|
|
245
|
+
if (!input || typeof input !== "object") {
|
|
246
|
+
throw new Error("Recipe input must be an object");
|
|
247
|
+
}
|
|
248
|
+
const recipe = JSON.parse(JSON.stringify(input));
|
|
249
|
+
const warnings = [];
|
|
250
|
+
const legacyField = ["mod", "ules"].join("");
|
|
251
|
+
if (legacyField in recipe) {
|
|
252
|
+
throw new Error("The legacy field is no longer supported. Use `stacks` instead.");
|
|
253
|
+
}
|
|
254
|
+
normalizeStacks(recipe, warnings);
|
|
255
|
+
if (!recipe.stacks) {
|
|
256
|
+
recipe.stacks = {};
|
|
257
|
+
}
|
|
258
|
+
if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
|
|
259
|
+
recipe.recipeVersion = recipe.version;
|
|
260
|
+
warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
|
|
261
|
+
}
|
|
262
|
+
normalizeTime(recipe);
|
|
263
|
+
return {
|
|
264
|
+
recipe,
|
|
265
|
+
warnings
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function normalizeStacks(recipe, warnings) {
|
|
269
|
+
let stacks = {};
|
|
270
|
+
if (recipe.stacks && typeof recipe.stacks === "object" && !Array.isArray(recipe.stacks)) {
|
|
271
|
+
for (const [key, value] of Object.entries(recipe.stacks)) {
|
|
272
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
|
|
273
|
+
stacks[key] = value;
|
|
274
|
+
} else {
|
|
275
|
+
warnings.push(`Invalid stack version for '${key}': expected positive integer, got ${value}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(recipe.stacks)) {
|
|
280
|
+
const stackIdentifiers = recipe.stacks.filter((s) => typeof s === "string");
|
|
281
|
+
for (const identifier of stackIdentifiers) {
|
|
282
|
+
const parsed = parseStackIdentifier(identifier);
|
|
283
|
+
if (parsed) {
|
|
284
|
+
const { name, version } = parsed;
|
|
285
|
+
if (!stacks[name] || stacks[name] < version) {
|
|
286
|
+
stacks[name] = version;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
warnings.push(`Invalid stack identifier '${identifier}': expected format 'name@version' (e.g., 'scaling@1')`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
recipe.stacks = stacks;
|
|
294
|
+
}
|
|
295
|
+
function parseStackIdentifier(identifier) {
|
|
296
|
+
if (typeof identifier !== "string" || !identifier.trim()) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const match = identifier.trim().match(/^([a-z0-9_-]+)@(\d+)$/i);
|
|
300
|
+
if (!match) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const [, name, versionStr] = match;
|
|
304
|
+
const version = parseInt(versionStr, 10);
|
|
305
|
+
if (isNaN(version) || version < 1) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return { name, version };
|
|
309
|
+
}
|
|
310
|
+
function normalizeTime(recipe) {
|
|
311
|
+
const time = recipe == null ? void 0 : recipe.time;
|
|
312
|
+
if (!time || typeof time !== "object" || Array.isArray(time)) return;
|
|
313
|
+
const structuredKeys = [
|
|
314
|
+
"prep",
|
|
315
|
+
"active",
|
|
316
|
+
"passive",
|
|
317
|
+
"total"
|
|
318
|
+
];
|
|
319
|
+
structuredKeys.forEach((key) => {
|
|
320
|
+
const value = time[key];
|
|
321
|
+
if (typeof value === "number") return;
|
|
322
|
+
const parsed = parseDuration(value);
|
|
323
|
+
if (parsed !== null) {
|
|
324
|
+
time[key] = parsed;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/conformance/index.ts
|
|
330
|
+
function validateConformance(recipe) {
|
|
331
|
+
const issues = [];
|
|
332
|
+
issues.push(...checkDAGValidity(recipe));
|
|
333
|
+
if (hasSchedulableProfile(recipe)) {
|
|
334
|
+
issues.push(...checkTimingSchedulability(recipe));
|
|
335
|
+
}
|
|
336
|
+
issues.push(...checkScalingSanity(recipe));
|
|
337
|
+
const ok = issues.filter((i) => i.severity === "error").length === 0;
|
|
338
|
+
return { ok, issues };
|
|
339
|
+
}
|
|
340
|
+
function hasSchedulableProfile(recipe) {
|
|
341
|
+
const schema = recipe.$schema;
|
|
342
|
+
if (typeof schema === "string") {
|
|
343
|
+
return schema.includes("schedulable") || schema === "http://soustack.org/schema/v0.3.0/profiles/schedulable";
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
function checkDAGValidity(recipe) {
|
|
348
|
+
const issues = [];
|
|
349
|
+
const instructions = recipe.instructions;
|
|
350
|
+
if (!Array.isArray(instructions)) {
|
|
351
|
+
return issues;
|
|
352
|
+
}
|
|
353
|
+
const instructionIds = /* @__PURE__ */ new Set();
|
|
354
|
+
const dependencyRefs = [];
|
|
355
|
+
const collect = (items, basePath) => {
|
|
356
|
+
items.forEach((item, index) => {
|
|
357
|
+
const currentPath = `${basePath}/${index}`;
|
|
358
|
+
if (isInstructionSubsection(item)) {
|
|
359
|
+
if (Array.isArray(item.items)) {
|
|
360
|
+
collect(item.items, `${currentPath}/items`);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (isInstruction(item)) {
|
|
365
|
+
const id = typeof item.id === "string" ? item.id : void 0;
|
|
366
|
+
if (id) {
|
|
367
|
+
instructionIds.add(id);
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(item.dependsOn)) {
|
|
370
|
+
item.dependsOn.forEach((depId, depIndex) => {
|
|
371
|
+
if (typeof depId === "string") {
|
|
372
|
+
dependencyRefs.push({
|
|
373
|
+
fromId: id,
|
|
374
|
+
toId: depId,
|
|
375
|
+
path: `${currentPath}/dependsOn/${depIndex}`
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
collect(instructions, "/instructions");
|
|
384
|
+
dependencyRefs.forEach((ref) => {
|
|
385
|
+
if (!instructionIds.has(ref.toId)) {
|
|
386
|
+
issues.push({
|
|
387
|
+
code: "DAG_MISSING_NODE",
|
|
388
|
+
path: ref.path,
|
|
389
|
+
message: `Instruction dependency references missing step id '${ref.toId}'.`,
|
|
390
|
+
severity: "error"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
395
|
+
dependencyRefs.forEach((ref) => {
|
|
396
|
+
var _a;
|
|
397
|
+
if (ref.fromId && instructionIds.has(ref.fromId) && instructionIds.has(ref.toId)) {
|
|
398
|
+
const list = (_a = adjacency.get(ref.fromId)) != null ? _a : [];
|
|
399
|
+
list.push({ toId: ref.toId, path: ref.path });
|
|
400
|
+
adjacency.set(ref.fromId, list);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
404
|
+
const visited = /* @__PURE__ */ new Set();
|
|
405
|
+
const detectCycles = (nodeId) => {
|
|
406
|
+
var _a;
|
|
407
|
+
if (visiting.has(nodeId)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (visited.has(nodeId)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
visiting.add(nodeId);
|
|
414
|
+
const neighbors = (_a = adjacency.get(nodeId)) != null ? _a : [];
|
|
415
|
+
neighbors.forEach((edge) => {
|
|
416
|
+
if (visiting.has(edge.toId)) {
|
|
417
|
+
issues.push({
|
|
418
|
+
code: "DAG_CYCLE",
|
|
419
|
+
path: edge.path,
|
|
420
|
+
message: `Circular dependency detected involving step id '${edge.toId}'.`,
|
|
421
|
+
severity: "error"
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
detectCycles(edge.toId);
|
|
426
|
+
});
|
|
427
|
+
visiting.delete(nodeId);
|
|
428
|
+
visited.add(nodeId);
|
|
429
|
+
};
|
|
430
|
+
instructionIds.forEach((id) => detectCycles(id));
|
|
431
|
+
return issues;
|
|
432
|
+
}
|
|
433
|
+
function checkTimingSchedulability(recipe) {
|
|
434
|
+
const issues = [];
|
|
435
|
+
const instructions = recipe.instructions;
|
|
436
|
+
if (!Array.isArray(instructions)) {
|
|
437
|
+
return issues;
|
|
438
|
+
}
|
|
439
|
+
const checkInstruction = (item, path) => {
|
|
440
|
+
if (isInstructionSubsection(item)) {
|
|
441
|
+
if (Array.isArray(item.items)) {
|
|
442
|
+
item.items.forEach((subItem, index) => {
|
|
443
|
+
checkInstruction(subItem, `${path}/items/${index}`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (isInstruction(item)) {
|
|
449
|
+
if (!item.id) {
|
|
450
|
+
issues.push({
|
|
451
|
+
code: "SCHEDULABLE_MISSING_ID",
|
|
452
|
+
path,
|
|
453
|
+
message: "Schedulable profile requires all instructions to have an id.",
|
|
454
|
+
severity: "error"
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (!item.timing) {
|
|
458
|
+
issues.push({
|
|
459
|
+
code: "SCHEDULABLE_MISSING_TIMING",
|
|
460
|
+
path,
|
|
461
|
+
message: "Schedulable profile requires all instructions to have timing information.",
|
|
462
|
+
severity: "error"
|
|
463
|
+
});
|
|
464
|
+
} else if (!item.timing.duration) {
|
|
465
|
+
issues.push({
|
|
466
|
+
code: "SCHEDULABLE_MISSING_DURATION",
|
|
467
|
+
path: `${path}/timing`,
|
|
468
|
+
message: "Schedulable profile requires timing.duration for all instructions.",
|
|
469
|
+
severity: "error"
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
instructions.forEach((item, index) => {
|
|
475
|
+
checkInstruction(item, `/instructions/${index}`);
|
|
476
|
+
});
|
|
477
|
+
return issues;
|
|
478
|
+
}
|
|
479
|
+
function checkScalingSanity(recipe) {
|
|
480
|
+
const issues = [];
|
|
481
|
+
const ingredients = recipe.ingredients;
|
|
482
|
+
if (!Array.isArray(ingredients)) {
|
|
483
|
+
return issues;
|
|
484
|
+
}
|
|
485
|
+
const ingredientIds = /* @__PURE__ */ new Set();
|
|
486
|
+
const collectIngredientIds = (items, basePath) => {
|
|
487
|
+
items.forEach((item, index) => {
|
|
488
|
+
if (isIngredientSubsection(item)) {
|
|
489
|
+
if (Array.isArray(item.items)) {
|
|
490
|
+
collectIngredientIds(item.items);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (isIngredient(item)) {
|
|
495
|
+
if (typeof item.id === "string") {
|
|
496
|
+
ingredientIds.add(item.id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
collectIngredientIds(ingredients);
|
|
502
|
+
const checkIngredient = (item, path) => {
|
|
503
|
+
if (isIngredientSubsection(item)) {
|
|
504
|
+
if (Array.isArray(item.items)) {
|
|
505
|
+
item.items.forEach((subItem, index) => {
|
|
506
|
+
checkIngredient(subItem, `${path}/items/${index}`);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (isIngredient(item)) {
|
|
512
|
+
const scaling = item.scaling;
|
|
513
|
+
if (scaling && typeof scaling === "object" && "type" in scaling && scaling.type === "bakers_percentage") {
|
|
514
|
+
const bakersScaling = scaling;
|
|
515
|
+
if (bakersScaling.referenceId) {
|
|
516
|
+
if (!ingredientIds.has(bakersScaling.referenceId)) {
|
|
517
|
+
issues.push({
|
|
518
|
+
code: "SCALING_INVALID_REFERENCE",
|
|
519
|
+
path: `${path}/scaling/referenceId`,
|
|
520
|
+
message: `Baker's percentage references missing ingredient id '${bakersScaling.referenceId}'.`,
|
|
521
|
+
severity: "error"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
issues.push({
|
|
526
|
+
code: "SCALING_MISSING_REFERENCE",
|
|
527
|
+
path: `${path}/scaling`,
|
|
528
|
+
message: "Baker's percentage scaling requires a referenceId.",
|
|
529
|
+
severity: "error"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
ingredients.forEach((item, index) => {
|
|
536
|
+
checkIngredient(item, `/ingredients/${index}`);
|
|
537
|
+
});
|
|
538
|
+
return issues;
|
|
539
|
+
}
|
|
540
|
+
function isInstruction(item) {
|
|
541
|
+
return item && typeof item === "object" && !Array.isArray(item) && "text" in item;
|
|
542
|
+
}
|
|
543
|
+
function isInstructionSubsection(item) {
|
|
544
|
+
return item && typeof item === "object" && !Array.isArray(item) && "items" in item && "subsection" in item;
|
|
545
|
+
}
|
|
546
|
+
function isIngredient(item) {
|
|
547
|
+
return item && typeof item === "object" && !Array.isArray(item) && "item" in item;
|
|
548
|
+
}
|
|
549
|
+
function isIngredientSubsection(item) {
|
|
550
|
+
return item && typeof item === "object" && !Array.isArray(item) && "items" in item && "subsection" in item;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/soustack.schema.json
|
|
554
|
+
var soustack_schema_default = {
|
|
555
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
556
|
+
$id: "http://soustack.org/schema/v0.3.0",
|
|
557
|
+
title: "Soustack Recipe Schema v0.3.0",
|
|
558
|
+
description: "A portable, scalable, interoperable recipe format.",
|
|
559
|
+
type: "object",
|
|
560
|
+
required: ["name", "ingredients", "instructions"],
|
|
561
|
+
additionalProperties: false,
|
|
562
|
+
patternProperties: {
|
|
563
|
+
"^x-": {}
|
|
564
|
+
},
|
|
565
|
+
properties: {
|
|
566
|
+
$schema: {
|
|
567
|
+
type: "string",
|
|
568
|
+
format: "uri",
|
|
569
|
+
description: "Optional schema hint for tooling compatibility"
|
|
570
|
+
},
|
|
571
|
+
id: {
|
|
572
|
+
type: "string",
|
|
573
|
+
description: "Unique identifier (slug or UUID)"
|
|
574
|
+
},
|
|
575
|
+
name: {
|
|
576
|
+
type: "string",
|
|
577
|
+
description: "The title of the recipe"
|
|
578
|
+
},
|
|
579
|
+
title: {
|
|
580
|
+
type: "string",
|
|
581
|
+
description: "Optional display title; alias for name"
|
|
582
|
+
},
|
|
583
|
+
version: {
|
|
584
|
+
type: "string",
|
|
585
|
+
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
586
|
+
description: "DEPRECATED: use recipeVersion for authoring revisions"
|
|
587
|
+
},
|
|
588
|
+
recipeVersion: {
|
|
589
|
+
type: "string",
|
|
590
|
+
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
591
|
+
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
592
|
+
},
|
|
593
|
+
description: {
|
|
594
|
+
type: "string"
|
|
595
|
+
},
|
|
596
|
+
category: {
|
|
597
|
+
type: "string",
|
|
598
|
+
examples: ["Main Course", "Dessert"]
|
|
599
|
+
},
|
|
600
|
+
tags: {
|
|
601
|
+
type: "array",
|
|
602
|
+
items: { type: "string" }
|
|
603
|
+
},
|
|
604
|
+
image: {
|
|
605
|
+
description: "Recipe-level hero image(s)",
|
|
606
|
+
anyOf: [
|
|
607
|
+
{
|
|
608
|
+
type: "string",
|
|
609
|
+
format: "uri"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
type: "array",
|
|
613
|
+
minItems: 1,
|
|
614
|
+
items: {
|
|
615
|
+
type: "string",
|
|
616
|
+
format: "uri"
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
dateAdded: {
|
|
622
|
+
type: "string",
|
|
623
|
+
format: "date-time"
|
|
624
|
+
},
|
|
625
|
+
metadata: {
|
|
626
|
+
type: "object",
|
|
627
|
+
additionalProperties: true,
|
|
628
|
+
description: "Free-form vendor metadata"
|
|
629
|
+
},
|
|
630
|
+
source: {
|
|
631
|
+
type: "object",
|
|
632
|
+
properties: {
|
|
633
|
+
author: { type: "string" },
|
|
634
|
+
url: { type: "string", format: "uri" },
|
|
635
|
+
name: { type: "string" },
|
|
636
|
+
adapted: { type: "boolean" }
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
yield: {
|
|
640
|
+
$ref: "#/definitions/yield"
|
|
641
|
+
},
|
|
642
|
+
time: {
|
|
643
|
+
$ref: "#/definitions/time"
|
|
644
|
+
},
|
|
645
|
+
equipment: {
|
|
646
|
+
type: "array",
|
|
647
|
+
items: { $ref: "#/definitions/equipment" }
|
|
648
|
+
},
|
|
649
|
+
ingredients: {
|
|
650
|
+
type: "array",
|
|
651
|
+
items: {
|
|
652
|
+
anyOf: [
|
|
653
|
+
{ type: "string" },
|
|
654
|
+
{ $ref: "#/definitions/ingredient" },
|
|
655
|
+
{ $ref: "#/definitions/ingredientSubsection" }
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
instructions: {
|
|
660
|
+
type: "array",
|
|
661
|
+
items: {
|
|
662
|
+
anyOf: [
|
|
663
|
+
{ type: "string" },
|
|
664
|
+
{ $ref: "#/definitions/instruction" },
|
|
665
|
+
{ $ref: "#/definitions/instructionSubsection" }
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
storage: {
|
|
670
|
+
$ref: "#/definitions/storage"
|
|
671
|
+
},
|
|
672
|
+
substitutions: {
|
|
673
|
+
type: "array",
|
|
674
|
+
items: { $ref: "#/definitions/substitution" }
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
definitions: {
|
|
678
|
+
yield: {
|
|
679
|
+
type: "object",
|
|
680
|
+
required: ["amount", "unit"],
|
|
681
|
+
properties: {
|
|
682
|
+
amount: { type: "number" },
|
|
683
|
+
unit: { type: "string" },
|
|
684
|
+
servings: { type: "number" },
|
|
685
|
+
description: { type: "string" }
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
time: {
|
|
689
|
+
type: "object",
|
|
690
|
+
properties: {
|
|
691
|
+
prep: { type: "number" },
|
|
692
|
+
active: { type: "number" },
|
|
693
|
+
passive: { type: "number" },
|
|
694
|
+
total: { type: "number" },
|
|
695
|
+
prepTime: { type: "string", format: "duration" },
|
|
696
|
+
cookTime: { type: "string", format: "duration" }
|
|
697
|
+
},
|
|
698
|
+
minProperties: 1
|
|
699
|
+
},
|
|
700
|
+
quantity: {
|
|
701
|
+
type: "object",
|
|
702
|
+
required: ["amount"],
|
|
703
|
+
properties: {
|
|
704
|
+
amount: { type: "number" },
|
|
705
|
+
unit: {
|
|
706
|
+
type: ["string", "null"],
|
|
707
|
+
description: "Display-friendly unit text; implementations may normalize or canonicalize units separately."
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
scaling: {
|
|
712
|
+
type: "object",
|
|
713
|
+
required: ["type"],
|
|
714
|
+
properties: {
|
|
715
|
+
type: {
|
|
716
|
+
type: "string",
|
|
717
|
+
enum: ["linear", "discrete", "proportional", "fixed", "bakers_percentage"]
|
|
718
|
+
},
|
|
719
|
+
factor: { type: "number" },
|
|
720
|
+
referenceId: { type: "string" },
|
|
721
|
+
roundTo: { type: "number" },
|
|
722
|
+
min: { type: "number" },
|
|
723
|
+
max: { type: "number" }
|
|
724
|
+
},
|
|
725
|
+
if: {
|
|
726
|
+
properties: { type: { const: "bakers_percentage" } }
|
|
727
|
+
},
|
|
728
|
+
then: {
|
|
729
|
+
required: ["referenceId"]
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
ingredient: {
|
|
733
|
+
type: "object",
|
|
734
|
+
required: ["item"],
|
|
735
|
+
properties: {
|
|
736
|
+
id: { type: "string" },
|
|
737
|
+
item: { type: "string" },
|
|
738
|
+
quantity: { $ref: "#/definitions/quantity" },
|
|
739
|
+
name: { type: "string" },
|
|
740
|
+
aisle: { type: "string" },
|
|
741
|
+
prep: { type: "string" },
|
|
742
|
+
prepAction: { type: "string" },
|
|
743
|
+
prepActions: {
|
|
744
|
+
type: "array",
|
|
745
|
+
items: { type: "string" },
|
|
746
|
+
description: "Structured prep verbs (e.g., peel, dice) for mise en place workflows."
|
|
747
|
+
},
|
|
748
|
+
prepTime: { type: "number" },
|
|
749
|
+
form: {
|
|
750
|
+
type: "string",
|
|
751
|
+
description: "State of the ingredient as used (packed, sifted, melted, room_temperature, etc.)."
|
|
752
|
+
},
|
|
753
|
+
destination: { type: "string" },
|
|
754
|
+
scaling: { $ref: "#/definitions/scaling" },
|
|
755
|
+
critical: { type: "boolean" },
|
|
756
|
+
optional: { type: "boolean" },
|
|
757
|
+
notes: { type: "string" }
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
ingredientSubsection: {
|
|
761
|
+
type: "object",
|
|
762
|
+
required: ["subsection", "items"],
|
|
763
|
+
properties: {
|
|
764
|
+
subsection: { type: "string" },
|
|
765
|
+
items: {
|
|
766
|
+
type: "array",
|
|
767
|
+
items: { $ref: "#/definitions/ingredient" }
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
equipment: {
|
|
772
|
+
type: "object",
|
|
773
|
+
required: ["name"],
|
|
774
|
+
properties: {
|
|
775
|
+
id: { type: "string" },
|
|
776
|
+
name: { type: "string" },
|
|
777
|
+
required: { type: "boolean" },
|
|
778
|
+
label: { type: "string" },
|
|
779
|
+
capacity: { $ref: "#/definitions/quantity" },
|
|
780
|
+
scalingLimit: { type: "number" },
|
|
781
|
+
alternatives: {
|
|
782
|
+
type: "array",
|
|
783
|
+
items: { type: "string" }
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
instruction: {
|
|
788
|
+
type: "object",
|
|
789
|
+
required: ["text"],
|
|
790
|
+
properties: {
|
|
791
|
+
id: { type: "string" },
|
|
792
|
+
text: { type: "string" },
|
|
793
|
+
image: {
|
|
794
|
+
type: "string",
|
|
795
|
+
format: "uri",
|
|
796
|
+
description: "Optional image that illustrates this instruction"
|
|
797
|
+
},
|
|
798
|
+
destination: { type: "string" },
|
|
799
|
+
dependsOn: {
|
|
800
|
+
type: "array",
|
|
801
|
+
items: { type: "string" }
|
|
802
|
+
},
|
|
803
|
+
inputs: {
|
|
804
|
+
type: "array",
|
|
805
|
+
items: { type: "string" }
|
|
806
|
+
},
|
|
807
|
+
timing: {
|
|
808
|
+
type: "object",
|
|
809
|
+
required: ["duration", "type"],
|
|
810
|
+
properties: {
|
|
811
|
+
duration: {
|
|
812
|
+
anyOf: [
|
|
813
|
+
{ type: "number" },
|
|
814
|
+
{ type: "string", pattern: "^P" }
|
|
815
|
+
],
|
|
816
|
+
description: "Minutes as a number or ISO8601 duration string"
|
|
817
|
+
},
|
|
818
|
+
type: { type: "string", enum: ["active", "passive"] },
|
|
819
|
+
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
instructionSubsection: {
|
|
825
|
+
type: "object",
|
|
826
|
+
required: ["subsection", "items"],
|
|
827
|
+
properties: {
|
|
828
|
+
subsection: { type: "string" },
|
|
829
|
+
items: {
|
|
830
|
+
type: "array",
|
|
831
|
+
items: {
|
|
832
|
+
anyOf: [
|
|
833
|
+
{ type: "string" },
|
|
834
|
+
{ $ref: "#/definitions/instruction" }
|
|
835
|
+
]
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
storage: {
|
|
841
|
+
type: "object",
|
|
842
|
+
properties: {
|
|
843
|
+
roomTemp: { $ref: "#/definitions/storageMethod" },
|
|
844
|
+
refrigerated: { $ref: "#/definitions/storageMethod" },
|
|
845
|
+
frozen: {
|
|
846
|
+
allOf: [
|
|
847
|
+
{ $ref: "#/definitions/storageMethod" },
|
|
848
|
+
{
|
|
849
|
+
type: "object",
|
|
850
|
+
properties: { thawing: { type: "string" } }
|
|
851
|
+
}
|
|
852
|
+
]
|
|
853
|
+
},
|
|
854
|
+
reheating: { type: "string" },
|
|
855
|
+
makeAhead: {
|
|
856
|
+
type: "array",
|
|
857
|
+
items: {
|
|
858
|
+
allOf: [
|
|
859
|
+
{ $ref: "#/definitions/storageMethod" },
|
|
860
|
+
{
|
|
861
|
+
type: "object",
|
|
862
|
+
required: ["component", "storage"],
|
|
863
|
+
properties: {
|
|
864
|
+
component: { type: "string" },
|
|
865
|
+
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
]
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
storageMethod: {
|
|
874
|
+
type: "object",
|
|
875
|
+
required: ["duration"],
|
|
876
|
+
properties: {
|
|
877
|
+
duration: { type: "string", pattern: "^P" },
|
|
878
|
+
method: { type: "string" },
|
|
879
|
+
notes: { type: "string" }
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
substitution: {
|
|
883
|
+
type: "object",
|
|
884
|
+
required: ["ingredient"],
|
|
885
|
+
properties: {
|
|
886
|
+
ingredient: { type: "string" },
|
|
887
|
+
critical: { type: "boolean" },
|
|
888
|
+
notes: { type: "string" },
|
|
889
|
+
alternatives: {
|
|
890
|
+
type: "array",
|
|
891
|
+
items: {
|
|
892
|
+
type: "object",
|
|
893
|
+
required: ["name", "ratio"],
|
|
894
|
+
properties: {
|
|
895
|
+
name: { type: "string" },
|
|
896
|
+
ratio: { type: "string" },
|
|
897
|
+
notes: { type: "string" },
|
|
898
|
+
impact: { type: "string" },
|
|
899
|
+
dietary: {
|
|
900
|
+
type: "array",
|
|
901
|
+
items: { type: "string" }
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
243
911
|
// src/schemas/recipe/base.schema.json
|
|
244
912
|
var base_schema_default = {
|
|
245
913
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
246
914
|
$id: "http://soustack.org/schema/recipe/base.schema.json",
|
|
247
915
|
title: "Soustack Recipe Base Schema",
|
|
248
|
-
description: "Base document shape for Soustack recipe documents. Profiles and
|
|
916
|
+
description: "Base document shape for Soustack recipe documents. Profiles and stacks build on this baseline.",
|
|
249
917
|
type: "object",
|
|
250
918
|
additionalProperties: true,
|
|
251
919
|
properties: {
|
|
@@ -257,11 +925,12 @@ var base_schema_default = {
|
|
|
257
925
|
type: "string",
|
|
258
926
|
description: "Profile identifier applied to this recipe"
|
|
259
927
|
},
|
|
260
|
-
|
|
261
|
-
type: "
|
|
262
|
-
description: "
|
|
263
|
-
|
|
264
|
-
type: "
|
|
928
|
+
stacks: {
|
|
929
|
+
type: "object",
|
|
930
|
+
description: "Stack declarations as a map: Record<stackName, versionNumber>",
|
|
931
|
+
additionalProperties: {
|
|
932
|
+
type: "integer",
|
|
933
|
+
minimum: 1
|
|
265
934
|
}
|
|
266
935
|
},
|
|
267
936
|
name: {
|
|
@@ -270,50 +939,22 @@ var base_schema_default = {
|
|
|
270
939
|
},
|
|
271
940
|
ingredients: {
|
|
272
941
|
type: "array",
|
|
273
|
-
description: "Ingredients payload; content is validated by profiles/
|
|
942
|
+
description: "Ingredients payload; content is validated by profiles/stacks"
|
|
274
943
|
},
|
|
275
944
|
instructions: {
|
|
276
945
|
type: "array",
|
|
277
|
-
description: "Instruction payload; content is validated by profiles/
|
|
946
|
+
description: "Instruction payload; content is validated by profiles/stacks"
|
|
278
947
|
}
|
|
279
948
|
},
|
|
280
949
|
required: ["@type"]
|
|
281
950
|
};
|
|
282
951
|
|
|
283
|
-
// src/schemas/recipe/profiles/core.schema.json
|
|
284
|
-
var core_schema_default = {
|
|
285
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
286
|
-
$id: "http://soustack.org/schema/recipe/profiles/core.schema.json",
|
|
287
|
-
title: "Soustack Recipe Core Profile",
|
|
288
|
-
description: "Core profile that builds on the minimal profile and is intended to be combined with recipe modules.",
|
|
289
|
-
allOf: [
|
|
290
|
-
{ $ref: "http://soustack.org/schema/recipe/base.schema.json" },
|
|
291
|
-
{
|
|
292
|
-
type: "object",
|
|
293
|
-
properties: {
|
|
294
|
-
profile: { const: "core" },
|
|
295
|
-
modules: {
|
|
296
|
-
type: "array",
|
|
297
|
-
items: { type: "string" },
|
|
298
|
-
uniqueItems: true,
|
|
299
|
-
default: []
|
|
300
|
-
},
|
|
301
|
-
name: { type: "string", minLength: 1 },
|
|
302
|
-
ingredients: { type: "array", minItems: 1 },
|
|
303
|
-
instructions: { type: "array", minItems: 1 }
|
|
304
|
-
},
|
|
305
|
-
required: ["profile", "name", "ingredients", "instructions"],
|
|
306
|
-
additionalProperties: true
|
|
307
|
-
}
|
|
308
|
-
]
|
|
309
|
-
};
|
|
310
|
-
|
|
311
952
|
// src/schemas/recipe/profiles/minimal.schema.json
|
|
312
953
|
var minimal_schema_default = {
|
|
313
954
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
314
955
|
$id: "http://soustack.org/schema/recipe/profiles/minimal.schema.json",
|
|
315
956
|
title: "Soustack Recipe Minimal Profile",
|
|
316
|
-
description: "Minimal profile that ensures the basic Recipe structure is present while allowing
|
|
957
|
+
description: "Minimal profile that ensures the basic Recipe structure is present while allowing stacks to extend it.",
|
|
317
958
|
allOf: [
|
|
318
959
|
{
|
|
319
960
|
$ref: "http://soustack.org/schema/recipe/base.schema.json"
|
|
@@ -324,19 +965,19 @@ var minimal_schema_default = {
|
|
|
324
965
|
profile: {
|
|
325
966
|
const: "minimal"
|
|
326
967
|
},
|
|
327
|
-
|
|
328
|
-
type: "
|
|
329
|
-
|
|
330
|
-
type: "
|
|
331
|
-
|
|
332
|
-
"attribution@1",
|
|
333
|
-
"taxonomy@1",
|
|
334
|
-
"media@1",
|
|
335
|
-
"nutrition@1",
|
|
336
|
-
"times@1"
|
|
337
|
-
]
|
|
968
|
+
stacks: {
|
|
969
|
+
type: "object",
|
|
970
|
+
additionalProperties: {
|
|
971
|
+
type: "integer",
|
|
972
|
+
minimum: 1
|
|
338
973
|
},
|
|
339
|
-
|
|
974
|
+
properties: {
|
|
975
|
+
attribution: { type: "integer", minimum: 1 },
|
|
976
|
+
taxonomy: { type: "integer", minimum: 1 },
|
|
977
|
+
media: { type: "integer", minimum: 1 },
|
|
978
|
+
nutrition: { type: "integer", minimum: 1 },
|
|
979
|
+
times: { type: "integer", minimum: 1 }
|
|
980
|
+
}
|
|
340
981
|
},
|
|
341
982
|
name: {
|
|
342
983
|
type: "string",
|
|
@@ -362,23 +1003,304 @@ var minimal_schema_default = {
|
|
|
362
1003
|
]
|
|
363
1004
|
};
|
|
364
1005
|
|
|
365
|
-
// src/schemas/recipe/
|
|
1006
|
+
// src/schemas/recipe/profiles/core.schema.json
|
|
1007
|
+
var core_schema_default = {
|
|
1008
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1009
|
+
$id: "http://soustack.org/schema/recipe/profiles/core.schema.json",
|
|
1010
|
+
title: "Soustack Recipe Core Profile",
|
|
1011
|
+
description: "Core profile that builds on the minimal profile and is intended to be combined with recipe stacks.",
|
|
1012
|
+
allOf: [
|
|
1013
|
+
{ $ref: "http://soustack.org/schema/recipe/base.schema.json" },
|
|
1014
|
+
{
|
|
1015
|
+
type: "object",
|
|
1016
|
+
properties: {
|
|
1017
|
+
profile: { const: "core" },
|
|
1018
|
+
stacks: {
|
|
1019
|
+
type: "object",
|
|
1020
|
+
additionalProperties: {
|
|
1021
|
+
type: "integer",
|
|
1022
|
+
minimum: 1
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
name: { type: "string", minLength: 1 },
|
|
1026
|
+
ingredients: { type: "array", minItems: 1 },
|
|
1027
|
+
instructions: { type: "array", minItems: 1 }
|
|
1028
|
+
},
|
|
1029
|
+
required: ["profile", "name", "ingredients", "instructions"],
|
|
1030
|
+
additionalProperties: true
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// spec/profiles/base.schema.json
|
|
1036
|
+
var base_schema_default2 = {
|
|
1037
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1038
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/base",
|
|
1039
|
+
title: "Soustack Base Profile Schema",
|
|
1040
|
+
description: "Wrapper schema that exposes the unmodified Soustack base schema.",
|
|
1041
|
+
allOf: [
|
|
1042
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" }
|
|
1043
|
+
]
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// spec/profiles/cookable.schema.json
|
|
1047
|
+
var cookable_schema_default = {
|
|
1048
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1049
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/cookable",
|
|
1050
|
+
title: "Soustack Cookable Profile Schema",
|
|
1051
|
+
description: "Extends the base schema to require structured yield + time metadata and non-empty ingredient/instruction lists.",
|
|
1052
|
+
allOf: [
|
|
1053
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1054
|
+
{
|
|
1055
|
+
required: ["yield", "time", "ingredients", "instructions"],
|
|
1056
|
+
properties: {
|
|
1057
|
+
yield: { $ref: "http://soustack.org/schema/v0.3.0#/definitions/yield" },
|
|
1058
|
+
time: { $ref: "http://soustack.org/schema/v0.3.0#/definitions/time" },
|
|
1059
|
+
ingredients: { type: "array", minItems: 1 },
|
|
1060
|
+
instructions: { type: "array", minItems: 1 }
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
]
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// spec/profiles/illustrated.schema.json
|
|
1067
|
+
var illustrated_schema_default = {
|
|
1068
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1069
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/illustrated",
|
|
1070
|
+
title: "Soustack Illustrated Profile Schema",
|
|
1071
|
+
description: "Extends the base schema to guarantee at least one illustrative image.",
|
|
1072
|
+
allOf: [
|
|
1073
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1074
|
+
{
|
|
1075
|
+
anyOf: [
|
|
1076
|
+
{ required: ["image"] },
|
|
1077
|
+
{
|
|
1078
|
+
properties: {
|
|
1079
|
+
instructions: {
|
|
1080
|
+
type: "array",
|
|
1081
|
+
contains: {
|
|
1082
|
+
anyOf: [
|
|
1083
|
+
{ $ref: "#/definitions/imageInstruction" },
|
|
1084
|
+
{ $ref: "#/definitions/instructionSubsectionWithImage" }
|
|
1085
|
+
]
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
]
|
|
1091
|
+
}
|
|
1092
|
+
],
|
|
1093
|
+
definitions: {
|
|
1094
|
+
imageInstruction: {
|
|
1095
|
+
allOf: [
|
|
1096
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instruction" },
|
|
1097
|
+
{ required: ["image"] }
|
|
1098
|
+
]
|
|
1099
|
+
},
|
|
1100
|
+
instructionSubsectionWithImage: {
|
|
1101
|
+
allOf: [
|
|
1102
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instructionSubsection" },
|
|
1103
|
+
{
|
|
1104
|
+
properties: {
|
|
1105
|
+
items: {
|
|
1106
|
+
type: "array",
|
|
1107
|
+
contains: { $ref: "#/definitions/imageInstruction" }
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
]
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// spec/profiles/quantified.schema.json
|
|
1117
|
+
var quantified_schema_default = {
|
|
1118
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1119
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/quantified",
|
|
1120
|
+
title: "Soustack Quantified Profile Schema",
|
|
1121
|
+
description: "Extends the base schema to require quantified ingredient entries.",
|
|
1122
|
+
allOf: [
|
|
1123
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1124
|
+
{
|
|
1125
|
+
properties: {
|
|
1126
|
+
ingredients: {
|
|
1127
|
+
type: "array",
|
|
1128
|
+
items: {
|
|
1129
|
+
anyOf: [
|
|
1130
|
+
{ $ref: "#/definitions/quantifiedIngredient" },
|
|
1131
|
+
{ $ref: "#/definitions/quantifiedIngredientSubsection" }
|
|
1132
|
+
]
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
],
|
|
1138
|
+
definitions: {
|
|
1139
|
+
quantifiedIngredient: {
|
|
1140
|
+
allOf: [
|
|
1141
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredient" },
|
|
1142
|
+
{ required: ["item", "quantity"] }
|
|
1143
|
+
]
|
|
1144
|
+
},
|
|
1145
|
+
quantifiedIngredientSubsection: {
|
|
1146
|
+
allOf: [
|
|
1147
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredientSubsection" },
|
|
1148
|
+
{
|
|
1149
|
+
properties: {
|
|
1150
|
+
items: {
|
|
1151
|
+
type: "array",
|
|
1152
|
+
items: { $ref: "#/definitions/quantifiedIngredient" }
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
]
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// spec/profiles/scalable.schema.json
|
|
1162
|
+
var scalable_schema_default = {
|
|
1163
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1164
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/scalable",
|
|
1165
|
+
title: "Soustack Scalable Profile Schema",
|
|
1166
|
+
description: "Extends the base schema to guarantee quantified ingredients plus a structured yield for deterministic scaling.",
|
|
1167
|
+
allOf: [
|
|
1168
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1169
|
+
{
|
|
1170
|
+
required: ["yield", "ingredients"],
|
|
1171
|
+
properties: {
|
|
1172
|
+
yield: {
|
|
1173
|
+
allOf: [
|
|
1174
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/yield" },
|
|
1175
|
+
{ properties: { amount: { type: "number", exclusiveMinimum: 0 } } }
|
|
1176
|
+
]
|
|
1177
|
+
},
|
|
1178
|
+
ingredients: {
|
|
1179
|
+
type: "array",
|
|
1180
|
+
minItems: 1,
|
|
1181
|
+
items: {
|
|
1182
|
+
anyOf: [
|
|
1183
|
+
{ $ref: "#/definitions/scalableIngredient" },
|
|
1184
|
+
{ $ref: "#/definitions/scalableIngredientSubsection" }
|
|
1185
|
+
]
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
],
|
|
1191
|
+
definitions: {
|
|
1192
|
+
scalableIngredient: {
|
|
1193
|
+
allOf: [
|
|
1194
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredient" },
|
|
1195
|
+
{ required: ["item", "quantity"] },
|
|
1196
|
+
{
|
|
1197
|
+
properties: {
|
|
1198
|
+
quantity: {
|
|
1199
|
+
allOf: [
|
|
1200
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/quantity" },
|
|
1201
|
+
{ properties: { amount: { type: "number", exclusiveMinimum: 0 } } }
|
|
1202
|
+
]
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
if: {
|
|
1208
|
+
properties: {
|
|
1209
|
+
scaling: {
|
|
1210
|
+
type: "object",
|
|
1211
|
+
properties: { type: { const: "bakers_percentage" } },
|
|
1212
|
+
required: ["type"]
|
|
1213
|
+
}
|
|
1214
|
+
},
|
|
1215
|
+
required: ["scaling"]
|
|
1216
|
+
},
|
|
1217
|
+
then: { required: ["id"] }
|
|
1218
|
+
}
|
|
1219
|
+
]
|
|
1220
|
+
},
|
|
1221
|
+
scalableIngredientSubsection: {
|
|
1222
|
+
allOf: [
|
|
1223
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredientSubsection" },
|
|
1224
|
+
{
|
|
1225
|
+
properties: {
|
|
1226
|
+
items: {
|
|
1227
|
+
type: "array",
|
|
1228
|
+
minItems: 1,
|
|
1229
|
+
items: { $ref: "#/definitions/scalableIngredient" }
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// spec/profiles/schedulable.schema.json
|
|
1239
|
+
var schedulable_schema_default = {
|
|
1240
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1241
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/schedulable",
|
|
1242
|
+
title: "Soustack Schedulable Profile Schema",
|
|
1243
|
+
description: "Extends the base schema to ensure every instruction is fully scheduled.",
|
|
1244
|
+
allOf: [
|
|
1245
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
1246
|
+
{
|
|
1247
|
+
properties: {
|
|
1248
|
+
instructions: {
|
|
1249
|
+
type: "array",
|
|
1250
|
+
items: {
|
|
1251
|
+
anyOf: [
|
|
1252
|
+
{ $ref: "#/definitions/schedulableInstruction" },
|
|
1253
|
+
{ $ref: "#/definitions/schedulableInstructionSubsection" }
|
|
1254
|
+
]
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
],
|
|
1260
|
+
definitions: {
|
|
1261
|
+
schedulableInstruction: {
|
|
1262
|
+
allOf: [
|
|
1263
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instruction" },
|
|
1264
|
+
{ required: ["id", "timing"] }
|
|
1265
|
+
]
|
|
1266
|
+
},
|
|
1267
|
+
schedulableInstructionSubsection: {
|
|
1268
|
+
allOf: [
|
|
1269
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instructionSubsection" },
|
|
1270
|
+
{
|
|
1271
|
+
properties: {
|
|
1272
|
+
items: {
|
|
1273
|
+
type: "array",
|
|
1274
|
+
items: { $ref: "#/definitions/schedulableInstruction" }
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
]
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/schemas/recipe/stacks/attribution/1.schema.json
|
|
366
1284
|
var schema_default = {
|
|
367
1285
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
368
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
369
|
-
title: "Soustack Recipe
|
|
370
|
-
description: "Schema for the
|
|
1286
|
+
$id: "https://soustack.org/schemas/recipe/stacks/attribution/1.schema.json",
|
|
1287
|
+
title: "Soustack Recipe Stack: attribution v1",
|
|
1288
|
+
description: "Schema for the attribution stack. Ensures namespace data is present when the stack is enabled and vice versa.",
|
|
371
1289
|
type: "object",
|
|
372
1290
|
properties: {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1291
|
+
stacks: {
|
|
1292
|
+
type: "object",
|
|
1293
|
+
additionalProperties: {
|
|
1294
|
+
type: "integer",
|
|
1295
|
+
minimum: 1
|
|
1296
|
+
}
|
|
377
1297
|
},
|
|
378
|
-
|
|
1298
|
+
attribution: {
|
|
379
1299
|
type: "object",
|
|
380
1300
|
properties: {
|
|
381
|
-
|
|
1301
|
+
url: { type: "string" },
|
|
1302
|
+
author: { type: "string" },
|
|
1303
|
+
datePublished: { type: "string" }
|
|
382
1304
|
},
|
|
383
1305
|
additionalProperties: false
|
|
384
1306
|
}
|
|
@@ -387,32 +1309,33 @@ var schema_default = {
|
|
|
387
1309
|
{
|
|
388
1310
|
if: {
|
|
389
1311
|
properties: {
|
|
390
|
-
|
|
391
|
-
type: "
|
|
392
|
-
|
|
1312
|
+
stacks: {
|
|
1313
|
+
type: "object",
|
|
1314
|
+
properties: {
|
|
1315
|
+
attribution: { const: 1 }
|
|
1316
|
+
},
|
|
1317
|
+
required: ["attribution"]
|
|
393
1318
|
}
|
|
394
1319
|
}
|
|
395
1320
|
},
|
|
396
1321
|
then: {
|
|
397
|
-
required: ["
|
|
398
|
-
properties: {
|
|
399
|
-
profile: { const: "core" }
|
|
400
|
-
}
|
|
1322
|
+
required: ["attribution"]
|
|
401
1323
|
}
|
|
402
1324
|
},
|
|
403
1325
|
{
|
|
404
1326
|
if: {
|
|
405
|
-
required: ["
|
|
1327
|
+
required: ["attribution"]
|
|
406
1328
|
},
|
|
407
1329
|
then: {
|
|
408
|
-
required: ["
|
|
1330
|
+
required: ["stacks"],
|
|
409
1331
|
properties: {
|
|
410
|
-
|
|
411
|
-
type: "
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1332
|
+
stacks: {
|
|
1333
|
+
type: "object",
|
|
1334
|
+
properties: {
|
|
1335
|
+
attribution: { const: 1 }
|
|
1336
|
+
},
|
|
1337
|
+
required: ["attribution"]
|
|
1338
|
+
}
|
|
416
1339
|
}
|
|
417
1340
|
}
|
|
418
1341
|
}
|
|
@@ -420,23 +1343,26 @@ var schema_default = {
|
|
|
420
1343
|
additionalProperties: true
|
|
421
1344
|
};
|
|
422
1345
|
|
|
423
|
-
// src/schemas/recipe/
|
|
1346
|
+
// src/schemas/recipe/stacks/media/1.schema.json
|
|
424
1347
|
var schema_default2 = {
|
|
425
1348
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
426
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
427
|
-
title: "Soustack Recipe
|
|
428
|
-
description: "Schema for the
|
|
1349
|
+
$id: "https://soustack.org/schemas/recipe/stacks/media/1.schema.json",
|
|
1350
|
+
title: "Soustack Recipe Stack: media v1",
|
|
1351
|
+
description: "Schema for the media stack. Guards media blocks based on stack activation and ensures declarations accompany payloads.",
|
|
429
1352
|
type: "object",
|
|
430
1353
|
properties: {
|
|
431
|
-
|
|
432
|
-
type: "
|
|
433
|
-
|
|
1354
|
+
stacks: {
|
|
1355
|
+
type: "object",
|
|
1356
|
+
additionalProperties: {
|
|
1357
|
+
type: "integer",
|
|
1358
|
+
minimum: 1
|
|
1359
|
+
}
|
|
434
1360
|
},
|
|
435
|
-
|
|
1361
|
+
media: {
|
|
436
1362
|
type: "object",
|
|
437
1363
|
properties: {
|
|
438
|
-
|
|
439
|
-
|
|
1364
|
+
images: { type: "array", items: { type: "string" } },
|
|
1365
|
+
videos: { type: "array", items: { type: "string" } }
|
|
440
1366
|
},
|
|
441
1367
|
additionalProperties: false
|
|
442
1368
|
}
|
|
@@ -445,27 +1371,32 @@ var schema_default2 = {
|
|
|
445
1371
|
{
|
|
446
1372
|
if: {
|
|
447
1373
|
properties: {
|
|
448
|
-
|
|
449
|
-
type: "
|
|
450
|
-
|
|
1374
|
+
stacks: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: {
|
|
1377
|
+
media: { const: 1 }
|
|
1378
|
+
},
|
|
1379
|
+
required: ["media"]
|
|
451
1380
|
}
|
|
452
1381
|
}
|
|
453
1382
|
},
|
|
454
1383
|
then: {
|
|
455
|
-
required: ["
|
|
1384
|
+
required: ["media"]
|
|
456
1385
|
}
|
|
457
1386
|
},
|
|
458
1387
|
{
|
|
459
1388
|
if: {
|
|
460
|
-
required: ["
|
|
1389
|
+
required: ["media"]
|
|
461
1390
|
},
|
|
462
1391
|
then: {
|
|
463
|
-
required: ["
|
|
1392
|
+
required: ["stacks"],
|
|
464
1393
|
properties: {
|
|
465
|
-
|
|
466
|
-
type: "
|
|
467
|
-
|
|
468
|
-
|
|
1394
|
+
stacks: {
|
|
1395
|
+
type: "object",
|
|
1396
|
+
properties: {
|
|
1397
|
+
media: { const: 1 }
|
|
1398
|
+
},
|
|
1399
|
+
required: ["media"]
|
|
469
1400
|
}
|
|
470
1401
|
}
|
|
471
1402
|
}
|
|
@@ -474,24 +1405,26 @@ var schema_default2 = {
|
|
|
474
1405
|
additionalProperties: true
|
|
475
1406
|
};
|
|
476
1407
|
|
|
477
|
-
// src/schemas/recipe/
|
|
1408
|
+
// src/schemas/recipe/stacks/nutrition/1.schema.json
|
|
478
1409
|
var schema_default3 = {
|
|
479
1410
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
480
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
481
|
-
title: "Soustack Recipe
|
|
482
|
-
description: "Schema for the
|
|
1411
|
+
$id: "https://soustack.org/schemas/recipe/stacks/nutrition/1.schema.json",
|
|
1412
|
+
title: "Soustack Recipe Stack: nutrition v1",
|
|
1413
|
+
description: "Schema for the nutrition stack. Keeps nutrition data aligned with stack declarations and vice versa.",
|
|
483
1414
|
type: "object",
|
|
484
1415
|
properties: {
|
|
485
|
-
|
|
486
|
-
type: "
|
|
487
|
-
|
|
1416
|
+
stacks: {
|
|
1417
|
+
type: "object",
|
|
1418
|
+
additionalProperties: {
|
|
1419
|
+
type: "integer",
|
|
1420
|
+
minimum: 1
|
|
1421
|
+
}
|
|
488
1422
|
},
|
|
489
|
-
|
|
1423
|
+
nutrition: {
|
|
490
1424
|
type: "object",
|
|
491
1425
|
properties: {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
datePublished: { type: "string" }
|
|
1426
|
+
calories: { type: "number" },
|
|
1427
|
+
protein_g: { type: "number" }
|
|
495
1428
|
},
|
|
496
1429
|
additionalProperties: false
|
|
497
1430
|
}
|
|
@@ -500,27 +1433,32 @@ var schema_default3 = {
|
|
|
500
1433
|
{
|
|
501
1434
|
if: {
|
|
502
1435
|
properties: {
|
|
503
|
-
|
|
504
|
-
type: "
|
|
505
|
-
|
|
1436
|
+
stacks: {
|
|
1437
|
+
type: "object",
|
|
1438
|
+
properties: {
|
|
1439
|
+
nutrition: { const: 1 }
|
|
1440
|
+
},
|
|
1441
|
+
required: ["nutrition"]
|
|
506
1442
|
}
|
|
507
1443
|
}
|
|
508
1444
|
},
|
|
509
1445
|
then: {
|
|
510
|
-
required: ["
|
|
1446
|
+
required: ["nutrition"]
|
|
511
1447
|
}
|
|
512
1448
|
},
|
|
513
1449
|
{
|
|
514
1450
|
if: {
|
|
515
|
-
required: ["
|
|
1451
|
+
required: ["nutrition"]
|
|
516
1452
|
},
|
|
517
1453
|
then: {
|
|
518
|
-
required: ["
|
|
1454
|
+
required: ["stacks"],
|
|
519
1455
|
properties: {
|
|
520
|
-
|
|
521
|
-
type: "
|
|
522
|
-
|
|
523
|
-
|
|
1456
|
+
stacks: {
|
|
1457
|
+
type: "object",
|
|
1458
|
+
properties: {
|
|
1459
|
+
nutrition: { const: 1 }
|
|
1460
|
+
},
|
|
1461
|
+
required: ["nutrition"]
|
|
524
1462
|
}
|
|
525
1463
|
}
|
|
526
1464
|
}
|
|
@@ -529,24 +1467,26 @@ var schema_default3 = {
|
|
|
529
1467
|
additionalProperties: true
|
|
530
1468
|
};
|
|
531
1469
|
|
|
532
|
-
// src/schemas/recipe/
|
|
1470
|
+
// src/schemas/recipe/stacks/schedule/1.schema.json
|
|
533
1471
|
var schema_default4 = {
|
|
534
1472
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
535
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
536
|
-
title: "Soustack Recipe
|
|
537
|
-
description: "Schema for the
|
|
1473
|
+
$id: "https://soustack.org/schemas/recipe/stacks/schedule/1.schema.json",
|
|
1474
|
+
title: "Soustack Recipe Stack: schedule v1",
|
|
1475
|
+
description: "Schema for the schedule stack. Enforces bidirectional stack gating and restricts usage to the core profile.",
|
|
538
1476
|
type: "object",
|
|
539
1477
|
properties: {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1478
|
+
profile: { type: "string" },
|
|
1479
|
+
stacks: {
|
|
1480
|
+
type: "object",
|
|
1481
|
+
additionalProperties: {
|
|
1482
|
+
type: "integer",
|
|
1483
|
+
minimum: 1
|
|
1484
|
+
}
|
|
543
1485
|
},
|
|
544
|
-
|
|
1486
|
+
schedule: {
|
|
545
1487
|
type: "object",
|
|
546
1488
|
properties: {
|
|
547
|
-
|
|
548
|
-
category: { type: "string" },
|
|
549
|
-
cuisine: { type: "string" }
|
|
1489
|
+
tasks: { type: "array" }
|
|
550
1490
|
},
|
|
551
1491
|
additionalProperties: false
|
|
552
1492
|
}
|
|
@@ -555,28 +1495,37 @@ var schema_default4 = {
|
|
|
555
1495
|
{
|
|
556
1496
|
if: {
|
|
557
1497
|
properties: {
|
|
558
|
-
|
|
559
|
-
type: "
|
|
560
|
-
|
|
1498
|
+
stacks: {
|
|
1499
|
+
type: "object",
|
|
1500
|
+
properties: {
|
|
1501
|
+
schedule: { const: 1 }
|
|
1502
|
+
},
|
|
1503
|
+
required: ["schedule"]
|
|
561
1504
|
}
|
|
562
1505
|
}
|
|
563
1506
|
},
|
|
564
1507
|
then: {
|
|
565
|
-
required: ["
|
|
1508
|
+
required: ["schedule", "profile"],
|
|
1509
|
+
properties: {
|
|
1510
|
+
profile: { const: "core" }
|
|
1511
|
+
}
|
|
566
1512
|
}
|
|
567
1513
|
},
|
|
568
1514
|
{
|
|
569
1515
|
if: {
|
|
570
|
-
required: ["
|
|
1516
|
+
required: ["schedule"]
|
|
571
1517
|
},
|
|
572
1518
|
then: {
|
|
573
|
-
required: ["
|
|
1519
|
+
required: ["stacks", "profile"],
|
|
574
1520
|
properties: {
|
|
575
|
-
|
|
576
|
-
type: "
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1521
|
+
stacks: {
|
|
1522
|
+
type: "object",
|
|
1523
|
+
properties: {
|
|
1524
|
+
schedule: { const: 1 }
|
|
1525
|
+
},
|
|
1526
|
+
required: ["schedule"]
|
|
1527
|
+
},
|
|
1528
|
+
profile: { const: "core" }
|
|
580
1529
|
}
|
|
581
1530
|
}
|
|
582
1531
|
}
|
|
@@ -584,23 +1533,27 @@ var schema_default4 = {
|
|
|
584
1533
|
additionalProperties: true
|
|
585
1534
|
};
|
|
586
1535
|
|
|
587
|
-
// src/schemas/recipe/
|
|
1536
|
+
// src/schemas/recipe/stacks/taxonomy/1.schema.json
|
|
588
1537
|
var schema_default5 = {
|
|
589
1538
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
590
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
591
|
-
title: "Soustack Recipe
|
|
592
|
-
description: "Schema for the
|
|
1539
|
+
$id: "https://soustack.org/schemas/recipe/stacks/taxonomy/1.schema.json",
|
|
1540
|
+
title: "Soustack Recipe Stack: taxonomy v1",
|
|
1541
|
+
description: "Schema for the taxonomy stack. Enforces keyword and categorization data when enabled and ensures stack declaration accompanies the namespace block.",
|
|
593
1542
|
type: "object",
|
|
594
1543
|
properties: {
|
|
595
|
-
|
|
596
|
-
type: "
|
|
597
|
-
|
|
1544
|
+
stacks: {
|
|
1545
|
+
type: "object",
|
|
1546
|
+
additionalProperties: {
|
|
1547
|
+
type: "integer",
|
|
1548
|
+
minimum: 1
|
|
1549
|
+
}
|
|
598
1550
|
},
|
|
599
|
-
|
|
1551
|
+
taxonomy: {
|
|
600
1552
|
type: "object",
|
|
601
1553
|
properties: {
|
|
602
|
-
|
|
603
|
-
|
|
1554
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
1555
|
+
category: { type: "string" },
|
|
1556
|
+
cuisine: { type: "string" }
|
|
604
1557
|
},
|
|
605
1558
|
additionalProperties: false
|
|
606
1559
|
}
|
|
@@ -609,27 +1562,32 @@ var schema_default5 = {
|
|
|
609
1562
|
{
|
|
610
1563
|
if: {
|
|
611
1564
|
properties: {
|
|
612
|
-
|
|
613
|
-
type: "
|
|
614
|
-
|
|
1565
|
+
stacks: {
|
|
1566
|
+
type: "object",
|
|
1567
|
+
properties: {
|
|
1568
|
+
taxonomy: { const: 1 }
|
|
1569
|
+
},
|
|
1570
|
+
required: ["taxonomy"]
|
|
615
1571
|
}
|
|
616
1572
|
}
|
|
617
1573
|
},
|
|
618
1574
|
then: {
|
|
619
|
-
required: ["
|
|
1575
|
+
required: ["taxonomy"]
|
|
620
1576
|
}
|
|
621
1577
|
},
|
|
622
1578
|
{
|
|
623
1579
|
if: {
|
|
624
|
-
required: ["
|
|
1580
|
+
required: ["taxonomy"]
|
|
625
1581
|
},
|
|
626
1582
|
then: {
|
|
627
|
-
required: ["
|
|
1583
|
+
required: ["stacks"],
|
|
628
1584
|
properties: {
|
|
629
|
-
|
|
630
|
-
type: "
|
|
631
|
-
|
|
632
|
-
|
|
1585
|
+
stacks: {
|
|
1586
|
+
type: "object",
|
|
1587
|
+
properties: {
|
|
1588
|
+
taxonomy: { const: 1 }
|
|
1589
|
+
},
|
|
1590
|
+
required: ["taxonomy"]
|
|
633
1591
|
}
|
|
634
1592
|
}
|
|
635
1593
|
}
|
|
@@ -638,17 +1596,20 @@ var schema_default5 = {
|
|
|
638
1596
|
additionalProperties: true
|
|
639
1597
|
};
|
|
640
1598
|
|
|
641
|
-
// src/schemas/recipe/
|
|
1599
|
+
// src/schemas/recipe/stacks/times/1.schema.json
|
|
642
1600
|
var schema_default6 = {
|
|
643
1601
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
644
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
645
|
-
title: "Soustack Recipe
|
|
646
|
-
description: "Schema for the times
|
|
1602
|
+
$id: "https://soustack.org/schemas/recipe/stacks/times/1.schema.json",
|
|
1603
|
+
title: "Soustack Recipe Stack: times v1",
|
|
1604
|
+
description: "Schema for the times stack. Maintains alignment between stack declarations and timing payloads.",
|
|
647
1605
|
type: "object",
|
|
648
1606
|
properties: {
|
|
649
|
-
|
|
650
|
-
type: "
|
|
651
|
-
|
|
1607
|
+
stacks: {
|
|
1608
|
+
type: "object",
|
|
1609
|
+
additionalProperties: {
|
|
1610
|
+
type: "integer",
|
|
1611
|
+
minimum: 1
|
|
1612
|
+
}
|
|
652
1613
|
},
|
|
653
1614
|
times: {
|
|
654
1615
|
type: "object",
|
|
@@ -664,9 +1625,12 @@ var schema_default6 = {
|
|
|
664
1625
|
{
|
|
665
1626
|
if: {
|
|
666
1627
|
properties: {
|
|
667
|
-
|
|
668
|
-
type: "
|
|
669
|
-
|
|
1628
|
+
stacks: {
|
|
1629
|
+
type: "object",
|
|
1630
|
+
properties: {
|
|
1631
|
+
times: { const: 1 }
|
|
1632
|
+
},
|
|
1633
|
+
required: ["times"]
|
|
670
1634
|
}
|
|
671
1635
|
}
|
|
672
1636
|
},
|
|
@@ -679,12 +1643,14 @@ var schema_default6 = {
|
|
|
679
1643
|
required: ["times"]
|
|
680
1644
|
},
|
|
681
1645
|
then: {
|
|
682
|
-
required: ["
|
|
1646
|
+
required: ["stacks"],
|
|
683
1647
|
properties: {
|
|
684
|
-
|
|
685
|
-
type: "
|
|
686
|
-
|
|
687
|
-
|
|
1648
|
+
stacks: {
|
|
1649
|
+
type: "object",
|
|
1650
|
+
properties: {
|
|
1651
|
+
times: { const: 1 }
|
|
1652
|
+
},
|
|
1653
|
+
required: ["times"]
|
|
688
1654
|
}
|
|
689
1655
|
}
|
|
690
1656
|
}
|
|
@@ -694,69 +1660,71 @@ var schema_default6 = {
|
|
|
694
1660
|
};
|
|
695
1661
|
|
|
696
1662
|
// src/validator.ts
|
|
697
|
-
var
|
|
698
|
-
var
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
if (profile === "core") {
|
|
703
|
-
return core_schema_default.$id;
|
|
704
|
-
}
|
|
705
|
-
throw new Error(`Unknown profile: ${profile}`);
|
|
706
|
-
};
|
|
707
|
-
var moduleIdToSchemaRef = (moduleId) => {
|
|
708
|
-
const match = moduleId.match(/^([a-z0-9_-]+)@(\d+(?:\.\d+)*)$/i);
|
|
709
|
-
if (!match) {
|
|
710
|
-
throw new Error(`Invalid module identifier '${moduleId}'. Expected <name>@<version>.`);
|
|
711
|
-
}
|
|
712
|
-
const [, name, version] = match;
|
|
713
|
-
const moduleSchemas2 = {
|
|
714
|
-
"schedule@1": schema_default,
|
|
715
|
-
"nutrition@1": schema_default2,
|
|
716
|
-
"attribution@1": schema_default3,
|
|
717
|
-
"taxonomy@1": schema_default4,
|
|
718
|
-
"media@1": schema_default5,
|
|
719
|
-
"times@1": schema_default6
|
|
720
|
-
};
|
|
721
|
-
const schema = moduleSchemas2[moduleId];
|
|
722
|
-
if (schema && schema.$id) {
|
|
723
|
-
return schema.$id;
|
|
724
|
-
}
|
|
725
|
-
return `https://soustack.org/schemas/recipe/modules/${name}/${version}.schema.json`;
|
|
726
|
-
};
|
|
727
|
-
var profileSchemas = {
|
|
728
|
-
minimal: minimal_schema_default,
|
|
729
|
-
core: core_schema_default
|
|
730
|
-
};
|
|
731
|
-
var moduleSchemas = {
|
|
732
|
-
"schedule@1": schema_default,
|
|
733
|
-
"nutrition@1": schema_default2,
|
|
734
|
-
"attribution@1": schema_default3,
|
|
735
|
-
"taxonomy@1": schema_default4,
|
|
736
|
-
"media@1": schema_default5,
|
|
737
|
-
"times@1": schema_default6
|
|
738
|
-
};
|
|
1663
|
+
var LEGACY_ROOT_SCHEMA_ID = "http://soustack.org/schema/v0.3.0";
|
|
1664
|
+
var DEFAULT_ROOT_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
|
|
1665
|
+
var BASE_SCHEMA_ID = "http://soustack.org/schema/recipe/base.schema.json";
|
|
1666
|
+
var PROFILE_SCHEMA_PREFIX = "http://soustack.org/schema/recipe/profiles/";
|
|
739
1667
|
var validationContexts = /* @__PURE__ */ new Map();
|
|
1668
|
+
function loadAllSchemas(ajv) {
|
|
1669
|
+
const schemas = [
|
|
1670
|
+
soustack_schema_default,
|
|
1671
|
+
base_schema_default,
|
|
1672
|
+
minimal_schema_default,
|
|
1673
|
+
core_schema_default,
|
|
1674
|
+
base_schema_default2,
|
|
1675
|
+
cookable_schema_default,
|
|
1676
|
+
illustrated_schema_default,
|
|
1677
|
+
quantified_schema_default,
|
|
1678
|
+
scalable_schema_default,
|
|
1679
|
+
schedulable_schema_default,
|
|
1680
|
+
schema_default,
|
|
1681
|
+
schema_default2,
|
|
1682
|
+
schema_default3,
|
|
1683
|
+
schema_default4,
|
|
1684
|
+
schema_default5,
|
|
1685
|
+
schema_default6
|
|
1686
|
+
];
|
|
1687
|
+
for (const schema of schemas) {
|
|
1688
|
+
if (schema && typeof schema === "object" && "$id" in schema) {
|
|
1689
|
+
const schemaWithId = schema;
|
|
1690
|
+
if (schemaWithId.$id) {
|
|
1691
|
+
ajv.addSchema(schemaWithId, schemaWithId.$id);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
ajv.addSchema(
|
|
1696
|
+
{
|
|
1697
|
+
$id: DEFAULT_ROOT_SCHEMA_ID,
|
|
1698
|
+
allOf: [
|
|
1699
|
+
{ $ref: LEGACY_ROOT_SCHEMA_ID },
|
|
1700
|
+
{
|
|
1701
|
+
type: "object",
|
|
1702
|
+
properties: {
|
|
1703
|
+
$schema: { const: DEFAULT_ROOT_SCHEMA_ID }
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
},
|
|
1708
|
+
DEFAULT_ROOT_SCHEMA_ID
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
740
1711
|
function createContext(collectAllErrors) {
|
|
741
|
-
const ajv = new
|
|
1712
|
+
const ajv = new Ajv2020({
|
|
1713
|
+
strict: false,
|
|
1714
|
+
allErrors: collectAllErrors,
|
|
1715
|
+
validateSchema: false
|
|
1716
|
+
// Don't validate schemas themselves
|
|
1717
|
+
});
|
|
742
1718
|
addFormats(ajv);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1719
|
+
loadAllSchemas(ajv);
|
|
1720
|
+
const rootValidator = ajv.getSchema(DEFAULT_ROOT_SCHEMA_ID) || ajv.getSchema(LEGACY_ROOT_SCHEMA_ID);
|
|
1721
|
+
const baseValidator = ajv.getSchema(BASE_SCHEMA_ID);
|
|
1722
|
+
return {
|
|
1723
|
+
ajv,
|
|
1724
|
+
rootValidator: rootValidator || void 0,
|
|
1725
|
+
baseValidator: baseValidator || void 0,
|
|
1726
|
+
validators: /* @__PURE__ */ new Map()
|
|
751
1727
|
};
|
|
752
|
-
addSchemaWithAlias(base_schema_default, CANONICAL_BASE_SCHEMA_ID);
|
|
753
|
-
Object.entries(profileSchemas).forEach(([name, schema]) => {
|
|
754
|
-
addSchemaWithAlias(schema, canonicalProfileId(name));
|
|
755
|
-
});
|
|
756
|
-
Object.entries(moduleSchemas).forEach(([moduleId, schema]) => {
|
|
757
|
-
addSchemaWithAlias(schema, moduleIdToSchemaRef(moduleId));
|
|
758
|
-
});
|
|
759
|
-
return { ajv, validators: /* @__PURE__ */ new Map() };
|
|
760
1728
|
}
|
|
761
1729
|
function getContext(collectAllErrors) {
|
|
762
1730
|
if (!validationContexts.has(collectAllErrors)) {
|
|
@@ -770,288 +1738,227 @@ function cloneRecipe(recipe) {
|
|
|
770
1738
|
}
|
|
771
1739
|
return JSON.parse(JSON.stringify(recipe));
|
|
772
1740
|
}
|
|
773
|
-
function
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
const
|
|
778
|
-
|
|
1741
|
+
function formatAjvError(error) {
|
|
1742
|
+
var _a;
|
|
1743
|
+
let path = error.instancePath || "/";
|
|
1744
|
+
if (error.keyword === "additionalProperties" && ((_a = error.params) == null ? void 0 : _a.additionalProperty)) {
|
|
1745
|
+
const extra = error.params.additionalProperty;
|
|
1746
|
+
path = `${error.instancePath || ""}/${extra}`.replace(/\/+/g, "/") || "/";
|
|
779
1747
|
}
|
|
780
|
-
return
|
|
1748
|
+
return {
|
|
1749
|
+
path,
|
|
1750
|
+
keyword: error.keyword,
|
|
1751
|
+
message: error.message || "Validation error"
|
|
1752
|
+
};
|
|
781
1753
|
}
|
|
782
|
-
function
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
nutrition: "nutrition@1",
|
|
795
|
-
schedule: "schedule@1"
|
|
1754
|
+
function isSoustackSchemaId(schemaId) {
|
|
1755
|
+
return schemaId.startsWith("http://soustack.org/schema") || schemaId.startsWith("https://soustack.org/schema") || schemaId.startsWith("https://soustack.spec/") || schemaId.startsWith("https://soustack.org/schemas/");
|
|
1756
|
+
}
|
|
1757
|
+
function inferStacksFromPayload(recipe) {
|
|
1758
|
+
const inferred = {};
|
|
1759
|
+
const payloadToStack = {
|
|
1760
|
+
attribution: "attribution",
|
|
1761
|
+
taxonomy: "taxonomy",
|
|
1762
|
+
media: "media",
|
|
1763
|
+
times: "times",
|
|
1764
|
+
nutrition: "nutrition",
|
|
1765
|
+
schedule: "schedule"
|
|
796
1766
|
};
|
|
797
|
-
for (const [field,
|
|
798
|
-
if (recipe && typeof recipe === "object" && field in recipe && recipe[field]
|
|
799
|
-
|
|
800
|
-
if (typeof payload === "object" && !Array.isArray(payload)) {
|
|
801
|
-
if (Object.keys(payload).length > 0) {
|
|
802
|
-
inferred.push(moduleId);
|
|
803
|
-
}
|
|
804
|
-
} else if (Array.isArray(payload) && payload.length > 0) {
|
|
805
|
-
inferred.push(moduleId);
|
|
806
|
-
} else if (payload !== null && payload !== void 0) {
|
|
807
|
-
inferred.push(moduleId);
|
|
808
|
-
}
|
|
1767
|
+
for (const [field, stackName] of Object.entries(payloadToStack)) {
|
|
1768
|
+
if (recipe && typeof recipe === "object" && field in recipe && recipe[field] !== void 0) {
|
|
1769
|
+
inferred[stackName] = 1;
|
|
809
1770
|
}
|
|
810
1771
|
}
|
|
811
1772
|
return inferred;
|
|
812
1773
|
}
|
|
813
|
-
function
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
const sortedModules = Array.from(allModules).sort();
|
|
817
|
-
const cacheKey = `${profile}::${sortedModules.join(",")}`;
|
|
1774
|
+
function getComposedValidator(profile, stacks, context) {
|
|
1775
|
+
const stackIdentifiers = Object.entries(stacks).map(([name, version]) => `${name}@${version}`).sort();
|
|
1776
|
+
const cacheKey = `${profile}::${stackIdentifiers.join(",")}`;
|
|
818
1777
|
const cached = context.validators.get(cacheKey);
|
|
819
1778
|
if (cached) return cached;
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1779
|
+
const allOf = [{ $ref: BASE_SCHEMA_ID }];
|
|
1780
|
+
if (!context.ajv.getSchema(BASE_SCHEMA_ID)) {
|
|
1781
|
+
throw new Error(`Base schema not loaded: ${BASE_SCHEMA_ID}. Ensure schemas are loaded before creating validators.`);
|
|
1782
|
+
}
|
|
1783
|
+
const profileSchemaId = `${PROFILE_SCHEMA_PREFIX}${profile}.schema.json`;
|
|
1784
|
+
if (!context.ajv.getSchema(profileSchemaId)) {
|
|
1785
|
+
throw new Error(`Profile schema not loaded: ${profileSchemaId}`);
|
|
1786
|
+
}
|
|
1787
|
+
allOf.push({ $ref: profileSchemaId });
|
|
1788
|
+
for (const [name, version] of Object.entries(stacks)) {
|
|
1789
|
+
if (typeof version === "number" && version >= 1) {
|
|
1790
|
+
const stackSchemaId = `https://soustack.org/schemas/recipe/stacks/${name}/${version}.schema.json`;
|
|
1791
|
+
if (!context.ajv.getSchema(stackSchemaId)) {
|
|
1792
|
+
throw new Error(`Stack schema not loaded: ${stackSchemaId}`);
|
|
1793
|
+
}
|
|
1794
|
+
allOf.push({ $ref: stackSchemaId });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
const composedSchema = {
|
|
1798
|
+
$id: `urn:soustack:composed:${cacheKey}`,
|
|
1799
|
+
allOf
|
|
830
1800
|
};
|
|
831
|
-
const validateFn = context.ajv.compile(
|
|
1801
|
+
const validateFn = context.ajv.compile(composedSchema);
|
|
832
1802
|
context.validators.set(cacheKey, validateFn);
|
|
833
1803
|
return validateFn;
|
|
834
1804
|
}
|
|
835
|
-
function
|
|
836
|
-
const normalized = cloneRecipe(
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
];
|
|
854
|
-
structuredKeys.forEach((key) => {
|
|
855
|
-
const value = time[key];
|
|
856
|
-
if (typeof value === "number") return;
|
|
857
|
-
const parsed = parseDuration(value);
|
|
858
|
-
if (parsed !== null) {
|
|
859
|
-
time[key] = parsed;
|
|
1805
|
+
function validateRecipeSchemaNormalized(normalizedInput, inputHasStacks, collectAllErrors, schemaOverride) {
|
|
1806
|
+
const normalized = cloneRecipe(normalizedInput);
|
|
1807
|
+
const context = getContext(collectAllErrors);
|
|
1808
|
+
const schemaId = typeof schemaOverride === "string" ? schemaOverride : typeof normalized.$schema === "string" ? normalized.$schema : void 0;
|
|
1809
|
+
const hasSchemaOverride = typeof schemaOverride === "string";
|
|
1810
|
+
const isSoustackSchema = schemaId ? isSoustackSchemaId(schemaId) : false;
|
|
1811
|
+
if (schemaId && isSoustackSchema) {
|
|
1812
|
+
const schemaValidator = context.ajv.getSchema(schemaId);
|
|
1813
|
+
if (!schemaValidator) {
|
|
1814
|
+
return {
|
|
1815
|
+
ok: false,
|
|
1816
|
+
errors: [
|
|
1817
|
+
{
|
|
1818
|
+
path: "/$schema",
|
|
1819
|
+
message: `Unknown schema: ${schemaId}`
|
|
1820
|
+
}
|
|
1821
|
+
]
|
|
1822
|
+
};
|
|
860
1823
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
"source",
|
|
880
|
-
"dateAdded",
|
|
881
|
-
"dateModified",
|
|
882
|
-
"yield",
|
|
883
|
-
"time",
|
|
884
|
-
"id",
|
|
885
|
-
"title",
|
|
886
|
-
"recipeVersion",
|
|
887
|
-
"version",
|
|
888
|
-
// deprecated but allowed
|
|
889
|
-
"equipment",
|
|
890
|
-
"storage",
|
|
891
|
-
"substitutions"
|
|
892
|
-
]);
|
|
893
|
-
function detectUnknownTopLevelKeys(recipe) {
|
|
894
|
-
if (!recipe || typeof recipe !== "object") return [];
|
|
895
|
-
const disallowedKeys = Object.keys(recipe).filter(
|
|
896
|
-
(key) => !allowedTopLevelProps.has(key) && !key.startsWith("x-")
|
|
897
|
-
);
|
|
898
|
-
return disallowedKeys.map((key) => ({
|
|
899
|
-
path: `/${key}`,
|
|
900
|
-
keyword: "additionalProperties",
|
|
901
|
-
message: `Unknown top-level property '${key}' is not allowed by the Soustack spec`
|
|
902
|
-
}));
|
|
903
|
-
}
|
|
904
|
-
function formatAjvError(error) {
|
|
905
|
-
var _a2;
|
|
906
|
-
let path = error.instancePath || "/";
|
|
907
|
-
if (error.keyword === "additionalProperties" && ((_a2 = error.params) == null ? void 0 : _a2.additionalProperty)) {
|
|
908
|
-
const extra = error.params.additionalProperty;
|
|
909
|
-
path = `${error.instancePath || ""}/${extra}`.replace(/\/+/g, "/") || "/";
|
|
1824
|
+
const schemaInput = cloneRecipe(normalized);
|
|
1825
|
+
if (hasSchemaOverride && "$schema" in schemaInput && schemaInput.$schema !== schemaId) {
|
|
1826
|
+
delete schemaInput.$schema;
|
|
1827
|
+
}
|
|
1828
|
+
const isLegacySchema = schemaId.startsWith(LEGACY_ROOT_SCHEMA_ID);
|
|
1829
|
+
const shouldRemoveStacks = (isLegacySchema || schemaId === DEFAULT_ROOT_SCHEMA_ID) && !inputHasStacks;
|
|
1830
|
+
if (isLegacySchema && "@type" in schemaInput) {
|
|
1831
|
+
delete schemaInput["@type"];
|
|
1832
|
+
}
|
|
1833
|
+
if (shouldRemoveStacks && "stacks" in schemaInput) {
|
|
1834
|
+
delete schemaInput.stacks;
|
|
1835
|
+
}
|
|
1836
|
+
const schemaValid = schemaValidator(schemaInput);
|
|
1837
|
+
const schemaErrors = schemaValidator.errors || [];
|
|
1838
|
+
return {
|
|
1839
|
+
ok: !!schemaValid,
|
|
1840
|
+
errors: schemaErrors.map(formatAjvError)
|
|
1841
|
+
};
|
|
910
1842
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
function runAjvValidation(data, profile, modules, context) {
|
|
918
|
-
try {
|
|
919
|
-
const validateFn = getCombinedValidator(profile, modules, data, context);
|
|
920
|
-
const isValid = validateFn(data);
|
|
921
|
-
return !isValid && validateFn.errors ? validateFn.errors.map(formatAjvError) : [];
|
|
922
|
-
} catch (error) {
|
|
923
|
-
return [
|
|
924
|
-
{
|
|
925
|
-
path: "/",
|
|
926
|
-
message: error instanceof Error ? error.message : "Validation failed to initialize"
|
|
1843
|
+
const hasProfile = normalized.profile && typeof normalized.profile === "string";
|
|
1844
|
+
let declaredStacks = {};
|
|
1845
|
+
if (normalized.stacks && typeof normalized.stacks === "object" && !Array.isArray(normalized.stacks)) {
|
|
1846
|
+
for (const [name, version] of Object.entries(normalized.stacks)) {
|
|
1847
|
+
if (typeof version === "number" && version >= 1) {
|
|
1848
|
+
declaredStacks[name] = version;
|
|
927
1849
|
}
|
|
928
|
-
|
|
1850
|
+
}
|
|
929
1851
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1852
|
+
const inferredStacks = inferStacksFromPayload(normalized);
|
|
1853
|
+
const allStacks = { ...declaredStacks };
|
|
1854
|
+
for (const [name, version] of Object.entries(inferredStacks)) {
|
|
1855
|
+
if (!allStacks[name] || allStacks[name] < version) {
|
|
1856
|
+
allStacks[name] = version;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
let isValid;
|
|
1860
|
+
let errors = [];
|
|
1861
|
+
const profile = hasProfile ? normalized.profile.toLowerCase() : "core";
|
|
1862
|
+
const profileSchemaId = `${PROFILE_SCHEMA_PREFIX}${profile}.schema.json`;
|
|
1863
|
+
if (!context.ajv.getSchema(profileSchemaId)) {
|
|
1864
|
+
return {
|
|
1865
|
+
ok: false,
|
|
1866
|
+
errors: [
|
|
1867
|
+
{
|
|
1868
|
+
path: "/profile",
|
|
1869
|
+
message: `Profile schema not loaded: ${profileSchemaId}`
|
|
1870
|
+
}
|
|
1871
|
+
]
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
{
|
|
1875
|
+
const validationCopy = cloneRecipe(normalized);
|
|
1876
|
+
if (!validationCopy.stacks || typeof validationCopy.stacks !== "object" || Array.isArray(validationCopy.stacks)) {
|
|
1877
|
+
validationCopy.stacks = declaredStacks;
|
|
1878
|
+
}
|
|
1879
|
+
if (!validationCopy.profile) {
|
|
1880
|
+
validationCopy.profile = profile;
|
|
1881
|
+
}
|
|
1882
|
+
const validator = getComposedValidator(profile, allStacks, context);
|
|
1883
|
+
isValid = validator(validationCopy);
|
|
1884
|
+
errors = validator.errors || [];
|
|
1885
|
+
if (isValid && context.rootValidator) {
|
|
1886
|
+
const rootCheckCopy = cloneRecipe(normalized);
|
|
1887
|
+
if ("@type" in rootCheckCopy) {
|
|
1888
|
+
delete rootCheckCopy["@type"];
|
|
948
1889
|
}
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
});
|
|
1890
|
+
if ("stacks" in rootCheckCopy) {
|
|
1891
|
+
delete rootCheckCopy.stacks;
|
|
1892
|
+
}
|
|
1893
|
+
if ("profile" in rootCheckCopy) {
|
|
1894
|
+
delete rootCheckCopy.profile;
|
|
1895
|
+
}
|
|
1896
|
+
const stackPayloadFields = ["attribution", "taxonomy", "media", "times", "nutrition", "schedule"];
|
|
1897
|
+
for (const field of stackPayloadFields) {
|
|
1898
|
+
if (field in rootCheckCopy) {
|
|
1899
|
+
delete rootCheckCopy[field];
|
|
962
1900
|
}
|
|
963
1901
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
dependencyRefs.forEach((ref) => {
|
|
978
|
-
var _a2;
|
|
979
|
-
if (ref.fromId && instructionIds.has(ref.fromId) && instructionIds.has(ref.toId)) {
|
|
980
|
-
const list = (_a2 = adjacency.get(ref.fromId)) != null ? _a2 : [];
|
|
981
|
-
list.push({ toId: ref.toId, path: ref.path });
|
|
982
|
-
adjacency.set(ref.fromId, list);
|
|
983
|
-
}
|
|
984
|
-
});
|
|
985
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
986
|
-
const visited = /* @__PURE__ */ new Set();
|
|
987
|
-
const detectCycles = (nodeId) => {
|
|
988
|
-
var _a2;
|
|
989
|
-
if (visiting.has(nodeId)) return;
|
|
990
|
-
if (visited.has(nodeId)) return;
|
|
991
|
-
visiting.add(nodeId);
|
|
992
|
-
const neighbors = (_a2 = adjacency.get(nodeId)) != null ? _a2 : [];
|
|
993
|
-
neighbors.forEach((edge) => {
|
|
994
|
-
if (visiting.has(edge.toId)) {
|
|
995
|
-
errors.push({
|
|
996
|
-
path: edge.path,
|
|
997
|
-
message: `Circular dependency detected involving instruction id '${edge.toId}'.`
|
|
998
|
-
});
|
|
999
|
-
return;
|
|
1902
|
+
const rootValid = context.rootValidator(rootCheckCopy);
|
|
1903
|
+
if (!rootValid && context.rootValidator.errors) {
|
|
1904
|
+
const unknownKeyErrors = context.rootValidator.errors.filter(
|
|
1905
|
+
(e) => e.keyword === "additionalProperties" && (e.instancePath === "" || e.instancePath === "/")
|
|
1906
|
+
);
|
|
1907
|
+
const schemaConstErrors = context.rootValidator.errors.filter(
|
|
1908
|
+
(e) => e.keyword === "const" && e.instancePath === "/$schema"
|
|
1909
|
+
);
|
|
1910
|
+
const relevantErrors = [...unknownKeyErrors, ...schemaConstErrors];
|
|
1911
|
+
if (relevantErrors.length > 0) {
|
|
1912
|
+
errors.push(...relevantErrors);
|
|
1913
|
+
isValid = false;
|
|
1914
|
+
}
|
|
1000
1915
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
ok: isValid,
|
|
1920
|
+
errors: errors.map(formatAjvError)
|
|
1005
1921
|
};
|
|
1006
|
-
instructionIds.forEach((id) => detectCycles(id));
|
|
1007
|
-
return errors;
|
|
1008
1922
|
}
|
|
1009
1923
|
function validateRecipe(input, options = {}) {
|
|
1010
|
-
var
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1924
|
+
var _a, _b;
|
|
1925
|
+
const { recipe: normalized, warnings } = normalizeRecipe(input);
|
|
1926
|
+
if (options.profile) {
|
|
1927
|
+
normalized.profile = options.profile;
|
|
1928
|
+
}
|
|
1929
|
+
const inputHasStacks = !!input && typeof input === "object" && !Array.isArray(input) && "stacks" in input;
|
|
1930
|
+
const { ok: schemaOk, errors: schemaErrors } = validateRecipeSchemaNormalized(
|
|
1931
|
+
normalized,
|
|
1932
|
+
inputHasStacks,
|
|
1933
|
+
(_a = options.collectAllErrors) != null ? _a : true,
|
|
1934
|
+
options.schema
|
|
1935
|
+
);
|
|
1936
|
+
const mode = (_b = options.mode) != null ? _b : "full";
|
|
1937
|
+
let conformanceIssues = [];
|
|
1938
|
+
let conformanceOk = true;
|
|
1939
|
+
if (mode === "full") {
|
|
1940
|
+
if (schemaOk) {
|
|
1941
|
+
const conformanceResult = validateConformance(normalized);
|
|
1942
|
+
conformanceIssues = conformanceResult.issues;
|
|
1943
|
+
conformanceOk = conformanceResult.ok;
|
|
1944
|
+
} else {
|
|
1945
|
+
conformanceOk = false;
|
|
1946
|
+
}
|
|
1028
1947
|
}
|
|
1029
|
-
const
|
|
1030
|
-
const
|
|
1031
|
-
const graphErrors = modules.includes("schedule@1") && validationErrors.length === 0 ? checkInstructionGraph(normalized) : [];
|
|
1032
|
-
const errors = [...unknownKeyErrors, ...validationErrors, ...graphErrors];
|
|
1948
|
+
const ok = schemaOk && (mode === "schema" ? true : conformanceOk);
|
|
1949
|
+
const normalizedRecipe = ok || options.includeNormalized ? normalized : void 0;
|
|
1033
1950
|
return {
|
|
1034
|
-
|
|
1035
|
-
|
|
1951
|
+
ok,
|
|
1952
|
+
schemaErrors,
|
|
1953
|
+
conformanceIssues,
|
|
1036
1954
|
warnings,
|
|
1037
|
-
|
|
1955
|
+
normalizedRecipe
|
|
1038
1956
|
};
|
|
1039
1957
|
}
|
|
1040
1958
|
function detectProfiles(recipe) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const normalizedRecipe = (_a2 = result.normalized) != null ? _a2 : recipe;
|
|
1045
|
-
const profiles = [];
|
|
1046
|
-
const context = getContext(false);
|
|
1047
|
-
Object.keys(profileSchemas).forEach((profile) => {
|
|
1048
|
-
if (!profileSchemas[profile]) return;
|
|
1049
|
-
const errors = runAjvValidation(normalizedRecipe, profile, [], context);
|
|
1050
|
-
if (errors.length === 0) {
|
|
1051
|
-
profiles.push(profile);
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
return profiles;
|
|
1959
|
+
const result = validateRecipe(recipe, { collectAllErrors: false });
|
|
1960
|
+
if (!result.ok) return [];
|
|
1961
|
+
return ["core"];
|
|
1055
1962
|
}
|
|
1056
1963
|
|
|
1057
1964
|
// src/converters/yield.ts
|
|
@@ -1094,12 +2001,12 @@ function parseYield(value) {
|
|
|
1094
2001
|
return void 0;
|
|
1095
2002
|
}
|
|
1096
2003
|
function formatYield(yieldValue) {
|
|
1097
|
-
var
|
|
2004
|
+
var _a;
|
|
1098
2005
|
if (!yieldValue) return void 0;
|
|
1099
2006
|
if (!yieldValue.amount && !yieldValue.unit) {
|
|
1100
2007
|
return void 0;
|
|
1101
2008
|
}
|
|
1102
|
-
const amount = (
|
|
2009
|
+
const amount = (_a = yieldValue.amount) != null ? _a : "";
|
|
1103
2010
|
const unit = yieldValue.unit ? ` ${yieldValue.unit}` : "";
|
|
1104
2011
|
return `${amount}${unit}`.trim() || yieldValue.description;
|
|
1105
2012
|
}
|
|
@@ -1140,7 +2047,7 @@ function extractUrl(value) {
|
|
|
1140
2047
|
|
|
1141
2048
|
// src/fromSchemaOrg.ts
|
|
1142
2049
|
function fromSchemaOrg(input) {
|
|
1143
|
-
var
|
|
2050
|
+
var _a;
|
|
1144
2051
|
const recipeNode = extractRecipeNode(input);
|
|
1145
2052
|
if (!recipeNode) {
|
|
1146
2053
|
return null;
|
|
@@ -1158,18 +2065,18 @@ function fromSchemaOrg(input) {
|
|
|
1158
2065
|
const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
|
|
1159
2066
|
const media = convertMedia(recipeNode.image, recipeNode.video);
|
|
1160
2067
|
const times = convertTimes(time);
|
|
1161
|
-
const
|
|
1162
|
-
if (attribution)
|
|
1163
|
-
if (taxonomy)
|
|
1164
|
-
if (media)
|
|
1165
|
-
if (nutrition)
|
|
1166
|
-
if (times)
|
|
1167
|
-
|
|
2068
|
+
const stacks = {};
|
|
2069
|
+
if (attribution) stacks.attribution = 1;
|
|
2070
|
+
if (taxonomy) stacks.taxonomy = 1;
|
|
2071
|
+
if (media) stacks.media = 1;
|
|
2072
|
+
if (nutrition) stacks.nutrition = 1;
|
|
2073
|
+
if (times) stacks.times = 1;
|
|
2074
|
+
const rawRecipe = {
|
|
1168
2075
|
"@type": "Recipe",
|
|
1169
2076
|
profile: "minimal",
|
|
1170
|
-
|
|
2077
|
+
stacks,
|
|
1171
2078
|
name: recipeNode.name.trim(),
|
|
1172
|
-
description: ((
|
|
2079
|
+
description: ((_a = recipeNode.description) == null ? void 0 : _a.trim()) || void 0,
|
|
1173
2080
|
image: normalizeImage(recipeNode.image),
|
|
1174
2081
|
category,
|
|
1175
2082
|
tags: tags.length ? tags : void 0,
|
|
@@ -1186,6 +2093,8 @@ function fromSchemaOrg(input) {
|
|
|
1186
2093
|
...media ? { media } : {},
|
|
1187
2094
|
...times ? { times } : {}
|
|
1188
2095
|
};
|
|
2096
|
+
const { recipe } = normalizeRecipe(rawRecipe);
|
|
2097
|
+
return recipe;
|
|
1189
2098
|
}
|
|
1190
2099
|
function extractRecipeNode(input) {
|
|
1191
2100
|
if (!input) return null;
|
|
@@ -1232,7 +2141,7 @@ function convertIngredients(value) {
|
|
|
1232
2141
|
return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
|
|
1233
2142
|
}
|
|
1234
2143
|
function convertInstructions(value) {
|
|
1235
|
-
var
|
|
2144
|
+
var _a;
|
|
1236
2145
|
if (!value) return [];
|
|
1237
2146
|
const normalized = Array.isArray(value) ? value : [value];
|
|
1238
2147
|
const result = [];
|
|
@@ -1249,7 +2158,7 @@ function convertInstructions(value) {
|
|
|
1249
2158
|
const subsectionItems = extractSectionItems(entry.itemListElement);
|
|
1250
2159
|
if (subsectionItems.length) {
|
|
1251
2160
|
result.push({
|
|
1252
|
-
subsection: ((
|
|
2161
|
+
subsection: ((_a = entry.name) == null ? void 0 : _a.trim()) || "Section",
|
|
1253
2162
|
items: subsectionItems
|
|
1254
2163
|
});
|
|
1255
2164
|
}
|
|
@@ -1333,9 +2242,9 @@ function isHowToSection(value) {
|
|
|
1333
2242
|
return Boolean(value) && typeof value === "object" && value["@type"] === "HowToSection" && Array.isArray(value.itemListElement);
|
|
1334
2243
|
}
|
|
1335
2244
|
function convertTime(recipe) {
|
|
1336
|
-
var
|
|
1337
|
-
const prep = smartParseDuration((
|
|
1338
|
-
const cook = smartParseDuration((
|
|
2245
|
+
var _a, _b, _c;
|
|
2246
|
+
const prep = smartParseDuration((_a = recipe.prepTime) != null ? _a : "");
|
|
2247
|
+
const cook = smartParseDuration((_b = recipe.cookTime) != null ? _b : "");
|
|
1339
2248
|
const total = smartParseDuration((_c = recipe.totalTime) != null ? _c : "");
|
|
1340
2249
|
const structured = {};
|
|
1341
2250
|
if (prep !== null && prep !== void 0) structured.prep = prep;
|
|
@@ -1369,10 +2278,10 @@ function extractFirst(value) {
|
|
|
1369
2278
|
return arr.length ? arr[0] : void 0;
|
|
1370
2279
|
}
|
|
1371
2280
|
function convertSource(recipe) {
|
|
1372
|
-
var
|
|
2281
|
+
var _a;
|
|
1373
2282
|
const author = extractEntityName(recipe.author);
|
|
1374
2283
|
const publisher = extractEntityName(recipe.publisher);
|
|
1375
|
-
const url = (
|
|
2284
|
+
const url = (_a = recipe.url || recipe.mainEntityOfPage) == null ? void 0 : _a.trim();
|
|
1376
2285
|
const source = {};
|
|
1377
2286
|
if (author) source.author = author;
|
|
1378
2287
|
if (publisher) source.name = publisher;
|
|
@@ -1401,11 +2310,11 @@ function extractEntityName(value) {
|
|
|
1401
2310
|
return void 0;
|
|
1402
2311
|
}
|
|
1403
2312
|
function convertAttribution(recipe) {
|
|
1404
|
-
var
|
|
2313
|
+
var _a, _b;
|
|
1405
2314
|
const attribution = {};
|
|
1406
|
-
const url = (
|
|
2315
|
+
const url = (_a = recipe.url || recipe.mainEntityOfPage) == null ? void 0 : _a.trim();
|
|
1407
2316
|
const author = extractEntityName(recipe.author);
|
|
1408
|
-
const datePublished = (
|
|
2317
|
+
const datePublished = (_b = recipe.datePublished) == null ? void 0 : _b.trim();
|
|
1409
2318
|
if (url) attribution.url = url;
|
|
1410
2319
|
if (author) attribution.author = author;
|
|
1411
2320
|
if (datePublished) attribution.datePublished = datePublished;
|
|
@@ -1486,17 +2395,15 @@ function convertNutrition(nutrition) {
|
|
|
1486
2395
|
return hasData ? result : void 0;
|
|
1487
2396
|
}
|
|
1488
2397
|
|
|
1489
|
-
// src/schemas/registry/
|
|
1490
|
-
var
|
|
1491
|
-
|
|
2398
|
+
// src/schemas/registry/stacks.json
|
|
2399
|
+
var stacks_default = {
|
|
2400
|
+
stacks: [
|
|
1492
2401
|
{
|
|
1493
2402
|
id: "attribution",
|
|
1494
|
-
versions: [
|
|
1495
|
-
1
|
|
1496
|
-
],
|
|
2403
|
+
versions: [1],
|
|
1497
2404
|
latest: 1,
|
|
1498
|
-
namespace: "
|
|
1499
|
-
schema: "
|
|
2405
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/attribution",
|
|
2406
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/attribution",
|
|
1500
2407
|
schemaOrgMappable: true,
|
|
1501
2408
|
schemaOrgConfidence: "medium",
|
|
1502
2409
|
minProfile: "minimal",
|
|
@@ -1504,12 +2411,10 @@ var modules_default = {
|
|
|
1504
2411
|
},
|
|
1505
2412
|
{
|
|
1506
2413
|
id: "taxonomy",
|
|
1507
|
-
versions: [
|
|
1508
|
-
1
|
|
1509
|
-
],
|
|
2414
|
+
versions: [1],
|
|
1510
2415
|
latest: 1,
|
|
1511
|
-
namespace: "
|
|
1512
|
-
schema: "
|
|
2416
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/taxonomy",
|
|
2417
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/taxonomy",
|
|
1513
2418
|
schemaOrgMappable: true,
|
|
1514
2419
|
schemaOrgConfidence: "high",
|
|
1515
2420
|
minProfile: "minimal",
|
|
@@ -1517,12 +2422,10 @@ var modules_default = {
|
|
|
1517
2422
|
},
|
|
1518
2423
|
{
|
|
1519
2424
|
id: "media",
|
|
1520
|
-
versions: [
|
|
1521
|
-
1
|
|
1522
|
-
],
|
|
2425
|
+
versions: [1],
|
|
1523
2426
|
latest: 1,
|
|
1524
|
-
namespace: "
|
|
1525
|
-
schema: "
|
|
2427
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/media",
|
|
2428
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/media",
|
|
1526
2429
|
schemaOrgMappable: true,
|
|
1527
2430
|
schemaOrgConfidence: "medium",
|
|
1528
2431
|
minProfile: "minimal",
|
|
@@ -1530,12 +2433,10 @@ var modules_default = {
|
|
|
1530
2433
|
},
|
|
1531
2434
|
{
|
|
1532
2435
|
id: "nutrition",
|
|
1533
|
-
versions: [
|
|
1534
|
-
1
|
|
1535
|
-
],
|
|
2436
|
+
versions: [1],
|
|
1536
2437
|
latest: 1,
|
|
1537
|
-
namespace: "
|
|
1538
|
-
schema: "
|
|
2438
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/nutrition",
|
|
2439
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/nutrition",
|
|
1539
2440
|
schemaOrgMappable: false,
|
|
1540
2441
|
schemaOrgConfidence: "low",
|
|
1541
2442
|
minProfile: "minimal",
|
|
@@ -1543,12 +2444,10 @@ var modules_default = {
|
|
|
1543
2444
|
},
|
|
1544
2445
|
{
|
|
1545
2446
|
id: "times",
|
|
1546
|
-
versions: [
|
|
1547
|
-
1
|
|
1548
|
-
],
|
|
2447
|
+
versions: [1],
|
|
1549
2448
|
latest: 1,
|
|
1550
|
-
namespace: "
|
|
1551
|
-
schema: "
|
|
2449
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/times",
|
|
2450
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/times",
|
|
1552
2451
|
schemaOrgMappable: true,
|
|
1553
2452
|
schemaOrgConfidence: "medium",
|
|
1554
2453
|
minProfile: "minimal",
|
|
@@ -1556,12 +2455,10 @@ var modules_default = {
|
|
|
1556
2455
|
},
|
|
1557
2456
|
{
|
|
1558
2457
|
id: "schedule",
|
|
1559
|
-
versions: [
|
|
1560
|
-
1
|
|
1561
|
-
],
|
|
2458
|
+
versions: [1],
|
|
1562
2459
|
latest: 1,
|
|
1563
|
-
namespace: "
|
|
1564
|
-
schema: "
|
|
2460
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/schedule",
|
|
2461
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/schedule",
|
|
1565
2462
|
schemaOrgMappable: false,
|
|
1566
2463
|
schemaOrgConfidence: "low",
|
|
1567
2464
|
minProfile: "core",
|
|
@@ -1572,14 +2469,14 @@ var modules_default = {
|
|
|
1572
2469
|
|
|
1573
2470
|
// src/converters/toSchemaOrg.ts
|
|
1574
2471
|
function convertBasicMetadata(recipe) {
|
|
1575
|
-
var
|
|
2472
|
+
var _a;
|
|
1576
2473
|
return cleanOutput({
|
|
1577
2474
|
"@context": "https://schema.org",
|
|
1578
2475
|
"@type": "Recipe",
|
|
1579
2476
|
name: recipe.name,
|
|
1580
2477
|
description: recipe.description,
|
|
1581
2478
|
image: recipe.image,
|
|
1582
|
-
url: (
|
|
2479
|
+
url: (_a = recipe.source) == null ? void 0 : _a.url,
|
|
1583
2480
|
datePublished: recipe.dateAdded,
|
|
1584
2481
|
dateModified: recipe.dateModified
|
|
1585
2482
|
});
|
|
@@ -1587,7 +2484,7 @@ function convertBasicMetadata(recipe) {
|
|
|
1587
2484
|
function convertIngredients2(ingredients = []) {
|
|
1588
2485
|
const result = [];
|
|
1589
2486
|
ingredients.forEach((ingredient) => {
|
|
1590
|
-
var
|
|
2487
|
+
var _a;
|
|
1591
2488
|
if (!ingredient) {
|
|
1592
2489
|
return;
|
|
1593
2490
|
}
|
|
@@ -1617,7 +2514,7 @@ function convertIngredients2(ingredients = []) {
|
|
|
1617
2514
|
});
|
|
1618
2515
|
return;
|
|
1619
2516
|
}
|
|
1620
|
-
const value = (
|
|
2517
|
+
const value = (_a = ingredient.item) == null ? void 0 : _a.trim();
|
|
1621
2518
|
if (value) {
|
|
1622
2519
|
result.push(value);
|
|
1623
2520
|
}
|
|
@@ -1652,13 +2549,13 @@ function convertInstruction(entry) {
|
|
|
1652
2549
|
return createHowToStep(String(entry));
|
|
1653
2550
|
}
|
|
1654
2551
|
function createHowToStep(entry) {
|
|
1655
|
-
var
|
|
2552
|
+
var _a;
|
|
1656
2553
|
if (!entry) return null;
|
|
1657
2554
|
if (typeof entry === "string") {
|
|
1658
2555
|
const trimmed2 = entry.trim();
|
|
1659
2556
|
return trimmed2 || null;
|
|
1660
2557
|
}
|
|
1661
|
-
const trimmed = (
|
|
2558
|
+
const trimmed = (_a = entry.text) == null ? void 0 : _a.trim();
|
|
1662
2559
|
if (!trimmed) {
|
|
1663
2560
|
return null;
|
|
1664
2561
|
}
|
|
@@ -1790,23 +2687,30 @@ function cleanOutput(obj) {
|
|
|
1790
2687
|
Object.entries(obj).filter(([, value]) => value !== void 0)
|
|
1791
2688
|
);
|
|
1792
2689
|
}
|
|
1793
|
-
function
|
|
1794
|
-
const
|
|
1795
|
-
|
|
2690
|
+
function getSchemaOrgMappableStacks(stacks = {}) {
|
|
2691
|
+
const mappableStackIds = /* @__PURE__ */ new Set();
|
|
2692
|
+
const mappableFromRegistry = stacks_default.stacks.filter((stack) => stack.schemaOrgMappable).map((stack) => `${stack.id}@${stack.latest}`);
|
|
2693
|
+
for (const [name, version] of Object.entries(stacks)) {
|
|
2694
|
+
const stackId = `${name}@${version}`;
|
|
2695
|
+
if (mappableFromRegistry.includes(stackId)) {
|
|
2696
|
+
mappableStackIds.add(stackId);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return mappableStackIds;
|
|
1796
2700
|
}
|
|
1797
2701
|
function toSchemaOrg(recipe) {
|
|
1798
2702
|
const base = convertBasicMetadata(recipe);
|
|
1799
2703
|
const ingredients = convertIngredients2(recipe.ingredients);
|
|
1800
2704
|
const instructions = convertInstructions2(recipe.instructions);
|
|
1801
|
-
const
|
|
1802
|
-
const
|
|
1803
|
-
const hasMappableNutrition =
|
|
2705
|
+
const recipeStacks = recipe.stacks && typeof recipe.stacks === "object" && !Array.isArray(recipe.stacks) ? recipe.stacks : {};
|
|
2706
|
+
const mappableStacks = getSchemaOrgMappableStacks(recipeStacks);
|
|
2707
|
+
const hasMappableNutrition = mappableStacks.has("nutrition@1");
|
|
1804
2708
|
const nutrition = hasMappableNutrition ? convertNutrition2(recipe.nutrition) : void 0;
|
|
1805
|
-
const hasMappableTimes =
|
|
2709
|
+
const hasMappableTimes = mappableStacks.has("times@1");
|
|
1806
2710
|
const timeData = hasMappableTimes ? recipe.times ? convertTimesModule(recipe.times) : convertTime2(recipe.time) : {};
|
|
1807
|
-
const hasMappableAttribution =
|
|
2711
|
+
const hasMappableAttribution = mappableStacks.has("attribution@1");
|
|
1808
2712
|
const attributionData = hasMappableAttribution ? convertAuthor(recipe.source) : {};
|
|
1809
|
-
const hasMappableTaxonomy =
|
|
2713
|
+
const hasMappableTaxonomy = mappableStacks.has("taxonomy@1");
|
|
1810
2714
|
const taxonomyData = hasMappableTaxonomy ? convertCategoryTags(recipe.category, recipe.tags) : {};
|
|
1811
2715
|
return cleanOutput({
|
|
1812
2716
|
...base,
|
|
@@ -1834,8 +2738,6 @@ function isRecipeNode(value) {
|
|
|
1834
2738
|
return false;
|
|
1835
2739
|
}
|
|
1836
2740
|
const type = value["@type"];
|
|
1837
|
-
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(() => {
|
|
1838
|
-
});
|
|
1839
2741
|
if (typeof type === "string") {
|
|
1840
2742
|
return RECIPE_TYPES.has(type.toLowerCase());
|
|
1841
2743
|
}
|
|
@@ -1873,7 +2775,7 @@ function extractRecipeBrowser(html) {
|
|
|
1873
2775
|
return { recipe: null, source: null };
|
|
1874
2776
|
}
|
|
1875
2777
|
function extractJsonLdBrowser(html) {
|
|
1876
|
-
var
|
|
2778
|
+
var _a;
|
|
1877
2779
|
if (typeof globalThis.DOMParser === "undefined") {
|
|
1878
2780
|
return null;
|
|
1879
2781
|
}
|
|
@@ -1888,7 +2790,7 @@ function extractJsonLdBrowser(html) {
|
|
|
1888
2790
|
if (!parsed) return;
|
|
1889
2791
|
collectCandidates(parsed, candidates);
|
|
1890
2792
|
});
|
|
1891
|
-
return (
|
|
2793
|
+
return (_a = candidates[0]) != null ? _a : null;
|
|
1892
2794
|
}
|
|
1893
2795
|
function extractMicrodataBrowser(html) {
|
|
1894
2796
|
if (typeof globalThis.DOMParser === "undefined") {
|
|
@@ -1921,8 +2823,8 @@ function extractMicrodataBrowser(html) {
|
|
|
1921
2823
|
}
|
|
1922
2824
|
const instructions = [];
|
|
1923
2825
|
recipeEl.querySelectorAll('[itemprop="recipeInstructions"]').forEach((el) => {
|
|
1924
|
-
var
|
|
1925
|
-
const text = normalizeText(el.getAttribute("content")) || normalizeText(((
|
|
2826
|
+
var _a;
|
|
2827
|
+
const text = normalizeText(el.getAttribute("content")) || normalizeText(((_a = el.querySelector('[itemprop="text"]')) == null ? void 0 : _a.textContent) || void 0) || normalizeText(el.textContent || void 0);
|
|
1926
2828
|
if (text) instructions.push(text);
|
|
1927
2829
|
});
|
|
1928
2830
|
if (instructions.length) {
|
|
@@ -2093,12 +2995,12 @@ var UNIT_DEFINITIONS = {
|
|
|
2093
2995
|
...COUNT_UNITS
|
|
2094
2996
|
};
|
|
2095
2997
|
function normalizeUnitToken(unit) {
|
|
2096
|
-
var
|
|
2998
|
+
var _a;
|
|
2097
2999
|
if (!unit) {
|
|
2098
3000
|
return null;
|
|
2099
3001
|
}
|
|
2100
3002
|
const token = unit.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
2101
|
-
const canonical = (
|
|
3003
|
+
const canonical = (_a = UNIT_SYNONYMS[token]) != null ? _a : token;
|
|
2102
3004
|
return canonical in UNIT_DEFINITIONS ? canonical : null;
|
|
2103
3005
|
}
|
|
2104
3006
|
var UNIT_SYNONYMS = {
|
|
@@ -2204,8 +3106,8 @@ var VOLUME_TO_MASS_EQUIV_G_PER_UNIT = {
|
|
|
2204
3106
|
};
|
|
2205
3107
|
var DEFAULT_ROUND_MODE = "sane";
|
|
2206
3108
|
function convertLineItemToMetric(item, mode, opts) {
|
|
2207
|
-
var
|
|
2208
|
-
const roundMode = (
|
|
3109
|
+
var _a, _b, _c, _d;
|
|
3110
|
+
const roundMode = (_a = opts == null ? void 0 : opts.round) != null ? _a : DEFAULT_ROUND_MODE;
|
|
2209
3111
|
const normalizedUnit = normalizeUnitToken(item.unit);
|
|
2210
3112
|
if (!normalizedUnit) {
|
|
2211
3113
|
if (!item.unit || item.unit.trim() === "") {
|
|
@@ -2219,7 +3121,7 @@ function convertLineItemToMetric(item, mode, opts) {
|
|
|
2219
3121
|
}
|
|
2220
3122
|
if (mode === "volume") {
|
|
2221
3123
|
if (definition.dimension !== "volume") {
|
|
2222
|
-
throw new UnsupportedConversionError((
|
|
3124
|
+
throw new UnsupportedConversionError((_b = item.unit) != null ? _b : "", mode);
|
|
2223
3125
|
}
|
|
2224
3126
|
const { quantity, unit } = finalizeMetricVolume(
|
|
2225
3127
|
convertToMetricBase(item.quantity, normalizedUnit).quantity,
|
|
@@ -2305,9 +3207,9 @@ function roundLargeMetric(value) {
|
|
|
2305
3207
|
return Math.round(value * 100) / 100;
|
|
2306
3208
|
}
|
|
2307
3209
|
function lookupEquivalency(ingredient, unit) {
|
|
2308
|
-
var
|
|
3210
|
+
var _a;
|
|
2309
3211
|
const key = ingredient.trim().toLowerCase();
|
|
2310
|
-
return (
|
|
3212
|
+
return (_a = VOLUME_TO_MASS_EQUIV_G_PER_UNIT[key]) == null ? void 0 : _a[unit];
|
|
2311
3213
|
}
|
|
2312
3214
|
|
|
2313
3215
|
// src/mise-en-place/index.ts
|
|
@@ -2406,8 +3308,8 @@ function miseEnPlace(ingredients) {
|
|
|
2406
3308
|
return { tasks, ungrouped };
|
|
2407
3309
|
}
|
|
2408
3310
|
function deriveIngredientLabel(ingredient) {
|
|
2409
|
-
var
|
|
2410
|
-
return (_c = (
|
|
3311
|
+
var _a, _b, _c;
|
|
3312
|
+
return (_c = (_b = (_a = toDisplayString(ingredient.name)) != null ? _a : toDisplayString(ingredient.item)) != null ? _b : toDisplayString(ingredient.id)) != null ? _c : "ingredient";
|
|
2411
3313
|
}
|
|
2412
3314
|
function extractNormalizedList(values) {
|
|
2413
3315
|
if (!Array.isArray(values)) {
|
|
@@ -2479,6 +3381,6 @@ function localeCompare(left, right) {
|
|
|
2479
3381
|
return (left != null ? left : "").localeCompare(right != null ? right : "");
|
|
2480
3382
|
}
|
|
2481
3383
|
|
|
2482
|
-
export { MissingEquivalencyError, SOUSTACK_SPEC_VERSION, UnknownUnitError, UnsupportedConversionError, convertLineItemToMetric, detectProfiles, extractSchemaOrgRecipeFromHTML, fromSchemaOrg, miseEnPlace, scaleRecipe, toSchemaOrg, validateRecipe };
|
|
3384
|
+
export { MissingEquivalencyError, SOUSTACK_SPEC_VERSION, UnknownUnitError, UnsupportedConversionError, convertLineItemToMetric, detectProfiles, extractSchemaOrgRecipeFromHTML, fromSchemaOrg, miseEnPlace, normalizeRecipe, scaleRecipe, toSchemaOrg, validateRecipe };
|
|
2483
3385
|
//# sourceMappingURL=index.mjs.map
|
|
2484
3386
|
//# sourceMappingURL=index.mjs.map
|