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