soustack 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ npm install soustack
45
45
  - **Web Extraction**:
46
46
  - Browser-safe HTML parsing: `extractSchemaOrgRecipeFromHTML()` (convert to Soustack with `fromSchemaOrg()`)
47
47
  - Node-only scraping entrypoint: `scrapeRecipe()` and helpers via `import { ... } from 'soustack/scrape'`
48
+ - **Unit Conversion**: `convertLineItemToMetric()` converts ingredient line items from imperial volumes/masses into metric with deterministic rounding and ingredient-aware equivalencies.
48
49
 
49
50
  ## 🚀 Quickstart
50
51
 
@@ -68,21 +69,61 @@ const scaled = scaleRecipe(recipe, { multiplier: 2 });
68
69
 
69
70
  ### Profile-aware validation
70
71
 
71
- Use profiles to enforce integration contracts (e.g., **Block** vs **Integrator** payloads).
72
+ Use profiles to enforce integration contracts. Available profiles:
73
+ - **minimal**: Basic recipe structure with minimal requirements
74
+ - **core**: Enhanced profile with structured ingredients and instructions
72
75
 
73
76
  ```ts
74
77
  import { detectProfiles, validateRecipe } from 'soustack';
75
78
 
76
79
  // Discover which profiles a recipe already satisfies
77
- const profiles = detectProfiles(recipe); // e.g. ['block']
80
+ const profiles = detectProfiles(recipe); // e.g. ['minimal', 'core']
78
81
 
79
- // Validate while requiring specific profiles
80
- const result = validateRecipe(recipe, { profiles: ['block', 'integrator'] });
82
+ // Validate with a specific profile (defaults to 'core' if not specified)
83
+ const result = validateRecipe(recipe, { profile: 'minimal' });
81
84
  if (!result.valid) {
82
85
  console.error('Profile validation failed', result.errors);
83
86
  }
87
+
88
+ // Validate with modules
89
+ const recipeWithModules = {
90
+ profile: 'minimal',
91
+ modules: ['nutrition@1', 'times@1'],
92
+ name: 'Test Recipe',
93
+ ingredients: ['1 cup flour'],
94
+ instructions: ['Mix'],
95
+ nutrition: { calories: 100, protein_g: 5 }, // Module payload required if declared
96
+ times: { prepMinutes: 10, cookMinutes: 20, totalMinutes: 30 }, // v0.3: uses *Minutes fields
97
+ };
98
+ const result2 = validateRecipe(recipeWithModules);
99
+ // Validates using: base + minimal profile + nutrition@1 module + times@1 module
100
+ // Module contract: if module is declared, payload must exist (and vice versa)
101
+ ```
102
+
103
+ ### Imperial → metric ingredient conversion
104
+
105
+ ```ts
106
+ import { convertLineItemToMetric } from 'soustack';
107
+
108
+ const flour = convertLineItemToMetric(
109
+ { ingredient: 'flour', quantity: 2, unit: 'cup' },
110
+ 'mass'
111
+ );
112
+ // -> { ingredient: 'flour', quantity: 240, unit: 'g', notes: 'Converted using 120g per cup...' }
113
+
114
+ const liquid = convertLineItemToMetric(
115
+ { ingredient: 'milk', quantity: 2, unit: 'cup' },
116
+ 'volume'
117
+ );
118
+ // -> { ingredient: 'milk', quantity: 473, unit: 'ml' }
84
119
  ```
85
120
 
121
+ The converter rounds using “sane” defaults (1 g/ml under 1 kg/1 L, then 5 g/10 ml and 2 decimal places for kg/L) and surfaces typed errors:
122
+
123
+ - `UnknownUnitError` for unsupported unit tokens
124
+ - `UnsupportedConversionError` if you request a mismatched dimension
125
+ - `MissingEquivalencyError` when no volume→mass density is registered for the ingredient/unit combo
126
+
86
127
  ### Browser-safe vs. Node-only entrypoints
87
128
 
88
129
  - **Browser-safe:** `import { extractSchemaOrgRecipeFromHTML, fromSchemaOrg, validateRecipe, scaleRecipe } from 'soustack';`
