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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ a git tag (see [RELEASING.md](RELEASING.md)).

## [Unreleased]

### 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.

## [0.9.4] — 2026-07-10

### Added
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/focus-substrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { createField } from './engine/field.ts';
import type { FieldHost } from './engine/host.ts';
import type { FocusEvent } from './engine/types.ts';
import { FOCUS_WELL } from './recipes/focus.ts';
import { validateRecipe, EXPERIMENTAL_PATTERNS } from './recipes/index.ts';
import { validatePattern, EXPERIMENTAL_PATTERNS } from './recipes/index.ts';
import { FIELD_PATTERNS } from './recipes/catalog.ts';

function drivableHost(reducedMotion = false): { host: FieldHost; step: (frames: number) => void } {
Expand Down Expand Up @@ -212,7 +212,7 @@ test('focus well (active): a focused attract body gathers matter tighter than an
});

test('FOCUS_WELL Pattern validates and lives in EXPERIMENTAL_PATTERNS, not the locked 64', () => {
assert.deepEqual(validateRecipe(FOCUS_WELL), [], 'the focus-well Pattern is valid');
assert.deepEqual(validatePattern(FOCUS_WELL), [], 'the focus-well Pattern is valid');
assert.ok(EXPERIMENTAL_PATTERNS.some((p) => p.id === 'focus-well'), 'surfaced in EXPERIMENTAL_PATTERNS');
assert.ok(!FIELD_PATTERNS.some((p) => p.id === 'focus-well'), 'NEVER in the locked 64 FIELD_PATTERNS');
});
4 changes: 2 additions & 2 deletions packages/core/src/inspect/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CONTRACTS } from '../contracts/index.ts';
import { PASSPORTS } from '../contracts/passport.ts';
import { AGENT_CONTRACTS } from '../agents/index.ts';
import { VISUAL_CONTRACTS } from '../visual/index.ts';
import { RECIPE_CONTRACTS, FIELD_RECIPES } from '../recipes/index.ts';
import { PATTERN_CONTRACTS, FIELD_RECIPES } from '../recipes/index.ts';
import { EXPERIMENTS } from '../conformance/experiments.ts';
import { allForces } from '../conformance/run.ts';

