soustack 0.2.3 → 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 CHANGED
@@ -37,7 +37,7 @@ npm install soustack
37
37
 
38
38
  ## What's Included
39
39
 
40
- - **Validation**: `validateRecipe()` validates Soustack JSON against the bundled schema.
40
+ - **Validation**: `validateRecipe()` validates Soustack JSON against the bundled schema and optional conformance checks.
41
41
  - **Scaling & Computation**: `scaleRecipe()` scales a recipe while honoring per-ingredient scaling rules and instruction timing.
42
42
  - **Schema.org Conversion**:
43
43
  - `fromSchemaOrg()` (Schema.org JSON-LD → Soustack)
@@ -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
 
@@ -53,36 +54,87 @@ Validate and scale a recipe in just a few lines:
53
54
  ```ts
54
55
  import { validateRecipe, scaleRecipe } from 'soustack';
55
56
 
56
- // Validate against the bundled Soustack schema
57
- const { valid, errors, warnings } = validateRecipe(recipe);
58
- if (!valid) {
59
- throw new Error(JSON.stringify(errors, null, 2));
57
+ // Validate against the bundled Soustack schema + conformance rules
58
+ const { ok, schemaErrors, conformanceIssues, warnings } = validateRecipe(recipe);
59
+ if (!ok) {
60
+ throw new Error(JSON.stringify({ schemaErrors, conformanceIssues }, null, 2));
60
61
  }
61
62
  if (warnings?.length) {
62
63
  console.warn('Non-blocking warnings', warnings);
63
64
  }
64
65
 
66
+ // Schema-only validation (skip conformance checks)
67
+ const schemaOnly = validateRecipe(recipe, { mode: 'schema' });
68
+ if (!schemaOnly.ok) {
69
+ console.error(schemaOnly.schemaErrors);
70
+ }
71
+
65
72
  // Scale to a new yield (multiplier, target yield, or servings)
66
73
  const scaled = scaleRecipe(recipe, { multiplier: 2 });
67
74
  ```
68
75
 
69
76
  ### Profile-aware validation
70
77
 
71
- Use profiles to enforce integration contracts (e.g., **Block** vs **Integrator** payloads).
78
+ Use profiles to enforce integration contracts. Available profiles:
79
+ - **base**
80
+ - **equipped**
81
+ - **illustrated**
82
+ - **lite**
83
+ - **prepped**
84
+ - **scalable**
85
+ - **timed**
72
86
 
73
87
  ```ts
74
88
  import { detectProfiles, validateRecipe } from 'soustack';
75
89
 
76
90
  // Discover which profiles a recipe already satisfies
77
- const profiles = detectProfiles(recipe); // e.g. ['block']
91
+ const profiles = detectProfiles(recipe);
78
92
 
79
- // Validate while requiring specific profiles
80
- const result = validateRecipe(recipe, { profiles: ['block', 'integrator'] });
81
- if (!result.valid) {
82
- console.error('Profile validation failed', result.errors);
93
+ // Validate with a specific profile
94
+ const result = validateRecipe(recipe, { profile: 'base' });
95
+ if (!result.ok) {
96
+ console.error('Profile validation failed', result.schemaErrors);
83
97
  }
98
+
99
+ // Validate with modules
100
+ const recipeWithModules = {
101
+ profile: 'base',
102
+ modules: ['nutrition@1', 'times@1'],
103
+ name: 'Test Recipe',
104
+ ingredients: ['1 cup flour'],
105
+ instructions: ['Mix'],
106
+ nutrition: { calories: 100, protein_g: 5 }, // Module payload required if declared
107
+ times: { prepMinutes: 10, cookMinutes: 20, totalMinutes: 30 }, // v0.3: uses *Minutes fields
108
+ };
109
+ const result2 = validateRecipe(recipeWithModules);
110
+ // Validates using: base + profile + nutrition@1 module + times@1 module
111
+ // Module contract: if module is declared, payload must exist (and vice versa)
112
+ ```
113
+
114
+ ### Imperial → metric ingredient conversion
115
+
116
+ ```ts
117
+ import { convertLineItemToMetric } from 'soustack';
118
+
119
+ const flour = convertLineItemToMetric(
120
+ { ingredient: 'flour', quantity: 2, unit: 'cup' },
121
+ 'mass'
122
+ );
123
+ // -> { ingredient: 'flour', quantity: 240, unit: 'g', notes: 'Converted using 120g per cup...' }
124
+
125
+ const liquid = convertLineItemToMetric(
126
+ { ingredient: 'milk', quantity: 2, unit: 'cup' },
127
+ 'volume'
128
+ );
129
+ // -> { ingredient: 'milk', quantity: 473, unit: 'ml' }
84
130
  ```
85
131
 
132
+ 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:
133
+
134
+ - `UnknownUnitError` for unsupported unit tokens
135
+ - `UnsupportedConversionError` if you request a mismatched dimension
136
+ - `MissingEquivalencyError` when no volume→mass density is registered for the ingredient/unit combo
137
+
86
138
  ### Browser-safe vs. Node-only entrypoints
87
139
 
88
140
  - **Browser-safe:** `import { extractSchemaOrgRecipeFromHTML, fromSchemaOrg, validateRecipe, scaleRecipe } from 'soustack';`
