diff --git a/CHANGELOG.md b/CHANGELOG.md index 9676bef2..a0b49da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ a git tag (see [RELEASING.md](RELEASING.md)). ### Deprecated - **`@fundamental-engine/core` + `@fundamental-engine/dom` — the recipe → `Pattern` rename reaches the internal helper surface (phase 4, additive).** The remaining `recipe`-named exported helpers are renamed to `Pattern`, each keeping a `@deprecated` alias of the old name (removed at `1.0`): `BodyPattern` (was `BodyRecipe`), `RelationshipPattern`, `AccessibilityPattern`, `PatternTier`, `PatternStatus`, `PatternProblem`, `PatternTierGroup`, `PatternRenderPlan`, `PatternBodyRegistration`, `PatternRelationshipRegistration`, `PatternFeedbackBinding`, `PatternReducedMotionPlan`, `PatternAuthoring`, `PatternFieldTarget` (dom), the consts `PATTERN_TIERS` / `PATTERN_CONTRACTS`, and the functions `validatePattern` / `serializePattern` / `patternById` / `patternBodyAttributes` / `patternRenderPlan` / `patternToMarkup` / `patternAuthoring`. `bindData` gains a `pattern` option (the `recipe` option is still read as a deprecated fallback). Every old name still works. Intentionally left unchanged (invisible plumbing, not concept surface): the `recipes/` source folder, `apply-recipe.ts`, `data/recipes.json`, and the `RelationshipSource` `'recipe'` value tag. +- **Swift + Kotlin ports — the phase-4 helper rename is mirrored.** `FundamentalCore` (Swift) and `:fundamental-core` (Kotlin) rename the same helper family (`BodyRecipe`→`BodyPattern`, `RecipeProblem`→`PatternProblem`, `validateRecipe`→`validatePattern`, the registrations, …). A clean rename (no deprecation aliases): the ports have no external consumers, so nothing needs the old names — unlike the JS packages, whose site consumes them. Verified: `swift test` (234 incl. the conformance golden) + Kotlin `:fundamental-core:test :lab:test` (217) green. ## [0.9.4] — 2026-07-10 diff --git a/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Compile.kt b/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Compile.kt index 48d87155..7a579fc4 100644 --- a/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Compile.kt +++ b/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Compile.kt @@ -15,7 +15,7 @@ import kotlin.math.sin fun metricVar(metric: String): String = "field-$metric" /** One compiled body: configured params + runtime tokens; `makeBody()` builds a ready Body. */ -data class RecipeBodyRegistration( +data class PatternBodyRegistration( val tokens: List, val strength: Float, val range: Float, @@ -31,21 +31,21 @@ data class RecipeBodyRegistration( } } -data class RecipeRelationshipRegistration(val from: String, val to: String, val type: String, val strength: Float?) -data class RecipeFeedbackBinding(val metric: String, val variable: String) -data class RecipeReducedMotionPlan(val reducedMotion: String, val meaningWithoutMotion: String, val staticOutputs: List) +data class PatternRelationshipRegistration(val from: String, val to: String, val type: String, val strength: Float?) +data class PatternFeedbackBinding(val metric: String, val variable: String) +data class PatternReducedMotionPlan(val reducedMotion: String, val meaningWithoutMotion: String, val staticOutputs: List) /** A compiled recipe — the runtime plan, lanes preserved. */ data class CompiledPattern( val id: String, val recipe: FieldPattern, - val bodies: List, - val relationships: List, - val feedback: List, + val bodies: List, + val relationships: List, + val feedback: List, val diagnostics: List, val metrics: List, val conditions: List, - val reducedMotion: RecipeReducedMotionPlan, + val reducedMotion: PatternReducedMotionPlan, ) private fun staticOutputs(r: FieldPattern): List { @@ -64,7 +64,7 @@ fun compilePattern(r: FieldPattern): CompiledPattern = CompiledPattern( recipe = r, bodies = r.bodies.map { b -> val angle = b.angle ?: (-PI.toFloat() / 2f) // the JS default heading: up - RecipeBodyRegistration( + PatternBodyRegistration( tokens = b.body.split(" ").filter { it.isNotEmpty() }, strength = b.strength ?: 1f, range = b.range ?: 100f, @@ -74,13 +74,13 @@ fun compilePattern(r: FieldPattern): CompiledPattern = CompiledPattern( ) }, relationships = (r.relationships ?: emptyList()).map { - RecipeRelationshipRegistration(it.from, it.to, it.type, it.strength) + PatternRelationshipRegistration(it.from, it.to, it.type, it.strength) }, - feedback = r.metrics.map { RecipeFeedbackBinding(it, metricVar(it)) }, + feedback = r.metrics.map { PatternFeedbackBinding(it, metricVar(it)) }, diagnostics = r.diagnostics, metrics = r.metrics, conditions = r.conditions ?: emptyList(), - reducedMotion = RecipeReducedMotionPlan( + reducedMotion = PatternReducedMotionPlan( reducedMotion = r.accessibility.reducedMotion, meaningWithoutMotion = r.accessibility.meaningWithoutMotion, staticOutputs = staticOutputs(r), diff --git a/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Schema.kt b/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Schema.kt index 92fabeba..290a7de5 100644 --- a/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Schema.kt +++ b/android/fundamental-core/src/main/kotlin/com/fundamental/core/recipes/Schema.kt @@ -19,7 +19,7 @@ private val VALID_FIELDS: Set = setOf("gravity", "electromagnetic", "str /** One body in a recipe: a force token (or space-separated tokens) + attributes. */ @Serializable -data class BodyRecipe( +data class BodyPattern( val body: String, val strength: Float? = null, val range: Float? = null, @@ -30,10 +30,10 @@ data class BodyRecipe( ) @Serializable -data class RelationshipRecipe(val from: String, val to: String, val type: String, val strength: Float? = null) +data class RelationshipPattern(val from: String, val to: String, val type: String, val strength: Float? = null) @Serializable -data class AccessibilityRecipe(val reducedMotion: String, val meaningWithoutMotion: String) +data class AccessibilityPattern(val reducedMotion: String, val meaningWithoutMotion: String) /** A portable field recipe (authoring §5). Only `primitives` (runtime tokens) becomes behavior. */ @Serializable @@ -49,10 +49,10 @@ data class FieldPattern( val metrics: List = emptyList(), val diagnostics: List = emptyList(), val conditions: List? = null, - val bodies: List = emptyList(), - val relationships: List? = null, + val bodies: List = emptyList(), + val relationships: List? = null, val render: List = emptyList(), - val accessibility: AccessibilityRecipe, + val accessibility: AccessibilityPattern, val status: String? = null, val notes: String? = null, ) @@ -61,36 +61,36 @@ data class FieldPattern( private data class RecipeFile(val count: Int = 0, val recipes: List = emptyList()) /** A validation problem: a JSON-ish path + the issue. */ -data class RecipeProblem(val path: String, val issue: String) +data class PatternProblem(val path: String, val issue: String) /** The distinct engine primitives across a recipe's bodies, in first-seen order. */ -fun primitivesOf(bodies: List): List { +fun primitivesOf(bodies: List): List { val seen = LinkedHashSet() for (b in bodies) for (t in b.body.split(" ")) if (t.isNotEmpty()) seen.add(t) return seen.toList() } /** Validate a recipe's shape and references against a force registry. Empty = valid. */ -fun validateRecipe(r: FieldPattern, forces: ForceRegistry): List { - val problems = ArrayList() - if (r.id.isEmpty()) problems.add(RecipeProblem("id", "required")) - if (r.name.isEmpty()) problems.add(RecipeProblem("name", "required")) - if (r.bodies.isEmpty()) problems.add(RecipeProblem("bodies", "at least one body is required")) +fun validatePattern(r: FieldPattern, forces: ForceRegistry): List { + val problems = ArrayList() + if (r.id.isEmpty()) problems.add(PatternProblem("id", "required")) + if (r.name.isEmpty()) problems.add(PatternProblem("name", "required")) + if (r.bodies.isEmpty()) problems.add(PatternProblem("bodies", "at least one body is required")) r.bodies.forEachIndexed { i, b -> val tokens = b.body.split(" ").filter { it.isNotEmpty() } - if (tokens.isEmpty()) problems.add(RecipeProblem("bodies[$i].body", "empty force token list")) - for (t in tokens) if (!forces.containsKey(t)) problems.add(RecipeProblem("bodies[$i].body", "unknown force token \"$t\"")) + if (tokens.isEmpty()) problems.add(PatternProblem("bodies[$i].body", "empty force token list")) + for (t in tokens) if (!forces.containsKey(t)) problems.add(PatternProblem("bodies[$i].body", "unknown force token \"$t\"")) } val derived = primitivesOf(r.bodies) val declared = r.primitives if (declared.size != derived.size || derived.any { it !in declared } || declared.any { it !in derived }) { - problems.add(RecipeProblem("primitives", "must list exactly the body tokens (expected: ${derived.joinToString(", ")})")) + problems.add(PatternProblem("primitives", "must list exactly the body tokens (expected: ${derived.joinToString(", ")})")) } - r.render.forEachIndexed { i, layer -> if (layer !in RENDER_LAYER_IDS) problems.add(RecipeProblem("render[$i]", "unknown render layer \"$layer\"")) } - r.diagnostics.forEachIndexed { i, mode -> if (mode !in FIELD_MODES) problems.add(RecipeProblem("diagnostics[$i]", "unknown diagnostic mode \"$mode\"")) } - r.naturalField?.let { if (it !in VALID_FIELDS) problems.add(RecipeProblem("naturalField", "unknown fundamental field \"$it\"")) } + r.render.forEachIndexed { i, layer -> if (layer !in RENDER_LAYER_IDS) problems.add(PatternProblem("render[$i]", "unknown render layer \"$layer\"")) } + r.diagnostics.forEachIndexed { i, mode -> if (mode !in FIELD_MODES) problems.add(PatternProblem("diagnostics[$i]", "unknown diagnostic mode \"$mode\"")) } + r.naturalField?.let { if (it !in VALID_FIELDS) problems.add(PatternProblem("naturalField", "unknown fundamental field \"$it\"")) } if (r.accessibility.reducedMotion.isEmpty() || r.accessibility.meaningWithoutMotion.isEmpty()) { - problems.add(RecipeProblem("accessibility", "reducedMotion + meaningWithoutMotion are required (no recipe is motion-only)")) + problems.add(PatternProblem("accessibility", "reducedMotion + meaningWithoutMotion are required (no recipe is motion-only)")) } return problems } diff --git a/android/fundamental-core/src/test/kotlin/com/fundamental/core/RecipeTests.kt b/android/fundamental-core/src/test/kotlin/com/fundamental/core/RecipeTests.kt index 36670c50..228291aa 100644 --- a/android/fundamental-core/src/test/kotlin/com/fundamental/core/RecipeTests.kt +++ b/android/fundamental-core/src/test/kotlin/com/fundamental/core/RecipeTests.kt @@ -3,7 +3,7 @@ package com.fundamental.core import com.fundamental.core.engine.Registry import com.fundamental.core.recipes.FieldPatterns import com.fundamental.core.recipes.compilePattern -import com.fundamental.core.recipes.validateRecipe +import com.fundamental.core.recipes.validatePattern import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -24,7 +24,7 @@ class RecipeTests { fun everyRecipeValidatesAgainstTheStandardRegistry() { val failures = StringBuilder() for (r in FieldPatterns.all) { - val problems = validateRecipe(r, forces) + val problems = validatePattern(r, forces) if (problems.isNotEmpty()) failures.append("\n${r.id}: ${problems.joinToString("; ") { "${it.path} — ${it.issue}" }}") } assertTrue(failures.isEmpty(), "every canon recipe must validate:$failures") diff --git a/android/lab/src/test/kotlin/com/fundamental/lab/RecipeExportTests.kt b/android/lab/src/test/kotlin/com/fundamental/lab/RecipeExportTests.kt index be1a4344..426e11ae 100644 --- a/android/lab/src/test/kotlin/com/fundamental/lab/RecipeExportTests.kt +++ b/android/lab/src/test/kotlin/com/fundamental/lab/RecipeExportTests.kt @@ -1,9 +1,9 @@ package com.fundamental.lab -import com.fundamental.core.recipes.AccessibilityRecipe -import com.fundamental.core.recipes.BodyRecipe +import com.fundamental.core.recipes.AccessibilityPattern +import com.fundamental.core.recipes.BodyPattern import com.fundamental.core.recipes.FieldPattern -import com.fundamental.core.recipes.RelationshipRecipe +import com.fundamental.core.recipes.RelationshipPattern import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals @@ -29,12 +29,12 @@ class RecipeExportTests { metrics = listOf("density"), diagnostics = listOf("force-vectors"), bodies = listOf( - BodyRecipe(body = "attract", strength = 1.6f, range = 260f), - BodyRecipe(body = "spin", spin = 2f, angle = 90f), + BodyPattern(body = "attract", strength = 1.6f, range = 260f), + BodyPattern(body = "spin", spin = 2f, angle = 90f), ), - relationships = listOf(RelationshipRecipe(from = "attract", to = "spin", type = "drives", strength = 0.5f)), + relationships = listOf(RelationshipPattern(from = "attract", to = "spin", type = "drives", strength = 0.5f)), render = listOf("dots", "trails"), - accessibility = AccessibilityRecipe( + accessibility = AccessibilityPattern( reducedMotion = "freeze the orbit", meaningWithoutMotion = "the ring still reads as a relationship", ), diff --git a/swift/Sources/FundamentalCore/Recipes/Compile.swift b/swift/Sources/FundamentalCore/Recipes/Compile.swift index 6036a454..17963d1c 100644 --- a/swift/Sources/FundamentalCore/Recipes/Compile.swift +++ b/swift/Sources/FundamentalCore/Recipes/Compile.swift @@ -20,7 +20,7 @@ public func metricVar(_ metric: String) -> String { /// One compiled body: the configured parameters + the runtime tokens it carries. /// The Swift counterpart of the JS `data-*` attribute set — `makeBody()` produces a /// ready Body the caller attaches to a view (or places headless). -public struct RecipeBodyRegistration { +public struct PatternBodyRegistration { public var tokens: [String] public var strength: Float public var range: Float @@ -42,7 +42,7 @@ public struct RecipeBodyRegistration { } } -public struct RecipeRelationshipRegistration { +public struct PatternRelationshipRegistration { public var from: String public var to: String public var type: String @@ -50,13 +50,13 @@ public struct RecipeRelationshipRegistration { } /// A metric → feedback-variable binding (the metric lane becoming measurable state). -public struct RecipeFeedbackBinding { +public struct PatternFeedbackBinding { public var metric: String public var variable: String } /// The reduced-motion output plan — what the runtime renders when motion is reduced. -public struct RecipeReducedMotionPlan { +public struct PatternReducedMotionPlan { public var reducedMotion: String public var meaningWithoutMotion: String /// The static surfaces the runtime should render in place of motion. @@ -67,13 +67,13 @@ public struct RecipeReducedMotionPlan { public struct CompiledPattern { public var id: String public var recipe: FieldPattern - public var bodies: [RecipeBodyRegistration] - public var relationships: [RecipeRelationshipRegistration] - public var feedback: [RecipeFeedbackBinding] + public var bodies: [PatternBodyRegistration] + public var relationships: [PatternRelationshipRegistration] + public var feedback: [PatternFeedbackBinding] public var diagnostics: [String] public var metrics: [String] public var conditions: [String] - public var reducedMotion: RecipeReducedMotionPlan + public var reducedMotion: PatternReducedMotionPlan } /// Derive the static surfaces a reduced-motion render should produce from the lanes. @@ -96,7 +96,7 @@ public func compilePattern(_ r: FieldPattern) -> CompiledPattern { recipe: r, bodies: r.bodies.map { b in let angle = b.angle ?? (-Float.pi / 2) // the JS default heading: up - return RecipeBodyRegistration( + return PatternBodyRegistration( tokens: b.body.split(separator: " ").map(String.init), strength: b.strength ?? 1, range: b.range ?? 100, @@ -106,13 +106,13 @@ public func compilePattern(_ r: FieldPattern) -> CompiledPattern { ) }, relationships: (r.relationships ?? []).map { - RecipeRelationshipRegistration(from: $0.from, to: $0.to, type: $0.type, strength: $0.strength) + PatternRelationshipRegistration(from: $0.from, to: $0.to, type: $0.type, strength: $0.strength) }, - feedback: r.metrics.map { RecipeFeedbackBinding(metric: $0, variable: metricVar($0)) }, + feedback: r.metrics.map { PatternFeedbackBinding(metric: $0, variable: metricVar($0)) }, diagnostics: r.diagnostics, metrics: r.metrics, conditions: r.conditions ?? [], - reducedMotion: RecipeReducedMotionPlan( + reducedMotion: PatternReducedMotionPlan( reducedMotion: r.accessibility.reducedMotion, meaningWithoutMotion: r.accessibility.meaningWithoutMotion, staticOutputs: staticOutputs(r) diff --git a/swift/Sources/FundamentalCore/Recipes/Schema.swift b/swift/Sources/FundamentalCore/Recipes/Schema.swift index 00112000..99f95d15 100644 --- a/swift/Sources/FundamentalCore/Recipes/Schema.swift +++ b/swift/Sources/FundamentalCore/Recipes/Schema.swift @@ -23,7 +23,7 @@ public enum DiagnosticMode: String, Codable, Sendable, CaseIterable { } /// One body in a recipe: a force token (or space-separated tokens) + attributes. -public struct BodyRecipe: Codable, Sendable { +public struct BodyPattern: Codable, Sendable { public var body: String public var strength: Float? public var range: Float? @@ -33,14 +33,14 @@ public struct BodyRecipe: Codable, Sendable { public var scope: String? } -public struct RelationshipRecipe: Codable, Sendable { +public struct RelationshipPattern: Codable, Sendable { public var from: String public var to: String public var type: String public var strength: Float? } -public struct AccessibilityRecipe: Codable, Sendable { +public struct AccessibilityPattern: Codable, Sendable { /// What replaces motion under reduced motion. public var reducedMotion: String /// How meaning survives without color/motion. @@ -53,11 +53,11 @@ public struct ExpectedMetrics: Codable, Sendable { public var energyDriftMax: Float? } -public enum RecipeTier: String, Codable, Sendable { +public enum PatternTier: String, Codable, Sendable { case core, applied, systems, operational } -public enum RecipeStatus: String, Codable, Sendable { +public enum PatternStatus: String, Codable, Sendable { case shipped, experimental, planned, conceptual } @@ -70,7 +70,7 @@ public struct FieldPattern: Codable, Sendable { public var id: String public var name: String public var intent: String - public var tier: RecipeTier? + public var tier: PatternTier? public var naturalField: String? public var translation: String? /// RUNTIME TOKENS: the strict, real engine forces this recipe composes. @@ -83,11 +83,11 @@ public struct FieldPattern: Codable, Sendable { public var diagnostics: [String] /// CONDITIONS: activation logic — not forces. public var conditions: [String]? - public var bodies: [BodyRecipe] - public var relationships: [RelationshipRecipe]? + public var bodies: [BodyPattern] + public var relationships: [RelationshipPattern]? public var render: [String] - public var accessibility: AccessibilityRecipe - public var status: RecipeStatus? + public var accessibility: AccessibilityPattern + public var status: PatternStatus? public var notes: String? } @@ -99,13 +99,13 @@ let DIAGNOSTIC_MODE_IDS: Set = Set(DiagnosticMode.allCases.map(\.rawValu public let FIELD_MODES: Set = RENDER_LAYER_IDS.union(DIAGNOSTIC_MODE_IDS) let VALID_FIELDS: Set = ["gravity", "electromagnetic", "strong", "weak"] -public struct RecipeProblem: Sendable { +public struct PatternProblem: Sendable { public var path: String public var issue: String } /// The distinct engine primitives used across a recipe's bodies, in first-seen order. -public func primitivesOf(_ bodies: [BodyRecipe]) -> [String] { +public func primitivesOf(_ bodies: [BodyPattern]) -> [String] { var seen = Set() var out: [String] = [] for b in bodies { @@ -118,18 +118,18 @@ public func primitivesOf(_ bodies: [BodyRecipe]) -> [String] { /// Validate a recipe's shape and references against a force registry. Returns every /// problem (empty = valid). -public func validateRecipe(_ r: FieldPattern, against registry: Registry) -> [RecipeProblem] { - var problems: [RecipeProblem] = [] - if r.id.isEmpty { problems.append(RecipeProblem(path: "id", issue: "required")) } - if r.name.isEmpty { problems.append(RecipeProblem(path: "name", issue: "required")) } - if r.bodies.isEmpty { problems.append(RecipeProblem(path: "bodies", issue: "at least one body is required")) } +public func validatePattern(_ r: FieldPattern, against registry: Registry) -> [PatternProblem] { + var problems: [PatternProblem] = [] + if r.id.isEmpty { problems.append(PatternProblem(path: "id", issue: "required")) } + if r.name.isEmpty { problems.append(PatternProblem(path: "name", issue: "required")) } + if r.bodies.isEmpty { problems.append(PatternProblem(path: "bodies", issue: "at least one body is required")) } for (i, b) in r.bodies.enumerated() { let tokens = b.body.split(separator: " ").map(String.init) if tokens.isEmpty { - problems.append(RecipeProblem(path: "bodies[\(i)].body", issue: "empty force token list")) + problems.append(PatternProblem(path: "bodies[\(i)].body", issue: "empty force token list")) } for t in tokens where registry.forces[t] == nil { - problems.append(RecipeProblem(path: "bodies[\(i)].body", issue: "unknown force token \"\(t)\"")) + problems.append(PatternProblem(path: "bodies[\(i)].body", issue: "unknown force token \"\(t)\"")) } } // declared primitives must be exactly the distinct body tokens (no drift). @@ -138,22 +138,22 @@ public func validateRecipe(_ r: FieldPattern, against registry: Registry) -> [Re if declared.count != derived.count || derived.contains(where: { !declared.contains($0) }) || declared.contains(where: { !derived.contains($0) }) { - problems.append(RecipeProblem( + problems.append(PatternProblem( path: "primitives", issue: "must list exactly the body tokens (expected: \(derived.joined(separator: ", ")))" )) } for (i, layer) in r.render.enumerated() where !RENDER_LAYER_IDS.contains(layer) { - problems.append(RecipeProblem(path: "render[\(i)]", issue: "unknown render layer \"\(layer)\"")) + problems.append(PatternProblem(path: "render[\(i)]", issue: "unknown render layer \"\(layer)\"")) } for (i, mode) in r.diagnostics.enumerated() where !FIELD_MODES.contains(mode) { - problems.append(RecipeProblem(path: "diagnostics[\(i)]", issue: "unknown diagnostic mode \"\(mode)\"")) + problems.append(PatternProblem(path: "diagnostics[\(i)]", issue: "unknown diagnostic mode \"\(mode)\"")) } if let nf = r.naturalField, !VALID_FIELDS.contains(nf) { - problems.append(RecipeProblem(path: "naturalField", issue: "unknown fundamental field \"\(nf)\"")) + problems.append(PatternProblem(path: "naturalField", issue: "unknown fundamental field \"\(nf)\"")) } if r.accessibility.reducedMotion.isEmpty || r.accessibility.meaningWithoutMotion.isEmpty { - problems.append(RecipeProblem( + problems.append(PatternProblem( path: "accessibility", issue: "reducedMotion + meaningWithoutMotion are required (no recipe is motion-only)" )) @@ -185,7 +185,7 @@ public enum FieldPatterns { all.first { $0.id == id } } - public static func recipes(tier: RecipeTier) -> [FieldPattern] { + public static func recipes(tier: PatternTier) -> [FieldPattern] { all.filter { $0.tier == tier } } } diff --git a/swift/Tests/FundamentalCoreTests/ParityTests.swift b/swift/Tests/FundamentalCoreTests/ParityTests.swift index 887bf40a..27a50acd 100644 --- a/swift/Tests/FundamentalCoreTests/ParityTests.swift +++ b/swift/Tests/FundamentalCoreTests/ParityTests.swift @@ -505,7 +505,7 @@ struct RecipeTests { func allValid() { let reg = Registry.standard() for r in FieldPatterns.all { - let problems = validateRecipe(r, against: reg) + let problems = validatePattern(r, against: reg) #expect(problems.isEmpty, "recipe \(r.id): \(problems.map { "\($0.path): \($0.issue)" }.joined(separator: "; "))") } } @@ -526,8 +526,8 @@ struct RecipeTests { @Test("primitivesOf derives distinct tokens in first-seen order") func primitives() { let bodies = [ - BodyRecipe(body: "attract gravity", strength: nil, range: nil, spin: nil, angle: nil, feedback: nil, scope: nil), - BodyRecipe(body: "gravity sink", strength: nil, range: nil, spin: nil, angle: nil, feedback: nil, scope: nil), + BodyPattern(body: "attract gravity", strength: nil, range: nil, spin: nil, angle: nil, feedback: nil, scope: nil), + BodyPattern(body: "gravity sink", strength: nil, range: nil, spin: nil, angle: nil, feedback: nil, scope: nil), ] #expect(primitivesOf(bodies) == ["attract", "gravity", "sink"]) }