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 @@ -19,6 +19,7 @@ a git tag (see [RELEASING.md](RELEASING.md)).

- **`@fundamental-engine/core` (internal, no API change):** extracted the pure-math modules (`math.ts`, `geometry.ts`) from `src/core/` into a dedicated `src/math/` folder — step 1 of standardizing the core folder layout across all planes (`engine · forces · math · recipes` + the finer concern folders). Public exports and the barrel are unchanged; `check:api` intact.
- **`@fundamental-engine/core` (internal, no API change):** renamed `src/core/` → `src/engine/` to match the port layout (Swift `Engine/`, Kotlin `engine/`) — step 2 of the cross-plane folder standardization. 127 files moved with history; imports, the barrel, hardcoded read-paths in `contract-coverage.test.ts` / `check-docs.mjs` / `gen-parity-matrix.mjs`, and 26 doc links all repointed. Public exports unchanged; `check:api` 17-frozen intact.
- **Swift + Kotlin ports — the recipe → `Pattern` rename is mirrored (phase 2).** `FundamentalCore` (Swift) and `:fundamental-core` (Kotlin) rename `FieldRecipe` → `FieldPattern`, `CompiledRecipe` → `CompiledPattern`, `compileRecipe` → `compilePattern`, and the `FieldRecipes` catalog namespace → `FieldPatterns`, keeping the old names as `@available(*, deprecated)` / `@Deprecated` aliases (removed at `1.0`) — parity with phase 1's public boundary. Internal helper type names (`BodyRecipe`, `RecipeTier`, …) are deferred, matching phase 1. Verified: `swift build` + `swift test` (234 tests incl. the cross-plane conformance golden) and Kotlin `:fundamental-core:test :lab:test` (217 tests) green.

## [0.9.3] — 2026-07-03

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import kotlin.math.cos
import kotlin.math.sin

// Recipe compiler (recipes/compile.ts, authoring §5) — the Kotlin port of
// swift/Sources/FundamentalCore/Recipes/Compile.swift. Turns a portable FieldRecipe into a runtime
// swift/Sources/FundamentalCore/Recipes/Compile.swift. Turns a portable FieldPattern into a runtime
// plan. The lane split is preserved: concepts describe · tokens execute · metrics measure ·
// diagnostics explain · conditions activate. Only the `primitives`/body tokens lane becomes behavior.

Expand Down Expand Up @@ -36,9 +36,9 @@ data class RecipeFeedbackBinding(val metric: String, val variable: String)
data class RecipeReducedMotionPlan(val reducedMotion: String, val meaningWithoutMotion: String, val staticOutputs: List<String>)