@@ -92,10 +144,60 @@ if (!result.valid) {
92
144
 
93
145
  ## Spec compatibility & bundled schemas
94
146
 
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.
147
+ - Targets Soustack spec **v0.3.0** (`spec/SOUSTACK_SPEC_VERSION`, exported as `SOUSTACK_SPEC_VERSION`).
148
+ - Ships the base schema, profile schemas, and module schemas in `spec/schemas/recipe/` and mirrors them into `src/schemas/recipe/` for consumers.
97
149
  - Vendored fixtures live in `spec/fixtures` so tests can run offline, and version drift can be checked via `npm run validate:version`.
98
150
 
151
+ ### Composed Validation Model
152
+
153
+ Soustack v0.3.0 uses a **composed validation model** where recipes are validated using JSON Schema's `allOf` composition:
154
+
155
+ ```json
156
+ {
157
+ "allOf": [
158
+ { "$ref": "base.schema.json" },
159
+ { "$ref": "profiles/{profile}.schema.json" },
160
+ { "$ref": "modules/{module1}/{version}.schema.json" },
161
+ { "$ref": "modules/{module2}/{version}.schema.json" }
162
+ ]
163
+ }
164
+ ```
165
+
166
+ The validator:
167
+ - **Base schema**: Defines the core recipe structure (`@type`, `name`, `ingredients`, `instructions`, `profile`, `modules`)
168
+ - **Profile overlay**: Adds profile-specific requirements (e.g., `base` or `lite`)
169
+ - **Module overlays**: Each declared module adds its own validation rules
170
+
171
+ **Defaults:**
172
+ - If `profile` is missing, it defaults to the schema bundle's configured default
173
+ - If `modules` is missing, it defaults to `[]`
174
+
175
+ **Module Contract:** Modules enforce a symmetric contract:
176
+ - If a module is declared in `modules`, the corresponding payload must exist
177
+ - If a payload exists (e.g., `nutrition`, `times`), the module must be declared
178
+ - The validator automatically infers modules from payloads and enforces this contract
179
+
180
+ **Caching:** Validators are cached by `${profile}::${sortedModules.join(",")}` for performance.
181
+
182
+ ### Module Resolution
183
+
184
+ Modules are resolved to schema references using the pattern:
185
+ - Module identifier format: `<name>@<version>` (e.g., `nutrition@1`, `schedule@1`)
186
+ - Schema reference: `https://soustack.org/schemas/recipe/modules/<name>/<version>.schema.json`
187
+
188
+ The module registry (`schemas/registry/modules.json`) defines which modules are available and their properties, including:
189
+ - `schemaOrgMappable`: Whether the module can be converted to Schema.org format
190
+ - `minProfile`: Minimum profile required to use the module
191
+ - `allowedOnLite`: Whether the module can be used with the lite profile
192
+
193
+ **Available Modules (v0.3.0):**
194
+ - `attribution@1`: Source attribution (url, author, datePublished)
195
+ - `taxonomy@1`: Classification (keywords, category, cuisine)
196
+ - `media@1`: Images and videos (images, videos arrays)
197
+ - `times@1`: Timing information (prepMinutes, cookMinutes, totalMinutes)
198
+ - `nutrition@1`: Nutritional data (calories, protein_g as numbers)
199
+ - `schedule@1`: Task scheduling (requires timed profile, includes instruction dependencies)
200
+
99
201
  ## Programmatic Usage
100
202
 
101
203
  ```ts
@@ -113,9 +215,9 @@ import {
113
215
  } from 'soustack/scrape';
114
216
 
115
217
  // Validate a Soustack recipe JSON object with profile enforcement
116
- const validation = validateRecipe(recipe, { profiles: ['block'] });
117
- if (!validation.valid) {
118
- console.error(validation.errors);
218
+ const validation = validateRecipe(recipe, { profile: 'base' });
219
+ if (!validation.ok) {
220
+ console.error({ schemaErrors: validation.schemaErrors, conformanceIssues: validation.conformanceIssues });
119
221
  }
120
222
 
121
223
  // Scale a recipe to a target yield amount (returns a "computed recipe")
@@ -167,6 +269,8 @@ async function convert(url: string) {
167
269
 
168
270
  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
271
 
272
+ **BREAKING CHANGE in v0.3.0:** `toSchemaOrg()` now targets the **lite 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.
273
+
170
274
  ```ts
171
275
  import { fromSchemaOrg, toSchemaOrg, normalizeImage } from 'soustack';
172
276
 
@@ -268,10 +372,16 @@ const parsed = extractRecipeFromHTML(html);
268
372
 
269
373
  ```bash
270
374
  # Validate with profiles (JSON output for pipelines)
271
- npx soustack validate recipe.soustack.json --profile block --strict --json
375
+ npx soustack validate recipe.soustack.json --profile base --strict --json
376
+
377
+ # Schema-only validation (skip semantic conformance checks)
378
+ npx soustack validate recipe.soustack.json --schema-only
379
+
380
+ # Stable JSON conformance report for CI
381
+ npx soustack check recipe.soustack.json --json
272
382
 
273
383
  # Repo-wide test run (validates every *.soustack.json)
274
- npx soustack test --profile block
384
+ npx soustack test --profile base
275
385
 
276
386
  # Convert Schema.org ↔ Soustack
277
387
  npx soustack convert --from schemaorg --to soustack recipe.jsonld -o recipe.soustack.json