From 2fc81afb60daebdc23864b32c5ef85cffddc6394 Mon Sep 17 00:00:00 2001 From: Zach Shallbetter Date: Mon, 20 Jul 2026 12:31:35 -0700 Subject: [PATCH 1/2] docs: retire the API freeze, keep the removal check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The surface was described as frozen for 0.x. That list has been edited 40 times. A stability promise revised whenever it becomes inconvenient is not a promise — it is a claim that quietly stops being true, and it forced an is-this-breaking debate on changes that were plainly improvements. The check is kept, because it does something narrower and worth having: it fails when a listed symbol stops being exported, stops being declared, or stops registering. It has never failed on an addition and cannot. The point was never to prevent removals, only to make them deliberate — a removed export should be a decision with a migration note, not something a consumer finds before CI does. FROZEN_* → PROTECTED_* across the data, the type gate, the runtime check, the parity generator and four site pages. Failure messages now name what happened and what to do about it: drop the entry in the same change and add a CHANGELOG note. Verified the check can still fail: injecting a phantom protected symbol exits 1 with the new message. A gate that cannot fail is not a gate. Two inaccuracies fixed in passing. The docs page claimed the entries 'fail the build if they change' — only removal fails. The canonical doc cited 'the frozen 17' where the check reports 20. typecheck clean; full suite passes; check:api, check:docs, check:links green; site builds and the published page now reads '20 entries that fail the build if they disappear'. --- CHANGELOG.md | 1 + apps/site/src/components/ApiStamp.astro | 16 ++--- apps/site/src/pages/docs/api/attributes.astro | 4 +- apps/site/src/pages/docs/api/stability.astro | 29 ++++---- docs/canonical/api-stability.md | 71 ++++++++++--------- scripts/api-surface.data.mjs | 36 +++++----- scripts/api-surface.ts | 4 +- scripts/check-api-surface.mjs | 30 ++++---- scripts/gen-parity-matrix.mjs | 2 +- 9 files changed, 103 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8780b741..704cf2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ a git tag (see [RELEASING.md](RELEASING.md)). ## [Unreleased] - **CI: replaced the third-party path-filter action with plain git.** GitHub began forcing Node 24 onto actions that target Node 20; the filter action dies silently mid-run and takes the whole gate with it via `conclusion`. A path filter is one `git diff` and does not need a dependency that can break underneath the repository. All nine original path patterns preserved and verified against a fixture of paths that must and must not match; the replacement also logs which files it saw and which way it decided. +- **Retired the API "freeze" — it was never real.** The list was described as frozen for `0.x` and had been edited 40 times; a stability promise revised whenever it becomes inconvenient is not a promise, it is a claim that quietly stops being true. The check itself is kept and renamed to what it actually does: it protects listed symbols from **silent removal**. It has never failed on an addition and cannot — new exports land freely. Removing or renaming a listed symbol is allowed and expected; it just has to be deliberate, which means dropping the entry in the same change and adding a migration note so consumers are told. `FROZEN_*` → `PROTECTED_*` throughout, with failure messages that now say what happened and what to do. Two inaccuracies fixed in passing: the docs page claimed entries "fail the build if they change" (only removal fails) and the canonical doc still cited "the frozen 17" where the check reports **20**. - **Documentation accuracy pass.** `docs/README.md` now indexes `docs/method/` (a whole top-level docs directory that was unlisted) and the `planning/world-substrate/` program. `PLAN.md`'s status header said *"Nothing in F1–F5 or C2 is built"* — false since Stage 1 landed — and the document contradicted itself, carrying both a current "Not started" line and a superseded one, plus a note calling F1.4 equivalence "pending" when it is measured. `CLAUDE.md` listed **17** frozen API entries where `check:api` reports **20**, and its layout table omitted `apps/observatory` entirely. Corrected, with the authoritative source named in each case so the next drift is self-diagnosing. ### Added diff --git a/apps/site/src/components/ApiStamp.astro b/apps/site/src/components/ApiStamp.astro index bce5a221..e255eacb 100644 --- a/apps/site/src/components/ApiStamp.astro +++ b/apps/site/src/components/ApiStamp.astro @@ -9,10 +9,10 @@ // (disambiguate the two doors) // → experimental import { - FROZEN_VALUES, - FROZEN_TYPES, - FROZEN_ELEMENTS, - FROZEN_BODY_ATTR, + PROTECTED_VALUES, + PROTECTED_TYPES, + PROTECTED_ELEMENTS, + PROTECTED_BODY_ATTR, EXPERIMENTAL, } from '../../../../scripts/api-surface.data.mjs'; @@ -38,12 +38,12 @@ if (name) { const byPkg = (rows: T[]) => pkg ? rows.filter((r) => r.pkg === pkg) : rows; const hit = - byPkg(FROZEN_VALUES).find((v: { name: string }) => v.name === name) ?? - byPkg(FROZEN_TYPES).find((t: { name: string }) => t.name === name) ?? - byPkg(FROZEN_ELEMENTS).find((e: { tag: string }) => e.tag === name); + byPkg(PROTECTED_VALUES).find((v: { name: string }) => v.name === name) ?? + byPkg(PROTECTED_TYPES).find((t: { name: string }) => t.name === name) ?? + byPkg(PROTECTED_ELEMENTS).find((e: { tag: string }) => e.tag === name); if (hit) { stamp = { status: 'frozen', label: `frozen · ${hit.pkg}`, title: hit.note }; - } else if (name === FROZEN_BODY_ATTR) { + } else if (name === PROTECTED_BODY_ATTR) { stamp = { status: 'frozen', label: 'frozen · attribute contract', diff --git a/apps/site/src/pages/docs/api/attributes.astro b/apps/site/src/pages/docs/api/attributes.astro index 5169e5e0..9d29cb0f 100644 --- a/apps/site/src/pages/docs/api/attributes.astro +++ b/apps/site/src/pages/docs/api/attributes.astro @@ -3,7 +3,7 @@ import DocsLayout from '../../../layouts/DocsLayout.astro'; import { ATTRS, WRITEBACK, FIELD_ROOT_ATTRS } from '../../../lib/docs-api.ts'; import SeeAlso from '../../../components/SeeAlso.astro'; import ApiStamp from '../../../components/ApiStamp.astro'; -import { FROZEN_BODY_ATTR } from '../../../../../../scripts/api-surface.data.mjs'; +import { PROTECTED_BODY_ATTR } from '../../../../../../scripts/api-surface.data.mjs'; import { MANUAL_FORCES } from '@fundamental-engine/core'; import '../../../styles/force-glyphs.css'; @@ -35,7 +35,7 @@ const families = [
{a.name} - {a.name === FROZEN_BODY_ATTR && } + {a.name === PROTECTED_BODY_ATTR && } {a.type}{a.def ? <> · default {a.def} : null}
{a.desc}
diff --git a/apps/site/src/pages/docs/api/stability.astro b/apps/site/src/pages/docs/api/stability.astro index 825e36fc..b3127207 100644 --- a/apps/site/src/pages/docs/api/stability.astro +++ b/apps/site/src/pages/docs/api/stability.astro @@ -3,27 +3,28 @@ import DocsLayout from '../../../layouts/DocsLayout.astro'; import ApiStamp from '../../../components/ApiStamp.astro'; import StatusChip from '../../../components/StatusChip.astro'; import { - FROZEN_VALUES, FROZEN_TYPES, FROZEN_ELEMENTS, FROZEN_BODY_ATTR, EXPERIMENTAL, COMPAT_RULES, + PROTECTED_VALUES, PROTECTED_TYPES, PROTECTED_ELEMENTS, PROTECTED_BODY_ATTR, EXPERIMENTAL, COMPAT_RULES, } from '../../../../../../scripts/api-surface.data.mjs'; // This page renders the SAME data the lock enforces (scripts/api-surface.data.mjs), so it can't drift // from `pnpm check:api`. The canonical prose contract is docs/canonical/api-stability.md. const statusClass = (s: string) => `st st-${s}`; -const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS.length + 1; +const protectedCount = PROTECTED_VALUES.length + PROTECTED_TYPES.length + PROTECTED_ELEMENTS.length + 1; ---

