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

Expand Down
6 changes: 3 additions & 3 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -62,5 +62,5 @@ pnpm --filter "@fundamental-engine/*" exec npm version <patch|minor|major> --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.
14 changes: 7 additions & 7 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 `<field-root>`
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
Expand Down Expand Up @@ -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`.

Expand Down
16 changes: 8 additions & 8 deletions apps/site/src/components/ApiStamp.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
// <ApiStamp name="createField" pkg="@fundamental-engine/vanilla" /> (disambiguate the two doors)
// <ApiStamp item="FieldHandle.scrollV()" /> → 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';

Expand All @@ -38,12 +38,12 @@ if (name) {
const byPkg = <T extends { pkg: string }>(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',
Expand Down
4 changes: 2 additions & 2 deletions apps/site/src/pages/docs/api/attributes.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -35,7 +35,7 @@ const families = [
<div class="api-row">
<dt class="k">
<code>{a.name}</code>
{a.name === FROZEN_BODY_ATTR && <ApiStamp name={FROZEN_BODY_ATTR} />}
{a.name === PROTECTED_BODY_ATTR && <ApiStamp name={PROTECTED_BODY_ATTR} />}
<span class="ty">{a.type}{a.def ? <> · <b>default {a.def}</b></> : null}</span>
</dt>
<dd class="d">{a.desc}</dd>
Expand Down
29 changes: 15 additions & 14 deletions apps/site/src/pages/docs/api/stability.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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;
---

<DocsLayout
title="Fundamental — API stability"
description="The frozen 0.x public API surface, the experimental surface, and the compatibility rules. Enforced by pnpm check:api in CI, so a release can't break the contract by accident."
description="The removal-protected public API surface, what carries no guarantee, and the compatibility rules. Enforced by pnpm check:api in CI, so an export cannot vanish from a release by accident."
>
<p class="kicker">API reference</p>
<h1>API <span class="accent">stability</span></h1>
<StatusChip label="Stable · 0.x contract" tone="stable" />
<p class="lede">
The architecture has stopped being fluid. The surface below is <b>frozen for <code>0.x</code></b>:
each symbol stays exported, from the same package, with the same kind, and does not break for the
life of the <code>0.x</code> line. The freeze is enforced in CI by <code>pnpm check:api</code> —
{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 <b>silent removal</b>:
each symbol stays exported, from the same package, with the same kind. Enforced in CI by
<code>pnpm check:api</code> — {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.
</p>

<div class="callout">
Expand All @@ -37,7 +38,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
<table class="api-tbl">
<thead><tr><th>Symbol</th><th>Package</th><th>What it is</th></tr></thead>
<tbody>
{FROZEN_VALUES.map((v: any) => (
{PROTECTED_VALUES.map((v: any) => (
<tr>
<td><code>{v.name}</code> <span class="tag tag-value">value</span> <ApiStamp name={v.name} pkg={v.pkg} /></td>
<td><code>{v.pkg}</code></td>
Expand All @@ -49,7 +50,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
<p class="aside">
<code>createField</code> has <b>two doors on purpose</b>: the core primitive is renderer-agnostic
and host-required; <code>@fundamental-engine/vanilla</code> 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.
</p>
</section>

Expand All @@ -58,7 +59,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
<table class="api-tbl">
<thead><tr><th>Type</th><th>Package</th><th>What it is</th></tr></thead>
<tbody>
{FROZEN_TYPES.map((t: any) => (
{PROTECTED_TYPES.map((t: any) => (
<tr><td><code>{t.name}</code> <span class="tag tag-type">type</span> <ApiStamp name={t.name} pkg={t.pkg} /></td><td><code>{t.pkg}</code></td><td>{t.note}</td></tr>
))}
</tbody>
Expand All @@ -70,13 +71,13 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
<table class="api-tbl">
<thead><tr><th>Surface</th><th>Package</th><th>What it is</th></tr></thead>
<tbody>
{FROZEN_ELEMENTS.map((e: any) => (
{PROTECTED_ELEMENTS.map((e: any) => (
<tr><td><code>&lt;{e.tag}&gt;</code> <span class="tag tag-el">element</span> <ApiStamp name={e.tag} pkg={e.pkg} /></td><td><code>{e.pkg}</code></td><td>{e.note}</td></tr>
))}
<tr>
<td><code>{FROZEN_BODY_ATTR}</code> <span class="tag tag-attr">attribute</span> <ApiStamp name={FROZEN_BODY_ATTR} /></td>
<td><code>{PROTECTED_BODY_ATTR}</code> <span class="tag tag-attr">attribute</span> <ApiStamp name={PROTECTED_BODY_ATTR} /></td>
<td>core <code>BODY_SELECTOR</code></td>
<td>The body contract. "Every element is a body" via the <code>{FROZEN_BODY_ATTR}</code> attribute on ordinary elements.</td>
<td>The body contract. "Every element is a body" via the <code>{PROTECTED_BODY_ATTR}</code> attribute on ordinary elements.</td>
</tr>
</tbody>
</table>
Expand All @@ -89,7 +90,7 @@ const frozenCount = FROZEN_VALUES.length + FROZEN_TYPES.length + FROZEN_ELEMENTS
</section>

<section class="docs-sec">
<h2>Experimental — not frozen</h2>
<h2>Experimental — no guarantee</h2>
<p>
These carry <b>no</b> stability guarantee and may change shape or be removed in any release. Some
have exported building blocks today — those are <i>shipped-but-unfrozen</i>: present in the
Expand Down
2 changes: 1 addition & 1 deletion docs/canonical/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/canonical/agent-consumption-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/canonical/agent-safety-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading