soustack 0.2.3 → 0.3.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 +100 -7
- package/dist/cli/index.js +738 -793
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +117 -19
- package/dist/index.d.ts +117 -19
- package/dist/index.js +1264 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1260 -802
- package/dist/index.mjs.map +1 -1
- package/dist/scrape.d.mts +36 -10
- package/dist/scrape.d.ts +36 -10
- package/dist/scrape.js +105 -3
- package/dist/scrape.js.map +1 -1
- package/dist/scrape.mjs +105 -3
- package/dist/scrape.mjs.map +1 -1
- package/package.json +7 -4
- package/src/profiles/base.schema.json +2 -2
- package/src/profiles/cookable.schema.json +4 -4
- package/src/profiles/illustrated.schema.json +4 -4
- package/src/profiles/quantified.schema.json +4 -4
- package/src/profiles/scalable.schema.json +6 -6
- package/src/profiles/schedulable.schema.json +4 -4
- package/src/schema.json +15 -3
- package/src/soustack.schema.json +15 -3
package/dist/cli/index.js
CHANGED
|
@@ -364,8 +364,22 @@ function fromSchemaOrg(input) {
|
|
|
364
364
|
const tags = collectTags(recipeNode.recipeCuisine, recipeNode.keywords);
|
|
365
365
|
const category = extractFirst(recipeNode.recipeCategory);
|
|
366
366
|
const source = convertSource(recipeNode);
|
|
367
|
-
const
|
|
367
|
+
const dateModified = recipeNode.dateModified || void 0;
|
|
368
|
+
const nutrition = convertNutrition(recipeNode.nutrition);
|
|
369
|
+
const attribution = convertAttribution(recipeNode);
|
|
370
|
+
const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
|
|
371
|
+
const media = convertMedia(recipeNode.image, recipeNode.video);
|
|
372
|
+
const times = convertTimes(time);
|
|
373
|
+
const modules = [];
|
|
374
|
+
if (attribution) modules.push("attribution@1");
|
|
375
|
+
if (taxonomy) modules.push("taxonomy@1");
|
|
376
|
+
if (media) modules.push("media@1");
|
|
377
|
+
if (nutrition) modules.push("nutrition@1");
|
|
378
|
+
if (times) modules.push("times@1");
|
|
368
379
|
return {
|
|
380
|
+
"@type": "Recipe",
|
|
381
|
+
profile: "minimal",
|
|
382
|
+
modules: modules.sort(),
|
|
369
383
|
name: recipeNode.name.trim(),
|
|
370
384
|
description: recipeNode.description?.trim() || void 0,
|
|
371
385
|
image: normalizeImage(recipeNode.image),
|
|
@@ -373,12 +387,16 @@ function fromSchemaOrg(input) {
|
|
|
373
387
|
tags: tags.length ? tags : void 0,
|
|
374
388
|
source,
|
|
375
389
|
dateAdded: recipeNode.datePublished || void 0,
|
|
376
|
-
dateModified: recipeNode.dateModified || void 0,
|
|
377
390
|
yield: recipeYield,
|
|
378
391
|
time,
|
|
379
392
|
ingredients,
|
|
380
393
|
instructions,
|
|
381
|
-
|
|
394
|
+
...dateModified ? { dateModified } : {},
|
|
395
|
+
...nutrition ? { nutrition } : {},
|
|
396
|
+
...attribution ? { attribution } : {},
|
|
397
|
+
...taxonomy ? { taxonomy } : {},
|
|
398
|
+
...media ? { media } : {},
|
|
399
|
+
...times ? { times } : {}
|
|
382
400
|
};
|
|
383
401
|
}
|
|
384
402
|
function extractRecipeNode(input) {
|
|
@@ -591,6 +609,174 @@ function extractEntityName(value) {
|
|
|
591
609
|
}
|
|
592
610
|
return void 0;
|
|
593
611
|
}
|
|
612
|
+
function convertAttribution(recipe) {
|
|
613
|
+
const attribution = {};
|
|
614
|
+
const url = (recipe.url || recipe.mainEntityOfPage)?.trim();
|
|
615
|
+
const author = extractEntityName(recipe.author);
|
|
616
|
+
const datePublished = recipe.datePublished?.trim();
|
|
617
|
+
if (url) attribution.url = url;
|
|
618
|
+
if (author) attribution.author = author;
|
|
619
|
+
if (datePublished) attribution.datePublished = datePublished;
|
|
620
|
+
return Object.keys(attribution).length ? attribution : void 0;
|
|
621
|
+
}
|
|
622
|
+
function convertTaxonomy(keywords, category, cuisine) {
|
|
623
|
+
const taxonomy = {};
|
|
624
|
+
if (keywords.length) taxonomy.keywords = keywords;
|
|
625
|
+
if (category) taxonomy.category = category;
|
|
626
|
+
if (cuisine) taxonomy.cuisine = cuisine;
|
|
627
|
+
return Object.keys(taxonomy).length ? taxonomy : void 0;
|
|
628
|
+
}
|
|
629
|
+
function normalizeMediaList(value) {
|
|
630
|
+
if (!value) return [];
|
|
631
|
+
if (typeof value === "string") return [value.trim()].filter(Boolean);
|
|
632
|
+
if (Array.isArray(value)) {
|
|
633
|
+
return value.map((item) => typeof item === "string" ? item.trim() : extractMediaUrl(item)).filter((entry) => Boolean(entry?.length));
|
|
634
|
+
}
|
|
635
|
+
const url = extractMediaUrl(value);
|
|
636
|
+
return url ? [url] : [];
|
|
637
|
+
}
|
|
638
|
+
function extractMediaUrl(value) {
|
|
639
|
+
if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
|
|
640
|
+
const trimmed = value.url.trim();
|
|
641
|
+
return trimmed || void 0;
|
|
642
|
+
}
|
|
643
|
+
return void 0;
|
|
644
|
+
}
|
|
645
|
+
function convertMedia(image, video) {
|
|
646
|
+
const normalizedImage = normalizeImage(image);
|
|
647
|
+
const images = normalizedImage ? Array.isArray(normalizedImage) ? normalizedImage : [normalizedImage] : [];
|
|
648
|
+
const videos = normalizeMediaList(video);
|
|
649
|
+
const media = {};
|
|
650
|
+
if (images.length) media.images = images;
|
|
651
|
+
if (videos.length) media.videos = videos;
|
|
652
|
+
return Object.keys(media).length ? media : void 0;
|
|
653
|
+
}
|
|
654
|
+
function convertTimes(time) {
|
|
655
|
+
if (!time) return void 0;
|
|
656
|
+
const times = {};
|
|
657
|
+
if (typeof time.prep === "number") times.prepMinutes = time.prep;
|
|
658
|
+
if (typeof time.active === "number") times.cookMinutes = time.active;
|
|
659
|
+
if (typeof time.total === "number") times.totalMinutes = time.total;
|
|
660
|
+
return Object.keys(times).length ? times : void 0;
|
|
661
|
+
}
|
|
662
|
+
function convertNutrition(nutrition) {
|
|
663
|
+
if (!nutrition || typeof nutrition !== "object") {
|
|
664
|
+
return void 0;
|
|
665
|
+
}
|
|
666
|
+
const result = {};
|
|
667
|
+
let hasData = false;
|
|
668
|
+
if ("calories" in nutrition) {
|
|
669
|
+
const calories = nutrition.calories;
|
|
670
|
+
if (typeof calories === "number") {
|
|
671
|
+
result.calories = calories;
|
|
672
|
+
hasData = true;
|
|
673
|
+
} else if (typeof calories === "string") {
|
|
674
|
+
const parsed = parseFloat(calories.replace(/[^\d.-]/g, ""));
|
|
675
|
+
if (!isNaN(parsed)) {
|
|
676
|
+
result.calories = parsed;
|
|
677
|
+
hasData = true;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if ("proteinContent" in nutrition || "protein_g" in nutrition) {
|
|
682
|
+
const protein = nutrition.proteinContent || nutrition.protein_g;
|
|
683
|
+
if (typeof protein === "number") {
|
|
684
|
+
result.protein_g = protein;
|
|
685
|
+
hasData = true;
|
|
686
|
+
} else if (typeof protein === "string") {
|
|
687
|
+
const parsed = parseFloat(protein.replace(/[^\d.-]/g, ""));
|
|
688
|
+
if (!isNaN(parsed)) {
|
|
689
|
+
result.protein_g = parsed;
|
|
690
|
+
hasData = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return hasData ? result : void 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/schemas/registry/modules.json
|
|
698
|
+
var modules_default = {
|
|
699
|
+
modules: [
|
|
700
|
+
{
|
|
701
|
+
id: "attribution",
|
|
702
|
+
versions: [
|
|
703
|
+
1
|
|
704
|
+
],
|
|
705
|
+
latest: 1,
|
|
706
|
+
namespace: "https://soustack.org/schemas/recipe/modules/attribution",
|
|
707
|
+
schema: "https://soustack.org/schemas/recipe/modules/attribution/1.schema.json",
|
|
708
|
+
schemaOrgMappable: true,
|
|
709
|
+
schemaOrgConfidence: "medium",
|
|
710
|
+
minProfile: "minimal",
|
|
711
|
+
allowedOnMinimal: true
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
id: "taxonomy",
|
|
715
|
+
versions: [
|
|
716
|
+
1
|
|
717
|
+
],
|
|
718
|
+
latest: 1,
|
|
719
|
+
namespace: "https://soustack.org/schemas/recipe/modules/taxonomy",
|
|
720
|
+
schema: "https://soustack.org/schemas/recipe/modules/taxonomy/1.schema.json",
|
|
721
|
+
schemaOrgMappable: true,
|
|
722
|
+
schemaOrgConfidence: "high",
|
|
723
|
+
minProfile: "minimal",
|
|
724
|
+
allowedOnMinimal: true
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
id: "media",
|
|
728
|
+
versions: [
|
|
729
|
+
1
|
|
730
|
+
],
|
|
731
|
+
latest: 1,
|
|
732
|
+
namespace: "https://soustack.org/schemas/recipe/modules/media",
|
|
733
|
+
schema: "https://soustack.org/schemas/recipe/modules/media/1.schema.json",
|
|
734
|
+
schemaOrgMappable: true,
|
|
735
|
+
schemaOrgConfidence: "medium",
|
|
736
|
+
minProfile: "minimal",
|
|
737
|
+
allowedOnMinimal: true
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
id: "nutrition",
|
|
741
|
+
versions: [
|
|
742
|
+
1
|
|
743
|
+
],
|
|
744
|
+
latest: 1,
|
|
745
|
+
namespace: "https://soustack.org/schemas/recipe/modules/nutrition",
|
|
746
|
+
schema: "https://soustack.org/schemas/recipe/modules/nutrition/1.schema.json",
|
|
747
|
+
schemaOrgMappable: false,
|
|
748
|
+
schemaOrgConfidence: "low",
|
|
749
|
+
minProfile: "minimal",
|
|
750
|
+
allowedOnMinimal: true
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
id: "times",
|
|
754
|
+
versions: [
|
|
755
|
+
1
|
|
756
|
+
],
|
|
757
|
+
latest: 1,
|
|
758
|
+
namespace: "https://soustack.org/schemas/recipe/modules/times",
|
|
759
|
+
schema: "https://soustack.org/schemas/recipe/modules/times/1.schema.json",
|
|
760
|
+
schemaOrgMappable: true,
|
|
761
|
+
schemaOrgConfidence: "medium",
|
|
762
|
+
minProfile: "minimal",
|
|
763
|
+
allowedOnMinimal: true
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
id: "schedule",
|
|
767
|
+
versions: [
|
|
768
|
+
1
|
|
769
|
+
],
|
|
770
|
+
latest: 1,
|
|
771
|
+
namespace: "https://soustack.org/schemas/recipe/modules/schedule",
|
|
772
|
+
schema: "https://soustack.org/schemas/recipe/modules/schedule/1.schema.json",
|
|
773
|
+
schemaOrgMappable: false,
|
|
774
|
+
schemaOrgConfidence: "low",
|
|
775
|
+
minProfile: "core",
|
|
776
|
+
allowedOnMinimal: false
|
|
777
|
+
}
|
|
778
|
+
]
|
|
779
|
+
};
|
|
594
780
|
|
|
595
781
|
// src/converters/toSchemaOrg.ts
|
|
596
782
|
function convertBasicMetadata(recipe) {
|
|
@@ -729,6 +915,22 @@ function convertTime2(time) {
|
|
|
729
915
|
}
|
|
730
916
|
return result;
|
|
731
917
|
}
|
|
918
|
+
function convertTimesModule(times) {
|
|
919
|
+
if (!times) {
|
|
920
|
+
return {};
|
|
921
|
+
}
|
|
922
|
+
const result = {};
|
|
923
|
+
if (times.prepMinutes !== void 0) {
|
|
924
|
+
result.prepTime = formatDuration(times.prepMinutes);
|
|
925
|
+
}
|
|
926
|
+
if (times.cookMinutes !== void 0) {
|
|
927
|
+
result.cookTime = formatDuration(times.cookMinutes);
|
|
928
|
+
}
|
|
929
|
+
if (times.totalMinutes !== void 0) {
|
|
930
|
+
result.totalTime = formatDuration(times.totalMinutes);
|
|
931
|
+
}
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
732
934
|
function convertYield(yld) {
|
|
733
935
|
if (!yld) {
|
|
734
936
|
return void 0;
|
|
@@ -767,33 +969,58 @@ function convertCategoryTags(category, tags) {
|
|
|
767
969
|
}
|
|
768
970
|
return result;
|
|
769
971
|
}
|
|
770
|
-
function
|
|
972
|
+
function convertNutrition2(nutrition) {
|
|
771
973
|
if (!nutrition) {
|
|
772
974
|
return void 0;
|
|
773
975
|
}
|
|
774
|
-
|
|
775
|
-
...nutrition,
|
|
976
|
+
const result = {
|
|
776
977
|
"@type": "NutritionInformation"
|
|
777
978
|
};
|
|
979
|
+
if (nutrition.calories !== void 0) {
|
|
980
|
+
if (typeof nutrition.calories === "number") {
|
|
981
|
+
result.calories = `${nutrition.calories} calories`;
|
|
982
|
+
} else {
|
|
983
|
+
result.calories = nutrition.calories;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
Object.keys(nutrition).forEach((key) => {
|
|
987
|
+
if (key !== "calories" && key !== "@type") {
|
|
988
|
+
result[key] = nutrition[key];
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
return result;
|
|
778
992
|
}
|
|
779
993
|
function cleanOutput(obj) {
|
|
780
994
|
return Object.fromEntries(
|
|
781
995
|
Object.entries(obj).filter(([, value]) => value !== void 0)
|
|
782
996
|
);
|
|
783
997
|
}
|
|
998
|
+
function getSchemaOrgMappableModules(modules = []) {
|
|
999
|
+
const mappableModules = modules_default.modules.filter((m) => m.schemaOrgMappable).map((m) => `${m.id}@${m.latest}`);
|
|
1000
|
+
return modules.filter((moduleId) => mappableModules.includes(moduleId));
|
|
1001
|
+
}
|
|
784
1002
|
function toSchemaOrg(recipe) {
|
|
785
1003
|
const base = convertBasicMetadata(recipe);
|
|
786
1004
|
const ingredients = convertIngredients2(recipe.ingredients);
|
|
787
1005
|
const instructions = convertInstructions2(recipe.instructions);
|
|
788
|
-
const
|
|
1006
|
+
const recipeModules = Array.isArray(recipe.modules) ? recipe.modules : [];
|
|
1007
|
+
const mappableModules = getSchemaOrgMappableModules(recipeModules);
|
|
1008
|
+
const hasMappableNutrition = mappableModules.includes("nutrition@1");
|
|
1009
|
+
const nutrition = hasMappableNutrition ? convertNutrition2(recipe.nutrition) : void 0;
|
|
1010
|
+
const hasMappableTimes = mappableModules.includes("times@1");
|
|
1011
|
+
const timeData = hasMappableTimes ? recipe.times ? convertTimesModule(recipe.times) : convertTime2(recipe.time) : {};
|
|
1012
|
+
const hasMappableAttribution = mappableModules.includes("attribution@1");
|
|
1013
|
+
const attributionData = hasMappableAttribution ? convertAuthor(recipe.source) : {};
|
|
1014
|
+
const hasMappableTaxonomy = mappableModules.includes("taxonomy@1");
|
|
1015
|
+
const taxonomyData = hasMappableTaxonomy ? convertCategoryTags(recipe.category, recipe.tags) : {};
|
|
789
1016
|
return cleanOutput({
|
|
790
1017
|
...base,
|
|
791
1018
|
recipeIngredient: ingredients.length ? ingredients : void 0,
|
|
792
1019
|
recipeInstructions: instructions.length ? instructions : void 0,
|
|
793
1020
|
recipeYield: convertYield(recipe.yield),
|
|
794
|
-
...
|
|
795
|
-
...
|
|
796
|
-
...
|
|
1021
|
+
...timeData,
|
|
1022
|
+
...attributionData,
|
|
1023
|
+
...taxonomyData,
|
|
797
1024
|
nutrition
|
|
798
1025
|
});
|
|
799
1026
|
}
|
|
@@ -1226,894 +1453,523 @@ async function scrapeRecipe(url, options = {}) {
|
|
|
1226
1453
|
return soustackRecipe;
|
|
1227
1454
|
}
|
|
1228
1455
|
|
|
1229
|
-
// src/schema.json
|
|
1230
|
-
var
|
|
1456
|
+
// src/schemas/recipe/base.schema.json
|
|
1457
|
+
var base_schema_default = {
|
|
1231
1458
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1232
|
-
$id: "http://soustack.org/schema/
|
|
1233
|
-
title: "Soustack Recipe Schema
|
|
1234
|
-
description: "
|
|
1459
|
+
$id: "http://soustack.org/schema/recipe/base.schema.json",
|
|
1460
|
+
title: "Soustack Recipe Base Schema",
|
|
1461
|
+
description: "Base document shape for Soustack recipe documents. Profiles and modules build on this baseline.",
|
|
1235
1462
|
type: "object",
|
|
1236
|
-
|
|
1237
|
-
additionalProperties: false,
|
|
1238
|
-
patternProperties: {
|
|
1239
|
-
"^x-": {}
|
|
1240
|
-
},
|
|
1463
|
+
additionalProperties: true,
|
|
1241
1464
|
properties: {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
description: "Optional schema hint for tooling compatibility"
|
|
1246
|
-
},
|
|
1247
|
-
id: {
|
|
1248
|
-
type: "string",
|
|
1249
|
-
description: "Unique identifier (slug or UUID)"
|
|
1250
|
-
},
|
|
1251
|
-
name: {
|
|
1252
|
-
type: "string",
|
|
1253
|
-
description: "The title of the recipe"
|
|
1254
|
-
},
|
|
1255
|
-
title: {
|
|
1256
|
-
type: "string",
|
|
1257
|
-
description: "Optional display title; alias for name"
|
|
1465
|
+
"@type": {
|
|
1466
|
+
const: "Recipe",
|
|
1467
|
+
description: "Document marker for Soustack recipes"
|
|
1258
1468
|
},
|
|
1259
|
-
|
|
1469
|
+
profile: {
|
|
1260
1470
|
type: "string",
|
|
1261
|
-
|
|
1262
|
-
description: "DEPRECATED: use recipeVersion for authoring revisions"
|
|
1471
|
+
description: "Profile identifier applied to this recipe"
|
|
1263
1472
|
},
|
|
1264
|
-
|
|
1265
|
-
type: "string",
|
|
1266
|
-
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
1267
|
-
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
1268
|
-
},
|
|
1269
|
-
description: {
|
|
1270
|
-
type: "string"
|
|
1271
|
-
},
|
|
1272
|
-
category: {
|
|
1273
|
-
type: "string",
|
|
1274
|
-
examples: ["Main Course", "Dessert"]
|
|
1275
|
-
},
|
|
1276
|
-
tags: {
|
|
1473
|
+
modules: {
|
|
1277
1474
|
type: "array",
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
description: "Recipe-level hero image(s)",
|
|
1282
|
-
anyOf: [
|
|
1283
|
-
{
|
|
1284
|
-
type: "string",
|
|
1285
|
-
format: "uri"
|
|
1286
|
-
},
|
|
1287
|
-
{
|
|
1288
|
-
type: "array",
|
|
1289
|
-
minItems: 1,
|
|
1290
|
-
items: {
|
|
1291
|
-
type: "string",
|
|
1292
|
-
format: "uri"
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
]
|
|
1296
|
-
},
|
|
1297
|
-
dateAdded: {
|
|
1298
|
-
type: "string",
|
|
1299
|
-
format: "date-time"
|
|
1300
|
-
},
|
|
1301
|
-
metadata: {
|
|
1302
|
-
type: "object",
|
|
1303
|
-
additionalProperties: true,
|
|
1304
|
-
description: "Free-form vendor metadata"
|
|
1305
|
-
},
|
|
1306
|
-
source: {
|
|
1307
|
-
type: "object",
|
|
1308
|
-
properties: {
|
|
1309
|
-
author: { type: "string" },
|
|
1310
|
-
url: { type: "string", format: "uri" },
|
|
1311
|
-
name: { type: "string" },
|
|
1312
|
-
adapted: { type: "boolean" }
|
|
1475
|
+
description: "List of module identifiers applied to this recipe",
|
|
1476
|
+
items: {
|
|
1477
|
+
type: "string"
|
|
1313
1478
|
}
|
|
1314
1479
|
},
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
time: {
|
|
1319
|
-
$ref: "#/definitions/time"
|
|
1320
|
-
},
|
|
1321
|
-
equipment: {
|
|
1322
|
-
type: "array",
|
|
1323
|
-
items: { $ref: "#/definitions/equipment" }
|
|
1480
|
+
name: {
|
|
1481
|
+
type: "string",
|
|
1482
|
+
description: "Human-readable recipe name"
|
|
1324
1483
|
},
|
|
1325
1484
|
ingredients: {
|
|
1326
1485
|
type: "array",
|
|
1327
|
-
|
|
1328
|
-
anyOf: [
|
|
1329
|
-
{ type: "string" },
|
|
1330
|
-
{ $ref: "#/definitions/ingredient" },
|
|
1331
|
-
{ $ref: "#/definitions/ingredientSubsection" }
|
|
1332
|
-
]
|
|
1333
|
-
}
|
|
1486
|
+
description: "Ingredients payload; content is validated by profiles/modules"
|
|
1334
1487
|
},
|
|
1335
1488
|
instructions: {
|
|
1336
1489
|
type: "array",
|
|
1337
|
-
|
|
1338
|
-
anyOf: [
|
|
1339
|
-
{ type: "string" },
|
|
1340
|
-
{ $ref: "#/definitions/instruction" },
|
|
1341
|
-
{ $ref: "#/definitions/instructionSubsection" }
|
|
1342
|
-
]
|
|
1343
|
-
}
|
|
1344
|
-
},
|
|
1345
|
-
storage: {
|
|
1346
|
-
$ref: "#/definitions/storage"
|
|
1347
|
-
},
|
|
1348
|
-
substitutions: {
|
|
1349
|
-
type: "array",
|
|
1350
|
-
items: { $ref: "#/definitions/substitution" }
|
|
1490
|
+
description: "Instruction payload; content is validated by profiles/modules"
|
|
1351
1491
|
}
|
|
1352
1492
|
},
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
},
|
|
1364
|
-
|
|
1365
|
-
type: "object",
|
|
1366
|
-
properties: {
|
|
1367
|
-
prep: { type: "number" },
|
|
1368
|
-
active: { type: "number" },
|
|
1369
|
-
passive: { type: "number" },
|
|
1370
|
-
total: { type: "number" },
|
|
1371
|
-
prepTime: { type: "string", format: "duration" },
|
|
1372
|
-
cookTime: { type: "string", format: "duration" }
|
|
1373
|
-
},
|
|
1374
|
-
minProperties: 1
|
|
1375
|
-
},
|
|
1376
|
-
quantity: {
|
|
1377
|
-
type: "object",
|
|
1378
|
-
required: ["amount"],
|
|
1379
|
-
properties: {
|
|
1380
|
-
amount: { type: "number" },
|
|
1381
|
-
unit: { type: ["string", "null"] }
|
|
1382
|
-
}
|
|
1383
|
-
},
|
|
1384
|
-
scaling: {
|
|
1493
|
+
required: ["@type"]
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// src/schemas/recipe/profiles/core.schema.json
|
|
1497
|
+
var core_schema_default = {
|
|
1498
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1499
|
+
$id: "http://soustack.org/schema/recipe/profiles/core.schema.json",
|
|
1500
|
+
title: "Soustack Recipe Core Profile",
|
|
1501
|
+
description: "Core profile that builds on the minimal profile and is intended to be combined with recipe modules.",
|
|
1502
|
+
allOf: [
|
|
1503
|
+
{ $ref: "http://soustack.org/schema/recipe/base.schema.json" },
|
|
1504
|
+
{
|
|
1385
1505
|
type: "object",
|
|
1386
|
-
required: ["type"],
|
|
1387
1506
|
properties: {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1507
|
+
profile: { const: "core" },
|
|
1508
|
+
modules: {
|
|
1509
|
+
type: "array",
|
|
1510
|
+
items: { type: "string" },
|
|
1511
|
+
uniqueItems: true,
|
|
1512
|
+
default: []
|
|
1391
1513
|
},
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
min: { type: "number" },
|
|
1396
|
-
max: { type: "number" }
|
|
1397
|
-
},
|
|
1398
|
-
if: {
|
|
1399
|
-
properties: { type: { const: "bakers_percentage" } }
|
|
1514
|
+
name: { type: "string", minLength: 1 },
|
|
1515
|
+
ingredients: { type: "array", minItems: 1 },
|
|
1516
|
+
instructions: { type: "array", minItems: 1 }
|
|
1400
1517
|
},
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
prepTime: { type: "number" },
|
|
1417
|
-
destination: { type: "string" },
|
|
1418
|
-
scaling: { $ref: "#/definitions/scaling" },
|
|
1419
|
-
critical: { type: "boolean" },
|
|
1420
|
-
optional: { type: "boolean" },
|
|
1421
|
-
notes: { type: "string" }
|
|
1422
|
-
}
|
|
1423
|
-
},
|
|
1424
|
-
ingredientSubsection: {
|
|
1425
|
-
type: "object",
|
|
1426
|
-
required: ["subsection", "items"],
|
|
1427
|
-
properties: {
|
|
1428
|
-
subsection: { type: "string" },
|
|
1429
|
-
items: {
|
|
1430
|
-
type: "array",
|
|
1431
|
-
items: { $ref: "#/definitions/ingredient" }
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
},
|
|
1435
|
-
equipment: {
|
|
1436
|
-
type: "object",
|
|
1437
|
-
required: ["name"],
|
|
1438
|
-
properties: {
|
|
1439
|
-
id: { type: "string" },
|
|
1440
|
-
name: { type: "string" },
|
|
1441
|
-
required: { type: "boolean" },
|
|
1442
|
-
label: { type: "string" },
|
|
1443
|
-
capacity: { $ref: "#/definitions/quantity" },
|
|
1444
|
-
scalingLimit: { type: "number" },
|
|
1445
|
-
alternatives: {
|
|
1446
|
-
type: "array",
|
|
1447
|
-
items: { type: "string" }
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1518
|
+
required: ["profile", "name", "ingredients", "instructions"],
|
|
1519
|
+
additionalProperties: true
|
|
1520
|
+
}
|
|
1521
|
+
]
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
// src/schemas/recipe/profiles/minimal.schema.json
|
|
1525
|
+
var minimal_schema_default = {
|
|
1526
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1527
|
+
$id: "http://soustack.org/schema/recipe/profiles/minimal.schema.json",
|
|
1528
|
+
title: "Soustack Recipe Minimal Profile",
|
|
1529
|
+
description: "Minimal profile that ensures the basic Recipe structure is present while allowing modules to extend it.",
|
|
1530
|
+
allOf: [
|
|
1531
|
+
{
|
|
1532
|
+
$ref: "http://soustack.org/schema/recipe/base.schema.json"
|
|
1450
1533
|
},
|
|
1451
|
-
|
|
1534
|
+
{
|
|
1452
1535
|
type: "object",
|
|
1453
|
-
required: ["text"],
|
|
1454
1536
|
properties: {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
image: {
|
|
1458
|
-
type: "string",
|
|
1459
|
-
format: "uri",
|
|
1460
|
-
description: "Optional image that illustrates this instruction"
|
|
1537
|
+
profile: {
|
|
1538
|
+
const: "minimal"
|
|
1461
1539
|
},
|
|
1462
|
-
|
|
1463
|
-
dependsOn: {
|
|
1464
|
-
type: "array",
|
|
1465
|
-
items: { type: "string" }
|
|
1466
|
-
},
|
|
1467
|
-
inputs: {
|
|
1468
|
-
type: "array",
|
|
1469
|
-
items: { type: "string" }
|
|
1470
|
-
},
|
|
1471
|
-
timing: {
|
|
1472
|
-
type: "object",
|
|
1473
|
-
required: ["duration", "type"],
|
|
1474
|
-
properties: {
|
|
1475
|
-
duration: {
|
|
1476
|
-
anyOf: [
|
|
1477
|
-
{ type: "number" },
|
|
1478
|
-
{ type: "string", pattern: "^P" }
|
|
1479
|
-
],
|
|
1480
|
-
description: "Minutes as a number or ISO8601 duration string"
|
|
1481
|
-
},
|
|
1482
|
-
type: { type: "string", enum: ["active", "passive"] },
|
|
1483
|
-
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
},
|
|
1488
|
-
instructionSubsection: {
|
|
1489
|
-
type: "object",
|
|
1490
|
-
required: ["subsection", "items"],
|
|
1491
|
-
properties: {
|
|
1492
|
-
subsection: { type: "string" },
|
|
1493
|
-
items: {
|
|
1540
|
+
modules: {
|
|
1494
1541
|
type: "array",
|
|
1495
1542
|
items: {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1543
|
+
type: "string",
|
|
1544
|
+
enum: [
|
|
1545
|
+
"attribution@1",
|
|
1546
|
+
"taxonomy@1",
|
|
1547
|
+
"media@1",
|
|
1548
|
+
"nutrition@1",
|
|
1549
|
+
"times@1"
|
|
1499
1550
|
]
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
storage: {
|
|
1505
|
-
type: "object",
|
|
1506
|
-
properties: {
|
|
1507
|
-
roomTemp: { $ref: "#/definitions/storageMethod" },
|
|
1508
|
-
refrigerated: { $ref: "#/definitions/storageMethod" },
|
|
1509
|
-
frozen: {
|
|
1510
|
-
allOf: [
|
|
1511
|
-
{ $ref: "#/definitions/storageMethod" },
|
|
1512
|
-
{
|
|
1513
|
-
type: "object",
|
|
1514
|
-
properties: { thawing: { type: "string" } }
|
|
1515
|
-
}
|
|
1516
|
-
]
|
|
1551
|
+
},
|
|
1552
|
+
default: []
|
|
1517
1553
|
},
|
|
1518
|
-
|
|
1519
|
-
|
|
1554
|
+
name: {
|
|
1555
|
+
type: "string",
|
|
1556
|
+
minLength: 1
|
|
1557
|
+
},
|
|
1558
|
+
ingredients: {
|
|
1520
1559
|
type: "array",
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
{
|
|
1525
|
-
type: "object",
|
|
1526
|
-
required: ["component", "storage"],
|
|
1527
|
-
properties: {
|
|
1528
|
-
component: { type: "string" },
|
|
1529
|
-
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
]
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
},
|
|
1537
|
-
storageMethod: {
|
|
1538
|
-
type: "object",
|
|
1539
|
-
required: ["duration"],
|
|
1540
|
-
properties: {
|
|
1541
|
-
duration: { type: "string", pattern: "^P" },
|
|
1542
|
-
method: { type: "string" },
|
|
1543
|
-
notes: { type: "string" }
|
|
1544
|
-
}
|
|
1545
|
-
},
|
|
1546
|
-
substitution: {
|
|
1547
|
-
type: "object",
|
|
1548
|
-
required: ["ingredient"],
|
|
1549
|
-
properties: {
|
|
1550
|
-
ingredient: { type: "string" },
|
|
1551
|
-
critical: { type: "boolean" },
|
|
1552
|
-
notes: { type: "string" },
|
|
1553
|
-
alternatives: {
|
|
1560
|
+
minItems: 1
|
|
1561
|
+
},
|
|
1562
|
+
instructions: {
|
|
1554
1563
|
type: "array",
|
|
1555
|
-
|
|
1556
|
-
type: "object",
|
|
1557
|
-
required: ["name", "ratio"],
|
|
1558
|
-
properties: {
|
|
1559
|
-
name: { type: "string" },
|
|
1560
|
-
ratio: { type: "string" },
|
|
1561
|
-
notes: { type: "string" },
|
|
1562
|
-
impact: { type: "string" },
|
|
1563
|
-
dietary: {
|
|
1564
|
-
type: "array",
|
|
1565
|
-
items: { type: "string" }
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1564
|
+
minItems: 1
|
|
1569
1565
|
}
|
|
1570
|
-
}
|
|
1566
|
+
},
|
|
1567
|
+
required: [
|
|
1568
|
+
"profile",
|
|
1569
|
+
"name",
|
|
1570
|
+
"ingredients",
|
|
1571
|
+
"instructions"
|
|
1572
|
+
],
|
|
1573
|
+
additionalProperties: true
|
|
1571
1574
|
}
|
|
1572
|
-
|
|
1575
|
+
]
|
|
1573
1576
|
};
|
|
1574
1577
|
|
|
1575
|
-
// src/
|
|
1576
|
-
var
|
|
1578
|
+
// src/schemas/recipe/modules/schedule/1.schema.json
|
|
1579
|
+
var schema_default = {
|
|
1577
1580
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1578
|
-
$id: "
|
|
1579
|
-
title: "Soustack Recipe
|
|
1580
|
-
description: "
|
|
1581
|
+
$id: "https://soustack.org/schemas/recipe/modules/schedule/1.schema.json",
|
|
1582
|
+
title: "Soustack Recipe Module: schedule v1",
|
|
1583
|
+
description: "Schema for the schedule module. Enforces bidirectional module gating and restricts usage to the core profile.",
|
|
1581
1584
|
type: "object",
|
|
1582
|
-
required: ["name", "ingredients", "instructions"],
|
|
1583
|
-
additionalProperties: false,
|
|
1584
|
-
patternProperties: {
|
|
1585
|
-
"^x-": {}
|
|
1586
|
-
},
|
|
1587
1585
|
properties: {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
format: "uri",
|
|
1591
|
-
description: "Optional schema hint for tooling compatibility"
|
|
1592
|
-
},
|
|
1593
|
-
id: {
|
|
1594
|
-
type: "string",
|
|
1595
|
-
description: "Unique identifier (slug or UUID)"
|
|
1596
|
-
},
|
|
1597
|
-
name: {
|
|
1598
|
-
type: "string",
|
|
1599
|
-
description: "The title of the recipe"
|
|
1600
|
-
},
|
|
1601
|
-
title: {
|
|
1602
|
-
type: "string",
|
|
1603
|
-
description: "Optional display title; alias for name"
|
|
1604
|
-
},
|
|
1605
|
-
version: {
|
|
1606
|
-
type: "string",
|
|
1607
|
-
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
1608
|
-
description: "DEPRECATED: use recipeVersion for authoring revisions"
|
|
1609
|
-
},
|
|
1610
|
-
recipeVersion: {
|
|
1611
|
-
type: "string",
|
|
1612
|
-
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
1613
|
-
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
1614
|
-
},
|
|
1615
|
-
description: {
|
|
1616
|
-
type: "string"
|
|
1617
|
-
},
|
|
1618
|
-
category: {
|
|
1619
|
-
type: "string",
|
|
1620
|
-
examples: ["Main Course", "Dessert"]
|
|
1621
|
-
},
|
|
1622
|
-
tags: {
|
|
1586
|
+
profile: { type: "string" },
|
|
1587
|
+
modules: {
|
|
1623
1588
|
type: "array",
|
|
1624
1589
|
items: { type: "string" }
|
|
1625
1590
|
},
|
|
1626
|
-
|
|
1627
|
-
description: "Recipe-level hero image(s)",
|
|
1628
|
-
anyOf: [
|
|
1629
|
-
{
|
|
1630
|
-
type: "string",
|
|
1631
|
-
format: "uri"
|
|
1632
|
-
},
|
|
1633
|
-
{
|
|
1634
|
-
type: "array",
|
|
1635
|
-
minItems: 1,
|
|
1636
|
-
items: {
|
|
1637
|
-
type: "string",
|
|
1638
|
-
format: "uri"
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
]
|
|
1642
|
-
},
|
|
1643
|
-
dateAdded: {
|
|
1644
|
-
type: "string",
|
|
1645
|
-
format: "date-time"
|
|
1646
|
-
},
|
|
1647
|
-
metadata: {
|
|
1648
|
-
type: "object",
|
|
1649
|
-
additionalProperties: true,
|
|
1650
|
-
description: "Free-form vendor metadata"
|
|
1651
|
-
},
|
|
1652
|
-
source: {
|
|
1591
|
+
schedule: {
|
|
1653
1592
|
type: "object",
|
|
1654
1593
|
properties: {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
adapted: { type: "boolean" }
|
|
1659
|
-
}
|
|
1660
|
-
},
|
|
1661
|
-
yield: {
|
|
1662
|
-
$ref: "#/definitions/yield"
|
|
1663
|
-
},
|
|
1664
|
-
time: {
|
|
1665
|
-
$ref: "#/definitions/time"
|
|
1666
|
-
},
|
|
1667
|
-
equipment: {
|
|
1668
|
-
type: "array",
|
|
1669
|
-
items: { $ref: "#/definitions/equipment" }
|
|
1670
|
-
},
|
|
1671
|
-
ingredients: {
|
|
1672
|
-
type: "array",
|
|
1673
|
-
items: {
|
|
1674
|
-
anyOf: [
|
|
1675
|
-
{ type: "string" },
|
|
1676
|
-
{ $ref: "#/definitions/ingredient" },
|
|
1677
|
-
{ $ref: "#/definitions/ingredientSubsection" }
|
|
1678
|
-
]
|
|
1679
|
-
}
|
|
1680
|
-
},
|
|
1681
|
-
instructions: {
|
|
1682
|
-
type: "array",
|
|
1683
|
-
items: {
|
|
1684
|
-
anyOf: [
|
|
1685
|
-
{ type: "string" },
|
|
1686
|
-
{ $ref: "#/definitions/instruction" },
|
|
1687
|
-
{ $ref: "#/definitions/instructionSubsection" }
|
|
1688
|
-
]
|
|
1689
|
-
}
|
|
1690
|
-
},
|
|
1691
|
-
storage: {
|
|
1692
|
-
$ref: "#/definitions/storage"
|
|
1693
|
-
},
|
|
1694
|
-
substitutions: {
|
|
1695
|
-
type: "array",
|
|
1696
|
-
items: { $ref: "#/definitions/substitution" }
|
|
1594
|
+
tasks: { type: "array" }
|
|
1595
|
+
},
|
|
1596
|
+
additionalProperties: false
|
|
1697
1597
|
}
|
|
1698
1598
|
},
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
type: "object",
|
|
1702
|
-
required: ["amount", "unit"],
|
|
1703
|
-
properties: {
|
|
1704
|
-
amount: { type: "number" },
|
|
1705
|
-
unit: { type: "string" },
|
|
1706
|
-
servings: { type: "number" },
|
|
1707
|
-
description: { type: "string" }
|
|
1708
|
-
}
|
|
1709
|
-
},
|
|
1710
|
-
time: {
|
|
1711
|
-
type: "object",
|
|
1712
|
-
properties: {
|
|
1713
|
-
prep: { type: "number" },
|
|
1714
|
-
active: { type: "number" },
|
|
1715
|
-
passive: { type: "number" },
|
|
1716
|
-
total: { type: "number" },
|
|
1717
|
-
prepTime: { type: "string", format: "duration" },
|
|
1718
|
-
cookTime: { type: "string", format: "duration" }
|
|
1719
|
-
},
|
|
1720
|
-
minProperties: 1
|
|
1721
|
-
},
|
|
1722
|
-
quantity: {
|
|
1723
|
-
type: "object",
|
|
1724
|
-
required: ["amount"],
|
|
1725
|
-
properties: {
|
|
1726
|
-
amount: { type: "number" },
|
|
1727
|
-
unit: { type: ["string", "null"] }
|
|
1728
|
-
}
|
|
1729
|
-
},
|
|
1730
|
-
scaling: {
|
|
1731
|
-
type: "object",
|
|
1732
|
-
required: ["type"],
|
|
1733
|
-
properties: {
|
|
1734
|
-
type: {
|
|
1735
|
-
type: "string",
|
|
1736
|
-
enum: ["linear", "discrete", "proportional", "fixed", "bakers_percentage"]
|
|
1737
|
-
},
|
|
1738
|
-
factor: { type: "number" },
|
|
1739
|
-
referenceId: { type: "string" },
|
|
1740
|
-
roundTo: { type: "number" },
|
|
1741
|
-
min: { type: "number" },
|
|
1742
|
-
max: { type: "number" }
|
|
1743
|
-
},
|
|
1599
|
+
allOf: [
|
|
1600
|
+
{
|
|
1744
1601
|
if: {
|
|
1745
|
-
properties: {
|
|
1602
|
+
properties: {
|
|
1603
|
+
modules: {
|
|
1604
|
+
type: "array",
|
|
1605
|
+
contains: { const: "schedule@1" }
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1746
1608
|
},
|
|
1747
1609
|
then: {
|
|
1748
|
-
required: ["
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
ingredient: {
|
|
1752
|
-
type: "object",
|
|
1753
|
-
required: ["item"],
|
|
1754
|
-
properties: {
|
|
1755
|
-
id: { type: "string" },
|
|
1756
|
-
item: { type: "string" },
|
|
1757
|
-
quantity: { $ref: "#/definitions/quantity" },
|
|
1758
|
-
name: { type: "string" },
|
|
1759
|
-
aisle: { type: "string" },
|
|
1760
|
-
prep: { type: "string" },
|
|
1761
|
-
prepAction: { type: "string" },
|
|
1762
|
-
prepTime: { type: "number" },
|
|
1763
|
-
destination: { type: "string" },
|
|
1764
|
-
scaling: { $ref: "#/definitions/scaling" },
|
|
1765
|
-
critical: { type: "boolean" },
|
|
1766
|
-
optional: { type: "boolean" },
|
|
1767
|
-
notes: { type: "string" }
|
|
1768
|
-
}
|
|
1769
|
-
},
|
|
1770
|
-
ingredientSubsection: {
|
|
1771
|
-
type: "object",
|
|
1772
|
-
required: ["subsection", "items"],
|
|
1773
|
-
properties: {
|
|
1774
|
-
subsection: { type: "string" },
|
|
1775
|
-
items: {
|
|
1776
|
-
type: "array",
|
|
1777
|
-
items: { $ref: "#/definitions/ingredient" }
|
|
1610
|
+
required: ["schedule", "profile"],
|
|
1611
|
+
properties: {
|
|
1612
|
+
profile: { const: "core" }
|
|
1778
1613
|
}
|
|
1779
1614
|
}
|
|
1780
1615
|
},
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1616
|
+
{
|
|
1617
|
+
if: {
|
|
1618
|
+
required: ["schedule"]
|
|
1619
|
+
},
|
|
1620
|
+
then: {
|
|
1621
|
+
required: ["modules", "profile"],
|
|
1622
|
+
properties: {
|
|
1623
|
+
modules: {
|
|
1624
|
+
type: "array",
|
|
1625
|
+
items: { type: "string" },
|
|
1626
|
+
contains: { const: "schedule@1" }
|
|
1627
|
+
},
|
|
1628
|
+
profile: { const: "core" }
|
|
1794
1629
|
}
|
|
1795
1630
|
}
|
|
1631
|
+
}
|
|
1632
|
+
],
|
|
1633
|
+
additionalProperties: true
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
// src/schemas/recipe/modules/nutrition/1.schema.json
|
|
1637
|
+
var schema_default2 = {
|
|
1638
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1639
|
+
$id: "https://soustack.org/schemas/recipe/modules/nutrition/1.schema.json",
|
|
1640
|
+
title: "Soustack Recipe Module: nutrition v1",
|
|
1641
|
+
description: "Schema for the nutrition module. Keeps nutrition data aligned with module declarations and vice versa.",
|
|
1642
|
+
type: "object",
|
|
1643
|
+
properties: {
|
|
1644
|
+
modules: {
|
|
1645
|
+
type: "array",
|
|
1646
|
+
items: { type: "string" }
|
|
1796
1647
|
},
|
|
1797
|
-
|
|
1648
|
+
nutrition: {
|
|
1798
1649
|
type: "object",
|
|
1799
|
-
required: ["text"],
|
|
1800
1650
|
properties: {
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
type: "array",
|
|
1815
|
-
items: { type: "string" }
|
|
1816
|
-
},
|
|
1817
|
-
timing: {
|
|
1818
|
-
type: "object",
|
|
1819
|
-
required: ["duration", "type"],
|
|
1820
|
-
properties: {
|
|
1821
|
-
duration: {
|
|
1822
|
-
anyOf: [
|
|
1823
|
-
{ type: "number" },
|
|
1824
|
-
{ type: "string", pattern: "^P" }
|
|
1825
|
-
],
|
|
1826
|
-
description: "Minutes as a number or ISO8601 duration string"
|
|
1827
|
-
},
|
|
1828
|
-
type: { type: "string", enum: ["active", "passive"] },
|
|
1829
|
-
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
1651
|
+
calories: { type: "number" },
|
|
1652
|
+
protein_g: { type: "number" }
|
|
1653
|
+
},
|
|
1654
|
+
additionalProperties: false
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
allOf: [
|
|
1658
|
+
{
|
|
1659
|
+
if: {
|
|
1660
|
+
properties: {
|
|
1661
|
+
modules: {
|
|
1662
|
+
type: "array",
|
|
1663
|
+
contains: { const: "nutrition@1" }
|
|
1830
1664
|
}
|
|
1831
1665
|
}
|
|
1666
|
+
},
|
|
1667
|
+
then: {
|
|
1668
|
+
required: ["nutrition"]
|
|
1832
1669
|
}
|
|
1833
1670
|
},
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
]
|
|
1671
|
+
{
|
|
1672
|
+
if: {
|
|
1673
|
+
required: ["nutrition"]
|
|
1674
|
+
},
|
|
1675
|
+
then: {
|
|
1676
|
+
required: ["modules"],
|
|
1677
|
+
properties: {
|
|
1678
|
+
modules: {
|
|
1679
|
+
type: "array",
|
|
1680
|
+
items: { type: "string" },
|
|
1681
|
+
contains: { const: "nutrition@1" }
|
|
1846
1682
|
}
|
|
1847
1683
|
}
|
|
1848
1684
|
}
|
|
1685
|
+
}
|
|
1686
|
+
],
|
|
1687
|
+
additionalProperties: true
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// src/schemas/recipe/modules/attribution/1.schema.json
|
|
1691
|
+
var schema_default3 = {
|
|
1692
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1693
|
+
$id: "https://soustack.org/schemas/recipe/modules/attribution/1.schema.json",
|
|
1694
|
+
title: "Soustack Recipe Module: attribution v1",
|
|
1695
|
+
description: "Schema for the attribution module. Ensures namespace data is present when the module is enabled and vice versa.",
|
|
1696
|
+
type: "object",
|
|
1697
|
+
properties: {
|
|
1698
|
+
modules: {
|
|
1699
|
+
type: "array",
|
|
1700
|
+
items: { type: "string" }
|
|
1849
1701
|
},
|
|
1850
|
-
|
|
1702
|
+
attribution: {
|
|
1851
1703
|
type: "object",
|
|
1852
1704
|
properties: {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
items: {
|
|
1868
|
-
allOf: [
|
|
1869
|
-
{ $ref: "#/definitions/storageMethod" },
|
|
1870
|
-
{
|
|
1871
|
-
type: "object",
|
|
1872
|
-
required: ["component", "storage"],
|
|
1873
|
-
properties: {
|
|
1874
|
-
component: { type: "string" },
|
|
1875
|
-
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
]
|
|
1705
|
+
url: { type: "string" },
|
|
1706
|
+
author: { type: "string" },
|
|
1707
|
+
datePublished: { type: "string" }
|
|
1708
|
+
},
|
|
1709
|
+
additionalProperties: false
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
allOf: [
|
|
1713
|
+
{
|
|
1714
|
+
if: {
|
|
1715
|
+
properties: {
|
|
1716
|
+
modules: {
|
|
1717
|
+
type: "array",
|
|
1718
|
+
contains: { const: "attribution@1" }
|
|
1879
1719
|
}
|
|
1880
1720
|
}
|
|
1721
|
+
},
|
|
1722
|
+
then: {
|
|
1723
|
+
required: ["attribution"]
|
|
1881
1724
|
}
|
|
1882
1725
|
},
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
required: ["ingredient"],
|
|
1895
|
-
properties: {
|
|
1896
|
-
ingredient: { type: "string" },
|
|
1897
|
-
critical: { type: "boolean" },
|
|
1898
|
-
notes: { type: "string" },
|
|
1899
|
-
alternatives: {
|
|
1900
|
-
type: "array",
|
|
1901
|
-
items: {
|
|
1902
|
-
type: "object",
|
|
1903
|
-
required: ["name", "ratio"],
|
|
1904
|
-
properties: {
|
|
1905
|
-
name: { type: "string" },
|
|
1906
|
-
ratio: { type: "string" },
|
|
1907
|
-
notes: { type: "string" },
|
|
1908
|
-
impact: { type: "string" },
|
|
1909
|
-
dietary: {
|
|
1910
|
-
type: "array",
|
|
1911
|
-
items: { type: "string" }
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1726
|
+
{
|
|
1727
|
+
if: {
|
|
1728
|
+
required: ["attribution"]
|
|
1729
|
+
},
|
|
1730
|
+
then: {
|
|
1731
|
+
required: ["modules"],
|
|
1732
|
+
properties: {
|
|
1733
|
+
modules: {
|
|
1734
|
+
type: "array",
|
|
1735
|
+
items: { type: "string" },
|
|
1736
|
+
contains: { const: "attribution@1" }
|
|
1914
1737
|
}
|
|
1915
1738
|
}
|
|
1916
1739
|
}
|
|
1917
1740
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
// src/profiles/base.schema.json
|
|
1922
|
-
var base_schema_default = {
|
|
1923
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1924
|
-
$id: "http://soustack.org/schema/v0.2.1/profiles/base",
|
|
1925
|
-
title: "Soustack Base Profile Schema",
|
|
1926
|
-
description: "Wrapper schema that exposes the unmodified Soustack base schema.",
|
|
1927
|
-
allOf: [
|
|
1928
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" }
|
|
1929
|
-
]
|
|
1741
|
+
],
|
|
1742
|
+
additionalProperties: true
|
|
1930
1743
|
};
|
|
1931
1744
|
|
|
1932
|
-
// src/
|
|
1933
|
-
var
|
|
1745
|
+
// src/schemas/recipe/modules/taxonomy/1.schema.json
|
|
1746
|
+
var schema_default4 = {
|
|
1934
1747
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1935
|
-
$id: "
|
|
1936
|
-
title: "Soustack
|
|
1937
|
-
description: "
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
{
|
|
1941
|
-
|
|
1748
|
+
$id: "https://soustack.org/schemas/recipe/modules/taxonomy/1.schema.json",
|
|
1749
|
+
title: "Soustack Recipe Module: taxonomy v1",
|
|
1750
|
+
description: "Schema for the taxonomy module. Enforces keyword and categorization data when enabled and ensures module declaration accompanies the namespace block.",
|
|
1751
|
+
type: "object",
|
|
1752
|
+
properties: {
|
|
1753
|
+
modules: {
|
|
1754
|
+
type: "array",
|
|
1755
|
+
items: { type: "string" }
|
|
1756
|
+
},
|
|
1757
|
+
taxonomy: {
|
|
1758
|
+
type: "object",
|
|
1942
1759
|
properties: {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1760
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
1761
|
+
category: { type: "string" },
|
|
1762
|
+
cuisine: { type: "string" }
|
|
1763
|
+
},
|
|
1764
|
+
additionalProperties: false
|
|
1948
1765
|
}
|
|
1949
|
-
|
|
1950
|
-
};
|
|
1951
|
-
|
|
1952
|
-
// src/profiles/quantified.schema.json
|
|
1953
|
-
var quantified_schema_default = {
|
|
1954
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1955
|
-
$id: "http://soustack.org/schema/v0.2.1/profiles/quantified",
|
|
1956
|
-
title: "Soustack Quantified Profile Schema",
|
|
1957
|
-
description: "Extends the base schema to require quantified ingredient entries.",
|
|
1766
|
+
},
|
|
1958
1767
|
allOf: [
|
|
1959
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
1960
1768
|
{
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
{ $ref: "#/definitions/quantifiedIngredient" },
|
|
1967
|
-
{ $ref: "#/definitions/quantifiedIngredientSubsection" }
|
|
1968
|
-
]
|
|
1769
|
+
if: {
|
|
1770
|
+
properties: {
|
|
1771
|
+
modules: {
|
|
1772
|
+
type: "array",
|
|
1773
|
+
contains: { const: "taxonomy@1" }
|
|
1969
1774
|
}
|
|
1970
1775
|
}
|
|
1776
|
+
},
|
|
1777
|
+
then: {
|
|
1778
|
+
required: ["taxonomy"]
|
|
1971
1779
|
}
|
|
1972
|
-
}
|
|
1973
|
-
],
|
|
1974
|
-
definitions: {
|
|
1975
|
-
quantifiedIngredient: {
|
|
1976
|
-
allOf: [
|
|
1977
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/ingredient" },
|
|
1978
|
-
{ required: ["item", "quantity"] }
|
|
1979
|
-
]
|
|
1980
1780
|
},
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1781
|
+
{
|
|
1782
|
+
if: {
|
|
1783
|
+
required: ["taxonomy"]
|
|
1784
|
+
},
|
|
1785
|
+
then: {
|
|
1786
|
+
required: ["modules"],
|
|
1787
|
+
properties: {
|
|
1788
|
+
modules: {
|
|
1789
|
+
type: "array",
|
|
1790
|
+
items: { type: "string" },
|
|
1791
|
+
contains: { const: "taxonomy@1" }
|
|
1990
1792
|
}
|
|
1991
1793
|
}
|
|
1992
|
-
|
|
1794
|
+
}
|
|
1993
1795
|
}
|
|
1994
|
-
|
|
1796
|
+
],
|
|
1797
|
+
additionalProperties: true
|
|
1995
1798
|
};
|
|
1996
1799
|
|
|
1997
|
-
// src/
|
|
1998
|
-
var
|
|
1800
|
+
// src/schemas/recipe/modules/media/1.schema.json
|
|
1801
|
+
var schema_default5 = {
|
|
1999
1802
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2000
|
-
$id: "
|
|
2001
|
-
title: "Soustack
|
|
2002
|
-
description: "
|
|
1803
|
+
$id: "https://soustack.org/schemas/recipe/modules/media/1.schema.json",
|
|
1804
|
+
title: "Soustack Recipe Module: media v1",
|
|
1805
|
+
description: "Schema for the media module. Guards media blocks based on module activation and ensures declarations accompany payloads.",
|
|
1806
|
+
type: "object",
|
|
1807
|
+
properties: {
|
|
1808
|
+
modules: {
|
|
1809
|
+
type: "array",
|
|
1810
|
+
items: { type: "string" }
|
|
1811
|
+
},
|
|
1812
|
+
media: {
|
|
1813
|
+
type: "object",
|
|
1814
|
+
properties: {
|
|
1815
|
+
images: { type: "array", items: { type: "string" } },
|
|
1816
|
+
videos: { type: "array", items: { type: "string" } }
|
|
1817
|
+
},
|
|
1818
|
+
additionalProperties: false
|
|
1819
|
+
}
|
|
1820
|
+
},
|
|
2003
1821
|
allOf: [
|
|
2004
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
2005
1822
|
{
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
type: "array",
|
|
2012
|
-
contains: {
|
|
2013
|
-
anyOf: [
|
|
2014
|
-
{ $ref: "#/definitions/imageInstruction" },
|
|
2015
|
-
{ $ref: "#/definitions/instructionSubsectionWithImage" }
|
|
2016
|
-
]
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
1823
|
+
if: {
|
|
1824
|
+
properties: {
|
|
1825
|
+
modules: {
|
|
1826
|
+
type: "array",
|
|
1827
|
+
contains: { const: "media@1" }
|
|
2019
1828
|
}
|
|
2020
1829
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
imageInstruction: {
|
|
2026
|
-
allOf: [
|
|
2027
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/instruction" },
|
|
2028
|
-
{ required: ["image"] }
|
|
2029
|
-
]
|
|
1830
|
+
},
|
|
1831
|
+
then: {
|
|
1832
|
+
required: ["media"]
|
|
1833
|
+
}
|
|
2030
1834
|
},
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
1835
|
+
{
|
|
1836
|
+
if: {
|
|
1837
|
+
required: ["media"]
|
|
1838
|
+
},
|
|
1839
|
+
then: {
|
|
1840
|
+
required: ["modules"],
|
|
1841
|
+
properties: {
|
|
1842
|
+
modules: {
|
|
1843
|
+
type: "array",
|
|
1844
|
+
items: { type: "string" },
|
|
1845
|
+
contains: { const: "media@1" }
|
|
2040
1846
|
}
|
|
2041
1847
|
}
|
|
2042
|
-
|
|
1848
|
+
}
|
|
2043
1849
|
}
|
|
2044
|
-
|
|
1850
|
+
],
|
|
1851
|
+
additionalProperties: true
|
|
2045
1852
|
};
|
|
2046
1853
|
|
|
2047
|
-
// src/
|
|
2048
|
-
var
|
|
1854
|
+
// src/schemas/recipe/modules/times/1.schema.json
|
|
1855
|
+
var schema_default6 = {
|
|
2049
1856
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2050
|
-
$id: "
|
|
2051
|
-
title: "Soustack
|
|
2052
|
-
description: "
|
|
1857
|
+
$id: "https://soustack.org/schemas/recipe/modules/times/1.schema.json",
|
|
1858
|
+
title: "Soustack Recipe Module: times v1",
|
|
1859
|
+
description: "Schema for the times module. Maintains alignment between module declarations and timing payloads.",
|
|
1860
|
+
type: "object",
|
|
1861
|
+
properties: {
|
|
1862
|
+
modules: {
|
|
1863
|
+
type: "array",
|
|
1864
|
+
items: { type: "string" }
|
|
1865
|
+
},
|
|
1866
|
+
times: {
|
|
1867
|
+
type: "object",
|
|
1868
|
+
properties: {
|
|
1869
|
+
prepMinutes: { type: "number" },
|
|
1870
|
+
cookMinutes: { type: "number" },
|
|
1871
|
+
totalMinutes: { type: "number" }
|
|
1872
|
+
},
|
|
1873
|
+
additionalProperties: false
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
2053
1876
|
allOf: [
|
|
2054
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
2055
1877
|
{
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
{ $ref: "#/definitions/schedulableInstruction" },
|
|
2062
|
-
{ $ref: "#/definitions/schedulableInstructionSubsection" }
|
|
2063
|
-
]
|
|
1878
|
+
if: {
|
|
1879
|
+
properties: {
|
|
1880
|
+
modules: {
|
|
1881
|
+
type: "array",
|
|
1882
|
+
contains: { const: "times@1" }
|
|
2064
1883
|
}
|
|
2065
1884
|
}
|
|
1885
|
+
},
|
|
1886
|
+
then: {
|
|
1887
|
+
required: ["times"]
|
|
2066
1888
|
}
|
|
2067
|
-
}
|
|
2068
|
-
],
|
|
2069
|
-
definitions: {
|
|
2070
|
-
schedulableInstruction: {
|
|
2071
|
-
allOf: [
|
|
2072
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/instruction" },
|
|
2073
|
-
{ required: ["id", "timing"] }
|
|
2074
|
-
]
|
|
2075
1889
|
},
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
1890
|
+
{
|
|
1891
|
+
if: {
|
|
1892
|
+
required: ["times"]
|
|
1893
|
+
},
|
|
1894
|
+
then: {
|
|
1895
|
+
required: ["modules"],
|
|
1896
|
+
properties: {
|
|
1897
|
+
modules: {
|
|
1898
|
+
type: "array",
|
|
1899
|
+
items: { type: "string" },
|
|
1900
|
+
contains: { const: "times@1" }
|
|
2085
1901
|
}
|
|
2086
1902
|
}
|
|
2087
|
-
|
|
1903
|
+
}
|
|
2088
1904
|
}
|
|
2089
|
-
|
|
1905
|
+
],
|
|
1906
|
+
additionalProperties: true
|
|
2090
1907
|
};
|
|
2091
1908
|
|
|
2092
1909
|
// src/validator.ts
|
|
1910
|
+
var CANONICAL_BASE_SCHEMA_ID = base_schema_default.$id || "http://soustack.org/schema/recipe/base.schema.json";
|
|
1911
|
+
var canonicalProfileId = (profile) => {
|
|
1912
|
+
if (profile === "minimal") {
|
|
1913
|
+
return minimal_schema_default.$id;
|
|
1914
|
+
}
|
|
1915
|
+
if (profile === "core") {
|
|
1916
|
+
return core_schema_default.$id;
|
|
1917
|
+
}
|
|
1918
|
+
throw new Error(`Unknown profile: ${profile}`);
|
|
1919
|
+
};
|
|
1920
|
+
var moduleIdToSchemaRef = (moduleId) => {
|
|
1921
|
+
const match = moduleId.match(/^([a-z0-9_-]+)@(\d+(?:\.\d+)*)$/i);
|
|
1922
|
+
if (!match) {
|
|
1923
|
+
throw new Error(`Invalid module identifier '${moduleId}'. Expected <name>@<version>.`);
|
|
1924
|
+
}
|
|
1925
|
+
const [, name, version] = match;
|
|
1926
|
+
const moduleSchemas2 = {
|
|
1927
|
+
"schedule@1": schema_default,
|
|
1928
|
+
"nutrition@1": schema_default2,
|
|
1929
|
+
"attribution@1": schema_default3,
|
|
1930
|
+
"taxonomy@1": schema_default4,
|
|
1931
|
+
"media@1": schema_default5,
|
|
1932
|
+
"times@1": schema_default6
|
|
1933
|
+
};
|
|
1934
|
+
const schema = moduleSchemas2[moduleId];
|
|
1935
|
+
if (schema && schema.$id) {
|
|
1936
|
+
return schema.$id;
|
|
1937
|
+
}
|
|
1938
|
+
return `https://soustack.org/schemas/recipe/modules/${name}/${version}.schema.json`;
|
|
1939
|
+
};
|
|
2093
1940
|
var profileSchemas = {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
1941
|
+
minimal: minimal_schema_default,
|
|
1942
|
+
core: core_schema_default
|
|
1943
|
+
};
|
|
1944
|
+
var moduleSchemas = {
|
|
1945
|
+
"schedule@1": schema_default,
|
|
1946
|
+
"nutrition@1": schema_default2,
|
|
1947
|
+
"attribution@1": schema_default3,
|
|
1948
|
+
"taxonomy@1": schema_default4,
|
|
1949
|
+
"media@1": schema_default5,
|
|
1950
|
+
"times@1": schema_default6
|
|
2100
1951
|
};
|
|
2101
1952
|
var validationContexts = /* @__PURE__ */ new Map();
|
|
2102
1953
|
function createContext(collectAllErrors) {
|
|
2103
1954
|
const ajv = new Ajv__default.default({ strict: false, allErrors: collectAllErrors });
|
|
2104
1955
|
addFormats__default.default(ajv);
|
|
2105
|
-
const
|
|
2106
|
-
const addSchemaIfNew = (schema) => {
|
|
1956
|
+
const addSchemaWithAlias = (schema, alias) => {
|
|
2107
1957
|
if (!schema) return;
|
|
2108
|
-
const schemaId = schema
|
|
2109
|
-
if (schemaId
|
|
2110
|
-
|
|
2111
|
-
|
|
1958
|
+
const schemaId = schema.$id || alias;
|
|
1959
|
+
if (schemaId) {
|
|
1960
|
+
ajv.addSchema(schema, schemaId);
|
|
1961
|
+
} else {
|
|
1962
|
+
ajv.addSchema(schema);
|
|
1963
|
+
}
|
|
2112
1964
|
};
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
1965
|
+
addSchemaWithAlias(base_schema_default, CANONICAL_BASE_SCHEMA_ID);
|
|
1966
|
+
Object.entries(profileSchemas).forEach(([name, schema]) => {
|
|
1967
|
+
addSchemaWithAlias(schema, canonicalProfileId(name));
|
|
1968
|
+
});
|
|
1969
|
+
Object.entries(moduleSchemas).forEach(([moduleId, schema]) => {
|
|
1970
|
+
addSchemaWithAlias(schema, moduleIdToSchemaRef(moduleId));
|
|
1971
|
+
});
|
|
1972
|
+
return { ajv, validators: /* @__PURE__ */ new Map() };
|
|
2117
1973
|
}
|
|
2118
1974
|
function getContext(collectAllErrors) {
|
|
2119
1975
|
if (!validationContexts.has(collectAllErrors)) {
|
|
@@ -2136,14 +1992,58 @@ function detectProfileFromSchema(schemaRef) {
|
|
|
2136
1992
|
}
|
|
2137
1993
|
return void 0;
|
|
2138
1994
|
}
|
|
2139
|
-
function
|
|
1995
|
+
function resolveSchemaRef(inputSchema, requestedSchema) {
|
|
1996
|
+
if (typeof requestedSchema === "string") return requestedSchema;
|
|
1997
|
+
if (typeof inputSchema !== "string") return void 0;
|
|
1998
|
+
return detectProfileFromSchema(inputSchema) ? inputSchema : void 0;
|
|
1999
|
+
}
|
|
2000
|
+
function inferModulesFromPayload(recipe) {
|
|
2001
|
+
const inferred = [];
|
|
2002
|
+
const payloadToModule = {
|
|
2003
|
+
attribution: "attribution@1",
|
|
2004
|
+
taxonomy: "taxonomy@1",
|
|
2005
|
+
media: "media@1",
|
|
2006
|
+
times: "times@1",
|
|
2007
|
+
nutrition: "nutrition@1",
|
|
2008
|
+
schedule: "schedule@1"
|
|
2009
|
+
};
|
|
2010
|
+
for (const [field, moduleId] of Object.entries(payloadToModule)) {
|
|
2011
|
+
if (recipe && typeof recipe === "object" && field in recipe && recipe[field] != null) {
|
|
2012
|
+
const payload = recipe[field];
|
|
2013
|
+
if (typeof payload === "object" && !Array.isArray(payload)) {
|
|
2014
|
+
if (Object.keys(payload).length > 0) {
|
|
2015
|
+
inferred.push(moduleId);
|
|
2016
|
+
}
|
|
2017
|
+
} else if (Array.isArray(payload) && payload.length > 0) {
|
|
2018
|
+
inferred.push(moduleId);
|
|
2019
|
+
} else if (payload !== null && payload !== void 0) {
|
|
2020
|
+
inferred.push(moduleId);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return inferred;
|
|
2025
|
+
}
|
|
2026
|
+
function getCombinedValidator(profile, modules, recipe, context) {
|
|
2027
|
+
const inferredModules = inferModulesFromPayload(recipe);
|
|
2028
|
+
const allModules = /* @__PURE__ */ new Set([...modules, ...inferredModules]);
|
|
2029
|
+
const sortedModules = Array.from(allModules).sort();
|
|
2030
|
+
const cacheKey = `${profile}::${sortedModules.join(",")}`;
|
|
2031
|
+
const cached = context.validators.get(cacheKey);
|
|
2032
|
+
if (cached) return cached;
|
|
2140
2033
|
if (!profileSchemas[profile]) {
|
|
2141
2034
|
throw new Error(`Unknown Soustack profile: ${profile}`);
|
|
2142
2035
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2036
|
+
const schema = {
|
|
2037
|
+
$id: `urn:soustack:recipe:${cacheKey}`,
|
|
2038
|
+
allOf: [
|
|
2039
|
+
{ $ref: CANONICAL_BASE_SCHEMA_ID },
|
|
2040
|
+
{ $ref: canonicalProfileId(profile) },
|
|
2041
|
+
...sortedModules.map((moduleId) => ({ $ref: moduleIdToSchemaRef(moduleId) }))
|
|
2042
|
+
]
|
|
2043
|
+
};
|
|
2044
|
+
const validateFn = context.ajv.compile(schema);
|
|
2045
|
+
context.validators.set(cacheKey, validateFn);
|
|
2046
|
+
return validateFn;
|
|
2147
2047
|
}
|
|
2148
2048
|
function normalizeRecipe(recipe) {
|
|
2149
2049
|
const normalized = cloneRecipe(recipe);
|
|
@@ -2174,9 +2074,33 @@ function normalizeTime(recipe) {
|
|
|
2174
2074
|
});
|
|
2175
2075
|
}
|
|
2176
2076
|
var allowedTopLevelProps = /* @__PURE__ */ new Set([
|
|
2177
|
-
...Object.keys(
|
|
2178
|
-
"
|
|
2179
|
-
|
|
2077
|
+
...Object.keys(base_schema_default?.properties ?? {}),
|
|
2078
|
+
"$schema",
|
|
2079
|
+
// Module fields (validated by module schemas)
|
|
2080
|
+
"attribution",
|
|
2081
|
+
"taxonomy",
|
|
2082
|
+
"media",
|
|
2083
|
+
"times",
|
|
2084
|
+
"nutrition",
|
|
2085
|
+
"schedule",
|
|
2086
|
+
// Common recipe fields (allowed by base schema's additionalProperties: true)
|
|
2087
|
+
"description",
|
|
2088
|
+
"image",
|
|
2089
|
+
"category",
|
|
2090
|
+
"tags",
|
|
2091
|
+
"source",
|
|
2092
|
+
"dateAdded",
|
|
2093
|
+
"dateModified",
|
|
2094
|
+
"yield",
|
|
2095
|
+
"time",
|
|
2096
|
+
"id",
|
|
2097
|
+
"title",
|
|
2098
|
+
"recipeVersion",
|
|
2099
|
+
"version",
|
|
2100
|
+
// deprecated but allowed
|
|
2101
|
+
"equipment",
|
|
2102
|
+
"storage",
|
|
2103
|
+
"substitutions"
|
|
2180
2104
|
]);
|
|
2181
2105
|
function detectUnknownTopLevelKeys(recipe) {
|
|
2182
2106
|
if (!recipe || typeof recipe !== "object") return [];
|
|
@@ -2201,11 +2125,19 @@ function formatAjvError(error) {
|
|
|
2201
2125
|
message: error.message || "Validation error"
|
|
2202
2126
|
};
|
|
2203
2127
|
}
|
|
2204
|
-
function runAjvValidation(data, profile,
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2128
|
+
function runAjvValidation(data, profile, modules, context) {
|
|
2129
|
+
try {
|
|
2130
|
+
const validateFn = getCombinedValidator(profile, modules, data, context);
|
|
2131
|
+
const isValid = validateFn(data);
|
|
2132
|
+
return !isValid && validateFn.errors ? validateFn.errors.map(formatAjvError) : [];
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
return [
|
|
2135
|
+
{
|
|
2136
|
+
path: "/",
|
|
2137
|
+
message: error instanceof Error ? error.message : "Validation failed to initialize"
|
|
2138
|
+
}
|
|
2139
|
+
];
|
|
2140
|
+
}
|
|
2209
2141
|
}
|
|
2210
2142
|
function isInstruction(item) {
|
|
2211
2143
|
return item && typeof item === "object" && !Array.isArray(item) && "text" in item;
|
|
@@ -2286,12 +2218,25 @@ function checkInstructionGraph(recipe) {
|
|
|
2286
2218
|
function validateRecipe(input, options = {}) {
|
|
2287
2219
|
const collectAllErrors = options.collectAllErrors ?? true;
|
|
2288
2220
|
const context = getContext(collectAllErrors);
|
|
2289
|
-
const schemaRef =
|
|
2290
|
-
const
|
|
2221
|
+
const schemaRef = resolveSchemaRef(input?.$schema, options.schema);
|
|
2222
|
+
const profileFromDocument = typeof input?.profile === "string" ? input.profile : void 0;
|
|
2223
|
+
const profile = options.profile ?? profileFromDocument ?? detectProfileFromSchema(schemaRef) ?? "core";
|
|
2224
|
+
const modulesFromDocument = Array.isArray(input?.modules) ? input.modules.filter((value) => typeof value === "string") : [];
|
|
2225
|
+
const modules = modulesFromDocument.length > 0 ? [...modulesFromDocument].sort() : [];
|
|
2291
2226
|
const { normalized, warnings } = normalizeRecipe(input);
|
|
2227
|
+
if (!profileFromDocument) {
|
|
2228
|
+
normalized.profile = profile;
|
|
2229
|
+
} else {
|
|
2230
|
+
normalized.profile = profileFromDocument;
|
|
2231
|
+
}
|
|
2232
|
+
if (!("modules" in normalized) || normalized.modules === void 0 || normalized.modules === null) {
|
|
2233
|
+
normalized.modules = [];
|
|
2234
|
+
} else if (modulesFromDocument.length > 0) {
|
|
2235
|
+
normalized.modules = modules;
|
|
2236
|
+
}
|
|
2292
2237
|
const unknownKeyErrors = detectUnknownTopLevelKeys(normalized);
|
|
2293
|
-
const validationErrors = runAjvValidation(normalized, profile,
|
|
2294
|
-
const graphErrors =
|
|
2238
|
+
const validationErrors = runAjvValidation(normalized, profile, modules, context);
|
|
2239
|
+
const graphErrors = modules.includes("schedule@1") && validationErrors.length === 0 ? checkInstructionGraph(normalized) : [];
|
|
2295
2240
|
const errors = [...unknownKeyErrors, ...validationErrors, ...graphErrors];
|
|
2296
2241
|
return {
|
|
2297
2242
|
valid: errors.length === 0,
|