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/cli/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var fs = require('fs');
|
|
|
5
5
|
var path = require('path');
|
|
6
6
|
var glob = require('glob');
|
|
7
7
|
var cheerio = require('cheerio');
|
|
8
|
-
var
|
|
8
|
+
var Ajv2020 = require('ajv/dist/2020');
|
|
9
9
|
var addFormats = require('ajv-formats');
|
|
10
10
|
|
|
11
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -30,7 +30,7 @@ function _interopNamespace(e) {
|
|
|
30
30
|
|
|
31
31
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
32
32
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
33
|
-
var
|
|
33
|
+
var Ajv2020__default = /*#__PURE__*/_interopDefault(Ajv2020);
|
|
34
34
|
var addFormats__default = /*#__PURE__*/_interopDefault(addFormats);
|
|
35
35
|
|
|
36
36
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
@@ -351,6 +351,92 @@ function extractUrl(value) {
|
|
|
351
351
|
return trimmed || void 0;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
// src/normalize.ts
|
|
355
|
+
function normalizeRecipe(input) {
|
|
356
|
+
if (!input || typeof input !== "object") {
|
|
357
|
+
throw new Error("Recipe input must be an object");
|
|
358
|
+
}
|
|
359
|
+
const recipe = JSON.parse(JSON.stringify(input));
|
|
360
|
+
const warnings = [];
|
|
361
|
+
const legacyField = ["mod", "ules"].join("");
|
|
362
|
+
if (legacyField in recipe) {
|
|
363
|
+
throw new Error("The legacy field is no longer supported. Use `stacks` instead.");
|
|
364
|
+
}
|
|
365
|
+
normalizeStacks(recipe, warnings);
|
|
366
|
+
if (!recipe.stacks) {
|
|
367
|
+
recipe.stacks = {};
|
|
368
|
+
}
|
|
369
|
+
if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
|
|
370
|
+
recipe.recipeVersion = recipe.version;
|
|
371
|
+
warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
|
|
372
|
+
}
|
|
373
|
+
normalizeTime(recipe);
|
|
374
|
+
return {
|
|
375
|
+
recipe,
|
|
376
|
+
warnings
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function normalizeStacks(recipe, warnings) {
|
|
380
|
+
let stacks = {};
|
|
381
|
+
if (recipe.stacks && typeof recipe.stacks === "object" && !Array.isArray(recipe.stacks)) {
|
|
382
|
+
for (const [key, value] of Object.entries(recipe.stacks)) {
|
|
383
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) {
|
|
384
|
+
stacks[key] = value;
|
|
385
|
+
} else {
|
|
386
|
+
warnings.push(`Invalid stack version for '${key}': expected positive integer, got ${value}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (Array.isArray(recipe.stacks)) {
|
|
391
|
+
const stackIdentifiers = recipe.stacks.filter((s) => typeof s === "string");
|
|
392
|
+
for (const identifier of stackIdentifiers) {
|
|
393
|
+
const parsed = parseStackIdentifier(identifier);
|
|
394
|
+
if (parsed) {
|
|
395
|
+
const { name, version } = parsed;
|
|
396
|
+
if (!stacks[name] || stacks[name] < version) {
|
|
397
|
+
stacks[name] = version;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
warnings.push(`Invalid stack identifier '${identifier}': expected format 'name@version' (e.g., 'scaling@1')`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
recipe.stacks = stacks;
|
|
405
|
+
}
|
|
406
|
+
function parseStackIdentifier(identifier) {
|
|
407
|
+
if (typeof identifier !== "string" || !identifier.trim()) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const match = identifier.trim().match(/^([a-z0-9_-]+)@(\d+)$/i);
|
|
411
|
+
if (!match) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const [, name, versionStr] = match;
|
|
415
|
+
const version = parseInt(versionStr, 10);
|
|
416
|
+
if (isNaN(version) || version < 1) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return { name, version };
|
|
420
|
+
}
|
|
421
|
+
function normalizeTime(recipe) {
|
|
422
|
+
const time = recipe?.time;
|
|
423
|
+
if (!time || typeof time !== "object" || Array.isArray(time)) return;
|
|
424
|
+
const structuredKeys = [
|
|
425
|
+
"prep",
|
|
426
|
+
"active",
|
|
427
|
+
"passive",
|
|
428
|
+
"total"
|
|
429
|
+
];
|
|
430
|
+
structuredKeys.forEach((key) => {
|
|
431
|
+
const value = time[key];
|
|
432
|
+
if (typeof value === "number") return;
|
|
433
|
+
const parsed = parseDuration(value);
|
|
434
|
+
if (parsed !== null) {
|
|
435
|
+
time[key] = parsed;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
354
440
|
// src/fromSchemaOrg.ts
|
|
355
441
|
function fromSchemaOrg(input) {
|
|
356
442
|
const recipeNode = extractRecipeNode(input);
|
|
@@ -370,16 +456,16 @@ function fromSchemaOrg(input) {
|
|
|
370
456
|
const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
|
|
371
457
|
const media = convertMedia(recipeNode.image, recipeNode.video);
|
|
372
458
|
const times = convertTimes(time);
|
|
373
|
-
const
|
|
374
|
-
if (attribution)
|
|
375
|
-
if (taxonomy)
|
|
376
|
-
if (media)
|
|
377
|
-
if (nutrition)
|
|
378
|
-
if (times)
|
|
379
|
-
|
|
459
|
+
const stacks = {};
|
|
460
|
+
if (attribution) stacks.attribution = 1;
|
|
461
|
+
if (taxonomy) stacks.taxonomy = 1;
|
|
462
|
+
if (media) stacks.media = 1;
|
|
463
|
+
if (nutrition) stacks.nutrition = 1;
|
|
464
|
+
if (times) stacks.times = 1;
|
|
465
|
+
const rawRecipe = {
|
|
380
466
|
"@type": "Recipe",
|
|
381
467
|
profile: "minimal",
|
|
382
|
-
|
|
468
|
+
stacks,
|
|
383
469
|
name: recipeNode.name.trim(),
|
|
384
470
|
description: recipeNode.description?.trim() || void 0,
|
|
385
471
|
image: normalizeImage(recipeNode.image),
|
|
@@ -398,6 +484,8 @@ function fromSchemaOrg(input) {
|
|
|
398
484
|
...media ? { media } : {},
|
|
399
485
|
...times ? { times } : {}
|
|
400
486
|
};
|
|
487
|
+
const { recipe } = normalizeRecipe(rawRecipe);
|
|
488
|
+
return recipe;
|
|
401
489
|
}
|
|
402
490
|
function extractRecipeNode(input) {
|
|
403
491
|
if (!input) return null;
|
|
@@ -694,17 +782,15 @@ function convertNutrition(nutrition) {
|
|
|
694
782
|
return hasData ? result : void 0;
|
|
695
783
|
}
|
|
696
784
|
|
|
697
|
-
// src/schemas/registry/
|
|
698
|
-
var
|
|
699
|
-
|
|
785
|
+
// src/schemas/registry/stacks.json
|
|
786
|
+
var stacks_default = {
|
|
787
|
+
stacks: [
|
|
700
788
|
{
|
|
701
789
|
id: "attribution",
|
|
702
|
-
versions: [
|
|
703
|
-
1
|
|
704
|
-
],
|
|
790
|
+
versions: [1],
|
|
705
791
|
latest: 1,
|
|
706
|
-
namespace: "
|
|
707
|
-
schema: "
|
|
792
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/attribution",
|
|
793
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/attribution",
|
|
708
794
|
schemaOrgMappable: true,
|
|
709
795
|
schemaOrgConfidence: "medium",
|
|
710
796
|
minProfile: "minimal",
|
|
@@ -712,12 +798,10 @@ var modules_default = {
|
|
|
712
798
|
},
|
|
713
799
|
{
|
|
714
800
|
id: "taxonomy",
|
|
715
|
-
versions: [
|
|
716
|
-
1
|
|
717
|
-
],
|
|
801
|
+
versions: [1],
|
|
718
802
|
latest: 1,
|
|
719
|
-
namespace: "
|
|
720
|
-
schema: "
|
|
803
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/taxonomy",
|
|
804
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/taxonomy",
|
|
721
805
|
schemaOrgMappable: true,
|
|
722
806
|
schemaOrgConfidence: "high",
|
|
723
807
|
minProfile: "minimal",
|
|
@@ -725,12 +809,10 @@ var modules_default = {
|
|
|
725
809
|
},
|
|
726
810
|
{
|
|
727
811
|
id: "media",
|
|
728
|
-
versions: [
|
|
729
|
-
1
|
|
730
|
-
],
|
|
812
|
+
versions: [1],
|
|
731
813
|
latest: 1,
|
|
732
|
-
namespace: "
|
|
733
|
-
schema: "
|
|
814
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/media",
|
|
815
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/media",
|
|
734
816
|
schemaOrgMappable: true,
|
|
735
817
|
schemaOrgConfidence: "medium",
|
|
736
818
|
minProfile: "minimal",
|
|
@@ -738,12 +820,10 @@ var modules_default = {
|
|
|
738
820
|
},
|
|
739
821
|
{
|
|
740
822
|
id: "nutrition",
|
|
741
|
-
versions: [
|
|
742
|
-
1
|
|
743
|
-
],
|
|
823
|
+
versions: [1],
|
|
744
824
|
latest: 1,
|
|
745
|
-
namespace: "
|
|
746
|
-
schema: "
|
|
825
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/nutrition",
|
|
826
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/nutrition",
|
|
747
827
|
schemaOrgMappable: false,
|
|
748
828
|
schemaOrgConfidence: "low",
|
|
749
829
|
minProfile: "minimal",
|
|
@@ -751,12 +831,10 @@ var modules_default = {
|
|
|
751
831
|
},
|
|
752
832
|
{
|
|
753
833
|
id: "times",
|
|
754
|
-
versions: [
|
|
755
|
-
1
|
|
756
|
-
],
|
|
834
|
+
versions: [1],
|
|
757
835
|
latest: 1,
|
|
758
|
-
namespace: "
|
|
759
|
-
schema: "
|
|
836
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/times",
|
|
837
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/times",
|
|
760
838
|
schemaOrgMappable: true,
|
|
761
839
|
schemaOrgConfidence: "medium",
|
|
762
840
|
minProfile: "minimal",
|
|
@@ -764,12 +842,10 @@ var modules_default = {
|
|
|
764
842
|
},
|
|
765
843
|
{
|
|
766
844
|
id: "schedule",
|
|
767
|
-
versions: [
|
|
768
|
-
1
|
|
769
|
-
],
|
|
845
|
+
versions: [1],
|
|
770
846
|
latest: 1,
|
|
771
|
-
namespace: "
|
|
772
|
-
schema: "
|
|
847
|
+
namespace: "http://soustack.org/schema/v0.3.0/stacks/schedule",
|
|
848
|
+
schema: "http://soustack.org/schema/v0.3.0/stacks/schedule",
|
|
773
849
|
schemaOrgMappable: false,
|
|
774
850
|
schemaOrgConfidence: "low",
|
|
775
851
|
minProfile: "core",
|
|
@@ -995,23 +1071,30 @@ function cleanOutput(obj) {
|
|
|
995
1071
|
Object.entries(obj).filter(([, value]) => value !== void 0)
|
|
996
1072
|
);
|
|
997
1073
|
}
|
|
998
|
-
function
|
|
999
|
-
const
|
|
1000
|
-
|
|
1074
|
+
function getSchemaOrgMappableStacks(stacks = {}) {
|
|
1075
|
+
const mappableStackIds = /* @__PURE__ */ new Set();
|
|
1076
|
+
const mappableFromRegistry = stacks_default.stacks.filter((stack) => stack.schemaOrgMappable).map((stack) => `${stack.id}@${stack.latest}`);
|
|
1077
|
+
for (const [name, version] of Object.entries(stacks)) {
|
|
1078
|
+
const stackId = `${name}@${version}`;
|
|
1079
|
+
if (mappableFromRegistry.includes(stackId)) {
|
|
1080
|
+
mappableStackIds.add(stackId);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return mappableStackIds;
|
|
1001
1084
|
}
|
|
1002
1085
|
function toSchemaOrg(recipe) {
|
|
1003
1086
|
const base = convertBasicMetadata(recipe);
|
|
1004
1087
|
const ingredients = convertIngredients2(recipe.ingredients);
|
|
1005
1088
|
const instructions = convertInstructions2(recipe.instructions);
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1008
|
-
const hasMappableNutrition =
|
|
1089
|
+
const recipeStacks = recipe.stacks && typeof recipe.stacks === "object" && !Array.isArray(recipe.stacks) ? recipe.stacks : {};
|
|
1090
|
+
const mappableStacks = getSchemaOrgMappableStacks(recipeStacks);
|
|
1091
|
+
const hasMappableNutrition = mappableStacks.has("nutrition@1");
|
|
1009
1092
|
const nutrition = hasMappableNutrition ? convertNutrition2(recipe.nutrition) : void 0;
|
|
1010
|
-
const hasMappableTimes =
|
|
1093
|
+
const hasMappableTimes = mappableStacks.has("times@1");
|
|
1011
1094
|
const timeData = hasMappableTimes ? recipe.times ? convertTimesModule(recipe.times) : convertTime2(recipe.time) : {};
|
|
1012
|
-
const hasMappableAttribution =
|
|
1095
|
+
const hasMappableAttribution = mappableStacks.has("attribution@1");
|
|
1013
1096
|
const attributionData = hasMappableAttribution ? convertAuthor(recipe.source) : {};
|
|
1014
|
-
const hasMappableTaxonomy =
|
|
1097
|
+
const hasMappableTaxonomy = mappableStacks.has("taxonomy@1");
|
|
1015
1098
|
const taxonomyData = hasMappableTaxonomy ? convertCategoryTags(recipe.category, recipe.tags) : {};
|
|
1016
1099
|
return cleanOutput({
|
|
1017
1100
|
...base,
|
|
@@ -1092,13 +1175,16 @@ async function fetchPage(url, options = {}) {
|
|
|
1092
1175
|
const response = await resolvedFetch(url, requestInit);
|
|
1093
1176
|
clearTimeout(timeoutId);
|
|
1094
1177
|
if (response && typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
globalFetch
|
|
1099
|
-
|
|
1178
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1179
|
+
if (ingestUrl) {
|
|
1180
|
+
try {
|
|
1181
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1182
|
+
if (globalFetch) {
|
|
1183
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/fetch.ts:63", message: "fetch response", data: { url, status: response.status, statusText: response.statusText, ok: response.ok, isNYTimes: url.includes("nytimes.com") }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "B" }) }).catch(() => {
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
} catch {
|
|
1100
1187
|
}
|
|
1101
|
-
} catch {
|
|
1102
1188
|
}
|
|
1103
1189
|
}
|
|
1104
1190
|
if (!response.ok) {
|
|
@@ -1110,13 +1196,16 @@ async function fetchPage(url, options = {}) {
|
|
|
1110
1196
|
}
|
|
1111
1197
|
const html = await response.text();
|
|
1112
1198
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
globalFetch
|
|
1117
|
-
|
|
1199
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1200
|
+
if (ingestUrl) {
|
|
1201
|
+
try {
|
|
1202
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1203
|
+
if (globalFetch) {
|
|
1204
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/fetch.ts:75", message: "HTML received", data: { htmlLength: html.length, hasLoginPage: html.toLowerCase().includes("login") || html.toLowerCase().includes("sign in"), hasRecipeData: html.includes("application/ld+json") || html.includes("schema.org/Recipe") }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "B,D" }) }).catch(() => {
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
} catch {
|
|
1118
1208
|
}
|
|
1119
|
-
} catch {
|
|
1120
1209
|
}
|
|
1121
1210
|
}
|
|
1122
1211
|
return html;
|
|
@@ -1146,8 +1235,6 @@ function isRecipeNode(value) {
|
|
|
1146
1235
|
return false;
|
|
1147
1236
|
}
|
|
1148
1237
|
const type = value["@type"];
|
|
1149
|
-
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(() => {
|
|
1150
|
-
});
|
|
1151
1238
|
if (typeof type === "string") {
|
|
1152
1239
|
return RECIPE_TYPES.has(type.toLowerCase());
|
|
1153
1240
|
}
|
|
@@ -1175,20 +1262,14 @@ function normalizeText(value) {
|
|
|
1175
1262
|
function extractJsonLd(html) {
|
|
1176
1263
|
const $ = cheerio.load(html);
|
|
1177
1264
|
const scripts = $('script[type="application/ld+json"]');
|
|
1178
|
-
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/jsonld.ts:8", message: "JSON-LD scripts found", data: { scriptCount: scripts.length }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "C,D" }) }).catch(() => {
|
|
1179
|
-
});
|
|
1180
1265
|
const candidates = [];
|
|
1181
1266
|
scripts.each((_, element) => {
|
|
1182
1267
|
const content = $(element).html();
|
|
1183
1268
|
if (!content) return;
|
|
1184
1269
|
const parsed = safeJsonParse(content);
|
|
1185
1270
|
if (!parsed) return;
|
|
1186
|
-
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/jsonld.ts:18", message: "JSON-LD parsed", data: { hasGraph: !!(parsed && typeof parsed === "object" && "@graph" in parsed), type: parsed && typeof parsed === "object" && "@type" in parsed ? parsed["@type"] : void 0 }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "A,C" }) }).catch(() => {
|
|
1187
|
-
});
|
|
1188
1271
|
collectCandidates(parsed, candidates);
|
|
1189
1272
|
});
|
|
1190
|
-
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/jsonld.ts:22", message: "JSON-LD candidates", data: { candidateCount: candidates.length, candidateTypes: candidates.map((c) => c["@type"]) }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "A,C" }) }).catch(() => {
|
|
1191
|
-
});
|
|
1192
1273
|
return candidates[0] ?? null;
|
|
1193
1274
|
}
|
|
1194
1275
|
function collectCandidates(payload, bucket) {
|
|
@@ -1370,13 +1451,16 @@ function extractRecipe(html) {
|
|
|
1370
1451
|
}
|
|
1371
1452
|
const jsonLdRecipe = extractJsonLd(html);
|
|
1372
1453
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
globalFetch
|
|
1377
|
-
|
|
1454
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1455
|
+
if (ingestUrl) {
|
|
1456
|
+
try {
|
|
1457
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1458
|
+
if (globalFetch) {
|
|
1459
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/extractors/index.ts:6", message: "JSON-LD extraction result", data: { hasJsonLd: !!jsonLdRecipe }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "C,D" }) }).catch(() => {
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
} catch {
|
|
1378
1463
|
}
|
|
1379
|
-
} catch {
|
|
1380
1464
|
}
|
|
1381
1465
|
}
|
|
1382
1466
|
if (jsonLdRecipe) {
|
|
@@ -1384,13 +1468,16 @@ function extractRecipe(html) {
|
|
|
1384
1468
|
}
|
|
1385
1469
|
const microdataRecipe = extractMicrodata(html);
|
|
1386
1470
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
globalFetch
|
|
1391
|
-
|
|
1471
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1472
|
+
if (ingestUrl) {
|
|
1473
|
+
try {
|
|
1474
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1475
|
+
if (globalFetch) {
|
|
1476
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/extractors/index.ts:12", message: "Microdata extraction result", data: { hasMicrodata: !!microdataRecipe }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "D" }) }).catch(() => {
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
} catch {
|
|
1392
1480
|
}
|
|
1393
|
-
} catch {
|
|
1394
1481
|
}
|
|
1395
1482
|
}
|
|
1396
1483
|
if (microdataRecipe) {
|
|
@@ -1402,35 +1489,44 @@ function extractRecipe(html) {
|
|
|
1402
1489
|
// src/scraper/index.ts
|
|
1403
1490
|
async function scrapeRecipe(url, options = {}) {
|
|
1404
1491
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
globalFetch
|
|
1409
|
-
|
|
1492
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1493
|
+
if (ingestUrl) {
|
|
1494
|
+
try {
|
|
1495
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1496
|
+
if (globalFetch) {
|
|
1497
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/index.ts:7", message: "scrapeRecipe entry", data: { url, hasOptions: !!options }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "A,B,C,D,E" }) }).catch(() => {
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
} catch {
|
|
1410
1501
|
}
|
|
1411
|
-
} catch {
|
|
1412
1502
|
}
|
|
1413
1503
|
}
|
|
1414
1504
|
const html = await fetchPage(url, options);
|
|
1415
1505
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
globalFetch
|
|
1420
|
-
|
|
1506
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1507
|
+
if (ingestUrl) {
|
|
1508
|
+
try {
|
|
1509
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1510
|
+
if (globalFetch) {
|
|
1511
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/index.ts:9", message: "HTML fetched", data: { htmlLength: html?.length, htmlPreview: html?.substring(0, 200) }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "B" }) }).catch(() => {
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1421
1515
|
}
|
|
1422
|
-
} catch {
|
|
1423
1516
|
}
|
|
1424
1517
|
}
|
|
1425
1518
|
const { recipe } = extractRecipe(html);
|
|
1426
1519
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
globalFetch
|
|
1431
|
-
|
|
1520
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1521
|
+
if (ingestUrl) {
|
|
1522
|
+
try {
|
|
1523
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1524
|
+
if (globalFetch) {
|
|
1525
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/index.ts:11", message: "extractRecipe result", data: { hasRecipe: !!recipe, recipeType: recipe?.["@type"], recipeName: recipe?.name }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "A,C,D" }) }).catch(() => {
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
} catch {
|
|
1432
1529
|
}
|
|
1433
|
-
} catch {
|
|
1434
1530
|
}
|
|
1435
1531
|
}
|
|
1436
1532
|
if (!recipe) {
|
|
@@ -1438,13 +1534,16 @@ async function scrapeRecipe(url, options = {}) {
|
|
|
1438
1534
|
}
|
|
1439
1535
|
const soustackRecipe = fromSchemaOrg(recipe);
|
|
1440
1536
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
globalFetch
|
|
1445
|
-
|
|
1537
|
+
const ingestUrl = process.env.SOUSTACK_DEBUG_INGEST_URL;
|
|
1538
|
+
if (ingestUrl) {
|
|
1539
|
+
try {
|
|
1540
|
+
const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
|
|
1541
|
+
if (globalFetch) {
|
|
1542
|
+
globalFetch(ingestUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ location: "scraper/index.ts:17", message: "fromSchemaOrg result", data: { hasSoustackRecipe: !!soustackRecipe, soustackRecipeName: soustackRecipe?.name }, timestamp: Date.now(), sessionId: "debug-session", runId: "run1", hypothesisId: "A" }) }).catch(() => {
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
} catch {
|
|
1446
1546
|
}
|
|
1447
|
-
} catch {
|
|
1448
1547
|
}
|
|
1449
1548
|
}
|
|
1450
1549
|
if (!soustackRecipe) {
|
|
@@ -1453,72 +1552,625 @@ async function scrapeRecipe(url, options = {}) {
|
|
|
1453
1552
|
return soustackRecipe;
|
|
1454
1553
|
}
|
|
1455
1554
|
|
|
1456
|
-
// src/
|
|
1457
|
-
|
|
1555
|
+
// src/conformance/index.ts
|
|
1556
|
+
function validateConformance(recipe) {
|
|
1557
|
+
const issues = [];
|
|
1558
|
+
issues.push(...checkDAGValidity(recipe));
|
|
1559
|
+
if (hasSchedulableProfile(recipe)) {
|
|
1560
|
+
issues.push(...checkTimingSchedulability(recipe));
|
|
1561
|
+
}
|
|
1562
|
+
issues.push(...checkScalingSanity(recipe));
|
|
1563
|
+
const ok = issues.filter((i) => i.severity === "error").length === 0;
|
|
1564
|
+
return { ok, issues };
|
|
1565
|
+
}
|
|
1566
|
+
function hasSchedulableProfile(recipe) {
|
|
1567
|
+
const schema = recipe.$schema;
|
|
1568
|
+
if (typeof schema === "string") {
|
|
1569
|
+
return schema.includes("schedulable") || schema === "http://soustack.org/schema/v0.3.0/profiles/schedulable";
|
|
1570
|
+
}
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
function checkDAGValidity(recipe) {
|
|
1574
|
+
const issues = [];
|
|
1575
|
+
const instructions = recipe.instructions;
|
|
1576
|
+
if (!Array.isArray(instructions)) {
|
|
1577
|
+
return issues;
|
|
1578
|
+
}
|
|
1579
|
+
const instructionIds = /* @__PURE__ */ new Set();
|
|
1580
|
+
const dependencyRefs = [];
|
|
1581
|
+
const collect = (items, basePath) => {
|
|
1582
|
+
items.forEach((item, index) => {
|
|
1583
|
+
const currentPath = `${basePath}/${index}`;
|
|
1584
|
+
if (isInstructionSubsection(item)) {
|
|
1585
|
+
if (Array.isArray(item.items)) {
|
|
1586
|
+
collect(item.items, `${currentPath}/items`);
|
|
1587
|
+
}
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (isInstruction(item)) {
|
|
1591
|
+
const id = typeof item.id === "string" ? item.id : void 0;
|
|
1592
|
+
if (id) {
|
|
1593
|
+
instructionIds.add(id);
|
|
1594
|
+
}
|
|
1595
|
+
if (Array.isArray(item.dependsOn)) {
|
|
1596
|
+
item.dependsOn.forEach((depId, depIndex) => {
|
|
1597
|
+
if (typeof depId === "string") {
|
|
1598
|
+
dependencyRefs.push({
|
|
1599
|
+
fromId: id,
|
|
1600
|
+
toId: depId,
|
|
1601
|
+
path: `${currentPath}/dependsOn/${depIndex}`
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
};
|
|
1609
|
+
collect(instructions, "/instructions");
|
|
1610
|
+
dependencyRefs.forEach((ref) => {
|
|
1611
|
+
if (!instructionIds.has(ref.toId)) {
|
|
1612
|
+
issues.push({
|
|
1613
|
+
code: "DAG_MISSING_NODE",
|
|
1614
|
+
path: ref.path,
|
|
1615
|
+
message: `Instruction dependency references missing step id '${ref.toId}'.`,
|
|
1616
|
+
severity: "error"
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1621
|
+
dependencyRefs.forEach((ref) => {
|
|
1622
|
+
if (ref.fromId && instructionIds.has(ref.fromId) && instructionIds.has(ref.toId)) {
|
|
1623
|
+
const list = adjacency.get(ref.fromId) ?? [];
|
|
1624
|
+
list.push({ toId: ref.toId, path: ref.path });
|
|
1625
|
+
adjacency.set(ref.fromId, list);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1629
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1630
|
+
const detectCycles = (nodeId) => {
|
|
1631
|
+
if (visiting.has(nodeId)) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (visited.has(nodeId)) {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
visiting.add(nodeId);
|
|
1638
|
+
const neighbors = adjacency.get(nodeId) ?? [];
|
|
1639
|
+
neighbors.forEach((edge) => {
|
|
1640
|
+
if (visiting.has(edge.toId)) {
|
|
1641
|
+
issues.push({
|
|
1642
|
+
code: "DAG_CYCLE",
|
|
1643
|
+
path: edge.path,
|
|
1644
|
+
message: `Circular dependency detected involving step id '${edge.toId}'.`,
|
|
1645
|
+
severity: "error"
|
|
1646
|
+
});
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
detectCycles(edge.toId);
|
|
1650
|
+
});
|
|
1651
|
+
visiting.delete(nodeId);
|
|
1652
|
+
visited.add(nodeId);
|
|
1653
|
+
};
|
|
1654
|
+
instructionIds.forEach((id) => detectCycles(id));
|
|
1655
|
+
return issues;
|
|
1656
|
+
}
|
|
1657
|
+
function checkTimingSchedulability(recipe) {
|
|
1658
|
+
const issues = [];
|
|
1659
|
+
const instructions = recipe.instructions;
|
|
1660
|
+
if (!Array.isArray(instructions)) {
|
|
1661
|
+
return issues;
|
|
1662
|
+
}
|
|
1663
|
+
const checkInstruction = (item, path2) => {
|
|
1664
|
+
if (isInstructionSubsection(item)) {
|
|
1665
|
+
if (Array.isArray(item.items)) {
|
|
1666
|
+
item.items.forEach((subItem, index) => {
|
|
1667
|
+
checkInstruction(subItem, `${path2}/items/${index}`);
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (isInstruction(item)) {
|
|
1673
|
+
if (!item.id) {
|
|
1674
|
+
issues.push({
|
|
1675
|
+
code: "SCHEDULABLE_MISSING_ID",
|
|
1676
|
+
path: path2,
|
|
1677
|
+
message: "Schedulable profile requires all instructions to have an id.",
|
|
1678
|
+
severity: "error"
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
if (!item.timing) {
|
|
1682
|
+
issues.push({
|
|
1683
|
+
code: "SCHEDULABLE_MISSING_TIMING",
|
|
1684
|
+
path: path2,
|
|
1685
|
+
message: "Schedulable profile requires all instructions to have timing information.",
|
|
1686
|
+
severity: "error"
|
|
1687
|
+
});
|
|
1688
|
+
} else if (!item.timing.duration) {
|
|
1689
|
+
issues.push({
|
|
1690
|
+
code: "SCHEDULABLE_MISSING_DURATION",
|
|
1691
|
+
path: `${path2}/timing`,
|
|
1692
|
+
message: "Schedulable profile requires timing.duration for all instructions.",
|
|
1693
|
+
severity: "error"
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
instructions.forEach((item, index) => {
|
|
1699
|
+
checkInstruction(item, `/instructions/${index}`);
|
|
1700
|
+
});
|
|
1701
|
+
return issues;
|
|
1702
|
+
}
|
|
1703
|
+
function checkScalingSanity(recipe) {
|
|
1704
|
+
const issues = [];
|
|
1705
|
+
const ingredients = recipe.ingredients;
|
|
1706
|
+
if (!Array.isArray(ingredients)) {
|
|
1707
|
+
return issues;
|
|
1708
|
+
}
|
|
1709
|
+
const ingredientIds = /* @__PURE__ */ new Set();
|
|
1710
|
+
const collectIngredientIds = (items, basePath) => {
|
|
1711
|
+
items.forEach((item, index) => {
|
|
1712
|
+
if (isIngredientSubsection(item)) {
|
|
1713
|
+
if (Array.isArray(item.items)) {
|
|
1714
|
+
collectIngredientIds(item.items);
|
|
1715
|
+
}
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (isIngredient(item)) {
|
|
1719
|
+
if (typeof item.id === "string") {
|
|
1720
|
+
ingredientIds.add(item.id);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
};
|
|
1725
|
+
collectIngredientIds(ingredients);
|
|
1726
|
+
const checkIngredient = (item, path2) => {
|
|
1727
|
+
if (isIngredientSubsection(item)) {
|
|
1728
|
+
if (Array.isArray(item.items)) {
|
|
1729
|
+
item.items.forEach((subItem, index) => {
|
|
1730
|
+
checkIngredient(subItem, `${path2}/items/${index}`);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (isIngredient(item)) {
|
|
1736
|
+
const scaling = item.scaling;
|
|
1737
|
+
if (scaling && typeof scaling === "object" && "type" in scaling && scaling.type === "bakers_percentage") {
|
|
1738
|
+
const bakersScaling = scaling;
|
|
1739
|
+
if (bakersScaling.referenceId) {
|
|
1740
|
+
if (!ingredientIds.has(bakersScaling.referenceId)) {
|
|
1741
|
+
issues.push({
|
|
1742
|
+
code: "SCALING_INVALID_REFERENCE",
|
|
1743
|
+
path: `${path2}/scaling/referenceId`,
|
|
1744
|
+
message: `Baker's percentage references missing ingredient id '${bakersScaling.referenceId}'.`,
|
|
1745
|
+
severity: "error"
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
} else {
|
|
1749
|
+
issues.push({
|
|
1750
|
+
code: "SCALING_MISSING_REFERENCE",
|
|
1751
|
+
path: `${path2}/scaling`,
|
|
1752
|
+
message: "Baker's percentage scaling requires a referenceId.",
|
|
1753
|
+
severity: "error"
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
ingredients.forEach((item, index) => {
|
|
1760
|
+
checkIngredient(item, `/ingredients/${index}`);
|
|
1761
|
+
});
|
|
1762
|
+
return issues;
|
|
1763
|
+
}
|
|
1764
|
+
function isInstruction(item) {
|
|
1765
|
+
return item && typeof item === "object" && !Array.isArray(item) && "text" in item;
|
|
1766
|
+
}
|
|
1767
|
+
function isInstructionSubsection(item) {
|
|
1768
|
+
return item && typeof item === "object" && !Array.isArray(item) && "items" in item && "subsection" in item;
|
|
1769
|
+
}
|
|
1770
|
+
function isIngredient(item) {
|
|
1771
|
+
return item && typeof item === "object" && !Array.isArray(item) && "item" in item;
|
|
1772
|
+
}
|
|
1773
|
+
function isIngredientSubsection(item) {
|
|
1774
|
+
return item && typeof item === "object" && !Array.isArray(item) && "items" in item && "subsection" in item;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/soustack.schema.json
|
|
1778
|
+
var soustack_schema_default = {
|
|
1458
1779
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1459
|
-
$id: "http://soustack.org/schema/
|
|
1460
|
-
title: "Soustack Recipe
|
|
1461
|
-
description: "
|
|
1780
|
+
$id: "http://soustack.org/schema/v0.3.0",
|
|
1781
|
+
title: "Soustack Recipe Schema v0.3.0",
|
|
1782
|
+
description: "A portable, scalable, interoperable recipe format.",
|
|
1462
1783
|
type: "object",
|
|
1463
|
-
|
|
1784
|
+
required: ["name", "ingredients", "instructions"],
|
|
1785
|
+
additionalProperties: false,
|
|
1786
|
+
patternProperties: {
|
|
1787
|
+
"^x-": {}
|
|
1788
|
+
},
|
|
1464
1789
|
properties: {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1790
|
+
$schema: {
|
|
1791
|
+
type: "string",
|
|
1792
|
+
format: "uri",
|
|
1793
|
+
description: "Optional schema hint for tooling compatibility"
|
|
1468
1794
|
},
|
|
1469
|
-
|
|
1795
|
+
id: {
|
|
1470
1796
|
type: "string",
|
|
1471
|
-
description: "
|
|
1797
|
+
description: "Unique identifier (slug or UUID)"
|
|
1798
|
+
},
|
|
1799
|
+
name: {
|
|
1800
|
+
type: "string",
|
|
1801
|
+
description: "The title of the recipe"
|
|
1802
|
+
},
|
|
1803
|
+
title: {
|
|
1804
|
+
type: "string",
|
|
1805
|
+
description: "Optional display title; alias for name"
|
|
1806
|
+
},
|
|
1807
|
+
version: {
|
|
1808
|
+
type: "string",
|
|
1809
|
+
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
1810
|
+
description: "DEPRECATED: use recipeVersion for authoring revisions"
|
|
1472
1811
|
},
|
|
1473
|
-
|
|
1812
|
+
recipeVersion: {
|
|
1813
|
+
type: "string",
|
|
1814
|
+
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
1815
|
+
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
1816
|
+
},
|
|
1817
|
+
description: {
|
|
1818
|
+
type: "string"
|
|
1819
|
+
},
|
|
1820
|
+
category: {
|
|
1821
|
+
type: "string",
|
|
1822
|
+
examples: ["Main Course", "Dessert"]
|
|
1823
|
+
},
|
|
1824
|
+
tags: {
|
|
1474
1825
|
type: "array",
|
|
1475
|
-
|
|
1476
|
-
items: {
|
|
1477
|
-
type: "string"
|
|
1478
|
-
}
|
|
1826
|
+
items: { type: "string" }
|
|
1479
1827
|
},
|
|
1480
|
-
|
|
1828
|
+
image: {
|
|
1829
|
+
description: "Recipe-level hero image(s)",
|
|
1830
|
+
anyOf: [
|
|
1831
|
+
{
|
|
1832
|
+
type: "string",
|
|
1833
|
+
format: "uri"
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
type: "array",
|
|
1837
|
+
minItems: 1,
|
|
1838
|
+
items: {
|
|
1839
|
+
type: "string",
|
|
1840
|
+
format: "uri"
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
]
|
|
1844
|
+
},
|
|
1845
|
+
dateAdded: {
|
|
1481
1846
|
type: "string",
|
|
1482
|
-
|
|
1847
|
+
format: "date-time"
|
|
1848
|
+
},
|
|
1849
|
+
metadata: {
|
|
1850
|
+
type: "object",
|
|
1851
|
+
additionalProperties: true,
|
|
1852
|
+
description: "Free-form vendor metadata"
|
|
1853
|
+
},
|
|
1854
|
+
source: {
|
|
1855
|
+
type: "object",
|
|
1856
|
+
properties: {
|
|
1857
|
+
author: { type: "string" },
|
|
1858
|
+
url: { type: "string", format: "uri" },
|
|
1859
|
+
name: { type: "string" },
|
|
1860
|
+
adapted: { type: "boolean" }
|
|
1861
|
+
}
|
|
1862
|
+
},
|
|
1863
|
+
yield: {
|
|
1864
|
+
$ref: "#/definitions/yield"
|
|
1865
|
+
},
|
|
1866
|
+
time: {
|
|
1867
|
+
$ref: "#/definitions/time"
|
|
1868
|
+
},
|
|
1869
|
+
equipment: {
|
|
1870
|
+
type: "array",
|
|
1871
|
+
items: { $ref: "#/definitions/equipment" }
|
|
1483
1872
|
},
|
|
1484
1873
|
ingredients: {
|
|
1485
1874
|
type: "array",
|
|
1486
|
-
|
|
1875
|
+
items: {
|
|
1876
|
+
anyOf: [
|
|
1877
|
+
{ type: "string" },
|
|
1878
|
+
{ $ref: "#/definitions/ingredient" },
|
|
1879
|
+
{ $ref: "#/definitions/ingredientSubsection" }
|
|
1880
|
+
]
|
|
1881
|
+
}
|
|
1487
1882
|
},
|
|
1488
1883
|
instructions: {
|
|
1489
1884
|
type: "array",
|
|
1490
|
-
|
|
1885
|
+
items: {
|
|
1886
|
+
anyOf: [
|
|
1887
|
+
{ type: "string" },
|
|
1888
|
+
{ $ref: "#/definitions/instruction" },
|
|
1889
|
+
{ $ref: "#/definitions/instructionSubsection" }
|
|
1890
|
+
]
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
storage: {
|
|
1894
|
+
$ref: "#/definitions/storage"
|
|
1895
|
+
},
|
|
1896
|
+
substitutions: {
|
|
1897
|
+
type: "array",
|
|
1898
|
+
items: { $ref: "#/definitions/substitution" }
|
|
1491
1899
|
}
|
|
1492
1900
|
},
|
|
1493
|
-
|
|
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
|
-
{
|
|
1901
|
+
definitions: {
|
|
1902
|
+
yield: {
|
|
1505
1903
|
type: "object",
|
|
1904
|
+
required: ["amount", "unit"],
|
|
1506
1905
|
properties: {
|
|
1507
|
-
|
|
1508
|
-
|
|
1906
|
+
amount: { type: "number" },
|
|
1907
|
+
unit: { type: "string" },
|
|
1908
|
+
servings: { type: "number" },
|
|
1909
|
+
description: { type: "string" }
|
|
1910
|
+
}
|
|
1911
|
+
},
|
|
1912
|
+
time: {
|
|
1913
|
+
type: "object",
|
|
1914
|
+
properties: {
|
|
1915
|
+
prep: { type: "number" },
|
|
1916
|
+
active: { type: "number" },
|
|
1917
|
+
passive: { type: "number" },
|
|
1918
|
+
total: { type: "number" },
|
|
1919
|
+
prepTime: { type: "string", format: "duration" },
|
|
1920
|
+
cookTime: { type: "string", format: "duration" }
|
|
1921
|
+
},
|
|
1922
|
+
minProperties: 1
|
|
1923
|
+
},
|
|
1924
|
+
quantity: {
|
|
1925
|
+
type: "object",
|
|
1926
|
+
required: ["amount"],
|
|
1927
|
+
properties: {
|
|
1928
|
+
amount: { type: "number" },
|
|
1929
|
+
unit: {
|
|
1930
|
+
type: ["string", "null"],
|
|
1931
|
+
description: "Display-friendly unit text; implementations may normalize or canonicalize units separately."
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
},
|
|
1935
|
+
scaling: {
|
|
1936
|
+
type: "object",
|
|
1937
|
+
required: ["type"],
|
|
1938
|
+
properties: {
|
|
1939
|
+
type: {
|
|
1940
|
+
type: "string",
|
|
1941
|
+
enum: ["linear", "discrete", "proportional", "fixed", "bakers_percentage"]
|
|
1942
|
+
},
|
|
1943
|
+
factor: { type: "number" },
|
|
1944
|
+
referenceId: { type: "string" },
|
|
1945
|
+
roundTo: { type: "number" },
|
|
1946
|
+
min: { type: "number" },
|
|
1947
|
+
max: { type: "number" }
|
|
1948
|
+
},
|
|
1949
|
+
if: {
|
|
1950
|
+
properties: { type: { const: "bakers_percentage" } }
|
|
1951
|
+
},
|
|
1952
|
+
then: {
|
|
1953
|
+
required: ["referenceId"]
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
ingredient: {
|
|
1957
|
+
type: "object",
|
|
1958
|
+
required: ["item"],
|
|
1959
|
+
properties: {
|
|
1960
|
+
id: { type: "string" },
|
|
1961
|
+
item: { type: "string" },
|
|
1962
|
+
quantity: { $ref: "#/definitions/quantity" },
|
|
1963
|
+
name: { type: "string" },
|
|
1964
|
+
aisle: { type: "string" },
|
|
1965
|
+
prep: { type: "string" },
|
|
1966
|
+
prepAction: { type: "string" },
|
|
1967
|
+
prepActions: {
|
|
1509
1968
|
type: "array",
|
|
1510
1969
|
items: { type: "string" },
|
|
1511
|
-
|
|
1512
|
-
default: []
|
|
1970
|
+
description: "Structured prep verbs (e.g., peel, dice) for mise en place workflows."
|
|
1513
1971
|
},
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1972
|
+
prepTime: { type: "number" },
|
|
1973
|
+
form: {
|
|
1974
|
+
type: "string",
|
|
1975
|
+
description: "State of the ingredient as used (packed, sifted, melted, room_temperature, etc.)."
|
|
1976
|
+
},
|
|
1977
|
+
destination: { type: "string" },
|
|
1978
|
+
scaling: { $ref: "#/definitions/scaling" },
|
|
1979
|
+
critical: { type: "boolean" },
|
|
1980
|
+
optional: { type: "boolean" },
|
|
1981
|
+
notes: { type: "string" }
|
|
1982
|
+
}
|
|
1983
|
+
},
|
|
1984
|
+
ingredientSubsection: {
|
|
1985
|
+
type: "object",
|
|
1986
|
+
required: ["subsection", "items"],
|
|
1987
|
+
properties: {
|
|
1988
|
+
subsection: { type: "string" },
|
|
1989
|
+
items: {
|
|
1990
|
+
type: "array",
|
|
1991
|
+
items: { $ref: "#/definitions/ingredient" }
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
},
|
|
1995
|
+
equipment: {
|
|
1996
|
+
type: "object",
|
|
1997
|
+
required: ["name"],
|
|
1998
|
+
properties: {
|
|
1999
|
+
id: { type: "string" },
|
|
2000
|
+
name: { type: "string" },
|
|
2001
|
+
required: { type: "boolean" },
|
|
2002
|
+
label: { type: "string" },
|
|
2003
|
+
capacity: { $ref: "#/definitions/quantity" },
|
|
2004
|
+
scalingLimit: { type: "number" },
|
|
2005
|
+
alternatives: {
|
|
2006
|
+
type: "array",
|
|
2007
|
+
items: { type: "string" }
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
instruction: {
|
|
2012
|
+
type: "object",
|
|
2013
|
+
required: ["text"],
|
|
2014
|
+
properties: {
|
|
2015
|
+
id: { type: "string" },
|
|
2016
|
+
text: { type: "string" },
|
|
2017
|
+
image: {
|
|
2018
|
+
type: "string",
|
|
2019
|
+
format: "uri",
|
|
2020
|
+
description: "Optional image that illustrates this instruction"
|
|
2021
|
+
},
|
|
2022
|
+
destination: { type: "string" },
|
|
2023
|
+
dependsOn: {
|
|
2024
|
+
type: "array",
|
|
2025
|
+
items: { type: "string" }
|
|
2026
|
+
},
|
|
2027
|
+
inputs: {
|
|
2028
|
+
type: "array",
|
|
2029
|
+
items: { type: "string" }
|
|
2030
|
+
},
|
|
2031
|
+
timing: {
|
|
2032
|
+
type: "object",
|
|
2033
|
+
required: ["duration", "type"],
|
|
2034
|
+
properties: {
|
|
2035
|
+
duration: {
|
|
2036
|
+
anyOf: [
|
|
2037
|
+
{ type: "number" },
|
|
2038
|
+
{ type: "string", pattern: "^P" }
|
|
2039
|
+
],
|
|
2040
|
+
description: "Minutes as a number or ISO8601 duration string"
|
|
2041
|
+
},
|
|
2042
|
+
type: { type: "string", enum: ["active", "passive"] },
|
|
2043
|
+
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
2048
|
+
instructionSubsection: {
|
|
2049
|
+
type: "object",
|
|
2050
|
+
required: ["subsection", "items"],
|
|
2051
|
+
properties: {
|
|
2052
|
+
subsection: { type: "string" },
|
|
2053
|
+
items: {
|
|
2054
|
+
type: "array",
|
|
2055
|
+
items: {
|
|
2056
|
+
anyOf: [
|
|
2057
|
+
{ type: "string" },
|
|
2058
|
+
{ $ref: "#/definitions/instruction" }
|
|
2059
|
+
]
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
storage: {
|
|
2065
|
+
type: "object",
|
|
2066
|
+
properties: {
|
|
2067
|
+
roomTemp: { $ref: "#/definitions/storageMethod" },
|
|
2068
|
+
refrigerated: { $ref: "#/definitions/storageMethod" },
|
|
2069
|
+
frozen: {
|
|
2070
|
+
allOf: [
|
|
2071
|
+
{ $ref: "#/definitions/storageMethod" },
|
|
2072
|
+
{
|
|
2073
|
+
type: "object",
|
|
2074
|
+
properties: { thawing: { type: "string" } }
|
|
2075
|
+
}
|
|
2076
|
+
]
|
|
2077
|
+
},
|
|
2078
|
+
reheating: { type: "string" },
|
|
2079
|
+
makeAhead: {
|
|
2080
|
+
type: "array",
|
|
2081
|
+
items: {
|
|
2082
|
+
allOf: [
|
|
2083
|
+
{ $ref: "#/definitions/storageMethod" },
|
|
2084
|
+
{
|
|
2085
|
+
type: "object",
|
|
2086
|
+
required: ["component", "storage"],
|
|
2087
|
+
properties: {
|
|
2088
|
+
component: { type: "string" },
|
|
2089
|
+
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
]
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
storageMethod: {
|
|
2098
|
+
type: "object",
|
|
2099
|
+
required: ["duration"],
|
|
2100
|
+
properties: {
|
|
2101
|
+
duration: { type: "string", pattern: "^P" },
|
|
2102
|
+
method: { type: "string" },
|
|
2103
|
+
notes: { type: "string" }
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
substitution: {
|
|
2107
|
+
type: "object",
|
|
2108
|
+
required: ["ingredient"],
|
|
2109
|
+
properties: {
|
|
2110
|
+
ingredient: { type: "string" },
|
|
2111
|
+
critical: { type: "boolean" },
|
|
2112
|
+
notes: { type: "string" },
|
|
2113
|
+
alternatives: {
|
|
2114
|
+
type: "array",
|
|
2115
|
+
items: {
|
|
2116
|
+
type: "object",
|
|
2117
|
+
required: ["name", "ratio"],
|
|
2118
|
+
properties: {
|
|
2119
|
+
name: { type: "string" },
|
|
2120
|
+
ratio: { type: "string" },
|
|
2121
|
+
notes: { type: "string" },
|
|
2122
|
+
impact: { type: "string" },
|
|
2123
|
+
dietary: {
|
|
2124
|
+
type: "array",
|
|
2125
|
+
items: { type: "string" }
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
1520
2131
|
}
|
|
1521
|
-
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
// src/schemas/recipe/base.schema.json
|
|
2136
|
+
var base_schema_default = {
|
|
2137
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2138
|
+
$id: "http://soustack.org/schema/recipe/base.schema.json",
|
|
2139
|
+
title: "Soustack Recipe Base Schema",
|
|
2140
|
+
description: "Base document shape for Soustack recipe documents. Profiles and stacks build on this baseline.",
|
|
2141
|
+
type: "object",
|
|
2142
|
+
additionalProperties: true,
|
|
2143
|
+
properties: {
|
|
2144
|
+
"@type": {
|
|
2145
|
+
const: "Recipe",
|
|
2146
|
+
description: "Document marker for Soustack recipes"
|
|
2147
|
+
},
|
|
2148
|
+
profile: {
|
|
2149
|
+
type: "string",
|
|
2150
|
+
description: "Profile identifier applied to this recipe"
|
|
2151
|
+
},
|
|
2152
|
+
stacks: {
|
|
2153
|
+
type: "object",
|
|
2154
|
+
description: "Stack declarations as a map: Record<stackName, versionNumber>",
|
|
2155
|
+
additionalProperties: {
|
|
2156
|
+
type: "integer",
|
|
2157
|
+
minimum: 1
|
|
2158
|
+
}
|
|
2159
|
+
},
|
|
2160
|
+
name: {
|
|
2161
|
+
type: "string",
|
|
2162
|
+
description: "Human-readable recipe name"
|
|
2163
|
+
},
|
|
2164
|
+
ingredients: {
|
|
2165
|
+
type: "array",
|
|
2166
|
+
description: "Ingredients payload; content is validated by profiles/stacks"
|
|
2167
|
+
},
|
|
2168
|
+
instructions: {
|
|
2169
|
+
type: "array",
|
|
2170
|
+
description: "Instruction payload; content is validated by profiles/stacks"
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2173
|
+
required: ["@type"]
|
|
1522
2174
|
};
|
|
1523
2175
|
|
|
1524
2176
|
// src/schemas/recipe/profiles/minimal.schema.json
|
|
@@ -1526,7 +2178,7 @@ var minimal_schema_default = {
|
|
|
1526
2178
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1527
2179
|
$id: "http://soustack.org/schema/recipe/profiles/minimal.schema.json",
|
|
1528
2180
|
title: "Soustack Recipe Minimal Profile",
|
|
1529
|
-
description: "Minimal profile that ensures the basic Recipe structure is present while allowing
|
|
2181
|
+
description: "Minimal profile that ensures the basic Recipe structure is present while allowing stacks to extend it.",
|
|
1530
2182
|
allOf: [
|
|
1531
2183
|
{
|
|
1532
2184
|
$ref: "http://soustack.org/schema/recipe/base.schema.json"
|
|
@@ -1537,19 +2189,19 @@ var minimal_schema_default = {
|
|
|
1537
2189
|
profile: {
|
|
1538
2190
|
const: "minimal"
|
|
1539
2191
|
},
|
|
1540
|
-
|
|
1541
|
-
type: "
|
|
1542
|
-
|
|
1543
|
-
type: "
|
|
1544
|
-
|
|
1545
|
-
"attribution@1",
|
|
1546
|
-
"taxonomy@1",
|
|
1547
|
-
"media@1",
|
|
1548
|
-
"nutrition@1",
|
|
1549
|
-
"times@1"
|
|
1550
|
-
]
|
|
2192
|
+
stacks: {
|
|
2193
|
+
type: "object",
|
|
2194
|
+
additionalProperties: {
|
|
2195
|
+
type: "integer",
|
|
2196
|
+
minimum: 1
|
|
1551
2197
|
},
|
|
1552
|
-
|
|
2198
|
+
properties: {
|
|
2199
|
+
attribution: { type: "integer", minimum: 1 },
|
|
2200
|
+
taxonomy: { type: "integer", minimum: 1 },
|
|
2201
|
+
media: { type: "integer", minimum: 1 },
|
|
2202
|
+
nutrition: { type: "integer", minimum: 1 },
|
|
2203
|
+
times: { type: "integer", minimum: 1 }
|
|
2204
|
+
}
|
|
1553
2205
|
},
|
|
1554
2206
|
name: {
|
|
1555
2207
|
type: "string",
|
|
@@ -1575,23 +2227,304 @@ var minimal_schema_default = {
|
|
|
1575
2227
|
]
|
|
1576
2228
|
};
|
|
1577
2229
|
|
|
1578
|
-
// src/schemas/recipe/
|
|
2230
|
+
// src/schemas/recipe/profiles/core.schema.json
|
|
2231
|
+
var core_schema_default = {
|
|
2232
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2233
|
+
$id: "http://soustack.org/schema/recipe/profiles/core.schema.json",
|
|
2234
|
+
title: "Soustack Recipe Core Profile",
|
|
2235
|
+
description: "Core profile that builds on the minimal profile and is intended to be combined with recipe stacks.",
|
|
2236
|
+
allOf: [
|
|
2237
|
+
{ $ref: "http://soustack.org/schema/recipe/base.schema.json" },
|
|
2238
|
+
{
|
|
2239
|
+
type: "object",
|
|
2240
|
+
properties: {
|
|
2241
|
+
profile: { const: "core" },
|
|
2242
|
+
stacks: {
|
|
2243
|
+
type: "object",
|
|
2244
|
+
additionalProperties: {
|
|
2245
|
+
type: "integer",
|
|
2246
|
+
minimum: 1
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
name: { type: "string", minLength: 1 },
|
|
2250
|
+
ingredients: { type: "array", minItems: 1 },
|
|
2251
|
+
instructions: { type: "array", minItems: 1 }
|
|
2252
|
+
},
|
|
2253
|
+
required: ["profile", "name", "ingredients", "instructions"],
|
|
2254
|
+
additionalProperties: true
|
|
2255
|
+
}
|
|
2256
|
+
]
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
// spec/profiles/base.schema.json
|
|
2260
|
+
var base_schema_default2 = {
|
|
2261
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2262
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/base",
|
|
2263
|
+
title: "Soustack Base Profile Schema",
|
|
2264
|
+
description: "Wrapper schema that exposes the unmodified Soustack base schema.",
|
|
2265
|
+
allOf: [
|
|
2266
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" }
|
|
2267
|
+
]
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
// spec/profiles/cookable.schema.json
|
|
2271
|
+
var cookable_schema_default = {
|
|
2272
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2273
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/cookable",
|
|
2274
|
+
title: "Soustack Cookable Profile Schema",
|
|
2275
|
+
description: "Extends the base schema to require structured yield + time metadata and non-empty ingredient/instruction lists.",
|
|
2276
|
+
allOf: [
|
|
2277
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
2278
|
+
{
|
|
2279
|
+
required: ["yield", "time", "ingredients", "instructions"],
|
|
2280
|
+
properties: {
|
|
2281
|
+
yield: { $ref: "http://soustack.org/schema/v0.3.0#/definitions/yield" },
|
|
2282
|
+
time: { $ref: "http://soustack.org/schema/v0.3.0#/definitions/time" },
|
|
2283
|
+
ingredients: { type: "array", minItems: 1 },
|
|
2284
|
+
instructions: { type: "array", minItems: 1 }
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
]
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
// spec/profiles/illustrated.schema.json
|
|
2291
|
+
var illustrated_schema_default = {
|
|
2292
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2293
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/illustrated",
|
|
2294
|
+
title: "Soustack Illustrated Profile Schema",
|
|
2295
|
+
description: "Extends the base schema to guarantee at least one illustrative image.",
|
|
2296
|
+
allOf: [
|
|
2297
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
2298
|
+
{
|
|
2299
|
+
anyOf: [
|
|
2300
|
+
{ required: ["image"] },
|
|
2301
|
+
{
|
|
2302
|
+
properties: {
|
|
2303
|
+
instructions: {
|
|
2304
|
+
type: "array",
|
|
2305
|
+
contains: {
|
|
2306
|
+
anyOf: [
|
|
2307
|
+
{ $ref: "#/definitions/imageInstruction" },
|
|
2308
|
+
{ $ref: "#/definitions/instructionSubsectionWithImage" }
|
|
2309
|
+
]
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
]
|
|
2315
|
+
}
|
|
2316
|
+
],
|
|
2317
|
+
definitions: {
|
|
2318
|
+
imageInstruction: {
|
|
2319
|
+
allOf: [
|
|
2320
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instruction" },
|
|
2321
|
+
{ required: ["image"] }
|
|
2322
|
+
]
|
|
2323
|
+
},
|
|
2324
|
+
instructionSubsectionWithImage: {
|
|
2325
|
+
allOf: [
|
|
2326
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instructionSubsection" },
|
|
2327
|
+
{
|
|
2328
|
+
properties: {
|
|
2329
|
+
items: {
|
|
2330
|
+
type: "array",
|
|
2331
|
+
contains: { $ref: "#/definitions/imageInstruction" }
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
]
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
// spec/profiles/quantified.schema.json
|
|
2341
|
+
var quantified_schema_default = {
|
|
2342
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2343
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/quantified",
|
|
2344
|
+
title: "Soustack Quantified Profile Schema",
|
|
2345
|
+
description: "Extends the base schema to require quantified ingredient entries.",
|
|
2346
|
+
allOf: [
|
|
2347
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
2348
|
+
{
|
|
2349
|
+
properties: {
|
|
2350
|
+
ingredients: {
|
|
2351
|
+
type: "array",
|
|
2352
|
+
items: {
|
|
2353
|
+
anyOf: [
|
|
2354
|
+
{ $ref: "#/definitions/quantifiedIngredient" },
|
|
2355
|
+
{ $ref: "#/definitions/quantifiedIngredientSubsection" }
|
|
2356
|
+
]
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
],
|
|
2362
|
+
definitions: {
|
|
2363
|
+
quantifiedIngredient: {
|
|
2364
|
+
allOf: [
|
|
2365
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredient" },
|
|
2366
|
+
{ required: ["item", "quantity"] }
|
|
2367
|
+
]
|
|
2368
|
+
},
|
|
2369
|
+
quantifiedIngredientSubsection: {
|
|
2370
|
+
allOf: [
|
|
2371
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredientSubsection" },
|
|
2372
|
+
{
|
|
2373
|
+
properties: {
|
|
2374
|
+
items: {
|
|
2375
|
+
type: "array",
|
|
2376
|
+
items: { $ref: "#/definitions/quantifiedIngredient" }
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
]
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
|
|
2385
|
+
// spec/profiles/scalable.schema.json
|
|
2386
|
+
var scalable_schema_default = {
|
|
2387
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2388
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/scalable",
|
|
2389
|
+
title: "Soustack Scalable Profile Schema",
|
|
2390
|
+
description: "Extends the base schema to guarantee quantified ingredients plus a structured yield for deterministic scaling.",
|
|
2391
|
+
allOf: [
|
|
2392
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
2393
|
+
{
|
|
2394
|
+
required: ["yield", "ingredients"],
|
|
2395
|
+
properties: {
|
|
2396
|
+
yield: {
|
|
2397
|
+
allOf: [
|
|
2398
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/yield" },
|
|
2399
|
+
{ properties: { amount: { type: "number", exclusiveMinimum: 0 } } }
|
|
2400
|
+
]
|
|
2401
|
+
},
|
|
2402
|
+
ingredients: {
|
|
2403
|
+
type: "array",
|
|
2404
|
+
minItems: 1,
|
|
2405
|
+
items: {
|
|
2406
|
+
anyOf: [
|
|
2407
|
+
{ $ref: "#/definitions/scalableIngredient" },
|
|
2408
|
+
{ $ref: "#/definitions/scalableIngredientSubsection" }
|
|
2409
|
+
]
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
],
|
|
2415
|
+
definitions: {
|
|
2416
|
+
scalableIngredient: {
|
|
2417
|
+
allOf: [
|
|
2418
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredient" },
|
|
2419
|
+
{ required: ["item", "quantity"] },
|
|
2420
|
+
{
|
|
2421
|
+
properties: {
|
|
2422
|
+
quantity: {
|
|
2423
|
+
allOf: [
|
|
2424
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/quantity" },
|
|
2425
|
+
{ properties: { amount: { type: "number", exclusiveMinimum: 0 } } }
|
|
2426
|
+
]
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
if: {
|
|
2432
|
+
properties: {
|
|
2433
|
+
scaling: {
|
|
2434
|
+
type: "object",
|
|
2435
|
+
properties: { type: { const: "bakers_percentage" } },
|
|
2436
|
+
required: ["type"]
|
|
2437
|
+
}
|
|
2438
|
+
},
|
|
2439
|
+
required: ["scaling"]
|
|
2440
|
+
},
|
|
2441
|
+
then: { required: ["id"] }
|
|
2442
|
+
}
|
|
2443
|
+
]
|
|
2444
|
+
},
|
|
2445
|
+
scalableIngredientSubsection: {
|
|
2446
|
+
allOf: [
|
|
2447
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/ingredientSubsection" },
|
|
2448
|
+
{
|
|
2449
|
+
properties: {
|
|
2450
|
+
items: {
|
|
2451
|
+
type: "array",
|
|
2452
|
+
minItems: 1,
|
|
2453
|
+
items: { $ref: "#/definitions/scalableIngredient" }
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
]
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
|
|
2462
|
+
// spec/profiles/schedulable.schema.json
|
|
2463
|
+
var schedulable_schema_default = {
|
|
2464
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
2465
|
+
$id: "http://soustack.org/schema/v0.3.0/profiles/schedulable",
|
|
2466
|
+
title: "Soustack Schedulable Profile Schema",
|
|
2467
|
+
description: "Extends the base schema to ensure every instruction is fully scheduled.",
|
|
2468
|
+
allOf: [
|
|
2469
|
+
{ $ref: "http://soustack.org/schema/v0.3.0" },
|
|
2470
|
+
{
|
|
2471
|
+
properties: {
|
|
2472
|
+
instructions: {
|
|
2473
|
+
type: "array",
|
|
2474
|
+
items: {
|
|
2475
|
+
anyOf: [
|
|
2476
|
+
{ $ref: "#/definitions/schedulableInstruction" },
|
|
2477
|
+
{ $ref: "#/definitions/schedulableInstructionSubsection" }
|
|
2478
|
+
]
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
],
|
|
2484
|
+
definitions: {
|
|
2485
|
+
schedulableInstruction: {
|
|
2486
|
+
allOf: [
|
|
2487
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instruction" },
|
|
2488
|
+
{ required: ["id", "timing"] }
|
|
2489
|
+
]
|
|
2490
|
+
},
|
|
2491
|
+
schedulableInstructionSubsection: {
|
|
2492
|
+
allOf: [
|
|
2493
|
+
{ $ref: "http://soustack.org/schema/v0.3.0#/definitions/instructionSubsection" },
|
|
2494
|
+
{
|
|
2495
|
+
properties: {
|
|
2496
|
+
items: {
|
|
2497
|
+
type: "array",
|
|
2498
|
+
items: { $ref: "#/definitions/schedulableInstruction" }
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
]
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// src/schemas/recipe/stacks/attribution/1.schema.json
|
|
1579
2508
|
var schema_default = {
|
|
1580
2509
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1581
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
1582
|
-
title: "Soustack Recipe
|
|
1583
|
-
description: "Schema for the
|
|
2510
|
+
$id: "https://soustack.org/schemas/recipe/stacks/attribution/1.schema.json",
|
|
2511
|
+
title: "Soustack Recipe Stack: attribution v1",
|
|
2512
|
+
description: "Schema for the attribution stack. Ensures namespace data is present when the stack is enabled and vice versa.",
|
|
1584
2513
|
type: "object",
|
|
1585
2514
|
properties: {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2515
|
+
stacks: {
|
|
2516
|
+
type: "object",
|
|
2517
|
+
additionalProperties: {
|
|
2518
|
+
type: "integer",
|
|
2519
|
+
minimum: 1
|
|
2520
|
+
}
|
|
1590
2521
|
},
|
|
1591
|
-
|
|
2522
|
+
attribution: {
|
|
1592
2523
|
type: "object",
|
|
1593
2524
|
properties: {
|
|
1594
|
-
|
|
2525
|
+
url: { type: "string" },
|
|
2526
|
+
author: { type: "string" },
|
|
2527
|
+
datePublished: { type: "string" }
|
|
1595
2528
|
},
|
|
1596
2529
|
additionalProperties: false
|
|
1597
2530
|
}
|
|
@@ -1600,32 +2533,33 @@ var schema_default = {
|
|
|
1600
2533
|
{
|
|
1601
2534
|
if: {
|
|
1602
2535
|
properties: {
|
|
1603
|
-
|
|
1604
|
-
type: "
|
|
1605
|
-
|
|
2536
|
+
stacks: {
|
|
2537
|
+
type: "object",
|
|
2538
|
+
properties: {
|
|
2539
|
+
attribution: { const: 1 }
|
|
2540
|
+
},
|
|
2541
|
+
required: ["attribution"]
|
|
1606
2542
|
}
|
|
1607
2543
|
}
|
|
1608
2544
|
},
|
|
1609
2545
|
then: {
|
|
1610
|
-
required: ["
|
|
1611
|
-
properties: {
|
|
1612
|
-
profile: { const: "core" }
|
|
1613
|
-
}
|
|
2546
|
+
required: ["attribution"]
|
|
1614
2547
|
}
|
|
1615
2548
|
},
|
|
1616
2549
|
{
|
|
1617
2550
|
if: {
|
|
1618
|
-
required: ["
|
|
2551
|
+
required: ["attribution"]
|
|
1619
2552
|
},
|
|
1620
2553
|
then: {
|
|
1621
|
-
required: ["
|
|
2554
|
+
required: ["stacks"],
|
|
1622
2555
|
properties: {
|
|
1623
|
-
|
|
1624
|
-
type: "
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
2556
|
+
stacks: {
|
|
2557
|
+
type: "object",
|
|
2558
|
+
properties: {
|
|
2559
|
+
attribution: { const: 1 }
|
|
2560
|
+
},
|
|
2561
|
+
required: ["attribution"]
|
|
2562
|
+
}
|
|
1629
2563
|
}
|
|
1630
2564
|
}
|
|
1631
2565
|
}
|
|
@@ -1633,23 +2567,26 @@ var schema_default = {
|
|
|
1633
2567
|
additionalProperties: true
|
|
1634
2568
|
};
|
|
1635
2569
|
|
|
1636
|
-
// src/schemas/recipe/
|
|
2570
|
+
// src/schemas/recipe/stacks/media/1.schema.json
|
|
1637
2571
|
var schema_default2 = {
|
|
1638
2572
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1639
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
1640
|
-
title: "Soustack Recipe
|
|
1641
|
-
description: "Schema for the
|
|
2573
|
+
$id: "https://soustack.org/schemas/recipe/stacks/media/1.schema.json",
|
|
2574
|
+
title: "Soustack Recipe Stack: media v1",
|
|
2575
|
+
description: "Schema for the media stack. Guards media blocks based on stack activation and ensures declarations accompany payloads.",
|
|
1642
2576
|
type: "object",
|
|
1643
2577
|
properties: {
|
|
1644
|
-
|
|
1645
|
-
type: "
|
|
1646
|
-
|
|
2578
|
+
stacks: {
|
|
2579
|
+
type: "object",
|
|
2580
|
+
additionalProperties: {
|
|
2581
|
+
type: "integer",
|
|
2582
|
+
minimum: 1
|
|
2583
|
+
}
|
|
1647
2584
|
},
|
|
1648
|
-
|
|
2585
|
+
media: {
|
|
1649
2586
|
type: "object",
|
|
1650
2587
|
properties: {
|
|
1651
|
-
|
|
1652
|
-
|
|
2588
|
+
images: { type: "array", items: { type: "string" } },
|
|
2589
|
+
videos: { type: "array", items: { type: "string" } }
|
|
1653
2590
|
},
|
|
1654
2591
|
additionalProperties: false
|
|
1655
2592
|
}
|
|
@@ -1658,27 +2595,32 @@ var schema_default2 = {
|
|
|
1658
2595
|
{
|
|
1659
2596
|
if: {
|
|
1660
2597
|
properties: {
|
|
1661
|
-
|
|
1662
|
-
type: "
|
|
1663
|
-
|
|
2598
|
+
stacks: {
|
|
2599
|
+
type: "object",
|
|
2600
|
+
properties: {
|
|
2601
|
+
media: { const: 1 }
|
|
2602
|
+
},
|
|
2603
|
+
required: ["media"]
|
|
1664
2604
|
}
|
|
1665
2605
|
}
|
|
1666
2606
|
},
|
|
1667
2607
|
then: {
|
|
1668
|
-
required: ["
|
|
2608
|
+
required: ["media"]
|
|
1669
2609
|
}
|
|
1670
2610
|
},
|
|
1671
2611
|
{
|
|
1672
2612
|
if: {
|
|
1673
|
-
required: ["
|
|
2613
|
+
required: ["media"]
|
|
1674
2614
|
},
|
|
1675
2615
|
then: {
|
|
1676
|
-
required: ["
|
|
2616
|
+
required: ["stacks"],
|
|
1677
2617
|
properties: {
|
|
1678
|
-
|
|
1679
|
-
type: "
|
|
1680
|
-
|
|
1681
|
-
|
|
2618
|
+
stacks: {
|
|
2619
|
+
type: "object",
|
|
2620
|
+
properties: {
|
|
2621
|
+
media: { const: 1 }
|
|
2622
|
+
},
|
|
2623
|
+
required: ["media"]
|
|
1682
2624
|
}
|
|
1683
2625
|
}
|
|
1684
2626
|
}
|
|
@@ -1687,24 +2629,26 @@ var schema_default2 = {
|
|
|
1687
2629
|
additionalProperties: true
|
|
1688
2630
|
};
|
|
1689
2631
|
|
|
1690
|
-
// src/schemas/recipe/
|
|
2632
|
+
// src/schemas/recipe/stacks/nutrition/1.schema.json
|
|
1691
2633
|
var schema_default3 = {
|
|
1692
2634
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1693
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
1694
|
-
title: "Soustack Recipe
|
|
1695
|
-
description: "Schema for the
|
|
2635
|
+
$id: "https://soustack.org/schemas/recipe/stacks/nutrition/1.schema.json",
|
|
2636
|
+
title: "Soustack Recipe Stack: nutrition v1",
|
|
2637
|
+
description: "Schema for the nutrition stack. Keeps nutrition data aligned with stack declarations and vice versa.",
|
|
1696
2638
|
type: "object",
|
|
1697
2639
|
properties: {
|
|
1698
|
-
|
|
1699
|
-
type: "
|
|
1700
|
-
|
|
2640
|
+
stacks: {
|
|
2641
|
+
type: "object",
|
|
2642
|
+
additionalProperties: {
|
|
2643
|
+
type: "integer",
|
|
2644
|
+
minimum: 1
|
|
2645
|
+
}
|
|
1701
2646
|
},
|
|
1702
|
-
|
|
2647
|
+
nutrition: {
|
|
1703
2648
|
type: "object",
|
|
1704
2649
|
properties: {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
datePublished: { type: "string" }
|
|
2650
|
+
calories: { type: "number" },
|
|
2651
|
+
protein_g: { type: "number" }
|
|
1708
2652
|
},
|
|
1709
2653
|
additionalProperties: false
|
|
1710
2654
|
}
|
|
@@ -1713,27 +2657,32 @@ var schema_default3 = {
|
|
|
1713
2657
|
{
|
|
1714
2658
|
if: {
|
|
1715
2659
|
properties: {
|
|
1716
|
-
|
|
1717
|
-
type: "
|
|
1718
|
-
|
|
2660
|
+
stacks: {
|
|
2661
|
+
type: "object",
|
|
2662
|
+
properties: {
|
|
2663
|
+
nutrition: { const: 1 }
|
|
2664
|
+
},
|
|
2665
|
+
required: ["nutrition"]
|
|
1719
2666
|
}
|
|
1720
2667
|
}
|
|
1721
2668
|
},
|
|
1722
2669
|
then: {
|
|
1723
|
-
required: ["
|
|
2670
|
+
required: ["nutrition"]
|
|
1724
2671
|
}
|
|
1725
2672
|
},
|
|
1726
2673
|
{
|
|
1727
2674
|
if: {
|
|
1728
|
-
required: ["
|
|
2675
|
+
required: ["nutrition"]
|
|
1729
2676
|
},
|
|
1730
2677
|
then: {
|
|
1731
|
-
required: ["
|
|
2678
|
+
required: ["stacks"],
|
|
1732
2679
|
properties: {
|
|
1733
|
-
|
|
1734
|
-
type: "
|
|
1735
|
-
|
|
1736
|
-
|
|
2680
|
+
stacks: {
|
|
2681
|
+
type: "object",
|
|
2682
|
+
properties: {
|
|
2683
|
+
nutrition: { const: 1 }
|
|
2684
|
+
},
|
|
2685
|
+
required: ["nutrition"]
|
|
1737
2686
|
}
|
|
1738
2687
|
}
|
|
1739
2688
|
}
|
|
@@ -1742,24 +2691,26 @@ var schema_default3 = {
|
|
|
1742
2691
|
additionalProperties: true
|
|
1743
2692
|
};
|
|
1744
2693
|
|
|
1745
|
-
// src/schemas/recipe/
|
|
2694
|
+
// src/schemas/recipe/stacks/schedule/1.schema.json
|
|
1746
2695
|
var schema_default4 = {
|
|
1747
2696
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1748
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
1749
|
-
title: "Soustack Recipe
|
|
1750
|
-
description: "Schema for the
|
|
2697
|
+
$id: "https://soustack.org/schemas/recipe/stacks/schedule/1.schema.json",
|
|
2698
|
+
title: "Soustack Recipe Stack: schedule v1",
|
|
2699
|
+
description: "Schema for the schedule stack. Enforces bidirectional stack gating and restricts usage to the core profile.",
|
|
1751
2700
|
type: "object",
|
|
1752
2701
|
properties: {
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2702
|
+
profile: { type: "string" },
|
|
2703
|
+
stacks: {
|
|
2704
|
+
type: "object",
|
|
2705
|
+
additionalProperties: {
|
|
2706
|
+
type: "integer",
|
|
2707
|
+
minimum: 1
|
|
2708
|
+
}
|
|
1756
2709
|
},
|
|
1757
|
-
|
|
2710
|
+
schedule: {
|
|
1758
2711
|
type: "object",
|
|
1759
2712
|
properties: {
|
|
1760
|
-
|
|
1761
|
-
category: { type: "string" },
|
|
1762
|
-
cuisine: { type: "string" }
|
|
2713
|
+
tasks: { type: "array" }
|
|
1763
2714
|
},
|
|
1764
2715
|
additionalProperties: false
|
|
1765
2716
|
}
|
|
@@ -1768,28 +2719,37 @@ var schema_default4 = {
|
|
|
1768
2719
|
{
|
|
1769
2720
|
if: {
|
|
1770
2721
|
properties: {
|
|
1771
|
-
|
|
1772
|
-
type: "
|
|
1773
|
-
|
|
2722
|
+
stacks: {
|
|
2723
|
+
type: "object",
|
|
2724
|
+
properties: {
|
|
2725
|
+
schedule: { const: 1 }
|
|
2726
|
+
},
|
|
2727
|
+
required: ["schedule"]
|
|
1774
2728
|
}
|
|
1775
2729
|
}
|
|
1776
2730
|
},
|
|
1777
2731
|
then: {
|
|
1778
|
-
required: ["
|
|
2732
|
+
required: ["schedule", "profile"],
|
|
2733
|
+
properties: {
|
|
2734
|
+
profile: { const: "core" }
|
|
2735
|
+
}
|
|
1779
2736
|
}
|
|
1780
2737
|
},
|
|
1781
2738
|
{
|
|
1782
2739
|
if: {
|
|
1783
|
-
required: ["
|
|
2740
|
+
required: ["schedule"]
|
|
1784
2741
|
},
|
|
1785
2742
|
then: {
|
|
1786
|
-
required: ["
|
|
2743
|
+
required: ["stacks", "profile"],
|
|
1787
2744
|
properties: {
|
|
1788
|
-
|
|
1789
|
-
type: "
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2745
|
+
stacks: {
|
|
2746
|
+
type: "object",
|
|
2747
|
+
properties: {
|
|
2748
|
+
schedule: { const: 1 }
|
|
2749
|
+
},
|
|
2750
|
+
required: ["schedule"]
|
|
2751
|
+
},
|
|
2752
|
+
profile: { const: "core" }
|
|
1793
2753
|
}
|
|
1794
2754
|
}
|
|
1795
2755
|
}
|
|
@@ -1797,23 +2757,27 @@ var schema_default4 = {
|
|
|
1797
2757
|
additionalProperties: true
|
|
1798
2758
|
};
|
|
1799
2759
|
|
|
1800
|
-
// src/schemas/recipe/
|
|
2760
|
+
// src/schemas/recipe/stacks/taxonomy/1.schema.json
|
|
1801
2761
|
var schema_default5 = {
|
|
1802
2762
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1803
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
1804
|
-
title: "Soustack Recipe
|
|
1805
|
-
description: "Schema for the
|
|
2763
|
+
$id: "https://soustack.org/schemas/recipe/stacks/taxonomy/1.schema.json",
|
|
2764
|
+
title: "Soustack Recipe Stack: taxonomy v1",
|
|
2765
|
+
description: "Schema for the taxonomy stack. Enforces keyword and categorization data when enabled and ensures stack declaration accompanies the namespace block.",
|
|
1806
2766
|
type: "object",
|
|
1807
2767
|
properties: {
|
|
1808
|
-
|
|
1809
|
-
type: "
|
|
1810
|
-
|
|
2768
|
+
stacks: {
|
|
2769
|
+
type: "object",
|
|
2770
|
+
additionalProperties: {
|
|
2771
|
+
type: "integer",
|
|
2772
|
+
minimum: 1
|
|
2773
|
+
}
|
|
1811
2774
|
},
|
|
1812
|
-
|
|
2775
|
+
taxonomy: {
|
|
1813
2776
|
type: "object",
|
|
1814
2777
|
properties: {
|
|
1815
|
-
|
|
1816
|
-
|
|
2778
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
2779
|
+
category: { type: "string" },
|
|
2780
|
+
cuisine: { type: "string" }
|
|
1817
2781
|
},
|
|
1818
2782
|
additionalProperties: false
|
|
1819
2783
|
}
|
|
@@ -1822,27 +2786,32 @@ var schema_default5 = {
|
|
|
1822
2786
|
{
|
|
1823
2787
|
if: {
|
|
1824
2788
|
properties: {
|
|
1825
|
-
|
|
1826
|
-
type: "
|
|
1827
|
-
|
|
2789
|
+
stacks: {
|
|
2790
|
+
type: "object",
|
|
2791
|
+
properties: {
|
|
2792
|
+
taxonomy: { const: 1 }
|
|
2793
|
+
},
|
|
2794
|
+
required: ["taxonomy"]
|
|
1828
2795
|
}
|
|
1829
2796
|
}
|
|
1830
2797
|
},
|
|
1831
2798
|
then: {
|
|
1832
|
-
required: ["
|
|
2799
|
+
required: ["taxonomy"]
|
|
1833
2800
|
}
|
|
1834
2801
|
},
|
|
1835
2802
|
{
|
|
1836
2803
|
if: {
|
|
1837
|
-
required: ["
|
|
2804
|
+
required: ["taxonomy"]
|
|
1838
2805
|
},
|
|
1839
2806
|
then: {
|
|
1840
|
-
required: ["
|
|
2807
|
+
required: ["stacks"],
|
|
1841
2808
|
properties: {
|
|
1842
|
-
|
|
1843
|
-
type: "
|
|
1844
|
-
|
|
1845
|
-
|
|
2809
|
+
stacks: {
|
|
2810
|
+
type: "object",
|
|
2811
|
+
properties: {
|
|
2812
|
+
taxonomy: { const: 1 }
|
|
2813
|
+
},
|
|
2814
|
+
required: ["taxonomy"]
|
|
1846
2815
|
}
|
|
1847
2816
|
}
|
|
1848
2817
|
}
|
|
@@ -1851,17 +2820,20 @@ var schema_default5 = {
|
|
|
1851
2820
|
additionalProperties: true
|
|
1852
2821
|
};
|
|
1853
2822
|
|
|
1854
|
-
// src/schemas/recipe/
|
|
2823
|
+
// src/schemas/recipe/stacks/times/1.schema.json
|
|
1855
2824
|
var schema_default6 = {
|
|
1856
2825
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1857
|
-
$id: "https://soustack.org/schemas/recipe/
|
|
1858
|
-
title: "Soustack Recipe
|
|
1859
|
-
description: "Schema for the times
|
|
2826
|
+
$id: "https://soustack.org/schemas/recipe/stacks/times/1.schema.json",
|
|
2827
|
+
title: "Soustack Recipe Stack: times v1",
|
|
2828
|
+
description: "Schema for the times stack. Maintains alignment between stack declarations and timing payloads.",
|
|
1860
2829
|
type: "object",
|
|
1861
2830
|
properties: {
|
|
1862
|
-
|
|
1863
|
-
type: "
|
|
1864
|
-
|
|
2831
|
+
stacks: {
|
|
2832
|
+
type: "object",
|
|
2833
|
+
additionalProperties: {
|
|
2834
|
+
type: "integer",
|
|
2835
|
+
minimum: 1
|
|
2836
|
+
}
|
|
1865
2837
|
},
|
|
1866
2838
|
times: {
|
|
1867
2839
|
type: "object",
|
|
@@ -1877,9 +2849,12 @@ var schema_default6 = {
|
|
|
1877
2849
|
{
|
|
1878
2850
|
if: {
|
|
1879
2851
|
properties: {
|
|
1880
|
-
|
|
1881
|
-
type: "
|
|
1882
|
-
|
|
2852
|
+
stacks: {
|
|
2853
|
+
type: "object",
|
|
2854
|
+
properties: {
|
|
2855
|
+
times: { const: 1 }
|
|
2856
|
+
},
|
|
2857
|
+
required: ["times"]
|
|
1883
2858
|
}
|
|
1884
2859
|
}
|
|
1885
2860
|
},
|
|
@@ -1892,12 +2867,14 @@ var schema_default6 = {
|
|
|
1892
2867
|
required: ["times"]
|
|
1893
2868
|
},
|
|
1894
2869
|
then: {
|
|
1895
|
-
required: ["
|
|
2870
|
+
required: ["stacks"],
|
|
1896
2871
|
properties: {
|
|
1897
|
-
|
|
1898
|
-
type: "
|
|
1899
|
-
|
|
1900
|
-
|
|
2872
|
+
stacks: {
|
|
2873
|
+
type: "object",
|
|
2874
|
+
properties: {
|
|
2875
|
+
times: { const: 1 }
|
|
2876
|
+
},
|
|
2877
|
+
required: ["times"]
|
|
1901
2878
|
}
|
|
1902
2879
|
}
|
|
1903
2880
|
}
|
|
@@ -1907,69 +2884,71 @@ var schema_default6 = {
|
|
|
1907
2884
|
};
|
|
1908
2885
|
|
|
1909
2886
|
// src/validator.ts
|
|
1910
|
-
var
|
|
1911
|
-
var
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
-
};
|
|
1940
|
-
var profileSchemas = {
|
|
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
|
|
1951
|
-
};
|
|
2887
|
+
var LEGACY_ROOT_SCHEMA_ID = "http://soustack.org/schema/v0.3.0";
|
|
2888
|
+
var DEFAULT_ROOT_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
|
|
2889
|
+
var BASE_SCHEMA_ID = "http://soustack.org/schema/recipe/base.schema.json";
|
|
2890
|
+
var PROFILE_SCHEMA_PREFIX = "http://soustack.org/schema/recipe/profiles/";
|
|
1952
2891
|
var validationContexts = /* @__PURE__ */ new Map();
|
|
2892
|
+
function loadAllSchemas(ajv) {
|
|
2893
|
+
const schemas = [
|
|
2894
|
+
soustack_schema_default,
|
|
2895
|
+
base_schema_default,
|
|
2896
|
+
minimal_schema_default,
|
|
2897
|
+
core_schema_default,
|
|
2898
|
+
base_schema_default2,
|
|
2899
|
+
cookable_schema_default,
|
|
2900
|
+
illustrated_schema_default,
|
|
2901
|
+
quantified_schema_default,
|
|
2902
|
+
scalable_schema_default,
|
|
2903
|
+
schedulable_schema_default,
|
|
2904
|
+
schema_default,
|
|
2905
|
+
schema_default2,
|
|
2906
|
+
schema_default3,
|
|
2907
|
+
schema_default4,
|
|
2908
|
+
schema_default5,
|
|
2909
|
+
schema_default6
|
|
2910
|
+
];
|
|
2911
|
+
for (const schema of schemas) {
|
|
2912
|
+
if (schema && typeof schema === "object" && "$id" in schema) {
|
|
2913
|
+
const schemaWithId = schema;
|
|
2914
|
+
if (schemaWithId.$id) {
|
|
2915
|
+
ajv.addSchema(schemaWithId, schemaWithId.$id);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
ajv.addSchema(
|
|
2920
|
+
{
|
|
2921
|
+
$id: DEFAULT_ROOT_SCHEMA_ID,
|
|
2922
|
+
allOf: [
|
|
2923
|
+
{ $ref: LEGACY_ROOT_SCHEMA_ID },
|
|
2924
|
+
{
|
|
2925
|
+
type: "object",
|
|
2926
|
+
properties: {
|
|
2927
|
+
$schema: { const: DEFAULT_ROOT_SCHEMA_ID }
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
]
|
|
2931
|
+
},
|
|
2932
|
+
DEFAULT_ROOT_SCHEMA_ID
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
1953
2935
|
function createContext(collectAllErrors) {
|
|
1954
|
-
const ajv = new
|
|
2936
|
+
const ajv = new Ajv2020__default.default({
|
|
2937
|
+
strict: false,
|
|
2938
|
+
allErrors: collectAllErrors,
|
|
2939
|
+
validateSchema: false
|
|
2940
|
+
// Don't validate schemas themselves
|
|
2941
|
+
});
|
|
1955
2942
|
addFormats__default.default(ajv);
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2943
|
+
loadAllSchemas(ajv);
|
|
2944
|
+
const rootValidator = ajv.getSchema(DEFAULT_ROOT_SCHEMA_ID) || ajv.getSchema(LEGACY_ROOT_SCHEMA_ID);
|
|
2945
|
+
const baseValidator = ajv.getSchema(BASE_SCHEMA_ID);
|
|
2946
|
+
return {
|
|
2947
|
+
ajv,
|
|
2948
|
+
rootValidator: rootValidator || void 0,
|
|
2949
|
+
baseValidator: baseValidator || void 0,
|
|
2950
|
+
validators: /* @__PURE__ */ new Map()
|
|
1964
2951
|
};
|
|
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() };
|
|
1973
2952
|
}
|
|
1974
2953
|
function getContext(collectAllErrors) {
|
|
1975
2954
|
if (!validationContexts.has(collectAllErrors)) {
|
|
@@ -1983,275 +2962,239 @@ function cloneRecipe(recipe) {
|
|
|
1983
2962
|
}
|
|
1984
2963
|
return JSON.parse(JSON.stringify(recipe));
|
|
1985
2964
|
}
|
|
1986
|
-
function
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
if (profile in profileSchemas) return profile;
|
|
2965
|
+
function formatAjvError(error) {
|
|
2966
|
+
let path2 = error.instancePath || "/";
|
|
2967
|
+
if (error.keyword === "additionalProperties" && error.params?.additionalProperty) {
|
|
2968
|
+
const extra = error.params.additionalProperty;
|
|
2969
|
+
path2 = `${error.instancePath || ""}/${extra}`.replace(/\/+/g, "/") || "/";
|
|
1992
2970
|
}
|
|
1993
|
-
return
|
|
2971
|
+
return {
|
|
2972
|
+
path: path2,
|
|
2973
|
+
keyword: error.keyword,
|
|
2974
|
+
message: error.message || "Validation error"
|
|
2975
|
+
};
|
|
1994
2976
|
}
|
|
1995
|
-
function
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
nutrition: "nutrition@1",
|
|
2008
|
-
schedule: "schedule@1"
|
|
2977
|
+
function isSoustackSchemaId(schemaId) {
|
|
2978
|
+
return schemaId.startsWith("http://soustack.org/schema") || schemaId.startsWith("https://soustack.org/schema") || schemaId.startsWith("https://soustack.spec/") || schemaId.startsWith("https://soustack.org/schemas/");
|
|
2979
|
+
}
|
|
2980
|
+
function inferStacksFromPayload(recipe) {
|
|
2981
|
+
const inferred = {};
|
|
2982
|
+
const payloadToStack = {
|
|
2983
|
+
attribution: "attribution",
|
|
2984
|
+
taxonomy: "taxonomy",
|
|
2985
|
+
media: "media",
|
|
2986
|
+
times: "times",
|
|
2987
|
+
nutrition: "nutrition",
|
|
2988
|
+
schedule: "schedule"
|
|
2009
2989
|
};
|
|
2010
|
-
for (const [field,
|
|
2011
|
-
if (recipe && typeof recipe === "object" && field in recipe && recipe[field]
|
|
2012
|
-
|
|
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
|
-
}
|
|
2990
|
+
for (const [field, stackName] of Object.entries(payloadToStack)) {
|
|
2991
|
+
if (recipe && typeof recipe === "object" && field in recipe && recipe[field] !== void 0) {
|
|
2992
|
+
inferred[stackName] = 1;
|
|
2022
2993
|
}
|
|
2023
2994
|
}
|
|
2024
2995
|
return inferred;
|
|
2025
2996
|
}
|
|
2026
|
-
function
|
|
2027
|
-
const
|
|
2028
|
-
const
|
|
2029
|
-
const sortedModules = Array.from(allModules).sort();
|
|
2030
|
-
const cacheKey = `${profile}::${sortedModules.join(",")}`;
|
|
2997
|
+
function getComposedValidator(profile, stacks, context) {
|
|
2998
|
+
const stackIdentifiers = Object.entries(stacks).map(([name, version]) => `${name}@${version}`).sort();
|
|
2999
|
+
const cacheKey = `${profile}::${stackIdentifiers.join(",")}`;
|
|
2031
3000
|
const cached = context.validators.get(cacheKey);
|
|
2032
3001
|
if (cached) return cached;
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
3002
|
+
const allOf = [{ $ref: BASE_SCHEMA_ID }];
|
|
3003
|
+
if (!context.ajv.getSchema(BASE_SCHEMA_ID)) {
|
|
3004
|
+
throw new Error(`Base schema not loaded: ${BASE_SCHEMA_ID}. Ensure schemas are loaded before creating validators.`);
|
|
3005
|
+
}
|
|
3006
|
+
const profileSchemaId = `${PROFILE_SCHEMA_PREFIX}${profile}.schema.json`;
|
|
3007
|
+
if (!context.ajv.getSchema(profileSchemaId)) {
|
|
3008
|
+
throw new Error(`Profile schema not loaded: ${profileSchemaId}`);
|
|
3009
|
+
}
|
|
3010
|
+
allOf.push({ $ref: profileSchemaId });
|
|
3011
|
+
for (const [name, version] of Object.entries(stacks)) {
|
|
3012
|
+
if (typeof version === "number" && version >= 1) {
|
|
3013
|
+
const stackSchemaId = `https://soustack.org/schemas/recipe/stacks/${name}/${version}.schema.json`;
|
|
3014
|
+
if (!context.ajv.getSchema(stackSchemaId)) {
|
|
3015
|
+
throw new Error(`Stack schema not loaded: ${stackSchemaId}`);
|
|
3016
|
+
}
|
|
3017
|
+
allOf.push({ $ref: stackSchemaId });
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const composedSchema = {
|
|
3021
|
+
$id: `urn:soustack:composed:${cacheKey}`,
|
|
3022
|
+
allOf
|
|
2043
3023
|
};
|
|
2044
|
-
const validateFn = context.ajv.compile(
|
|
3024
|
+
const validateFn = context.ajv.compile(composedSchema);
|
|
2045
3025
|
context.validators.set(cacheKey, validateFn);
|
|
2046
3026
|
return validateFn;
|
|
2047
3027
|
}
|
|
2048
|
-
function
|
|
2049
|
-
const normalized = cloneRecipe(
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
];
|
|
2067
|
-
structuredKeys.forEach((key) => {
|
|
2068
|
-
const value = time[key];
|
|
2069
|
-
if (typeof value === "number") return;
|
|
2070
|
-
const parsed = parseDuration(value);
|
|
2071
|
-
if (parsed !== null) {
|
|
2072
|
-
time[key] = parsed;
|
|
3028
|
+
function validateRecipeSchemaNormalized(normalizedInput, inputHasStacks, collectAllErrors, schemaOverride) {
|
|
3029
|
+
const normalized = cloneRecipe(normalizedInput);
|
|
3030
|
+
const context = getContext(collectAllErrors);
|
|
3031
|
+
const schemaId = typeof schemaOverride === "string" ? schemaOverride : typeof normalized.$schema === "string" ? normalized.$schema : void 0;
|
|
3032
|
+
const hasSchemaOverride = typeof schemaOverride === "string";
|
|
3033
|
+
const isSoustackSchema = schemaId ? isSoustackSchemaId(schemaId) : false;
|
|
3034
|
+
if (schemaId && isSoustackSchema) {
|
|
3035
|
+
const schemaValidator = context.ajv.getSchema(schemaId);
|
|
3036
|
+
if (!schemaValidator) {
|
|
3037
|
+
return {
|
|
3038
|
+
ok: false,
|
|
3039
|
+
errors: [
|
|
3040
|
+
{
|
|
3041
|
+
path: "/$schema",
|
|
3042
|
+
message: `Unknown schema: ${schemaId}`
|
|
3043
|
+
}
|
|
3044
|
+
]
|
|
3045
|
+
};
|
|
2073
3046
|
}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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"
|
|
2104
|
-
]);
|
|
2105
|
-
function detectUnknownTopLevelKeys(recipe) {
|
|
2106
|
-
if (!recipe || typeof recipe !== "object") return [];
|
|
2107
|
-
const disallowedKeys = Object.keys(recipe).filter(
|
|
2108
|
-
(key) => !allowedTopLevelProps.has(key) && !key.startsWith("x-")
|
|
2109
|
-
);
|
|
2110
|
-
return disallowedKeys.map((key) => ({
|
|
2111
|
-
path: `/${key}`,
|
|
2112
|
-
keyword: "additionalProperties",
|
|
2113
|
-
message: `Unknown top-level property '${key}' is not allowed by the Soustack spec`
|
|
2114
|
-
}));
|
|
2115
|
-
}
|
|
2116
|
-
function formatAjvError(error) {
|
|
2117
|
-
let path2 = error.instancePath || "/";
|
|
2118
|
-
if (error.keyword === "additionalProperties" && error.params?.additionalProperty) {
|
|
2119
|
-
const extra = error.params.additionalProperty;
|
|
2120
|
-
path2 = `${error.instancePath || ""}/${extra}`.replace(/\/+/g, "/") || "/";
|
|
3047
|
+
const schemaInput = cloneRecipe(normalized);
|
|
3048
|
+
if (hasSchemaOverride && "$schema" in schemaInput && schemaInput.$schema !== schemaId) {
|
|
3049
|
+
delete schemaInput.$schema;
|
|
3050
|
+
}
|
|
3051
|
+
const isLegacySchema = schemaId.startsWith(LEGACY_ROOT_SCHEMA_ID);
|
|
3052
|
+
const shouldRemoveStacks = (isLegacySchema || schemaId === DEFAULT_ROOT_SCHEMA_ID) && !inputHasStacks;
|
|
3053
|
+
if (isLegacySchema && "@type" in schemaInput) {
|
|
3054
|
+
delete schemaInput["@type"];
|
|
3055
|
+
}
|
|
3056
|
+
if (shouldRemoveStacks && "stacks" in schemaInput) {
|
|
3057
|
+
delete schemaInput.stacks;
|
|
3058
|
+
}
|
|
3059
|
+
const schemaValid = schemaValidator(schemaInput);
|
|
3060
|
+
const schemaErrors = schemaValidator.errors || [];
|
|
3061
|
+
return {
|
|
3062
|
+
ok: !!schemaValid,
|
|
3063
|
+
errors: schemaErrors.map(formatAjvError)
|
|
3064
|
+
};
|
|
2121
3065
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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"
|
|
3066
|
+
const hasProfile = normalized.profile && typeof normalized.profile === "string";
|
|
3067
|
+
let declaredStacks = {};
|
|
3068
|
+
if (normalized.stacks && typeof normalized.stacks === "object" && !Array.isArray(normalized.stacks)) {
|
|
3069
|
+
for (const [name, version] of Object.entries(normalized.stacks)) {
|
|
3070
|
+
if (typeof version === "number" && version >= 1) {
|
|
3071
|
+
declaredStacks[name] = version;
|
|
2138
3072
|
}
|
|
2139
|
-
|
|
3073
|
+
}
|
|
2140
3074
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
3075
|
+
const inferredStacks = inferStacksFromPayload(normalized);
|
|
3076
|
+
const allStacks = { ...declaredStacks };
|
|
3077
|
+
for (const [name, version] of Object.entries(inferredStacks)) {
|
|
3078
|
+
if (!allStacks[name] || allStacks[name] < version) {
|
|
3079
|
+
allStacks[name] = version;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
let isValid;
|
|
3083
|
+
let errors = [];
|
|
3084
|
+
const profile = hasProfile ? normalized.profile.toLowerCase() : "core";
|
|
3085
|
+
const profileSchemaId = `${PROFILE_SCHEMA_PREFIX}${profile}.schema.json`;
|
|
3086
|
+
if (!context.ajv.getSchema(profileSchemaId)) {
|
|
3087
|
+
return {
|
|
3088
|
+
ok: false,
|
|
3089
|
+
errors: [
|
|
3090
|
+
{
|
|
3091
|
+
path: "/profile",
|
|
3092
|
+
message: `Profile schema not loaded: ${profileSchemaId}`
|
|
3093
|
+
}
|
|
3094
|
+
]
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
{
|
|
3098
|
+
const validationCopy = cloneRecipe(normalized);
|
|
3099
|
+
if (!validationCopy.stacks || typeof validationCopy.stacks !== "object" || Array.isArray(validationCopy.stacks)) {
|
|
3100
|
+
validationCopy.stacks = declaredStacks;
|
|
3101
|
+
}
|
|
3102
|
+
if (!validationCopy.profile) {
|
|
3103
|
+
validationCopy.profile = profile;
|
|
3104
|
+
}
|
|
3105
|
+
const validator = getComposedValidator(profile, allStacks, context);
|
|
3106
|
+
isValid = validator(validationCopy);
|
|
3107
|
+
errors = validator.errors || [];
|
|
3108
|
+
if (isValid && context.rootValidator) {
|
|
3109
|
+
const rootCheckCopy = cloneRecipe(normalized);
|
|
3110
|
+
if ("@type" in rootCheckCopy) {
|
|
3111
|
+
delete rootCheckCopy["@type"];
|
|
2159
3112
|
}
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
});
|
|
3113
|
+
if ("stacks" in rootCheckCopy) {
|
|
3114
|
+
delete rootCheckCopy.stacks;
|
|
3115
|
+
}
|
|
3116
|
+
if ("profile" in rootCheckCopy) {
|
|
3117
|
+
delete rootCheckCopy.profile;
|
|
3118
|
+
}
|
|
3119
|
+
const stackPayloadFields = ["attribution", "taxonomy", "media", "times", "nutrition", "schedule"];
|
|
3120
|
+
for (const field of stackPayloadFields) {
|
|
3121
|
+
if (field in rootCheckCopy) {
|
|
3122
|
+
delete rootCheckCopy[field];
|
|
2173
3123
|
}
|
|
2174
3124
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
dependencyRefs.forEach((ref) => {
|
|
2189
|
-
if (ref.fromId && instructionIds.has(ref.fromId) && instructionIds.has(ref.toId)) {
|
|
2190
|
-
const list = adjacency.get(ref.fromId) ?? [];
|
|
2191
|
-
list.push({ toId: ref.toId, path: ref.path });
|
|
2192
|
-
adjacency.set(ref.fromId, list);
|
|
2193
|
-
}
|
|
2194
|
-
});
|
|
2195
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
2196
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2197
|
-
const detectCycles = (nodeId) => {
|
|
2198
|
-
if (visiting.has(nodeId)) return;
|
|
2199
|
-
if (visited.has(nodeId)) return;
|
|
2200
|
-
visiting.add(nodeId);
|
|
2201
|
-
const neighbors = adjacency.get(nodeId) ?? [];
|
|
2202
|
-
neighbors.forEach((edge) => {
|
|
2203
|
-
if (visiting.has(edge.toId)) {
|
|
2204
|
-
errors.push({
|
|
2205
|
-
path: edge.path,
|
|
2206
|
-
message: `Circular dependency detected involving instruction id '${edge.toId}'.`
|
|
2207
|
-
});
|
|
2208
|
-
return;
|
|
3125
|
+
const rootValid = context.rootValidator(rootCheckCopy);
|
|
3126
|
+
if (!rootValid && context.rootValidator.errors) {
|
|
3127
|
+
const unknownKeyErrors = context.rootValidator.errors.filter(
|
|
3128
|
+
(e) => e.keyword === "additionalProperties" && (e.instancePath === "" || e.instancePath === "/")
|
|
3129
|
+
);
|
|
3130
|
+
const schemaConstErrors = context.rootValidator.errors.filter(
|
|
3131
|
+
(e) => e.keyword === "const" && e.instancePath === "/$schema"
|
|
3132
|
+
);
|
|
3133
|
+
const relevantErrors = [...unknownKeyErrors, ...schemaConstErrors];
|
|
3134
|
+
if (relevantErrors.length > 0) {
|
|
3135
|
+
errors.push(...relevantErrors);
|
|
3136
|
+
isValid = false;
|
|
3137
|
+
}
|
|
2209
3138
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return {
|
|
3142
|
+
ok: isValid,
|
|
3143
|
+
errors: errors.map(formatAjvError)
|
|
2214
3144
|
};
|
|
2215
|
-
instructionIds.forEach((id) => detectCycles(id));
|
|
2216
|
-
return errors;
|
|
2217
3145
|
}
|
|
2218
3146
|
function validateRecipe(input, options = {}) {
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
3147
|
+
const { recipe: normalized, warnings } = normalizeRecipe(input);
|
|
3148
|
+
if (options.profile) {
|
|
3149
|
+
normalized.profile = options.profile;
|
|
3150
|
+
}
|
|
3151
|
+
const inputHasStacks = !!input && typeof input === "object" && !Array.isArray(input) && "stacks" in input;
|
|
3152
|
+
const { ok: schemaOk, errors: schemaErrors } = validateRecipeSchemaNormalized(
|
|
3153
|
+
normalized,
|
|
3154
|
+
inputHasStacks,
|
|
3155
|
+
options.collectAllErrors ?? true,
|
|
3156
|
+
options.schema
|
|
3157
|
+
);
|
|
3158
|
+
const mode = options.mode ?? "full";
|
|
3159
|
+
let conformanceIssues = [];
|
|
3160
|
+
let conformanceOk = true;
|
|
3161
|
+
if (mode === "full") {
|
|
3162
|
+
if (schemaOk) {
|
|
3163
|
+
const conformanceResult = validateConformance(normalized);
|
|
3164
|
+
conformanceIssues = conformanceResult.issues;
|
|
3165
|
+
conformanceOk = conformanceResult.ok;
|
|
3166
|
+
} else {
|
|
3167
|
+
conformanceOk = false;
|
|
3168
|
+
}
|
|
2236
3169
|
}
|
|
2237
|
-
const
|
|
2238
|
-
const
|
|
2239
|
-
const graphErrors = modules.includes("schedule@1") && validationErrors.length === 0 ? checkInstructionGraph(normalized) : [];
|
|
2240
|
-
const errors = [...unknownKeyErrors, ...validationErrors, ...graphErrors];
|
|
3170
|
+
const ok = schemaOk && (mode === "schema" ? true : conformanceOk);
|
|
3171
|
+
const normalizedRecipe = ok || options.includeNormalized ? normalized : void 0;
|
|
2241
3172
|
return {
|
|
2242
|
-
|
|
2243
|
-
|
|
3173
|
+
ok,
|
|
3174
|
+
schemaErrors,
|
|
3175
|
+
conformanceIssues,
|
|
2244
3176
|
warnings,
|
|
2245
|
-
|
|
3177
|
+
normalizedRecipe
|
|
2246
3178
|
};
|
|
2247
3179
|
}
|
|
2248
3180
|
|
|
2249
3181
|
// bin/cli.ts
|
|
2250
|
-
var supportedProfiles = [
|
|
3182
|
+
var supportedProfiles = [
|
|
3183
|
+
"base",
|
|
3184
|
+
"equipped",
|
|
3185
|
+
"illustrated",
|
|
3186
|
+
"lite",
|
|
3187
|
+
"prepped",
|
|
3188
|
+
"scalable",
|
|
3189
|
+
"timed"
|
|
3190
|
+
];
|
|
2251
3191
|
async function runCli(argv) {
|
|
2252
3192
|
const [command, ...args] = argv;
|
|
2253
3193
|
try {
|
|
2254
3194
|
switch (command) {
|
|
3195
|
+
case "check":
|
|
3196
|
+
await handleCheck(args);
|
|
3197
|
+
return;
|
|
2255
3198
|
case "validate":
|
|
2256
3199
|
await handleValidate(args);
|
|
2257
3200
|
return;
|
|
@@ -2281,23 +3224,50 @@ async function runCli(argv) {
|
|
|
2281
3224
|
}
|
|
2282
3225
|
function printUsage() {
|
|
2283
3226
|
console.log("Usage:");
|
|
2284
|
-
console.log(" soustack
|
|
3227
|
+
console.log(" soustack check <file> --json");
|
|
3228
|
+
console.log(
|
|
3229
|
+
" soustack validate <fileOrGlob> [--profile <name>] [--force-profile] [--schema-only] [--strict] [--json]"
|
|
3230
|
+
);
|
|
2285
3231
|
console.log(" soustack convert --from <schemaorg|soustack> --to <schemaorg|soustack> <input> [-o <output>]");
|
|
2286
3232
|
console.log(" soustack import --url <url> [-o <soustack.json>]");
|
|
2287
|
-
console.log(" soustack test [--profile <name>] [--strict] [--json]");
|
|
3233
|
+
console.log(" soustack test [--profile <name>] [--force-profile] [--schema-only] [--strict] [--json]");
|
|
2288
3234
|
console.log(" soustack scale <soustack.json> <multiplier>");
|
|
2289
3235
|
console.log(" soustack scrape <url> -o <soustack.json>");
|
|
3236
|
+
console.log(`
|
|
3237
|
+
Profiles: ${supportedProfiles.join(", ")}`);
|
|
3238
|
+
}
|
|
3239
|
+
async function handleCheck(args) {
|
|
3240
|
+
const { target, json } = parseCheckArgs(args);
|
|
3241
|
+
if (!target) throw new Error("Path to Soustack recipe JSON is required");
|
|
3242
|
+
if (!json) throw new Error("Check usage: check <file> --json");
|
|
3243
|
+
try {
|
|
3244
|
+
const input = readJsonFile(target);
|
|
3245
|
+
const result = validateRecipe(input, { mode: "full", includeNormalized: true });
|
|
3246
|
+
const report = buildConformanceReport(result);
|
|
3247
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3248
|
+
if (!report.ok) process.exitCode = 1;
|
|
3249
|
+
} catch (error) {
|
|
3250
|
+
const report = buildConformanceReport({
|
|
3251
|
+
ok: false,
|
|
3252
|
+
warnings: [],
|
|
3253
|
+
schemaErrors: [{ path: "/", message: error?.message || "Validation failed" }],
|
|
3254
|
+
conformanceIssues: [],
|
|
3255
|
+
normalizedRecipe: void 0
|
|
3256
|
+
});
|
|
3257
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3258
|
+
process.exitCode = 1;
|
|
3259
|
+
}
|
|
2290
3260
|
}
|
|
2291
3261
|
async function handleValidate(args) {
|
|
2292
|
-
const { target, profile, strict, json } = parseValidateArgs(args);
|
|
3262
|
+
const { target, profile, forceProfile, strict, json, mode } = parseValidateArgs(args);
|
|
2293
3263
|
if (!target) throw new Error("Path or glob to Soustack recipe JSON is required");
|
|
2294
3264
|
const files = expandTargets(target);
|
|
2295
3265
|
if (files.length === 0) throw new Error(`No files matched pattern: ${target}`);
|
|
2296
|
-
const results = files.map((file) => validateFile(file, profile));
|
|
2297
|
-
reportValidation(results, { strict, json
|
|
3266
|
+
const results = files.map((file) => validateFile(file, profile, mode, forceProfile));
|
|
3267
|
+
reportValidation(results, { strict, json});
|
|
2298
3268
|
}
|
|
2299
3269
|
async function handleTest(args) {
|
|
2300
|
-
const { profile, strict, json } = parseValidationFlags(args);
|
|
3270
|
+
const { profile, forceProfile, strict, json, mode } = parseValidationFlags(args);
|
|
2301
3271
|
const cwd = process.cwd();
|
|
2302
3272
|
const files = glob.globSync("**/*.soustack.json", {
|
|
2303
3273
|
cwd,
|
|
@@ -2309,7 +3279,7 @@ async function handleTest(args) {
|
|
|
2309
3279
|
console.log("No *.soustack.json files found in the current repository.");
|
|
2310
3280
|
return;
|
|
2311
3281
|
}
|
|
2312
|
-
const results = files.map((file) => validateFile(file, profile));
|
|
3282
|
+
const results = files.map((file) => validateFile(file, profile, mode, forceProfile));
|
|
2313
3283
|
reportValidation(results, { strict, json, context: "test" });
|
|
2314
3284
|
}
|
|
2315
3285
|
async function handleConvert(args) {
|
|
@@ -2361,8 +3331,10 @@ async function handleScrape(args) {
|
|
|
2361
3331
|
}
|
|
2362
3332
|
function parseValidateArgs(args) {
|
|
2363
3333
|
let profile;
|
|
3334
|
+
let forceProfile = false;
|
|
2364
3335
|
let strict = false;
|
|
2365
3336
|
let json = false;
|
|
3337
|
+
let mode = "full";
|
|
2366
3338
|
let target;
|
|
2367
3339
|
for (let i = 0; i < args.length; i++) {
|
|
2368
3340
|
const arg = args[i];
|
|
@@ -2371,6 +3343,12 @@ function parseValidateArgs(args) {
|
|
|
2371
3343
|
profile = normalizeProfile(args[i + 1]);
|
|
2372
3344
|
i++;
|
|
2373
3345
|
break;
|
|
3346
|
+
case "--force-profile":
|
|
3347
|
+
forceProfile = true;
|
|
3348
|
+
break;
|
|
3349
|
+
case "--schema-only":
|
|
3350
|
+
mode = "schema";
|
|
3351
|
+
break;
|
|
2374
3352
|
case "--strict":
|
|
2375
3353
|
strict = true;
|
|
2376
3354
|
break;
|
|
@@ -2384,11 +3362,26 @@ function parseValidateArgs(args) {
|
|
|
2384
3362
|
break;
|
|
2385
3363
|
}
|
|
2386
3364
|
}
|
|
2387
|
-
return { profile, strict, json, target };
|
|
3365
|
+
return { profile, forceProfile, strict, json, mode, target };
|
|
3366
|
+
}
|
|
3367
|
+
function parseCheckArgs(args) {
|
|
3368
|
+
let json = false;
|
|
3369
|
+
let target;
|
|
3370
|
+
for (let i = 0; i < args.length; i++) {
|
|
3371
|
+
const arg = args[i];
|
|
3372
|
+
if (arg === "--json") {
|
|
3373
|
+
json = true;
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
if (!arg.startsWith("--") && !target) {
|
|
3377
|
+
target = arg;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
return { target, json };
|
|
2388
3381
|
}
|
|
2389
3382
|
function parseValidationFlags(args) {
|
|
2390
|
-
const { profile, strict, json } = parseValidateArgs(args);
|
|
2391
|
-
return { profile, strict, json };
|
|
3383
|
+
const { profile, forceProfile, strict, json, mode } = parseValidateArgs(args);
|
|
3384
|
+
return { profile, forceProfile, strict, json, mode };
|
|
2392
3385
|
}
|
|
2393
3386
|
function normalizeProfile(value) {
|
|
2394
3387
|
if (!value) return void 0;
|
|
@@ -2396,7 +3389,7 @@ function normalizeProfile(value) {
|
|
|
2396
3389
|
if (supportedProfiles.includes(normalized)) {
|
|
2397
3390
|
return normalized;
|
|
2398
3391
|
}
|
|
2399
|
-
throw new Error(`Unknown Soustack profile: ${value}`);
|
|
3392
|
+
throw new Error(`Unknown Soustack profile: ${value}. Supported profiles: ${supportedProfiles.join(", ")}`);
|
|
2400
3393
|
}
|
|
2401
3394
|
function parseConvertArgs(args) {
|
|
2402
3395
|
let from;
|
|
@@ -2462,16 +3455,64 @@ function expandTargets(target) {
|
|
|
2462
3455
|
const unique = Array.from(new Set(matches.map((match) => path__namespace.resolve(match))));
|
|
2463
3456
|
return unique;
|
|
2464
3457
|
}
|
|
2465
|
-
function validateFile(file, profile) {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
3458
|
+
function validateFile(file, profile, mode = "full", forceProfile = false) {
|
|
3459
|
+
try {
|
|
3460
|
+
const recipe = readJsonFile(file);
|
|
3461
|
+
const { recipe: validationRecipe, mismatchError } = resolveProfileForValidation(recipe, profile, forceProfile);
|
|
3462
|
+
if (mismatchError) {
|
|
3463
|
+
return {
|
|
3464
|
+
file,
|
|
3465
|
+
profile,
|
|
3466
|
+
ok: false,
|
|
3467
|
+
warnings: [],
|
|
3468
|
+
schemaErrors: [mismatchError],
|
|
3469
|
+
conformanceIssues: []
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
const result = validateRecipe(validationRecipe, profile ? { profile, mode } : { mode });
|
|
3473
|
+
return {
|
|
3474
|
+
file,
|
|
3475
|
+
profile,
|
|
3476
|
+
ok: result.ok,
|
|
3477
|
+
warnings: result.warnings,
|
|
3478
|
+
schemaErrors: result.schemaErrors,
|
|
3479
|
+
conformanceIssues: result.conformanceIssues
|
|
3480
|
+
};
|
|
3481
|
+
} catch (error) {
|
|
3482
|
+
return {
|
|
3483
|
+
file,
|
|
3484
|
+
profile,
|
|
3485
|
+
ok: false,
|
|
3486
|
+
warnings: [],
|
|
3487
|
+
schemaErrors: [{ path: "/", message: error?.message || "Validation failed", keyword: "error" }],
|
|
3488
|
+
conformanceIssues: []
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
function resolveProfileForValidation(recipe, profile, forceProfile = false) {
|
|
3493
|
+
if (!profile) return { recipe };
|
|
3494
|
+
if (!recipe || typeof recipe !== "object" || Array.isArray(recipe)) {
|
|
3495
|
+
return { recipe };
|
|
3496
|
+
}
|
|
3497
|
+
const recipeProfileRaw = recipe.profile;
|
|
3498
|
+
const recipeProfile = typeof recipeProfileRaw === "string" ? recipeProfileRaw.toLowerCase() : void 0;
|
|
3499
|
+
if (!recipeProfile) {
|
|
3500
|
+
return { recipe: { ...recipe, profile } };
|
|
3501
|
+
}
|
|
3502
|
+
if (recipeProfile !== profile) {
|
|
3503
|
+
if (!forceProfile) {
|
|
3504
|
+
return {
|
|
3505
|
+
recipe,
|
|
3506
|
+
mismatchError: {
|
|
3507
|
+
path: "/profile",
|
|
3508
|
+
keyword: "profile",
|
|
3509
|
+
message: `Recipe profile "${recipeProfile}" does not match --profile "${profile}". Use --force-profile to override.`
|
|
3510
|
+
}
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
return { recipe: { ...recipe, profile } };
|
|
3514
|
+
}
|
|
3515
|
+
return { recipe };
|
|
2475
3516
|
}
|
|
2476
3517
|
function reportValidation(results, options) {
|
|
2477
3518
|
const summary = {
|
|
@@ -2487,9 +3528,10 @@ function reportValidation(results, options) {
|
|
|
2487
3528
|
return {
|
|
2488
3529
|
file: path__namespace.relative(process.cwd(), result.file),
|
|
2489
3530
|
profile: result.profile,
|
|
2490
|
-
|
|
3531
|
+
ok: result.ok,
|
|
2491
3532
|
warnings: result.warnings,
|
|
2492
|
-
|
|
3533
|
+
schemaErrors: result.schemaErrors,
|
|
3534
|
+
conformanceIssues: result.conformanceIssues,
|
|
2493
3535
|
passed
|
|
2494
3536
|
};
|
|
2495
3537
|
});
|
|
@@ -2499,14 +3541,22 @@ function reportValidation(results, options) {
|
|
|
2499
3541
|
serializable.forEach((entry) => {
|
|
2500
3542
|
const prefix = entry.passed ? "\u2705" : "\u274C";
|
|
2501
3543
|
console.log(`${prefix} ${entry.file}`);
|
|
2502
|
-
if (!entry.passed && entry.
|
|
2503
|
-
|
|
3544
|
+
if (!entry.passed && entry.schemaErrors.length) {
|
|
3545
|
+
console.log(" Schema errors:");
|
|
3546
|
+
entry.schemaErrors.forEach((error) => {
|
|
2504
3547
|
console.log(` \u2022 [${error.path}] ${error.message}`);
|
|
2505
3548
|
});
|
|
2506
3549
|
}
|
|
3550
|
+
if (!entry.passed && entry.conformanceIssues.length) {
|
|
3551
|
+
console.log(" Conformance issues:");
|
|
3552
|
+
entry.conformanceIssues.forEach((issue) => {
|
|
3553
|
+
console.log(` \u2022 [${issue.path}] ${issue.message} (${issue.code})`);
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
2507
3556
|
if (!entry.passed && options.strict && entry.warnings.length) {
|
|
3557
|
+
console.log(" Warnings:");
|
|
2508
3558
|
entry.warnings.forEach((warning) => {
|
|
2509
|
-
console.log(` \u2022
|
|
3559
|
+
console.log(` \u2022 ${warning} (warning)`);
|
|
2510
3560
|
});
|
|
2511
3561
|
}
|
|
2512
3562
|
});
|
|
@@ -2518,7 +3568,7 @@ function reportValidation(results, options) {
|
|
|
2518
3568
|
}
|
|
2519
3569
|
}
|
|
2520
3570
|
function isEffectivelyValid(result, strict) {
|
|
2521
|
-
return result.
|
|
3571
|
+
return result.ok && (!strict || result.warnings.length === 0);
|
|
2522
3572
|
}
|
|
2523
3573
|
function readJsonFile(relativePath) {
|
|
2524
3574
|
const absolutePath = path__namespace.resolve(relativePath);
|
|
@@ -2550,6 +3600,52 @@ function writeOutput(data, outputPath) {
|
|
|
2550
3600
|
const absolutePath = path__namespace.resolve(outputPath);
|
|
2551
3601
|
fs__namespace.writeFileSync(absolutePath, serialized, "utf-8");
|
|
2552
3602
|
}
|
|
3603
|
+
function buildConformanceReport(result) {
|
|
3604
|
+
const recipe = result.normalizedRecipe;
|
|
3605
|
+
const level = typeof recipe?.level === "string" ? recipe.level : null;
|
|
3606
|
+
const stacks = normalizeStacksForReport(recipe?.stacks);
|
|
3607
|
+
const schemaErrors = sortSchemaErrors(result.schemaErrors).map((error) => ({
|
|
3608
|
+
path: error.path,
|
|
3609
|
+
keyword: error.keyword ?? null,
|
|
3610
|
+
message: error.message
|
|
3611
|
+
}));
|
|
3612
|
+
const conformanceIssues = sortConformanceIssues(result.conformanceIssues).map((issue) => ({
|
|
3613
|
+
code: issue.code,
|
|
3614
|
+
path: issue.path,
|
|
3615
|
+
severity: issue.severity === "warning" ? "warn" : "error",
|
|
3616
|
+
message: issue.message
|
|
3617
|
+
}));
|
|
3618
|
+
return {
|
|
3619
|
+
ok: result.ok,
|
|
3620
|
+
level,
|
|
3621
|
+
stacks,
|
|
3622
|
+
warnings: result.warnings,
|
|
3623
|
+
schemaErrors,
|
|
3624
|
+
conformanceIssues
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function normalizeStacksForReport(stacks) {
|
|
3628
|
+
if (!stacks || typeof stacks !== "object" || Array.isArray(stacks)) return {};
|
|
3629
|
+
const entries = Object.entries(stacks).filter(([, value]) => typeof value === "number");
|
|
3630
|
+
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
3631
|
+
return Object.fromEntries(entries);
|
|
3632
|
+
}
|
|
3633
|
+
function sortSchemaErrors(errors) {
|
|
3634
|
+
return [...errors].sort((left, right) => {
|
|
3635
|
+
const pathCompare = left.path.localeCompare(right.path);
|
|
3636
|
+
if (pathCompare !== 0) return pathCompare;
|
|
3637
|
+
const leftKeyword = left.keyword ?? "";
|
|
3638
|
+
const rightKeyword = right.keyword ?? "";
|
|
3639
|
+
return leftKeyword.localeCompare(rightKeyword);
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
function sortConformanceIssues(issues) {
|
|
3643
|
+
return [...issues].sort((left, right) => {
|
|
3644
|
+
const pathCompare = left.path.localeCompare(right.path);
|
|
3645
|
+
if (pathCompare !== 0) return pathCompare;
|
|
3646
|
+
return left.code.localeCompare(right.code);
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
2553
3649
|
if (__require.main === module) {
|
|
2554
3650
|
runCli(process.argv.slice(2)).catch((error) => {
|
|
2555
3651
|
console.error(`\u274C ${error?.message ?? error}`);
|