soustack 0.4.0 → 0.4.1

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.
Files changed (146) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/index.js +4412 -1275
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/index.d.mts +106 -80
  5. package/dist/index.d.ts +106 -80
  6. package/dist/index.js +4527 -1360
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4527 -1360
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/scrape/index.d.mts +86 -74
  11. package/dist/scrape/index.d.ts +86 -74
  12. package/dist/scrape/index.js +91 -64
  13. package/dist/scrape/index.js.map +1 -1
  14. package/dist/scrape/index.mjs +91 -64
  15. package/dist/scrape/index.mjs.map +1 -1
  16. package/package.json +15 -6
  17. package/spec/.sync-meta.json +149 -0
  18. package/spec/SOUSTACK_SPEC_VERSION +1 -0
  19. package/spec/defs/common.schema.json +46 -0
  20. package/spec/defs/duration.schema.json +33 -0
  21. package/spec/defs/entities.schema.json +111 -0
  22. package/spec/defs/ingredientQuantified.schema.json +9 -0
  23. package/spec/defs/quantity.schema.json +16 -0
  24. package/spec/defs/scalingRule.schema.json +127 -0
  25. package/spec/defs/temperature.schema.json +63 -0
  26. package/spec/fixtures/content/illustrated-step.valid.json +24 -0
  27. package/spec/fixtures/invalid/equipment-unknown-reference.invalid.json +38 -0
  28. package/spec/fixtures/invalid/mise-en-place-unknown-equipment.invalid.json +37 -0
  29. package/spec/fixtures/invalid/mise-en-place-unknown-input.invalid.json +41 -0
  30. package/spec/fixtures/invalid/storage-leftovers-missing-method.invalid.json +31 -0
  31. package/spec/fixtures/invalid/storage-leftovers-wrong-type.invalid.json +23 -0
  32. package/spec/fixtures/level/base-full.valid.json +162 -0
  33. package/spec/fixtures/level/base-missing-yield.invalid.json +12 -0
  34. package/spec/fixtures/level/lite-min.valid.json +14 -0
  35. package/spec/fixtures/profile/profile-base.valid.json +20 -0
  36. package/spec/fixtures/profile/profile-equipped.valid.json +28 -0
  37. package/spec/fixtures/profile/profile-illustrated.valid.json +28 -0
  38. package/spec/fixtures/profile/profile-lite.valid.json +13 -0
  39. package/spec/fixtures/profile/profile-prepped.valid.json +31 -0
  40. package/spec/fixtures/profile/profile-scalable-missing-scaling.invalid.json +29 -0
  41. package/spec/fixtures/profile/profile-scalable.valid.json +49 -0
  42. package/spec/fixtures/profile/profile-timed-missing-structured.invalid.json +30 -0
  43. package/spec/fixtures/scaling/bakers-percent-missing-ref.invalid.json +41 -0
  44. package/spec/fixtures/scaling/bakers-percent.valid.json +51 -0
  45. package/spec/fixtures/scaling/discrete-range.invalid.json +36 -0
  46. package/spec/fixtures/scaling/missing-quantified.invalid.json +40 -0
  47. package/spec/fixtures/scaling/reject-bakersPercentage.invalid.json +50 -0
  48. package/spec/fixtures/stacks/compute-missing-timed.invalid.json +32 -0
  49. package/spec/fixtures/stacks/dietary-no-signal.invalid.json +16 -0
  50. package/spec/fixtures/stacks/illustrated-empty.invalid.json +13 -0
  51. package/spec/fixtures/stacks/quantified-string.invalid.json +22 -0
  52. package/spec/fixtures/stacks/referenced-missing-input.invalid.json +32 -0
  53. package/spec/fixtures/stacks/storage-min.valid.json +20 -0
  54. package/spec/fixtures/stacks/storage-no-duration.invalid.json +16 -0
  55. package/spec/fixtures/stacks/timed-implies-structured.valid.json +50 -0
  56. package/spec/fixtures/stacks/timed-range.invalid.json +33 -0
  57. package/spec/fixtures/valid/equipment-scaling-rules.valid.json +76 -0
  58. package/spec/fixtures/valid/equipment-strings.valid.json +31 -0
  59. package/spec/fixtures/valid/equipment-structured-uses.valid.json +47 -0
  60. package/spec/fixtures/valid/mise-en-place-basic.valid.json +31 -0
  61. package/spec/fixtures/valid/mise-en-place-referenced-equipment.valid.json +51 -0
  62. package/spec/fixtures/valid/prep-ingredient-strings.valid.json +48 -0
  63. package/spec/fixtures/valid/prep-ingredient-structured.valid.json +45 -0
  64. package/spec/fixtures/valid/profile-equipped.valid.json +29 -0
  65. package/spec/fixtures/valid/profile-prepped.valid.json +32 -0
  66. package/spec/fixtures/valid/quantified-nested-ingredient-sections.valid.json +61 -0
  67. package/spec/fixtures/valid/referenced-scaling.valid.json +67 -0
  68. package/spec/fixtures/valid/storage-leftovers-simple.valid.json +27 -0
  69. package/spec/fixtures/valid/storage-leftovers-structured.valid.json +43 -0
  70. package/spec/fixtures/valid/structured-nested-step-sections.valid.json +84 -0
  71. package/spec/schemas/stacks-registry.schema.json +108 -0
  72. package/spec/soustack.schema.json +2379 -0
  73. package/spec/stacks/compute.schema.json +7 -0
  74. package/spec/stacks/compute@1.md +22 -0
  75. package/spec/stacks/dietary.schema.json +45 -0
  76. package/spec/stacks/dietary@1.md +24 -0
  77. package/spec/stacks/equipment.schema.json +98 -0
  78. package/spec/stacks/equipment@1.md +244 -0
  79. package/spec/stacks/illustrated.schema.json +54 -0
  80. package/spec/stacks/illustrated@1.md +24 -0
  81. package/spec/stacks/prep.schema.json +76 -0
  82. package/spec/stacks/prep@1.md +276 -0
  83. package/spec/stacks/quantified.schema.json +74 -0
  84. package/spec/stacks/quantified@1.md +24 -0
  85. package/spec/stacks/referenced.schema.json +96 -0
  86. package/spec/stacks/referenced@1.md +23 -0
  87. package/spec/stacks/registry.json +112 -0
  88. package/spec/stacks/scaling.schema.json +99 -0
  89. package/spec/stacks/scaling@1.md +238 -0
  90. package/spec/stacks/storage.schema.json +132 -0
  91. package/spec/stacks/storage@1.md +256 -0
  92. package/spec/stacks/structured.schema.json +48 -0
  93. package/spec/stacks/structured@1.md +24 -0
  94. package/spec/stacks/substitutions.schema.json +43 -0
  95. package/spec/stacks/substitutions@1.md +24 -0
  96. package/spec/stacks/techniques.schema.json +28 -0
  97. package/spec/stacks/techniques@1.md +23 -0
  98. package/spec/stacks/timed.schema.json +60 -0
  99. package/spec/stacks/timed@1.md +23 -0
  100. package/src/defs/common.schema.json +46 -0
  101. package/src/defs/duration.schema.json +33 -0
  102. package/src/defs/entities.schema.json +111 -0
  103. package/src/defs/ingredientQuantified.schema.json +9 -0
  104. package/src/defs/quantity.schema.json +16 -0
  105. package/src/defs/scalingRule.schema.json +127 -0
  106. package/src/defs/temperature.schema.json +63 -0
  107. package/src/profiles/base.schema.json +2 -2
  108. package/src/profiles/equipped.schema.json +10 -0
  109. package/src/profiles/illustrated.schema.json +4 -4
  110. package/src/profiles/lite.schema.json +10 -0
  111. package/src/profiles/prepped.schema.json +10 -0
  112. package/src/profiles/scalable.schema.json +6 -6
  113. package/src/profiles/timed.schema.json +10 -0
  114. package/src/schema.json +2271 -248
  115. package/src/schemas/stacks-registry.schema.json +108 -0
  116. package/src/soustack.schema.json +2271 -248
  117. package/src/stacks/compute.schema.json +7 -0
  118. package/src/stacks/compute@1.md +22 -0
  119. package/src/stacks/dietary.schema.json +45 -0
  120. package/src/stacks/dietary@1.md +24 -0
  121. package/src/stacks/equipment.schema.json +98 -0
  122. package/src/stacks/equipment@1.md +244 -0
  123. package/src/stacks/illustrated.schema.json +54 -0
  124. package/src/stacks/illustrated@1.md +24 -0
  125. package/src/stacks/prep.schema.json +76 -0
  126. package/src/stacks/prep@1.md +276 -0
  127. package/src/stacks/quantified.schema.json +74 -0
  128. package/src/stacks/quantified@1.md +24 -0
  129. package/src/stacks/referenced.schema.json +96 -0
  130. package/src/stacks/referenced@1.md +23 -0
  131. package/src/stacks/registry.json +112 -0
  132. package/src/stacks/scaling.schema.json +99 -0
  133. package/src/stacks/scaling@1.md +238 -0
  134. package/src/stacks/storage.schema.json +132 -0
  135. package/src/stacks/storage@1.md +256 -0
  136. package/src/stacks/structured.schema.json +48 -0
  137. package/src/stacks/structured@1.md +24 -0
  138. package/src/stacks/substitutions.schema.json +43 -0
  139. package/src/stacks/substitutions@1.md +24 -0
  140. package/src/stacks/techniques.schema.json +28 -0
  141. package/src/stacks/techniques@1.md +23 -0
  142. package/src/stacks/timed.schema.json +60 -0
  143. package/src/stacks/timed@1.md +23 -0
  144. package/src/profiles/cookable.schema.json +0 -18
  145. package/src/profiles/quantified.schema.json +0 -43
  146. package/src/profiles/schedulable.schema.json +0 -43
