Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
val strength: Float,
val range: Float,
Expand All @@ -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<String>)
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<String>)

/** A compiled recipe — the runtime plan, lanes preserved. */
data class CompiledPattern(
val id: String,
val recipe: FieldPattern,
val bodies: List<RecipeBodyRegistration>,
val relationships: List<RecipeRelationshipRegistration>,
val feedback: List<RecipeFeedbackBinding>,
val bodies: List<PatternBodyRegistration>,
val relationships: List<PatternRelationshipRegistration>,
val feedback: List<PatternFeedbackBinding>,
val diagnostics: List<String>,
val metrics: List<String>,
val conditions: List<String>,
val reducedMotion: RecipeReducedMotionPlan,
val reducedMotion: PatternReducedMotionPlan,
)

private fun staticOutputs(r: FieldPattern): List<String> {
Expand All @@ -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,
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private val VALID_FIELDS: Set<String> = 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,
Expand All @@ -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
Expand All @@ -49,10 +49,10 @@ data class FieldPattern(
val metrics: List<String> = emptyList(),
val diagnostics: List<String> = emptyList(),
val conditions: List<String>? = null,
val bodies: List<BodyRecipe> = emptyList(),
val relationships: List<RelationshipRecipe>? = null,
val bodies: List<BodyPattern> = emptyList(),
val relationships: List<RelationshipPattern>? = null,
val render: List<String> = emptyList(),
val accessibility: AccessibilityRecipe,
val accessibility: AccessibilityPattern,
val status: String? = null,
val notes: String? = null,
)
Expand All @@ -61,36 +61,36 @@ data class FieldPattern(
private data class RecipeFile(val count: Int = 0, val recipes: List<FieldPattern> = 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<BodyRecipe>): List<String> {
fun primitivesOf(bodies: List<BodyPattern>): List<String> {
val seen = LinkedHashSet<String>()
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<RecipeProblem> {
val problems = ArrayList<RecipeProblem>()
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<PatternProblem> {
val problems = ArrayList<PatternProblem>()
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
),
Expand Down
24 changes: 12 additions & 12 deletions swift/Sources/FundamentalCore/Recipes/Compile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,21 +42,21 @@ public struct RecipeBodyRegistration {
}
}

public struct RecipeRelationshipRegistration {
public struct PatternRelationshipRegistration {
public var from: String
public var to: String
public var type: String
public var strength: Float?
}

/// 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.
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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)
Expand Down
Loading
Loading