Expand All @@ -35,7 +35,7 @@ export function systemReport(): SystemReport {
forces: tokens.length,
passports: Object.keys(PASSPORTS).length,
conformanceExperiments: EXPERIMENTS.length,
contracts: CONTRACTS.length + AGENT_CONTRACTS.length + VISUAL_CONTRACTS.length + RECIPE_CONTRACTS.length,
contracts: CONTRACTS.length + AGENT_CONTRACTS.length + VISUAL_CONTRACTS.length + PATTERN_CONTRACTS.length,
agentTypes: AGENT_CONTRACTS.length,
recipes: FIELD_RECIPES.length,
forcesMissingPassport: tokens.filter((t) => !PASSPORTS[t]),
Expand Down
23 changes: 14 additions & 9 deletions packages/core/src/recipes/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/**
* The field-recipe catalog (authoring-and-recipes §7) — the data file for the 64 portable
* `FieldRecipe`s that map the four-field translation model onto practical interface patterns, grouped
* into four tiers (`RECIPE_TIERS`). Each doubles as a worked example and a conformance fixture: every
* into four tiers (`PATTERN_TIERS`). Each doubles as a worked example and a conformance fixture: every
* runtime token is a real passported force, every render layer + diagnostic is a known mode, the
* declared primitives match the body tokens, and no primitive is a diagnostic/metric/concept/condition
* — so `validateRecipe` passes for all of them (the conformance test enforces this).
* — so `validatePattern` passes for all of them (the conformance test enforces this).
*
* Lanes are kept separate: `primitives` are strict runtime tokens; `concepts`/`conditions` (layered in
* by id below) are product language and activation logic; `metrics`/`diagnostics` are measured state
* and inspection modes. These are classification + authoring artifacts — they compose existing
* primitives and add NO new engine behavior. Eight ids are the recommended first-release set.
*/
import type { FieldRecipe, RecipeTier } from './schema.ts';
import type { FieldRecipe, PatternTier } from './schema.ts';
import { EXPERIMENTAL_RECIPES } from './wayfinding.ts';

// ── Gravity: priority, convergence, hierarchy ───────────────────────────────────────
Expand Down Expand Up @@ -1500,7 +1500,7 @@ const CONDITIONS: Readonly<Record<string, readonly string[]>> = {
};

/** Layer tier + status + the concept/condition lanes onto a tier's raw records. */
const decorate = (recipes: readonly FieldRecipe[], tier: RecipeTier): FieldRecipe[] =>
const decorate = (recipes: readonly FieldRecipe[], tier: PatternTier): FieldRecipe[] =>
recipes.map((r) => ({
...r,
tier,
Expand All @@ -1509,22 +1509,22 @@ const decorate = (recipes: readonly FieldRecipe[], tier: RecipeTier): FieldRecip
...(CONDITIONS[r.id] ? { conditions: [...CONDITIONS[r.id]!] } : {}),
}));

export interface RecipeTierGroup {
key: RecipeTier;
export interface PatternTierGroup {
key: PatternTier;
label: string;
recipes: readonly FieldRecipe[];
}

/** The four catalog tiers, in order — the navigable structure over {@link FIELD_RECIPES}. */
export const RECIPE_TIERS: readonly RecipeTierGroup[] = [
export const PATTERN_TIERS: readonly PatternTierGroup[] = [
{ key: 'core', label: 'Core — interface & accessibility', recipes: decorate(TIER_CORE, 'core') },
{ key: 'applied', label: 'Applied — product, workflow & collaboration', recipes: decorate(TIER_PRODUCT, 'applied') },
{ key: 'systems', label: 'Systems — safety, provenance & governance', recipes: decorate(TIER_SYSTEMS, 'systems') },
{ key: 'operational', label: 'Operational — multi-actor, adaptive & live', recipes: decorate(TIER_ENTERPRISE, 'operational') },
];

/** The full field-pattern catalog (authoring §7) — 64 patterns across four tiers, in catalog order. */
export const FIELD_PATTERNS: readonly FieldRecipe[] = RECIPE_TIERS.flatMap((t) => t.recipes);
export const FIELD_PATTERNS: readonly FieldRecipe[] = PATTERN_TIERS.flatMap((t) => t.recipes);

/** @deprecated Renamed to {@link FIELD_PATTERNS} (recipe → Pattern); removed at 1.0. */
export const FIELD_RECIPES: readonly FieldRecipe[] = FIELD_PATTERNS;
Expand Down Expand Up @@ -1555,6 +1555,11 @@ export const FIRST_RELEASE_RECIPES: readonly FieldRecipe[] = FIRST_RELEASE_RECIP
);

/** Look up a recipe by id — the canonical 64 first, then the experimental set (undefined if unknown). */
export function recipeById(id: string): FieldRecipe | undefined {
export function patternById(id: string): FieldRecipe | undefined {
return FIELD_RECIPES.find((r) => r.id === id) ?? EXPERIMENTAL_RECIPES.find((r) => r.id === id);
}

// ── Deprecated aliases (recipe → Pattern rename, phase 4; removed at 1.0) ──────────────────────
/** @deprecated Renamed to {@link PatternTierGroup}. */ export type RecipeTierGroup = PatternTierGroup;
/** @deprecated Renamed to {@link PATTERN_TIERS}. */ export const RECIPE_TIERS = PATTERN_TIERS;
/** @deprecated Renamed to {@link patternById}. */ export const recipeById = patternById;
10 changes: 5 additions & 5 deletions packages/core/src/recipes/charge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { validateRecipe, serializeRecipe, parseRecipe, primitivesOf, FIELD_MODES } from './schema.ts';
import { validatePattern, serializePattern, parseRecipe, primitivesOf, FIELD_MODES } from './schema.ts';
import { CONTOUR_CHARGE, CHARGE_RECIPES } from './charge.ts';
import { EXPERIMENTAL_RECIPES } from './wayfinding.ts';
import { FIELD_RECIPES, recipeById } from './catalog.ts';
import { FIELD_RECIPES, patternById } from './catalog.ts';
import { compileRecipe } from './compile.ts';
import { passportFor } from '../contracts/passport.ts';

test('contour-charge validates: real tokens, known layers, primitives match body tokens', () => {
const problems = validateRecipe(CONTOUR_CHARGE);
const problems = validatePattern(CONTOUR_CHARGE);
assert.deepEqual(problems, [], problems.map((p) => `${p.path} ${p.issue}`).join(', '));
assert.deepEqual(CONTOUR_CHARGE.primitives, primitivesOf(CONTOUR_CHARGE.bodies));
for (const p of CONTOUR_CHARGE.primitives) {
Expand All @@ -30,15 +30,15 @@ test('the lane rule holds: the recipe is contour-charge, never bare "charge" (th

test('registered experimentally: in EXPERIMENTAL_RECIPES, resolvable by id, outside the 64', () => {
assert.ok(EXPERIMENTAL_RECIPES.some((r) => r.id === 'contour-charge'));
assert.equal(recipeById('contour-charge'), CONTOUR_CHARGE);
assert.equal(patternById('contour-charge'), CONTOUR_CHARGE);
assert.ok(!FIELD_RECIPES.includes(CONTOUR_CHARGE), 'the canonical 64 is undisturbed');
assert.equal(CHARGE_RECIPES.length, 1);
});

test('compiles and round-trips', () => {
const plan = compileRecipe(CONTOUR_CHARGE);
assert.ok(plan.bodies.length === 1, 'one vessel body');
assert.deepEqual(parseRecipe(serializeRecipe(CONTOUR_CHARGE)), CONTOUR_CHARGE);
assert.deepEqual(parseRecipe(serializePattern(CONTOUR_CHARGE)), CONTOUR_CHARGE);
});

test('the recipe is self-executing: the vessel body compiles with its data-when gate (#370)', () => {
Expand Down
56 changes: 34 additions & 22 deletions packages/core/src/recipes/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
* bindings; diagnostics become inspector/render toggles; conditions become activation gates;
* accessibility becomes a reduced-motion output plan.
*/
import type { BodyRecipe, FieldRecipe } from './schema.ts';
import type { BodyPattern, FieldRecipe } from './schema.ts';

/** The CSS feedback variable a metric writes to (`attention` → `--field-attention`). */
export function metricVar(metric: string): string {
return `--field-${metric}`;
}

/** The data-* attributes a single recipe body authors onto an element (token lane → behavior). */
export function recipeBodyAttributes(b: BodyRecipe): Record<string, string> {
export function patternBodyAttributes(b: BodyPattern): Record<string, string> {
const a: Record<string, string> = { 'data-body': b.body };
if (b.strength != null) a['data-strength'] = String(b.strength);
if (b.range != null) a['data-range'] = String(b.range);
Expand All @@ -33,7 +33,7 @@ export function recipeBodyAttributes(b: BodyRecipe): Record<string, string> {
* layer drives. One matter mode owns the underlay; overlay readings stack additively; heatmap
* is its own toggle. Layers with no executable surface yet are NAMED in `unapplied` — never
* silently dropped (the no-silent-caps rule). */
export interface RecipeRenderPlan {
export interface PatternRenderPlan {
/** the underlay matter mode (`setRender`), or null to leave the field's current mode alone. */
underlay: string | null;
/** the additive overlay reading stack (`setOverlay`); empty = 'off'. */
Expand All @@ -52,7 +52,7 @@ const OVERLAY_READINGS = new Set(['streamlines', 'force-vectors', 'field-lines',
/** Derive the render plan from a recipe's declared layers (pure; `particles` is the base matter
* layer and maps to `dots`). `streamlines` prefers the overlay (it reads over content) unless it
* is the recipe's ONLY layer, where the underlay render mode of the same name serves. */
export function recipeRenderPlan(layers: readonly string[]): RecipeRenderPlan {
export function patternRenderPlan(layers: readonly string[]): PatternRenderPlan {
let underlay: string | null = null;
const overlay: string[] = [];
let heatmap = false;
Expand All @@ -74,27 +74,27 @@ export function recipeRenderPlan(layers: readonly string[]): RecipeRenderPlan {
}

/** One compiled body: the attribute set + the runtime tokens it carries. */
export interface RecipeBodyRegistration {
export interface PatternBodyRegistration {
attributes: Record<string, string>;
tokens: string[];
}

/** A relationship the recipe declares (from/to are conceptual endpoints, resolved at apply time). */
export interface RecipeRelationshipRegistration {
export interface PatternRelationshipRegistration {
from: string;
to: string;
type: string;
strength?: number;
}

/** A metric → feedback-variable binding (the metric lane becoming measurable state). */
export interface RecipeFeedbackBinding {
export interface PatternFeedbackBinding {
metric: string;
var: string;
}

/** The reduced-motion output plan — what the runtime renders when motion is reduced (not just prose). */
export interface RecipeReducedMotionPlan {
export interface PatternReducedMotionPlan {
reducedMotion: string;
meaningWithoutMotion: string;
/** the static surfaces the runtime should render in place of motion. */
Expand All @@ -105,15 +105,15 @@ export interface RecipeReducedMotionPlan {
export interface CompiledPattern {
id: string;
recipe: FieldRecipe;
bodies: RecipeBodyRegistration[];
relationships: RecipeRelationshipRegistration[];
feedback: RecipeFeedbackBinding[];
bodies: PatternBodyRegistration[];
relationships: PatternRelationshipRegistration[];
feedback: PatternFeedbackBinding[];
diagnostics: string[];
metrics: string[];
conditions: string[];
/** the executable render plan (#370) — what applyRecipe drives when given a field target. */
render: RecipeRenderPlan;
reducedMotion: RecipeReducedMotionPlan;
render: PatternRenderPlan;
reducedMotion: PatternReducedMotionPlan;
}

/** @deprecated Renamed to {@link CompiledPattern} (recipe → Pattern); removed at 1.0. */
Expand Down Expand Up @@ -143,13 +143,13 @@ export function compilePattern(r: FieldRecipe): CompiledPattern {
return {
id: r.id,
recipe: r,
bodies: r.bodies.map((b) => ({ attributes: recipeBodyAttributes(b), tokens: tokensOf(b.body) })),
bodies: r.bodies.map((b) => ({ attributes: patternBodyAttributes(b), tokens: tokensOf(b.body) })),
relationships: (r.relationships ?? []).map((rel) => ({ from: rel.from, to: rel.to, type: rel.type, strength: rel.strength })),
feedback: r.metrics.map((m) => ({ metric: m, var: metricVar(m) })),
diagnostics: [...r.diagnostics],
metrics: [...r.metrics],
conditions: [...(r.conditions ?? [])],
render: recipeRenderPlan(r.render ?? []),
render: patternRenderPlan(r.render ?? []),
reducedMotion: {
reducedMotion: r.accessibility.reducedMotion,
meaningWithoutMotion: r.accessibility.meaningWithoutMotion,
Expand All @@ -167,28 +167,40 @@ const attrsToString = (a: Record<string, string>): string =>
* Emit the `[data-body]` markup a recipe authors — a `<field-root>` plus one element per body. This is
* the copy-paste authoring for a recipe; drop it on a page and the field runs the recipe.
*/
export function recipeToMarkup(r: FieldRecipe): string {
const bodies = r.bodies.map((b) => ` <div ${attrsToString(recipeBodyAttributes(b))}></div>`).join('\n');
export function patternToMarkup(r: FieldRecipe): string {
const bodies = r.bodies.map((b) => ` <div ${attrsToString(patternBodyAttributes(b))}></div>`).join('\n');
return `<field-root></field-root>\n${bodies}`;
}

const pascal = (id: string): string =>
id.split(/[^a-z0-9]+/i).filter(Boolean).map((s) => s[0]!.toUpperCase() + s.slice(1)).join('') || 'Field';

/** A recipe's copy-paste authoring across the three surfaces. */
export interface RecipeAuthoring {
export interface PatternAuthoring {
html: string;
webComponent: string;
react: string;
}

/** Emit a recipe's authoring for native HTML, the web component, and React (pure). */
export function recipeAuthoring(r: FieldRecipe): RecipeAuthoring {
const bodyLines = r.bodies.map((b) => ` <div ${attrsToString(recipeBodyAttributes(b))}></div>`).join('\n');
const reactBodies = r.bodies.map((b) => ` <div ${attrsToString(recipeBodyAttributes(b))} />`).join('\n');
export function patternAuthoring(r: FieldRecipe): PatternAuthoring {
const bodyLines = r.bodies.map((b) => ` <div ${attrsToString(patternBodyAttributes(b))}></div>`).join('\n');
const reactBodies = r.bodies.map((b) => ` <div ${attrsToString(patternBodyAttributes(b))} />`).join('\n');
return {
html: recipeToMarkup(r),
html: patternToMarkup(r),
webComponent: `<script type="module">\n import '@fundamental-engine/elements';\n</script>\n\n<field-root></field-root>\n${bodyLines}`,
react: `import { FieldField } from '@fundamental-engine/react';\n\nexport default function ${pascal(r.id)}() {\n return (\n <FieldField>\n${reactBodies}\n </FieldField>\n );\n}`,
};
}

// ── Deprecated aliases (recipe → Pattern rename, phase 4; removed at 1.0) ──────────────────────
/** @deprecated Renamed to {@link PatternRenderPlan}. */ export type RecipeRenderPlan = PatternRenderPlan;
/** @deprecated Renamed to {@link PatternBodyRegistration}. */ export type RecipeBodyRegistration = PatternBodyRegistration;
/** @deprecated Renamed to {@link PatternRelationshipRegistration}. */ export type RecipeRelationshipRegistration = PatternRelationshipRegistration;
/** @deprecated Renamed to {@link PatternFeedbackBinding}. */ export type RecipeFeedbackBinding = PatternFeedbackBinding;
/** @deprecated Renamed to {@link PatternReducedMotionPlan}. */ export type RecipeReducedMotionPlan = PatternReducedMotionPlan;
/** @deprecated Renamed to {@link PatternAuthoring}. */ export type RecipeAuthoring = PatternAuthoring;
/** @deprecated Renamed to {@link patternBodyAttributes}. */ export const recipeBodyAttributes = patternBodyAttributes;
/** @deprecated Renamed to {@link patternRenderPlan}. */ export const recipeRenderPlan = patternRenderPlan;
/** @deprecated Renamed to {@link patternToMarkup}. */ export const recipeToMarkup = patternToMarkup;
/** @deprecated Renamed to {@link patternAuthoring}. */ export const recipeAuthoring = patternAuthoring;
Loading
Loading