@@ -92,10 +133,60 @@ if (!result.valid) {
92
133
 
93
134
  ## Spec compatibility & bundled schemas
94
135
 
95
- - Targets Soustack spec **v0.2.1** (`spec/SOUSTACK_SPEC_VERSION`, exported as `SOUSTACK_SPEC_VERSION`).
96
- - Ships the base schema plus profile schemas in `spec/` and mirrors them into `src/` for consumers.
136
+ - Targets Soustack spec **v0.3.0** (`spec/SOUSTACK_SPEC_VERSION`, exported as `SOUSTACK_SPEC_VERSION`).
137
+ - Ships the base schema, profile schemas, and module schemas in `spec/schemas/recipe/` and mirrors them into `src/schemas/recipe/` for consumers.
97
138
  - Vendored fixtures live in `spec/fixtures` so tests can run offline, and version drift can be checked via `npm run validate:version`.
98
139
 
140
+ ### Composed Validation Model
141
+
142
+ Soustack v0.3.0 uses a **composed validation model** where recipes are validated using JSON Schema's `allOf` composition:
143
+
144
+ ```json
145
+ {
146
+ "allOf": [
147
+ { "$ref": "base.schema.json" },
148
+ { "$ref": "profiles/{profile}.schema.json" },
149
+ { "$ref": "modules/{module1}/{version}.schema.json" },
150
+ { "$ref": "modules/{module2}/{version}.schema.json" }
151
+ ]
152
+ }
153
+ ```
154
+
155
+ The validator:
156
+ - **Base schema**: Defines the core recipe structure (`@type`, `name`, `ingredients`, `instructions`, `profile`, `modules`)
157
+ - **Profile overlay**: Adds profile-specific requirements (e.g., `minimal` or `core`)
158
+ - **Module overlays**: Each declared module adds its own validation rules
159
+
160
+ **Defaults:**
161
+ - If `profile` is missing, it defaults to `"core"`
162
+ - If `modules` is missing, it defaults to `[]`
163
+
164
+ **Module Contract:** Modules enforce a symmetric contract:
165
+ - If a module is declared in `modules`, the corresponding payload must exist
166
+ - If a payload exists (e.g., `nutrition`, `times`), the module must be declared
167
+ - The validator automatically infers modules from payloads and enforces this contract
168
+
169
+ **Caching:** Validators are cached by `${profile}::${sortedModules.join(",")}` for performance.
170
+
171
+ ### Module Resolution
172
+
173
+ Modules are resolved to schema references using the pattern:
174
+ - Module identifier format: `<name>@<version>` (e.g., `nutrition@1`, `schedule@1`)
175
+ - Schema reference: `https://soustack.org/schemas/recipe/modules/<name>/<version>.schema.json`
176
+
177
+ The module registry (`schemas/registry/modules.json`) defines which modules are available and their properties, including:
178
+ - `schemaOrgMappable`: Whether the module can be converted to Schema.org format
179
+ - `minProfile`: Minimum profile required to use the module
180
+ - `allowedOnMinimal`: Whether the module can be used with the minimal profile
181
+
182
+ **Available Modules (v0.3.0):**
183
+ - `attribution@1`: Source attribution (url, author, datePublished)
184
+ - `taxonomy@1`: Classification (keywords, category, cuisine)
185
+ - `media@1`: Images and videos (images, videos arrays)
186
+ - `times@1`: Timing information (prepMinutes, cookMinutes, totalMinutes)
187
+ - `nutrition@1`: Nutritional data (calories, protein_g as numbers)
188
+ - `schedule@1`: Task scheduling (requires core profile, includes instruction dependencies)
189
+
99
190
  ## Programmatic Usage
100
191
 
101
192
  ```ts
@@ -113,7 +204,7 @@ import {
113
204
  } from 'soustack/scrape';
114
205
 
115
206
  // Validate a Soustack recipe JSON object with profile enforcement
116
- const validation = validateRecipe(recipe, { profiles: ['block'] });
207
+ const validation = validateRecipe(recipe, { profile: 'core' });
117
208
  if (!validation.valid) {
118
209
  console.error(validation.errors);
119
210
  }
@@ -167,6 +258,8 @@ async function convert(url: string) {
167
258
 
168
259
  Use the helpers to move between Schema.org JSON-LD and Soustack's structured recipe format. The conversion automatically handles image normalization, supporting multiple image formats from Schema.org.
169
260
 
261
+ **BREAKING CHANGE in v0.3.0:** `toSchemaOrg()` now targets the **minimal profile** and only includes modules that are marked as `schemaOrgMappable` in the modules registry. Non-mappable modules (e.g., `nutrition@1`, `schedule@1`) are excluded from the conversion.
262
+
170
263
  ```ts
171
264
  import { fromSchemaOrg, toSchemaOrg, normalizeImage } from 'soustack';
172
265