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
@@ -147,6 +147,7 @@ function normalizeRecipe(input) {
147
147
  }
148
148
  if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
149
149
  recipe.recipeVersion = recipe.version;
150
+ delete recipe.version;
150
151
  warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
151
152
  }
152
153
  normalizeTime(recipe);
@@ -216,6 +217,51 @@ function normalizeTime(recipe) {
216
217
  });
217
218
  }
218
219
 
220
+ // src/specVersion.ts
221
+ var SOUSTACK_SPEC_VERSION = "0.0.2";
222
+
223
+ // src/schemaMetadata.ts
224
+ var CANONICAL_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
225
+ var LEGACY_SCHEMA_ID = `http://soustack.org/schema/v${SOUSTACK_SPEC_VERSION}`;
226
+ var RAW_SPEC_BASE = "https://raw.githubusercontent.com/soustack/soustack-spec";
227
+ var RAW_SPEC_FORK_BASE = "https://raw.githubusercontent.com/RichardHerold/soustack-spec";
228
+ var SCHEMA_ALIAS_MAP = /* @__PURE__ */ new Map([
229
+ [CANONICAL_SCHEMA_ID, CANONICAL_SCHEMA_ID],
230
+ [LEGACY_SCHEMA_ID, CANONICAL_SCHEMA_ID],
231
+ [`${LEGACY_SCHEMA_ID}/`, CANONICAL_SCHEMA_ID],
232
+ ["https://soustack.org/schema/v0.0.2", CANONICAL_SCHEMA_ID],
233
+ ["https://soustack.org/schema/v0.0.2/", CANONICAL_SCHEMA_ID],
234
+ [`${RAW_SPEC_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
235
+ [`${RAW_SPEC_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID],
236
+ [`${RAW_SPEC_FORK_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
237
+ [`${RAW_SPEC_FORK_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID]
238
+ ]);
239
+ function resolveSchemaHint(value) {
240
+ if (typeof value !== "string" || !value) {
241
+ return { canonicalId: void 0, isSoustackSchema: false, wasAlias: false };
242
+ }
243
+ const trimmed = value.replace(/#$/, "");
244
+ const mapped = SCHEMA_ALIAS_MAP.get(trimmed) ?? trimmed;
245
+ 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/");
246
+ return {
247
+ canonicalId: mapped,
248
+ isSoustackSchema,
249
+ wasAlias: mapped !== trimmed || SCHEMA_ALIAS_MAP.has(trimmed)
250
+ };
251
+ }
252
+ function withCanonicalSchema(value) {
253
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
254
+ return value;
255
+ }
256
+ const existing = typeof value.$schema === "string" ? value.$schema : void 0;
257
+ const resolved = resolveSchemaHint(existing);
258
+ const schemaId = resolved.isSoustackSchema ? resolved.canonicalId : CANONICAL_SCHEMA_ID;
259
+ return {
260
+ ...value,
261
+ $schema: schemaId ?? CANONICAL_SCHEMA_ID
262
+ };
263
+ }
264
+
219
265
  // src/fromSchemaOrg.ts
220
266
  function fromSchemaOrg(input) {
221
267
  const recipeNode = extractRecipeNode(input);
@@ -231,23 +277,18 @@ function fromSchemaOrg(input) {
231
277
  const source = convertSource(recipeNode);
232
278
  const dateModified = recipeNode.dateModified || void 0;
233
279
  const nutrition = convertNutrition(recipeNode.nutrition);
234
- const attribution = convertAttribution(recipeNode);
235
- const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
236
- const media = convertMedia(recipeNode.image, recipeNode.video);
237
- const times = convertTimes(time);
280
+ const images = toArray(normalizeImage(recipeNode.image));
281
+ const videos = normalizeMediaList(recipeNode.video);
282
+ const profile = recipeYield && time ? "base" : "lite";
238
283
  const stacks = {};
239
- if (attribution) stacks.attribution = 1;
240
- if (taxonomy) stacks.taxonomy = 1;
241
- if (media) stacks.media = 1;
242
- if (nutrition) stacks.nutrition = 1;
243
- if (times) stacks.times = 1;
244
284
  const rawRecipe = {
245
285
  "@type": "Recipe",
246
- profile: "minimal",
286
+ profile,
247
287
  stacks,
248
288
  name: recipeNode.name.trim(),
249
289
  description: recipeNode.description?.trim() || void 0,
250
- image: normalizeImage(recipeNode.image),
290
+ images: images.length ? images : void 0,
291
+ videos: videos.length ? videos : void 0,
251
292
  category,
252
293
  tags: tags.length ? tags : void 0,
253
294
  source,
@@ -257,14 +298,10 @@ function fromSchemaOrg(input) {
257
298
  ingredients,
258
299
  instructions,
259
300
  ...dateModified ? { dateModified } : {},
260
- ...nutrition ? { nutrition } : {},
261
- ...attribution ? { attribution } : {},
262
- ...taxonomy ? { taxonomy } : {},
263
- ...media ? { media } : {},
264
- ...times ? { times } : {}
301
+ ...nutrition ? { nutrition } : {}
265
302
  };
266
303
  const { recipe } = normalizeRecipe(rawRecipe);
267
- return recipe;
304
+ return withCanonicalSchema(recipe);
268
305
  }
269
306
  function extractRecipeNode(input) {
270
307
  if (!input) return null;
@@ -308,7 +345,10 @@ function isValidName(name) {
308
345
  function convertIngredients(value) {
309
346
  if (!value) return [];
310
347
  const normalized = Array.isArray(value) ? value : [value];
311
- return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
348
+ return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean).map((name) => ({
349
+ name,
350
+ scaling: { mode: "linear" }
351
+ }));
312
352
  }
313
353
  function convertInstructions(value) {
314
354
  if (!value) return [];
@@ -327,8 +367,8 @@ function convertInstructions(value) {
327
367
  const subsectionItems = extractSectionItems(entry.itemListElement);
328
368
  if (subsectionItems.length) {
329
369
  result.push({
330
- subsection: entry.name?.trim() || "Section",
331
- items: subsectionItems
370
+ section: entry.name?.trim() || "Section",
371
+ steps: subsectionItems
332
372
  });
333
373
  }
334
374
  continue;
@@ -384,7 +424,7 @@ function convertHowToStep(step) {
384
424
  }
385
425
  const instruction = { text };
386
426
  if (id) instruction.id = id;
387
- if (image) instruction.image = image;
427
+ if (image) instruction.images = Array.isArray(image) ? image : [image];
388
428
  if (timing) instruction.timing = timing;
389
429
  return instruction;
390
430
  }
@@ -394,7 +434,13 @@ function extractInstructionTiming(step) {
394
434
  return void 0;
395
435
  }
396
436
  const parsed = smartParseDuration(duration);
397
- return { duration: parsed ?? duration, type: "active" };
437
+ if (parsed === null || parsed === void 0) {
438
+ return void 0;
439
+ }
440
+ return {
441
+ activity: "active",
442
+ duration: { minutes: parsed }
443
+ };
398
444
  }
399
445
  function extractInstructionId(step) {
400
446
  const raw = step["@id"] || step.id || step.url;
@@ -411,14 +457,22 @@ function isHowToSection(value) {
411
457
  return Boolean(value) && typeof value === "object" && value["@type"] === "HowToSection" && Array.isArray(value.itemListElement);
412
458
  }
413
459
  function convertTime(recipe) {
460
+ const total = smartParseDuration(recipe.totalTime ?? "");
414
461
  const prep = smartParseDuration(recipe.prepTime ?? "");
415
462
  const cook = smartParseDuration(recipe.cookTime ?? "");
416
- const total = smartParseDuration(recipe.totalTime ?? "");
417
- const structured = {};
418
- if (prep !== null && prep !== void 0) structured.prep = prep;
419
- if (cook !== null && cook !== void 0) structured.active = cook;
420
- if (total !== null && total !== void 0) structured.total = total;
421
- return Object.keys(structured).length ? structured : void 0;
463
+ const minutes = isPositiveDuration(total) ? total : [prep, cook].filter(isPositiveDuration).reduce((sum, value) => {
464
+ if (sum === null) return value;
465
+ return sum + value;
466
+ }, null);
467
+ if (!isPositiveDuration(minutes)) {
468
+ return void 0;
469
+ }
470
+ return {
471
+ total: { minutes }
472
+ };
473
+ }
474
+ function isPositiveDuration(value) {
475
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
422
476
  }
423
477
  function collectTags(cuisine, keywords) {
424
478
  const tags = /* @__PURE__ */ new Set();
@@ -476,23 +530,6 @@ function extractEntityName(value) {
476
530
  }
477
531
  return void 0;
478
532
  }
479
- function convertAttribution(recipe) {
480
- const attribution = {};
481
- const url = (recipe.url || recipe.mainEntityOfPage)?.trim();
482
- const author = extractEntityName(recipe.author);
483
- const datePublished = recipe.datePublished?.trim();
484
- if (url) attribution.url = url;
485
- if (author) attribution.author = author;
486
- if (datePublished) attribution.datePublished = datePublished;
487
- return Object.keys(attribution).length ? attribution : void 0;
488
- }
489
- function convertTaxonomy(keywords, category, cuisine) {
490
- const taxonomy = {};
491
- if (keywords.length) taxonomy.keywords = keywords;
492
- if (category) taxonomy.category = category;
493
- if (cuisine) taxonomy.cuisine = cuisine;
494
- return Object.keys(taxonomy).length ? taxonomy : void 0;
495
- }
496
533
  function normalizeMediaList(value) {
497
534
  if (!value) return [];
498
535
  if (typeof value === "string") return [value.trim()].filter(Boolean);
@@ -503,28 +540,18 @@ function normalizeMediaList(value) {
503
540
  return url ? [url] : [];
504
541
  }
505
542
  function extractMediaUrl(value) {
506
- if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
507
- const trimmed = value.url.trim();
508
- return trimmed || void 0;
543
+ if (value && typeof value === "object") {
544
+ const urlValue = typeof value.url === "string" ? value.url : typeof value.contentUrl === "string" ? value.contentUrl : void 0;
545
+ if (typeof urlValue === "string") {
546
+ const trimmed = urlValue.trim();
547
+ return trimmed || void 0;
548
+ }
509
549
  }
510
550
  return void 0;
511
551
  }
512
- function convertMedia(image, video) {
513
- const normalizedImage = normalizeImage(image);
514
- const images = normalizedImage ? Array.isArray(normalizedImage) ? normalizedImage : [normalizedImage] : [];
515
- const videos = normalizeMediaList(video);
516
- const media = {};
517
- if (images.length) media.images = images;
518
- if (videos.length) media.videos = videos;
519
- return Object.keys(media).length ? media : void 0;
520
- }
521
- function convertTimes(time) {
522
- if (!time) return void 0;
523
- const times = {};
524
- if (typeof time.prep === "number") times.prepMinutes = time.prep;
525
- if (typeof time.active === "number") times.cookMinutes = time.active;
526
- if (typeof time.total === "number") times.totalMinutes = time.total;
527
- return Object.keys(times).length ? times : void 0;
552
+ function toArray(value) {
553
+ if (!value) return [];
554
+ return Array.isArray(value) ? value : [value];
528
555
  }
529
556
  function convertNutrition(nutrition) {
530
557
  if (!nutrition || typeof nutrition !== "object") {