@@ -145,6 +145,7 @@ function normalizeRecipe(input) {
145
145
  }
146
146
  if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
147
147
  recipe.recipeVersion = recipe.version;
148
+ delete recipe.version;
148
149
  warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
149
150
  }
150
151
  normalizeTime(recipe);
@@ -214,6 +215,51 @@ function normalizeTime(recipe) {
214
215
  });
215
216
  }
216
217
 
218
+ // src/specVersion.ts
219
+ var SOUSTACK_SPEC_VERSION = "0.0.2";
220
+
221
+ // src/schemaMetadata.ts
222
+ var CANONICAL_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
223
+ var LEGACY_SCHEMA_ID = `http://soustack.org/schema/v${SOUSTACK_SPEC_VERSION}`;
224
+ var RAW_SPEC_BASE = "https://raw.githubusercontent.com/soustack/soustack-spec";
225
+ var RAW_SPEC_FORK_BASE = "https://raw.githubusercontent.com/RichardHerold/soustack-spec";
226
+ var SCHEMA_ALIAS_MAP = /* @__PURE__ */ new Map([
227
+ [CANONICAL_SCHEMA_ID, CANONICAL_SCHEMA_ID],
228
+ [LEGACY_SCHEMA_ID, CANONICAL_SCHEMA_ID],
229
+ [`${LEGACY_SCHEMA_ID}/`, CANONICAL_SCHEMA_ID],
230
+ ["https://soustack.org/schema/v0.0.2", CANONICAL_SCHEMA_ID],
231
+ ["https://soustack.org/schema/v0.0.2/", CANONICAL_SCHEMA_ID],
232
+ [`${RAW_SPEC_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
233
+ [`${RAW_SPEC_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID],
234
+ [`${RAW_SPEC_FORK_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
235
+ [`${RAW_SPEC_FORK_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID]
236
+ ]);
237
+ function resolveSchemaHint(value) {
238
+ if (typeof value !== "string" || !value) {
239
+ return { canonicalId: void 0, isSoustackSchema: false, wasAlias: false };
240
+ }
241
+ const trimmed = value.replace(/#$/, "");
242
+ const mapped = SCHEMA_ALIAS_MAP.get(trimmed) ?? trimmed;
243
+ const isSoustackSchema = SCHEMA_ALIAS_MAP.has(trimmed) || mapped.startsWith("http://soustack.org/schema") || mapped.startsWith("https://soustack.org/schema") || mapped.startsWith("https://soustack.spec/") || mapped.startsWith("https://soustack.org/schemas/");
244
+ return {
245
+ canonicalId: mapped,
246
+ isSoustackSchema,
247
+ wasAlias: mapped !== trimmed || SCHEMA_ALIAS_MAP.has(trimmed)
248
+ };
249
+ }
250
+ function withCanonicalSchema(value) {
251
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
252
+ return value;
253
+ }
254
+ const existing = typeof value.$schema === "string" ? value.$schema : void 0;
255
+ const resolved = resolveSchemaHint(existing);
256
+ const schemaId = resolved.isSoustackSchema ? resolved.canonicalId : CANONICAL_SCHEMA_ID;
257
+ return {
258
+ ...value,
259
+ $schema: schemaId ?? CANONICAL_SCHEMA_ID
260
+ };
261
+ }
262
+
217
263
  // src/fromSchemaOrg.ts
218
264
  function fromSchemaOrg(input) {
219
265
  const recipeNode = extractRecipeNode(input);
@@ -229,23 +275,18 @@ function fromSchemaOrg(input) {
229
275
  const source = convertSource(recipeNode);
230
276
  const dateModified = recipeNode.dateModified || void 0;
231
277
  const nutrition = convertNutrition(recipeNode.nutrition);
232
- const attribution = convertAttribution(recipeNode);
233
- const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
234
- const media = convertMedia(recipeNode.image, recipeNode.video);
235
- const times = convertTimes(time);
278
+ const images = toArray(normalizeImage(recipeNode.image));
279
+ const videos = normalizeMediaList(recipeNode.video);
280
+ const profile = recipeYield && time ? "base" : "lite";
236
281
  const stacks = {};
237
- if (attribution) stacks.attribution = 1;
238
- if (taxonomy) stacks.taxonomy = 1;
239
- if (media) stacks.media = 1;
240
- if (nutrition) stacks.nutrition = 1;
241
- if (times) stacks.times = 1;
242
282
  const rawRecipe = {
243
283
  "@type": "Recipe",
244
- profile: "minimal",
284
+ profile,
245
285
  stacks,
246
286
  name: recipeNode.name.trim(),
247
287
  description: recipeNode.description?.trim() || void 0,
248
- image: normalizeImage(recipeNode.image),
288
+ images: images.length ? images : void 0,
289
+ videos: videos.length ? videos : void 0,
249
290
  category,
250
291
  tags: tags.length ? tags : void 0,
251
292
  source,
@@ -255,14 +296,10 @@ function fromSchemaOrg(input) {
255
296
  ingredients,
256
297
  instructions,
257
298
  ...dateModified ? { dateModified } : {},
258
- ...nutrition ? { nutrition } : {},
259
- ...attribution ? { attribution } : {},
260
- ...taxonomy ? { taxonomy } : {},
261
- ...media ? { media } : {},
262
- ...times ? { times } : {}
299
+ ...nutrition ? { nutrition } : {}
263
300
  };
264
301
  const { recipe } = normalizeRecipe(rawRecipe);
265
- return recipe;
302
+ return withCanonicalSchema(recipe);
266
303
  }
267
304
  function extractRecipeNode(input) {
268
305
  if (!input) return null;
@@ -306,7 +343,10 @@ function isValidName(name) {
306
343
  function convertIngredients(value) {
307
344
  if (!value) return [];
308
345
  const normalized = Array.isArray(value) ? value : [value];
309
- return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
346
+ return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean).map((name) => ({
347
+ name,
348
+ scaling: { mode: "linear" }
349
+ }));
310
350
  }
311
351
  function convertInstructions(value) {
312
352
  if (!value) return [];
@@ -325,8 +365,8 @@ function convertInstructions(value) {
325
365
  const subsectionItems = extractSectionItems(entry.itemListElement);
326
366
  if (subsectionItems.length) {
327
367
  result.push({
328
- subsection: entry.name?.trim() || "Section",
329
- items: subsectionItems
368
+ section: entry.name?.trim() || "Section",
369
+ steps: subsectionItems
330
370
  });
331
371
  }
332
372
  continue;
@@ -382,7 +422,7 @@ function convertHowToStep(step) {
382
422
  }
383
423
  const instruction = { text };
384
424
  if (id) instruction.id = id;
385
- if (image) instruction.image = image;
425
+ if (image) instruction.images = Array.isArray(image) ? image : [image];
386
426
  if (timing) instruction.timing = timing;
387
427
  return instruction;
388
428
  }
@@ -392,7 +432,13 @@ function extractInstructionTiming(step) {
392
432
  return void 0;
393
433
  }
394
434
  const parsed = smartParseDuration(duration);
395
- return { duration: parsed ?? duration, type: "active" };
435
+ if (parsed === null || parsed === void 0) {
436
+ return void 0;
437
+ }
438
+ return {
439
+ activity: "active",
440
+ duration: { minutes: parsed }
441
+ };
396
442
  }
397
443
  function extractInstructionId(step) {
398
444
  const raw = step["@id"] || step.id || step.url;
@@ -409,14 +455,22 @@ function isHowToSection(value) {
409
455
  return Boolean(value) && typeof value === "object" && value["@type"] === "HowToSection" && Array.isArray(value.itemListElement);
410
456
  }
411
457
  function convertTime(recipe) {
458
+ const total = smartParseDuration(recipe.totalTime ?? "");
412
459
  const prep = smartParseDuration(recipe.prepTime ?? "");
413
460
  const cook = smartParseDuration(recipe.cookTime ?? "");
414
- const total = smartParseDuration(recipe.totalTime ?? "");
415
- const structured = {};
416
- if (prep !== null && prep !== void 0) structured.prep = prep;
417
- if (cook !== null && cook !== void 0) structured.active = cook;
418
- if (total !== null && total !== void 0) structured.total = total;
419
- return Object.keys(structured).length ? structured : void 0;
461
+ const minutes = isPositiveDuration(total) ? total : [prep, cook].filter(isPositiveDuration).reduce((sum, value) => {
462
+ if (sum === null) return value;
463
+ return sum + value;
464
+ }, null);
465
+ if (!isPositiveDuration(minutes)) {
466
+ return void 0;
467
+ }
468
+ return {
469
+ total: { minutes }
470
+ };
471
+ }
472
+ function isPositiveDuration(value) {
473
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
420
474
  }
421
475
  function collectTags(cuisine, keywords) {
422
476
  const tags = /* @__PURE__ */ new Set();
@@ -474,23 +528,6 @@ function extractEntityName(value) {
474
528
  }
475
529
  return void 0;
476
530
  }
477
- function convertAttribution(recipe) {
478
- const attribution = {};
479
- const url = (recipe.url || recipe.mainEntityOfPage)?.trim();
480
- const author = extractEntityName(recipe.author);
481
- const datePublished = recipe.datePublished?.trim();
482
- if (url) attribution.url = url;
483
- if (author) attribution.author = author;
484
- if (datePublished) attribution.datePublished = datePublished;
485
- return Object.keys(attribution).length ? attribution : void 0;
486
- }
487
- function convertTaxonomy(keywords, category, cuisine) {
488
- const taxonomy = {};
489
- if (keywords.length) taxonomy.keywords = keywords;
490
- if (category) taxonomy.category = category;
491
- if (cuisine) taxonomy.cuisine = cuisine;
492
- return Object.keys(taxonomy).length ? taxonomy : void 0;
493
- }
494
531
  function normalizeMediaList(value) {
495
532
  if (!value) return [];
496
533
  if (typeof value === "string") return [value.trim()].filter(Boolean);
@@ -501,28 +538,18 @@ function normalizeMediaList(value) {
501
538
  return url ? [url] : [];
502
539
  }
503
540
  function extractMediaUrl(value) {
504
- if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
505
- const trimmed = value.url.trim();
506
- return trimmed || void 0;
541
+ if (value && typeof value === "object") {
542
+ const urlValue = typeof value.url === "string" ? value.url : typeof value.contentUrl === "string" ? value.contentUrl : void 0;
543
+ if (typeof urlValue === "string") {
544
+ const trimmed = urlValue.trim();
545
+ return trimmed || void 0;
546
+ }
507
547
  }
508
548
  return void 0;
509
549
  }
510
- function convertMedia(image, video) {
511
- const normalizedImage = normalizeImage(image);
512
- const images = normalizedImage ? Array.isArray(normalizedImage) ? normalizedImage : [normalizedImage] : [];
513
- const videos = normalizeMediaList(video);
514
- const media = {};
515
- if (images.length) media.images = images;
516
- if (videos.length) media.videos = videos;
517
- return Object.keys(media).length ? media : void 0;
518
- }
519
- function convertTimes(time) {
520
- if (!time) return void 0;
521
- const times = {};
522
- if (typeof time.prep === "number") times.prepMinutes = time.prep;
523
- if (typeof time.active === "number") times.cookMinutes = time.active;
524
- if (typeof time.total === "number") times.totalMinutes = time.total;
525
- return Object.keys(times).length ? times : void 0;
550
+ function toArray(value) {
551
+ if (!value) return [];
552
+ return Array.isArray(value) ? value : [value];
526
553
  }
527
554
  function convertNutrition(nutrition) {
528
555
  if (!nutrition || typeof nutrition !== "object") {