API reference

API stability

- The architecture has stopped being fluid. The surface below is frozen for 0.x: - each symbol stays exported, from the same package, with the same kind, and does not break for the - life of the 0.x line. The freeze is enforced in CI by pnpm check:api — - {frozenCount} entries that fail the build if they change. + Pre-1.0, the surface evolves — new exports land freely and the check never fails on an addition. What the surface below is protected from is silent removal: + each symbol stays exported, from the same package, with the same kind. Enforced in CI by + pnpm check:api — {protectedCount} entries that fail the build if they disappear. + Removing one is allowed and expected; it just has to be deliberate, and it comes with a changelog + migration note.

@@ -37,7 +38,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS - {FROZEN_VALUES.map((v: any) => ( + {PROTECTED_VALUES.map((v: any) => ( @@ -49,7 +50,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS

createField has two doors on purpose: the core primitive is renderer-agnostic and host-required; @fundamental-engine/vanilla re-exports the host-bundled convenience so the - no-framework path stays one call. Both are frozen. + no-framework path stays one call. Both are protected.

@@ -58,7 +59,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
SymbolPackageWhat it is
{v.name} value {v.pkg}
- {FROZEN_TYPES.map((t: any) => ( + {PROTECTED_TYPES.map((t: any) => ( ))} @@ -70,13 +71,13 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
TypePackageWhat it is
{t.name} type {t.pkg}{t.note}
- {FROZEN_ELEMENTS.map((e: any) => ( + {PROTECTED_ELEMENTS.map((e: any) => ( ))} - + - +
SurfacePackageWhat it is
<{e.tag}> element {e.pkg}{e.note}
{FROZEN_BODY_ATTR} attribute {PROTECTED_BODY_ATTR} attribute core BODY_SELECTORThe body contract. "Every element is a body" via the {FROZEN_BODY_ATTR} attribute on ordinary elements.The body contract. "Every element is a body" via the {PROTECTED_BODY_ATTR} attribute on ordinary elements.
@@ -89,7 +90,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
-

Experimental — not frozen

+

Experimental — no guarantee

These carry no stability guarantee and may change shape or be removed in any release. Some have exported building blocks today — those are shipped-but-unfrozen: present in the diff --git a/docs/canonical/api-stability.md b/docs/canonical/api-stability.md index 7ebc8f0c..25a77e79 100644 --- a/docs/canonical/api-stability.md +++ b/docs/canonical/api-stability.md @@ -1,7 +1,7 @@ > **Status: canonical.** -> This document defines the **frozen public API surface for Fundamental `0.x`** and the compatibility -> rules that govern it: what is stable, what is experimental, how versions move, and how the alias -> window works. It is enforced in code by `scripts/api-surface.ts` (typechecked) and +> This document defines the **removal-protected public API surface** and the compatibility rules that +> govern it: what CI protects, what carries no guarantee, how versions move, and how the alias window +> works. It is **not** a freeze — see below. It is enforced in code by `scripts/api-surface.ts` (typechecked) and > `scripts/check-api-surface.mjs` (runtime), both run by `pnpm check:api` in CI. The shared machine > data is [`scripts/api-surface.data.mjs`](../../scripts/api-surface.data.mjs); the live reference page > is [`/docs/api/stability`](../../apps/site/src/pages/docs/api/stability.astro). Related contracts: @@ -10,15 +10,22 @@ # Fundamental API stability -The architecture has stopped being fluid. Recipes execute, data binds, the inspector reads the live -runtime, the gallery runs, the starter installs, the studies are data-driven. Before adding more -capability, the public surface is **frozen for `0.x`** so consumers can build against it without the -ground shifting. +**This is not a freeze, and calling it one was inaccurate.** Pre-1.0 the surface evolves; the list +below has been edited dozens of times as the engine improved, and it should be. A promise of stability +that is revised whenever it becomes inconvenient is not a promise — it is a claim that quietly stops +being true. -A symbol is "frozen" when it appears in the table below. Freezing means: it stays exported, from the -same package, with the same kind (value / type / element), and its shape does not break, for the life -of the `0.x` line. The freeze is mechanically enforced — `pnpm check:api` fails the build if a frozen -symbol is removed, renamed, moved between packages, or changes kind. +What the list actually does is narrower and worth keeping. A symbol on it is **protected from silent +removal**: if it stops being exported, stops being declared, or stops registering, `pnpm check:api` +fails the build. Nothing else. **The check never fails on an addition** — new exports, new optional +fields and new patterns land freely, and always have. + +The point is not to prevent removals. It is to make them *deliberate*: a removed export should be a +decision recorded in the CHANGELOG, not something a consumer discovers before we do. + +**Removing or renaming a listed symbol is allowed and expected.** Do it in one change: drop the entry +from [`scripts/api-surface.data.mjs`](../../scripts/api-surface.data.mjs), and add a CHANGELOG +migration note so consumers get told. > Package npm names: core is published as **`@fundamental-engine/core`**; the rest are > `@fundamental-engine/dom`, `@fundamental-engine/elements`, `@fundamental-engine/react`, `@fundamental-engine/vanilla`, @@ -26,7 +33,7 @@ symbol is removed, renamed, moved between packages, or changes kind. > **retired in 0.7.0** — install the specific package you need. > > **0.7.0 migration — `@fundamental-engine/platform` → `@fundamental-engine/dom`.** The DOM-binding -> package was renamed (it *is* the DOM layer); the frozen symbols `browserHost`, `createFieldPlatform`, +> package was renamed (it *is* the DOM layer); the protected symbols `browserHost`, `createFieldPlatform`, > `applyRecipe`, `bindData`, and the `FieldPlatform` type now live in `@fundamental-engine/dom`. This is > the one sanctioned cross-package move of the `0.x` line, gated by the minor bump. `@fundamental-engine/platform` > stays published as a **deprecated alias** that re-exports `dom`, so existing imports keep working; @@ -43,19 +50,19 @@ symbol is removed, renamed, moved between packages, or changes kind. | `browserHost` | `@fundamental-engine/dom` | value | The canonical DOM `FieldHost` for `createField`. | | `browserHost` | `@fundamental-engine/vanilla` | value | Re-export of the platform host for the no-framework path. | | `createFieldPlatform` | `@fundamental-engine/dom` | value | Wires the six native-first registries on a root. | -| `applyPattern` | `@fundamental-engine/dom` | value | Applies a Field Pattern to a live platform. `applyRecipe` is the frozen `@deprecated` alias (removed at `1.0`). | +| `applyPattern` | `@fundamental-engine/dom` | value | Applies a Field Pattern to a live platform. `applyRecipe` is the protected `@deprecated` alias (removed at `1.0`). | | `bindData` | `@fundamental-engine/dom` | value | Binds records → bodies; data drives the field. | -| `compilePattern` | `@fundamental-engine/core` | value | Pure `FieldPattern` → compiled plan (no DOM). `compileRecipe` is the frozen `@deprecated` alias (removed at `1.0`). | +| `compilePattern` | `@fundamental-engine/core` | value | Pure `FieldPattern` → compiled plan (no DOM). `compileRecipe` is the protected `@deprecated` alias (removed at `1.0`). | `createField` has **two doors on purpose**: the core primitive is renderer-agnostic and host-required; `@fundamental-engine/vanilla` re-exports the host-bundled convenience so the no-framework path stays one call. -Both are frozen; the vanilla door must keep auto-supplying `browserHost()`. +Both are protected; the vanilla door must keep auto-supplying `browserHost()`. ### Types | Type | Package | What it is | | --- | --- | --- | -| `FieldPattern` | `@fundamental-engine/core` | The Field Pattern schema. `FieldRecipe` is the frozen `@deprecated` alias (removed at `1.0`). | +| `FieldPattern` | `@fundamental-engine/core` | The Field Pattern schema. `FieldRecipe` is the protected `@deprecated` alias (removed at `1.0`). | | `FieldHost` | `@fundamental-engine/core` | The renderer-agnostic host contract `createField` requires; `browserHost` implements it. | | `FieldPlatform` | `@fundamental-engine/dom` | The surface `createFieldPlatform` returns. | @@ -72,7 +79,7 @@ none will be introduced as the body mechanism. The custom elements are ``) and ``; the pre-rename ``/`` tags are **not** registered (the rename left no element aliases). -## The experimental surface (not frozen) +## The unprotected surface These carry **no** stability guarantee and may change shape or be removed in any release. Some have exported building blocks today — those are *shipped-but-unfrozen*: present in the package, but not part @@ -80,11 +87,11 @@ of the contract until they are added to the table above. | Area | Status | Notes | | --- | --- | --- | -| `FieldHandle` (full surface) | partial | The handle shape is not frozen as a type. Entry points that return it (`createField`, `createBrowserField`) are frozen, but new methods may be added in any patch. | -| `FieldHandle` diagnostic accessors | shipped-unfrozen | `particleCount(): number` and `energy(): { kinetic, thermal, total, count }` ship in `@fundamental-engine/core` and are proxied on ``. Safe to use; not frozen until 1.0. | -| Substrate read API | shipped-unfrozen · EXPERIMENTAL | `query` / `snapshot` / `diff` / `replay` / `projections` (+ `lint`), `data-authority` / `Body.authority`, `createField({ integrator: 'fixed' })`, and the accumulator channels — all shipped across the surfaces but **not** in the frozen 17 and may change shape or be removed. Full reference: [substrate-api.md](substrate-api.md). | +| `FieldHandle` (full surface) | partial | The handle shape is not protected as a type. Entry points that return it (`createField`, `createBrowserField`) are protected, but new methods may be added in any patch. | +| `FieldHandle` diagnostic accessors | shipped-unprotected | `particleCount(): number` and `energy(): { kinetic, thermal, total, count }` ship in `@fundamental-engine/core` and are proxied on ``. Safe to use; not on the protected list. | +| Substrate read API | shipped-unprotected · EXPERIMENTAL | `query` / `snapshot` / `diff` / `replay` / `projections` (+ `lint`), `data-authority` / `Body.authority`, `createField({ integrator: 'fixed' })`, and the accumulator channels — all shipped across the surfaces but **not** on the protected list, and may change shape or be removed. Full reference: [substrate-api.md](substrate-api.md). | | Advanced diagnostics | partial | `DIAGNOSTICS` / `DIAGNOSTIC_LENS` / `draw*` primitives ship but are unfrozen. | -| Performance budget | shipped-unfrozen | `inspectBudget()`, `withinBudget()`, `DEFAULT_BUDGET`, `BudgetFinding` ship in `@fundamental-engine/core`; `FieldPerf` (frame-duration split, adaptive governor) ships shipped-but-unfrozen in `@fundamental-engine/dom` as `createFieldPerf` + `QualityGovernor`. | +| Performance budget | shipped-unprotected | `inspectBudget()`, `withinBudget()`, `DEFAULT_BUDGET`, `BudgetFinding` ship in `@fundamental-engine/core`; `FieldPerf` (frame-duration split, adaptive governor) ships shipped-but-unfrozen in `@fundamental-engine/dom` as `createFieldPerf` + `QualityGovernor`. | | Visual recipe editor | absent | No editor UI; the authoring toolkit is the substrate to build one on. | | GPU / WebGPU backend | planned | A named direction; the 16 shipped render modes are CPU/canvas. | | Multi-root bridge | absent | No API for coordinating multiple `` instances yet. | @@ -93,20 +100,20 @@ of the contract until they are added to the table above. ## Compatibility rules -1. **Pre-1.0 semver.** In `0.x` the **minor** is the breaking position. A breaking change to a frozen +1. **Pre-1.0 semver.** In `0.x` the **minor** is the breaking position. A breaking change to a protected symbol bumps `0.MINOR` (`0.2 → 0.3`); additive and fix-only changes bump the patch. Consumers should pin to `~0.MINOR`. 2. **Additive-only within a minor.** New exports, new optional fields, and new recipes/modes may land - in a patch. Renaming, removing, or changing the signature/shape of a frozen symbol requires a minor + in a patch. Renaming, removing, or changing the signature/shape of a protected symbol requires a minor bump and a migration note. 3. **`createField` keeps both doors.** The host-required core primitive and the host-bundled vanilla convenience are both preserved; the vanilla door must keep auto-supplying `browserHost()`. -4. **Package ownership is part of the contract.** Frozen symbols do not move between packages within +4. **Package ownership is part of the contract.** Protected symbols do not move between packages within `0.x` (`compilePattern`/`FieldPattern`/`FieldHost` → core; `createFieldPlatform`/`applyPattern`/ `bindData`/`FieldPlatform`/`browserHost` → dom; `field-root`/`field-cell` → elements). The old - `compileRecipe`/`FieldRecipe`/`applyRecipe` names are frozen `@deprecated` aliases of these, same + `compileRecipe`/`FieldRecipe`/`applyRecipe` names are protected `@deprecated` aliases of these, same package, removed at `1.0`. -5. **Bodies are an attribute contract.** `[data-body]` on ordinary elements is the frozen authoring +5. **Bodies are an attribute contract.** `[data-body]` on ordinary elements is the protected authoring surface; there is no body element. 6. **The `forces-*` compatibility layer was removed — there is no `forces:*` event compatibility contract.** The `forces-ui → field-ui → Fundamental` rename was a **hard** rename: the ``/`` @@ -148,19 +155,19 @@ summary. deduped so it warns at most once. CSS-var aliases cannot be intercepted (CSS reads are invisible to JS), so they are doc-only. Everything here is **removed at `1.0`**. -## How the freeze is enforced +## How the check is enforced -- [`scripts/api-surface.ts`](../../scripts/api-surface.ts) imports every frozen value and type from its - owning package. Removing, renaming, or changing the kind of a frozen symbol is a **compile error** +- [`scripts/api-surface.ts`](../../scripts/api-surface.ts) imports every protected value and type from its + owning package. Removing, renaming, or changing the kind of a protected symbol is a **compile error** there (a value-import of a type, or vice versa, also fails — so the *kind* is locked, not just the name). - [`scripts/check-api-surface.mjs`](../../scripts/check-api-surface.mjs) verifies the parts tsc can't - see: each frozen value resolves at runtime in the built dist, each frozen type is exported in source, + see: each protected value resolves at runtime in the built dist, each protected type is exported in source, each element tag is registered (`customElements.define`), `data-body` is still in core's `BODY_SELECTOR`, and the data file and the type gate name the same symbols. - Both run via `pnpm check:api`, in CI right after `pnpm check:dist`. -Changing the frozen surface on purpose means editing +Changing the protected surface on purpose means editing [`scripts/api-surface.data.mjs`](../../scripts/api-surface.data.mjs) **and** [`scripts/api-surface.ts`](../../scripts/api-surface.ts) **and** this document together, with a migration note and a `0.MINOR` bump. @@ -168,5 +175,5 @@ migration note and a `0.MINOR` bump. ## Status The packages are **published to npm** under the `@fundamental-engine` scope (`@fundamental-engine/core` -and the five adapters: dom, elements, react, vanilla, three). This freeze defines the `0.x` contract +and the five adapters: dom, elements, react, vanilla, three). This list defines what CI protects from silent removal consumers build against; the publish steps and order are in [`PUBLISHING.md`](../../PUBLISHING.md). diff --git a/scripts/api-surface.data.mjs b/scripts/api-surface.data.mjs index 103f0b64..6f81c449 100644 --- a/scripts/api-surface.data.mjs +++ b/scripts/api-surface.data.mjs @@ -1,17 +1,20 @@ /** - * The frozen public API surface for Fundamental `0.x` — the single data source shared by the lock + * The removal-protected public API surface — the single data source shared by the check * (`scripts/check-api-surface.mjs`) and the docs page (`/docs/api/stability`). The prose contract is - * `docs/canonical/api-stability.md`; the hard type/value gate is `scripts/api-surface.ts`. + * `docs/canonical/api-stability.md`; the type/value gate is `scripts/api-surface.ts`. * - * Editing this list changes the public contract. Per the compatibility rules below, removing, - * renaming, or changing the kind/shape of any frozen entry is a BREAKING change (a 0.MINOR bump in - * the 0.x line) and needs a CHANGELOG migration note. Additions are fine in a PATCH. + * This is NOT a freeze. Pre-1.0 the surface evolves and new exports land freely — the check never + * fails on an addition. What it protects against is a listed symbol silently DISAPPEARING from a + * published package, which a consumer would otherwise discover before CI does. + * + * Removing or renaming a listed entry is therefore a deliberate act: update this list in the same + * change, and record the removal in the CHANGELOG so consumers get a migration note. * * Package npm names: core = `@fundamental-engine/core`; others = `@fundamental-engine/{dom,elements,react,vanilla,three}`. */ -/** Frozen value exports — functions/consts that must remain importable, with their owning package. */ -export const FROZEN_VALUES = [ +/** Value exports protected from silent removal — functions/consts, with their owning package. */ +export const PROTECTED_VALUES = [ { pkg: '@fundamental-engine/core', name: 'createField', note: 'host-required primitive — throws without opts.host (the renderer-agnostic door).' }, { pkg: '@fundamental-engine/core', name: 'compilePattern', note: 'pure FieldPattern → compiled plan (no DOM).' }, { pkg: '@fundamental-engine/core', name: 'compileRecipe', note: 'DEPRECATED alias of compilePattern (recipe → Pattern rename) — removed at 1.0; frozen until then.' }, @@ -28,7 +31,7 @@ export const FROZEN_VALUES = [ ]; /** Frozen type exports — interfaces that must remain exported, with their owning package. */ -export const FROZEN_TYPES = [ +export const PROTECTED_TYPES = [ { pkg: '@fundamental-engine/core', name: 'FieldPattern', note: 'the pattern schema (recipes/schema.ts).' }, { pkg: '@fundamental-engine/core', name: 'FieldRecipe', note: 'DEPRECATED alias of FieldPattern (recipe → Pattern rename) — removed at 1.0; frozen until then.' }, { pkg: '@fundamental-engine/core', name: 'FieldHost', note: 'the renderer-agnostic host contract createField requires; browserHost implements it.' }, @@ -36,7 +39,7 @@ export const FROZEN_TYPES = [ ]; /** Frozen custom-element tag names, owned by @fundamental-engine/elements. */ -export const FROZEN_ELEMENTS = [ +export const PROTECTED_ELEMENTS = [ { pkg: '@fundamental-engine/elements', tag: 'field-root', note: 'one background field per page; scans the document for [data-body].' }, { pkg: '@fundamental-engine/elements', tag: 'field-cell', note: 'a scoped local field region.' }, ]; @@ -46,7 +49,7 @@ export const FROZEN_ELEMENTS = [ * ordinary elements (core BODY_SELECTOR). There is NO tag and one must not be introduced * as the body mechanism. */ -export const FROZEN_BODY_ATTR = 'data-body'; +export const PROTECTED_BODY_ATTR = 'data-body'; /** Experimental surface — explicitly NOT frozen; may change shape or be removed in any release. */ export const EXPERIMENTAL = [ @@ -95,10 +98,11 @@ export const EXPERIMENTAL = [ /** Compatibility rules for the 0.x line. */ export const COMPAT_RULES = [ - 'Pre-1.0 semver: in 0.x the MINOR is the breaking position. A breaking change to any frozen symbol bumps 0.MINOR (e.g. 0.2 → 0.3); additive and fix-only changes bump PATCH. Consumers should pin to ~0.MINOR.', - 'The stable surface is additive-only within a 0.MINOR line: new exports, new optional fields, and new recipes/modes may land in a PATCH; renaming, removing, or changing the signature/shape of a frozen symbol requires a MINOR bump and a migration note.', - 'createField is frozen in BOTH Fundamental (host-required primitive; throws without opts.host) and @fundamental-engine/vanilla (host-bundled convenience). Both contracts are preserved; the vanilla door must keep auto-supplying browserHost.', - 'Package ownership is part of the contract and must not drift within 0.x: compilePattern / FieldPattern / FieldHost (and the deprecated aliases compileRecipe / FieldRecipe) are core (Fundamental); createFieldPlatform / applyPattern / bindData / FieldPlatform / browserHost (and the deprecated alias applyRecipe) are @fundamental-engine/dom; field-root / field-cell are @fundamental-engine/elements.', - 'Bodies are a stable ATTRIBUTE contract: [data-body] on ordinary elements is the frozen authoring surface. There is no tag and none will be introduced as the body mechanism.', - 'The experimental surface carries no semver guarantee. Diagnostics/agent/render-mode exports that happen to ship today are shipped-but-unfrozen — treat them as experimental until explicitly added to the frozen list.', + 'Pre-1.0, the surface evolves. New exports, new optional fields and new patterns/modes land freely; the check never fails on an addition. Treat 0.x as pre-stable and pin to ~0.MINOR.', + 'What is protected is REMOVAL, not change. If a listed symbol stops being exported, stops being declared, or stops registering, CI fails — because a consumer would otherwise find out before we did.', + 'Removing or renaming a listed symbol is allowed and expected. Do it deliberately: update the list in the same change and record it in the CHANGELOG so consumers get a migration note.', + 'createField is listed in BOTH Fundamental (host-required primitive; throws without opts.host) and @fundamental-engine/vanilla (host-bundled convenience). Both doors are protected; the vanilla door must keep auto-supplying browserHost.', + 'Package ownership is part of what is protected: compilePattern / FieldPattern / FieldHost (and the deprecated aliases compileRecipe / FieldRecipe) are core; createFieldPlatform / applyPattern / bindData / FieldPlatform / browserHost (and the deprecated alias applyRecipe) are @fundamental-engine/dom; field-root / field-cell are @fundamental-engine/elements. Moving one between packages is a removal from the old package.', + 'Bodies are an ATTRIBUTE contract: [data-body] on ordinary elements is the authoring surface. There is no tag and none will be introduced as the body mechanism.', + 'Anything not listed carries no guarantee at all — including diagnostics, agent and render-mode exports that happen to ship today. Absence from the list is not a promise of instability, only of silence.', ]; diff --git a/scripts/api-surface.ts b/scripts/api-surface.ts index 2fe3abb8..8ce77da5 100644 --- a/scripts/api-surface.ts +++ b/scripts/api-surface.ts @@ -20,7 +20,7 @@ import { FIELD_VERSION as reactFieldVersion } from '@fundamental-engine/react'; import { FIELD_VERSION as elementsFieldVersion } from '@fundamental-engine/elements'; // Reference every frozen VALUE so its removal/rename is a compile error here. -const FROZEN_VALUES = [ +const PROTECTED_VALUES = [ coreCreateField, compilePattern, compileRecipe, // deprecated alias of compilePattern (removed at 1.0) — still frozen until then @@ -37,7 +37,7 @@ const FROZEN_VALUES = [ reactFieldVersion, elementsFieldVersion, ] as const; -void FROZEN_VALUES; +void PROTECTED_VALUES; // Reference every frozen TYPE so its removal/rename is a compile error here. export type __FrozenTypes = [FieldPattern, FieldRecipe, FieldHost, FieldPlatform]; diff --git a/scripts/check-api-surface.mjs b/scripts/check-api-surface.mjs index de5ff62c..d239fab6 100644 --- a/scripts/check-api-surface.mjs +++ b/scripts/check-api-surface.mjs @@ -16,7 +16,7 @@ import { execFileSync } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import { - FROZEN_VALUES, FROZEN_TYPES, FROZEN_ELEMENTS, FROZEN_BODY_ATTR, + PROTECTED_VALUES, PROTECTED_TYPES, PROTECTED_ELEMENTS, PROTECTED_BODY_ATTR, } from './api-surface.data.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -49,39 +49,39 @@ function greps(dir, pattern) { } // 1 · frozen values resolve at runtime -for (const { pkg, name } of FROZEN_VALUES) { +for (const { pkg, name } of PROTECTED_VALUES) { const mod = await moduleFor(pkg); - if (!(name in mod)) fail(`frozen value missing: ${pkg} → ${name} (not exported by the built dist)`); + if (!(name in mod)) fail(`removed from the public surface: ${pkg} → ${name} (no longer exported by the built dist). If this removal is intended, drop it from api-surface.data.mjs in this change and add a CHANGELOG migration note.`); } // 2 · frozen types exported in source -for (const { pkg, name } of FROZEN_TYPES) { +for (const { pkg, name } of PROTECTED_TYPES) { const pat = `export (interface|type|class|enum) ${name}\\b|export (type )?\\{[^}]*\\b${name}\\b`; - if (!greps(srcOf(pkg), pat)) fail(`frozen type missing: ${pkg} → ${name} (no exported declaration found in src)`); + if (!greps(srcOf(pkg), pat)) fail(`removed from the public surface: ${pkg} → ${name} (no exported type declaration found in src). If intended, drop it from api-surface.data.mjs and add a CHANGELOG migration note.`); } // 3 · frozen custom-element tags registered -for (const { pkg, tag } of FROZEN_ELEMENTS) { +for (const { pkg, tag } of PROTECTED_ELEMENTS) { const distDir = join(root, 'packages', DIR[pkg], 'dist'); - if (!greps(distDir, `customElements\\.define\\('${tag}'`)) fail(`frozen element missing: <${tag}> (no customElements.define('${tag}') in ${pkg} dist)`); + if (!greps(distDir, `customElements\\.define\\('${tag}'`)) fail(`removed from the public surface: <${tag}> (no customElements.define('${tag}') in ${pkg} dist). If intended, drop it from api-surface.data.mjs and add a CHANGELOG migration note.`); } // 4 · the body attribute contract const core = await moduleFor('@fundamental-engine/core'); -if (typeof core.BODY_SELECTOR !== 'string' || !core.BODY_SELECTOR.includes(`[${FROZEN_BODY_ATTR}]`)) { - fail(`body contract broken: core BODY_SELECTOR no longer matches [${FROZEN_BODY_ATTR}] (got: ${JSON.stringify(core.BODY_SELECTOR)})`); +if (typeof core.BODY_SELECTOR !== 'string' || !core.BODY_SELECTOR.includes(`[${PROTECTED_BODY_ATTR}]`)) { + fail(`body contract broken: core BODY_SELECTOR no longer matches [${PROTECTED_BODY_ATTR}] (got: ${JSON.stringify(core.BODY_SELECTOR)})`); } // 5 · the hard gate (api-surface.ts) and this data file name the same frozen values const lockSrc = await readFile(join(root, 'scripts', 'api-surface.ts'), 'utf8'); -for (const { name } of FROZEN_VALUES) { - if (!new RegExp(`\\b${name}\\b`).test(lockSrc)) fail(`drift: ${name} is frozen in api-surface.data.mjs but not referenced in api-surface.ts (the type/value gate)`); +for (const { name } of PROTECTED_VALUES) { + if (!new RegExp(`\\b${name}\\b`).test(lockSrc)) fail(`drift: ${name} is listed in api-surface.data.mjs but not referenced in api-surface.ts (the type/value gate)`); } -for (const { name } of FROZEN_TYPES) { - if (!new RegExp(`\\b${name}\\b`).test(lockSrc)) fail(`drift: type ${name} is frozen in api-surface.data.mjs but not referenced in api-surface.ts`); +for (const { name } of PROTECTED_TYPES) { + if (!new RegExp(`\\b${name}\\b`).test(lockSrc)) fail(`drift: type ${name} is listed in api-surface.data.mjs but not referenced in api-surface.ts`); } -const total = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS.length + 1; +const total = PROTECTED_VALUES.length + PROTECTED_TYPES.length + PROTECTED_ELEMENTS.length + 1; if (problems.length) { console.error(`✗ public API surface check failed (${problems.length} problem(s)):`); for (const p of problems) console.error(` ${p}`); @@ -89,4 +89,4 @@ if (problems.length) { console.error('and docs/canonical/api-stability.md with a migration note + a 0.MINOR bump.'); process.exit(1); } -console.log(`✓ public API surface intact — ${total} frozen entries (${FROZEN_VALUES.length} values, ${FROZEN_TYPES.length} types, ${FROZEN_ELEMENTS.length} elements, the [${FROZEN_BODY_ATTR}] body contract).`); +console.log(`✓ public API surface intact (no removals) — ${total} protected entries (${PROTECTED_VALUES.length} values, ${PROTECTED_TYPES.length} types, ${PROTECTED_ELEMENTS.length} elements, the [${PROTECTED_BODY_ATTR}] body contract).`); diff --git a/scripts/gen-parity-matrix.mjs b/scripts/gen-parity-matrix.mjs index 06b2c8b4..7289b0e6 100644 --- a/scripts/gen-parity-matrix.mjs +++ b/scripts/gen-parity-matrix.mjs @@ -112,7 +112,7 @@ function jsOverlayModes() { /** Frozen public value exports (createField, browserHost, bindData, …) from the api-surface data. */ async function jsFrozenValues() { const mod = await import(resolve(root, 'scripts/api-surface.data.mjs')); - return new Set(mod.FROZEN_VALUES.map((v) => v.name)); + return new Set(mod.PROTECTED_VALUES.map((v) => v.name)); } // ── Swift extractors ────────────────────────────────────────────────────────────────────────────── From 8bdfca9330f20df674cbccdfd5edd8776cfb090c Mon Sep 17 00:00:00 2001 From: Zach Shallbetter Date: Mon, 20 Jul 2026 12:44:00 -0700 Subject: [PATCH 2/2] docs: extend the removal-check rename to the release and canonical docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RELEASING.md, PUBLISHING.md, docs/canonical/README.md, agent-consumption-model.md and agent-safety-model.md still described a freeze. substrate-api.md cited 'the frozen 17' twice where the check reports 20 — a stale count, not just stale wording. Deliberately untouched, because the word means something else in each: Object.freeze'd policy clones in agent-safety-model.md, 'frozen design history' for the planning archive, 'freeze the sim' for reduced motion, and 'frozen particles' in the poster render mode. A pattern-matched sweep would have damaged all four. Freeze wording survives in prose across several other canonical docs. That is stale phrasing rather than a wrong claim about the gate, and chasing it through fifteen files belongs in its own change rather than blocking a release. --- CHANGELOG.md | 2 +- PUBLISHING.md | 6 +++--- RELEASING.md | 14 +++++++------- docs/canonical/README.md | 2 +- docs/canonical/agent-consumption-model.md | 2 +- docs/canonical/agent-safety-model.md | 2 +- docs/canonical/substrate-api.md | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 704cf2bb..6866dfe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ a git tag (see [RELEASING.md](RELEASING.md)). ## [Unreleased] - **CI: replaced the third-party path-filter action with plain git.** GitHub began forcing Node 24 onto actions that target Node 20; the filter action dies silently mid-run and takes the whole gate with it via `conclusion`. A path filter is one `git diff` and does not need a dependency that can break underneath the repository. All nine original path patterns preserved and verified against a fixture of paths that must and must not match; the replacement also logs which files it saw and which way it decided. -- **Retired the API "freeze" — it was never real.** The list was described as frozen for `0.x` and had been edited 40 times; a stability promise revised whenever it becomes inconvenient is not a promise, it is a claim that quietly stops being true. The check itself is kept and renamed to what it actually does: it protects listed symbols from **silent removal**. It has never failed on an addition and cannot — new exports land freely. Removing or renaming a listed symbol is allowed and expected; it just has to be deliberate, which means dropping the entry in the same change and adding a migration note so consumers are told. `FROZEN_*` → `PROTECTED_*` throughout, with failure messages that now say what happened and what to do. Two inaccuracies fixed in passing: the docs page claimed entries "fail the build if they change" (only removal fails) and the canonical doc still cited "the frozen 17" where the check reports **20**. +- **Retired the API "freeze" — it was never real.** The list was described as frozen for `0.x` and had been edited 40 times; a stability promise revised whenever it becomes inconvenient is not a promise, it is a claim that quietly stops being true. The check itself is kept and renamed to what it actually does: it protects listed symbols from **silent removal**. It has never failed on an addition and cannot — new exports land freely. Removing or renaming a listed symbol is allowed and expected; it just has to be deliberate, which means dropping the entry in the same change and adding a migration note so consumers are told. `FROZEN_*` → `PROTECTED_*` throughout, with failure messages that now say what happened and what to do. Three inaccuracies fixed in passing: the docs page claimed entries "fail the build if they change" (only removal fails); `api-stability.md` and `substrate-api.md` cited "the frozen 17" where the check reports **20**. `RELEASING.md`, `PUBLISHING.md` and three canonical docs updated to match. Freeze language remains in prose elsewhere in `docs/canonical/` and is tracked as follow-up — it is stale wording, not a wrong claim about the gate. - **Documentation accuracy pass.** `docs/README.md` now indexes `docs/method/` (a whole top-level docs directory that was unlisted) and the `planning/world-substrate/` program. `PLAN.md`'s status header said *"Nothing in F1–F5 or C2 is built"* — false since Stage 1 landed — and the document contradicted itself, carrying both a current "Not started" line and a superseded one, plus a note calling F1.4 equivalence "pending" when it is measured. `CLAUDE.md` listed **17** frozen API entries where `check:api` reports **20**, and its layout table omitted `apps/observatory` entirely. Corrected, with the authoritative source named in each case so the next drift is self-diagnosing. ### Added diff --git a/PUBLISHING.md b/PUBLISHING.md index 9cf167d5..e7d14cd6 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -3,9 +3,9 @@ The six `@fundamental-engine/*` packages publish to npm **with provenance** via CI. This document is the mechanics; the policy (versioning, when to cut) is in [`RELEASING.md`](RELEASING.md). -> The public surface is frozen for `0.x` — see +> A listed subset of the public surface is protected from silent removal — see > [API stability](docs/canonical/api-stability.md). `pnpm check:api` (run in CI and the -> release workflow) fails if a frozen symbol changes, so a release can't break the contract by accident. +> release workflow) fails if a protected symbol disappears, so an export cannot vanish from a release by accident. ## The packages @@ -62,5 +62,5 @@ pnpm --filter "@fundamental-engine/*" exec npm version --no- ``` Per the `0.x` rules in [API stability](docs/canonical/api-stability.md), a breaking change to a -frozen symbol is a **minor** bump (`0.2 → 0.3`); additive and fix-only changes are patches. The private +protected symbol is a **minor** bump (`0.2 → 0.3`); additive and fix-only changes are patches. The private `site` / `starter` apps are versioned independently and not published. diff --git a/RELEASING.md b/RELEASING.md index 18f17a7a..2e138222 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -14,10 +14,10 @@ declares `three` as a peer dependency.) They follow [Semantic Versioning](https: - **patch** (`0.9.x`) — bug fixes, internal changes, and **additive** features (a new force, `FieldOption`, or `FieldHandle` method). Pre-1.0 a patch may also retire an **already-deprecated** - alias or change **experimental / unfrozen** surface — 0.9.3 dropped the retired alias packages in a - patch. What a patch never touches is the `check:api`-gated stable subset. -- **minor** (`0.x.0`) — a break to that **frozen stable subset** (renaming, removing, or changing the - signature of a `check:api`-gated symbol) bumps `0.MINOR`, under a **Breaking** heading with a + alias or change **unprotected** surface — 0.9.3 dropped the retired alias packages in a + patch. What a patch never does is remove a `check:api`-protected symbol. +- **minor** (`0.x.0`) — removing or renaming a **`check:api`-protected symbol**, or changing its + signature, bumps `0.MINOR`, under a **Breaking** heading with a migration note. - **major** (`1.0.0`) — the stability promise itself; from 1.0 on, standard SemVer applies. @@ -28,8 +28,8 @@ The engine's public surface is: the `@fundamental-engine/core` exports (`createF `FieldHandle`, the catalog, the conformance API), the `data-*` attribute vocabulary, the `` element attributes/methods, the `@fundamental-engine/vanilla` `FieldField` class and `mountField`, and the React adapter's props. The internal integrator, render code, and the site are not part of the public -contract. Its **stable subset** is frozen for `0.x` and gated by `pnpm check:api`; the broader surface -(deprecated aliases, the experimental substrate API) still evolves pre-1.0 — see +contract. A listed subset is **protected from silent removal** by `pnpm check:api` — that is a guard against +accidental breakage, not a freeze; the surface evolves pre-1.0 and additions never fail the check. See [API stability](docs/canonical/api-stability.md). > The packages are published to npm under the `@fundamental-engine` scope, **with provenance** (a signed @@ -76,7 +76,7 @@ contract. Its **stable subset** is frozen for `0.x` and gated by `pnpm check:api - **`pr-checks.yml`** — PR hygiene: a PR that changes `packages/` must add a CHANGELOG entry (the diff is checked, not the PR body), and every relative doc link must resolve (`pnpm check:links`). Its `conclusion-pr` job is a required check on `main`. -- **`api-surface.yml`** — on PRs touching the packages, posts the frozen-surface delta +- **`api-surface.yml`** — on PRs touching the packages, posts the protected-surface delta (base vs. head of `scripts/api-surface.data.mjs`) as a PR comment. Visibility only; the blocker is `check:api` in `ci.yml`. diff --git a/docs/canonical/README.md b/docs/canonical/README.md index d15d77cf..55e67b23 100644 --- a/docs/canonical/README.md +++ b/docs/canonical/README.md @@ -39,7 +39,7 @@ host, not the boundary. Canvas is one render surface, not the whole system. | [`authoring-and-recipes.md`](authoring-and-recipes.md) | Authoring surfaces and the FieldPattern system | | [`testing-and-conformance.md`](testing-and-conformance.md) | Test contracts and the conformance framework (the Lab-as-detector) | | [`stability-and-convergence.md`](stability-and-convergence.md) | The force-directed failure canon, per-mechanism: what the field damps or side-steps (seeded RNG, friction, DOM-owned placement, plausibility-over-accuracy) vs what it owns (`dynamic`-body jitter, tuning brittleness) — and the arguments we deliberately do NOT make | -| [`api-stability.md`](api-stability.md) | The freeze contract: the frozen surface, the experimental surface, the 0.x compatibility rules | +| [`api-stability.md`](api-stability.md) | The removal-protection contract: the protected surface, what carries no guarantee, the 0.x compatibility rules | | [`substrate-api.md`](substrate-api.md) | The shipped substrate read API (EXPERIMENTAL): `query` / `snapshot` / `diff` / `replay` / `projections` + governance lint, body authority + dynamic recoil, integrator modes, accumulator channels, first-class body identity | | [`agent-safety-model.md`](agent-safety-model.md) | The **Govern** verb: the safety/governance model for agents reading a field — three invariants (agent-readable ≠ agent-writable; snapshots withhold opaque `body.data` by default; projections reveal, never mutate) linking outward to `substrate-api.md` (EXPERIMENTAL) | | [`wallpaper-rule.md`](wallpaper-rule.md) | **"Substrate, not wallpaper" made operational** — the three-prong wallpaper test, the remedy order (derive → declare → demote → sugar; never deletion), the corollaries (injected-rng determinism, one writer per reading, proofs must be produced), and the per-plane guards | diff --git a/docs/canonical/agent-consumption-model.md b/docs/canonical/agent-consumption-model.md index 235e695a..b8c95c0b 100644 --- a/docs/canonical/agent-consumption-model.md +++ b/docs/canonical/agent-consumption-model.md @@ -17,7 +17,7 @@ > `field.snapshot()` return the live model — bodies, metrics, relationships, and per-force influence — > as plain structured data, and the projection registry's `agent-json` surface lets a Field Pattern > expose a tailored reading. `query()` is *the* substrate's agent-/tool-readable surface; see -> [substrate-api.md](substrate-api.md). (Shipped and callable, but not part of the frozen surface — its +> [substrate-api.md](substrate-api.md). (Shipped and callable, but not on the protected list — its > shape may change.) > > **Field Agent vs Software Agent — don't conflate them.** A **Field Agent** is a *runtime participant diff --git a/docs/canonical/agent-safety-model.md b/docs/canonical/agent-safety-model.md index 0e44398c..febf6fce 100644 --- a/docs/canonical/agent-safety-model.md +++ b/docs/canonical/agent-safety-model.md @@ -3,7 +3,7 @@ > consumers that *read* a field — the **Govern** verb of the substrate (Declare → Influence → Read > → Reveal → **Govern**). It is an overview: the mechanisms and their code live in > [`substrate-api.md`](substrate-api.md), which this doc links outward to. That surface is **shipped -> and code-confirmed but EXPERIMENTAL / unfrozen** (0.x) — it may change; it is not part of the frozen +> and code-confirmed but EXPERIMENTAL / unprotected** (0.x) — it may change; it is not part of the protected > stable baseline. # The agent safety model — the Govern verb diff --git a/docs/canonical/substrate-api.md b/docs/canonical/substrate-api.md index 174a5d70..6337eabb 100644 --- a/docs/canonical/substrate-api.md +++ b/docs/canonical/substrate-api.md @@ -3,7 +3,7 @@ > queryable, time-travelable, projectable* substrate — not just a thing that draws or writes CSS > variables. Every method below is **shipped** in `@fundamental-engine/core` (verified against > `packages/core/src/engine/types.ts` + `field.ts`), but the whole surface is **EXPERIMENTAL**: it is -> *not* part of the frozen 17 (see [api-stability.md](api-stability.md)) and may change shape or be +> *not* on the protected list (see [api-stability.md](api-stability.md)) and may change shape or be > removed in any release. Use it; don't pin a contract to it yet. Follows the > [status rule](documentation-standards.md): nothing here is called shipped unless code confirms it. @@ -474,7 +474,7 @@ behaves exactly as before. --- -## Status & relation to the frozen surface +## Status & relation to the protected surface | Capability | Surface | Status | |---|---|---| @@ -488,7 +488,7 @@ behaves exactly as before. | Accumulator channels | `Env.accum` / `FieldImpulseAccumulator` | shipped-unfrozen · EXPERIMENTAL | | Runtime policy + budgets | `createField({ policy })` · `setPolicy` · `field.policy` | shipped-unfrozen · EXPERIMENTAL (motion + privacy budgets wired; others declared) | -None of these are in the frozen 17 ([api-stability.md](api-stability.md)). They are present in the +None of these are on the protected list ([api-stability.md](api-stability.md)). They are present in the package and safe to use, but carry **no** stability guarantee until explicitly added to the stable table. The substrate API is JS-first; native-plane (`swift/`, `android/`) parity is tracked separately.