/** A compiled recipe — the runtime plan, lanes preserved. */
data class CompiledRecipe(
data class CompiledPattern(
val id: String,
val recipe: FieldRecipe,
val recipe: FieldPattern,
val bodies: List<RecipeBodyRegistration>,
val relationships: List<RecipeRelationshipRegistration>,
val feedback: List<RecipeFeedbackBinding>,
Expand All @@ -48,7 +48,7 @@ data class CompiledRecipe(
val reducedMotion: RecipeReducedMotionPlan,
)

private fun staticOutputs(r: FieldRecipe): List<String> {
private fun staticOutputs(r: FieldPattern): List<String> {
val out = ArrayList<String>()
if (r.metrics.isNotEmpty()) out.add("metric-badges")
if (!r.relationships.isNullOrEmpty()) out.add("relationship-list")
Expand All @@ -59,7 +59,7 @@ private fun staticOutputs(r: FieldRecipe): List<String> {
}

/** Compile a recipe into a runtime plan (pure). Behavior comes only from the strict token lane. */
fun compileRecipe(r: FieldRecipe): CompiledRecipe = CompiledRecipe(
fun compilePattern(r: FieldPattern): CompiledPattern = CompiledPattern(
id = r.id,
recipe = r,
bodies = r.bodies.map { b ->
Expand All @@ -86,3 +86,10 @@ fun compileRecipe(r: FieldRecipe): CompiledRecipe = CompiledRecipe(
staticOutputs = staticOutputs(r),
),
)

// Deprecated aliases (recipe -> Pattern rename; removed at 1.0)
@Deprecated("Renamed to CompiledPattern", ReplaceWith("CompiledPattern"))
typealias CompiledRecipe = CompiledPattern

@Deprecated("Renamed to compilePattern", ReplaceWith("compilePattern(r)"))
fun compileRecipe(r: FieldPattern): CompiledPattern = compilePattern(r)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.fundamental.core.engine.ForceRegistry
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

// FieldRecipe schema + validation (recipes/schema.ts) — the Kotlin port of
// FieldPattern schema + validation (recipes/schema.ts) — the Kotlin port of
// swift/Sources/FundamentalCore/Recipes/Schema.swift. A recipe is a portable, inspectable field
// program; the 64-recipe catalog is LOCKED CANON, decoded from the shared data/recipes.json.

Expand Down Expand Up @@ -37,7 +37,7 @@ data class AccessibilityRecipe(val reducedMotion: String, val meaningWithoutMoti

/** A portable field recipe (authoring §5). Only `primitives` (runtime tokens) becomes behavior. */
@Serializable
data class FieldRecipe(
data class FieldPattern(
val id: String,
val name: String,
val intent: String,
Expand All @@ -58,7 +58,7 @@ data class FieldRecipe(
)

@Serializable
private data class RecipeFile(val count: Int = 0, val recipes: List<FieldRecipe> = emptyList())
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)
Expand All @@ -71,7 +71,7 @@ fun primitivesOf(bodies: List<BodyRecipe>): List<String> {
}

/** Validate a recipe's shape and references against a force registry. Empty = valid. */
fun validateRecipe(r: FieldRecipe, forces: ForceRegistry): List<RecipeProblem> {
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"))
Expand All @@ -96,16 +96,23 @@ fun validateRecipe(r: FieldRecipe, forces: ForceRegistry): List<RecipeProblem> {
}

/** The locked 64-recipe catalog (4 tiers × 16), decoded from the embedded shared canon. */
object FieldRecipes {
object FieldPatterns {
private val json = Json { ignoreUnknownKeys = true }

val all: List<FieldRecipe> by lazy {
val stream = FieldRecipes::class.java.getResourceAsStream("/recipes.json")
val all: List<FieldPattern> by lazy {
val stream = FieldPatterns::class.java.getResourceAsStream("/recipes.json")
?: error("FundamentalCore: bundled recipes.json is missing (the syncRecipes Gradle task pulls it in)")
val text = stream.bufferedReader().use { it.readText() }
json.decodeFromString(RecipeFile.serializer(), text).recipes
}

fun recipe(id: String): FieldRecipe? = all.firstOrNull { it.id == id }
fun recipes(tier: String): List<FieldRecipe> = all.filter { it.tier == tier }
fun recipe(id: String): FieldPattern? = all.firstOrNull { it.id == id }
fun recipes(tier: String): List<FieldPattern> = all.filter { it.tier == tier }
}

// Deprecated aliases (recipe -> Pattern rename; removed at 1.0)
@Deprecated("Renamed to FieldPattern", ReplaceWith("FieldPattern"))
typealias FieldRecipe = FieldPattern

@Deprecated("Renamed to FieldPatterns", ReplaceWith("FieldPatterns"))
typealias FieldRecipes = FieldPatterns
Comment on lines +115 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the deprecated Kotlin JVM symbols

For Java callers or already-compiled Kotlin/JVM clients that still use the 0.x recipe API, these typealias declarations are erased and do not emit the old com.fundamental.core.recipes.FieldRecipe / FieldRecipes bytecode. That makes the advertised deprecation window source-only for Kotlin and a hard compile/load break for those clients instead of a warning; keep deprecated wrapper classes/objects (and the old CompiledRecipe JVM type/signatures) if the old names are meant to remain usable until 1.0.

Useful? React with 👍 / 👎.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.fundamental.core

import com.fundamental.core.engine.Registry
import com.fundamental.core.recipes.FieldRecipes
import com.fundamental.core.recipes.compileRecipe
import com.fundamental.core.recipes.FieldPatterns
import com.fundamental.core.recipes.compilePattern
import com.fundamental.core.recipes.validateRecipe
import kotlin.test.Test
import kotlin.test.assertEquals
Expand All @@ -17,13 +17,13 @@ class RecipeTests {

@Test
fun theCanonLoadsAll64() {
assertEquals(64, FieldRecipes.all.size, "the locked catalog is 64 recipes")
assertEquals(64, FieldPatterns.all.size, "the locked catalog is 64 recipes")
}

@Test
fun everyRecipeValidatesAgainstTheStandardRegistry() {
val failures = StringBuilder()
for (r in FieldRecipes.all) {
for (r in FieldPatterns.all) {
val problems = validateRecipe(r, forces)
if (problems.isNotEmpty()) failures.append("\n${r.id}: ${problems.joinToString("; ") { "${it.path} — ${it.issue}" }}")
}
Expand All @@ -32,7 +32,7 @@ class RecipeTests {

@Test
fun everyBodyTokenIsARegisteredForce() {
for (r in FieldRecipes.all) {
for (r in FieldPatterns.all) {
for (b in r.bodies) {
for (t in b.body.split(" ").filter { it.isNotEmpty() }) {
assertTrue(forces.containsKey(t), "recipe ${r.id} references unknown force '$t'")
Expand All @@ -43,8 +43,8 @@ class RecipeTests {

@Test
fun compileProducesRunnableBodies() {
val r = FieldRecipes.all.first { it.bodies.isNotEmpty() }
val compiled = compileRecipe(r)
val r = FieldPatterns.all.first { it.bodies.isNotEmpty() }
val compiled = compilePattern(r)
assertEquals(r.bodies.size, compiled.bodies.size)
assertTrue(compiled.bodies.all { it.tokens.isNotEmpty() }, "every compiled body carries tokens")
// a metric becomes a feedback binding (field-<metric>)
Expand All @@ -55,7 +55,7 @@ class RecipeTests {

@Test
fun fourTiersAreRepresented() {
val tiers = FieldRecipes.all.mapNotNull { it.tier }.toSet()
val tiers = FieldPatterns.all.mapNotNull { it.tier }.toSet()
assertTrue(tiers.containsAll(setOf("core", "applied", "systems", "operational")), "all four tiers present (got $tiers)")
}
}
8 changes: 4 additions & 4 deletions android/lab/src/main/kotlin/com/fundamental/lab/LabUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.fundamental.core.engine.energyReport
import com.fundamental.core.overlay.Overlays
import com.fundamental.core.overlay.energyContours
import com.fundamental.core.overlay.temperatureContours
import com.fundamental.core.recipes.FieldRecipes
import com.fundamental.core.recipes.FieldPatterns
import com.fundamental.core.runtime.FieldController
import javax.swing.JCheckBox
import java.awt.BorderLayout
Expand Down Expand Up @@ -89,7 +89,7 @@ private fun buildRows(): List<Row> {
for (e in ForceCatalog.group(g)) rows.add(SceneRow(e.label, e.blurb) { forceScene(e) })
}
rows.add(Header("The canon — 64 recipes"))
for (r in FieldRecipes.all) rows.add(SceneRow(r.name, r.intent) { recipeScene(r) })
for (r in FieldPatterns.all) rows.add(SceneRow(r.name, r.intent) { recipeScene(r) })
return rows
}

Expand All @@ -114,14 +114,14 @@ class LabCanvas : JPanel() {
private val timer = Timer(16) { tickOnce() }
// Path traces: a rolling buffer of recent sampled particle positions (the `path` reading machinery).
private val pathHistory = ArrayDeque<FloatArray>()
private var currentRecipe: com.fundamental.core.recipes.FieldRecipe? = null
private var currentRecipe: com.fundamental.core.recipes.FieldPattern? = null

fun toggleReading(r: Reading, on: Boolean) {
if (on) readings.add(r) else { readings.remove(r); if (r == Reading.PATH) pathHistory.clear() }
}

/** The recipe backing the loaded scene, if any (drives the inspector's Export). */
fun currentRecipe(): com.fundamental.core.recipes.FieldRecipe? = currentRecipe
fun currentRecipe(): com.fundamental.core.recipes.FieldPattern? = currentRecipe
fun setAttention(on: Boolean) { attentionOn = on; controller?.attentionEnabled = on }
fun setCausality(on: Boolean) { causalityOn = on; controller?.causalityEnabled = on }
fun setHeatmap(on: Boolean) { heatmapOn = on; controller?.heatmapEnabled = on }
Expand Down
4 changes: 2 additions & 2 deletions android/lab/src/main/kotlin/com/fundamental/lab/Main.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.fundamental.lab

import com.fundamental.core.math.Vec3
import com.fundamental.core.recipes.FieldRecipes
import com.fundamental.core.recipes.FieldPatterns
import com.fundamental.core.runtime.FieldController
import java.awt.image.BufferedImage
import java.io.File
Expand All @@ -25,7 +25,7 @@ private fun renderScenes(): List<LabScene> =
listOf("gravity", "magnetism", "hunt", "wall", "spawn", "pressure")
.mapNotNull { ForceCatalog.entry(it) }
.map { forceScene(it) } +
FieldRecipes.all.filter { it.tier == "core" }.take(2).map { recipeScene(it) }
FieldPatterns.all.filter { it.tier == "core" }.take(2).map { recipeScene(it) }

private fun slug(name: String) = name.lowercase().replace(Regex("[^a-z0-9]+"), "-").trim('-')

Expand Down
12 changes: 6 additions & 6 deletions android/lab/src/main/kotlin/com/fundamental/lab/RecipeExport.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
package com.fundamental.lab

import com.fundamental.core.recipes.FieldRecipe
import com.fundamental.core.recipes.FieldPattern
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File

/**
* Recipe save/export — the FieldLab "export the current recipe" action. `FieldRecipe` is
* Recipe save/export — the FieldLab "export the current recipe" action. `FieldPattern` is
* `@Serializable`, so a recipe *value* round-trips: decoding the export reproduces an equal
* `FieldRecipe` (default-valued fields are omitted, so the bytes need not match the canon's
* `FieldPattern` (default-valued fields are omitted, so the bytes need not match the canon's
* `data/recipes.json` exactly). Mirror of the Swift FieldLab's recipe export.
*/
object RecipeExport {
private val json = Json { prettyPrint = true; encodeDefaults = false }

/** Serialize a recipe to canonical pretty JSON. */
fun toJson(recipe: FieldRecipe): String = json.encodeToString(recipe)
fun toJson(recipe: FieldPattern): String = json.encodeToString(recipe)

/** Write a recipe to [file] as JSON (creating parent dirs); returns the file written. */
fun write(recipe: FieldRecipe, file: File): File {
fun write(recipe: FieldPattern, file: File): File {
file.parentFile?.mkdirs()
file.writeText(toJson(recipe))
return file
}

/** A safe default filename for a recipe (`recipe-<id>.json`). */
fun defaultFileName(recipe: FieldRecipe): String =
fun defaultFileName(recipe: FieldPattern): String =
"recipe-${recipe.id.ifBlank { recipe.name }}.json".replace(Regex("[^A-Za-z0-9._-]"), "_")
}
10 changes: 5 additions & 5 deletions android/lab/src/main/kotlin/com/fundamental/lab/Scenes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.fundamental.lab
import com.fundamental.core.engine.Body
import com.fundamental.core.engine.Box
import com.fundamental.core.math.Vec3
import com.fundamental.core.recipes.FieldRecipe
import com.fundamental.core.recipes.compileRecipe
import com.fundamental.core.recipes.FieldPattern
import com.fundamental.core.recipes.compilePattern
import com.fundamental.core.runtime.FieldController
import kotlin.math.PI
import kotlin.math.cos
Expand All @@ -23,7 +23,7 @@ class LabScene(
val formation: String = "ambient",
val density: Int = 600,
/** The recipe this scene was built from, if any — lets the inspector export it. */
val recipe: FieldRecipe? = null,
val recipe: FieldPattern? = null,
val setup: (c: FieldController, w: Float, h: Float) -> Unit = { _, _, _ -> },
)

Expand Down Expand Up @@ -73,11 +73,11 @@ private fun recipeMode(render: List<String>): LabMode = when (render.firstOrNull
}

/** Compile a canon recipe into a runnable scene: place its compiled bodies, prep matter as needed. */
fun recipeScene(r: FieldRecipe): LabScene = LabScene(
fun recipeScene(r: FieldPattern): LabScene = LabScene(
name = r.name, blurb = r.intent, token = r.primitives.firstOrNull(), renderMode = recipeMode(r.render),
recipe = r,
) { c, w, h ->
val compiled = compileRecipe(r)
val compiled = compilePattern(r)
val n = compiled.bodies.size.coerceAtLeast(1)
compiled.bodies.forEachIndexed { i, reg ->
val b = reg.makeBody()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package com.fundamental.lab

import com.fundamental.core.recipes.AccessibilityRecipe
import com.fundamental.core.recipes.BodyRecipe
import com.fundamental.core.recipes.FieldRecipe
import com.fundamental.core.recipes.FieldPattern
import com.fundamental.core.recipes.RelationshipRecipe
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals

// RecipeExport — the FieldLab "export the current recipe" action. A @Serializable FieldRecipe value
// RecipeExport — the FieldLab "export the current recipe" action. A @Serializable FieldPattern value
// must round-trip: RecipeExport.toJson(it) → decode → an EQUAL recipe. Default-valued fields are
// omitted from the bytes (encodeDefaults = false), so this checks the *value* survives, not the bytes.
// Built from a minimal hand-authored recipe so the test carries no resource/classpath dependency on
Expand All @@ -19,7 +19,7 @@ class RecipeExportTests {
// A plain decoder: tolerant of missing keys (the omitted defaults), strict on shape otherwise.
private val json = Json { ignoreUnknownKeys = true }

private fun minimalRecipe(): FieldRecipe = FieldRecipe(
private fun minimalRecipe(): FieldPattern = FieldPattern(
id = "test-orbit",
name = "Test Orbit",
intent = "verify the export round-trip",
Expand All @@ -44,14 +44,14 @@ class RecipeExportTests {
fun toJsonRoundTripsToAnEqualRecipe() {
val original = minimalRecipe()
val text = RecipeExport.toJson(original)
val decoded = json.decodeFromString(FieldRecipe.serializer(), text)
val decoded = json.decodeFromString(FieldPattern.serializer(), text)
assertEquals(original, decoded, "the decoded recipe equals the original")
}

@Test
fun roundTripPreservesIdNamePrimitivesBodiesAndRender() {
val original = minimalRecipe()
val decoded = json.decodeFromString(FieldRecipe.serializer(), RecipeExport.toJson(original))
val decoded = json.decodeFromString(FieldPattern.serializer(), RecipeExport.toJson(original))
assertEquals(original.id, decoded.id, "id preserved")
assertEquals(original.name, decoded.name, "name preserved")
assertEquals(original.primitives, decoded.primitives, "primitives preserved")
Expand Down
6 changes: 3 additions & 3 deletions swift/Sources/FieldLab/FieldLabApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ struct LabRootView: View {
}
}
Section("The canon — 64 recipes") {
ForEach(FieldRecipes.all, id: \.id) { r in
ForEach(FieldPatterns.all, id: \.id) { r in
SidebarRow(symbol: "book.closed", title: r.name, subtitle: r.intent)
.tag(LabSelection.recipe(r.id))
}
Expand Down Expand Up @@ -360,7 +360,7 @@ struct LabRootView: View {
.foregroundStyle(.secondary)
.padding(.top, 2)
}
if case .recipe(let id) = selection, let r = FieldRecipes.recipe(id: id) {
if case .recipe(let id) = selection, let r = FieldPatterns.recipe(id: id) {
recipeDetails(r)
}
Text("click → burst · drag → flow · hover a card → engage · adjust the formula in the inspector ›")
Expand All @@ -378,7 +378,7 @@ struct LabRootView: View {
/// The recipe's canon lanes, straight from the locked data: tokens execute, metrics
/// measure, accessibility survives without motion.
@ViewBuilder
private func recipeDetails(_ r: FieldRecipe) -> some View {
private func recipeDetails(_ r: FieldPattern) -> some View {
VStack(alignment: .leading, spacing: 3) {
// the token lane, each in its force's identity color — the same hue the
// cards carrying that token wear
Expand Down
4 changes: 2 additions & 2 deletions swift/Sources/FieldLabKit/Scenes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ public enum LabScenes {

/// The canon, runnable: any of the locked 64 recipes compiled into live bodies.
public static func recipe(_ id: String, width: Float = 1, height: Float = 1) -> LabScene? {
guard let r = FieldRecipes.recipe(id: id) else { return nil }
let compiled = compileRecipe(r)
guard let r = FieldPatterns.recipe(id: id) else { return nil }
let compiled = compilePattern(r)
// lay the recipe's bodies out on a ring around the canvas centre
let n = compiled.bodies.count
var cards: [CardSpec] = []
Expand Down
Loading
Loading