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
2 changes: 1 addition & 1 deletion .agents/skills/contract-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Contracts are JSON Schema 2020-12 files in `packages/core-contracts/schemas/`. B

1. **Decide the version impact.** Adding an optional field is non-breaking. Removing/renaming a field, changing a type, or adding a required field is breaking → bump `SchemaVersion` enum in `common.schema.json` only with an accepted RFC (`write-rfc`), and keep the old version's fixtures.
2. **Edit the schema.** Shared shapes go in `common.schema.json#/$defs`; root-specific helpers in that root's `$defs`. Stay inside the generator-safe subset:
- unions: `oneOf` + inline object variants + `"kind": {"type":"string","enum":["one"]}` + `required` includes `kind`
- unions: `oneOf` + inline object variants + a one-value enum discriminator, `"kind": {"type":"string","enum":["one"]}` unless the domain says otherwise (field definitions use `type`, ADR-0043), listed in `required`
- no `default`, `$dynamicRef`, `unevaluatedProperties`, `dependentSchemas`, `if/then`
- cross-file refs only `common.schema.json#/$defs/X` or `<root>.schema.json` (top level)
- `additionalProperties: false` on objects; maps use `additionalProperties: {schema}`
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pnpm i18n:export / pnpm i18n:import [--locale xx] # translations as nested ICU

## Hard rules
1. **Never edit generated files.** Change the schema, run `pnpm gen`, commit both.
2. **Schema subset** (so TS and Rust generators agree): discriminated unions = `oneOf` with inline variants and `kind: {"type":"string","enum":["x"]}`; no `default`; no `$dynamicRef`/`unevaluated*`/`dependentSchemas`; cross-file `$ref` only to `common.schema.json#/$defs/*` or another root schema's top level; `additionalProperties: false` on every object except declared maps.
2. **Schema subset** (so TS and Rust generators agree): discriminated unions = `oneOf` with inline variants and a one-value enum discriminator, `kind: {"type":"string","enum":["x"]}` unless the domain says otherwise (field definitions use `type`, ADR-0043); no `default`; no `$dynamicRef`/`unevaluated*`/`dependentSchemas`; cross-file `$ref` only to `common.schema.json#/$defs/*` or another root schema's top level; `additionalProperties: false` on every object except declared maps.
3. **Every root type** has `schemaVersion` (enum `[1]`); mutable nodes have `rev`. New fields are optional unless an RFC says otherwise.
4. **No interactive DB transactions** anywhere (D1 has none); only atomic `batch()`. Composite writes guard later statements on the earlier CAS (match on the new content `hash`, not `rev + 1`).
5. **Worker-safe imports:** code reachable from a Worker never imports Node-only modules at top level (`better-sqlite3`, `node:fs` helpers, `@cloudhal/core-contracts/bundle`, `@cloudhal/api/auth/node`). Use the package subpaths.
Expand Down
51 changes: 51 additions & 0 deletions docs/adr/0043-typed-custom-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ADR-0043: Typed custom fields: nine closed types on `Site.collections.<key>.fields`, one eval-free checker, explicit field operations
- Status: Accepted
- Date: 2026-10-23 (decided by Halil)
- Related: RFC-0041, RFC-0040 / ADR-0042, RFC-0015 / ADR-0030, RFC-0033 / ADR-0035, RFC-0008 / ADR-0023, #71
- Sources: [Payload fields](https://payloadcms.com/docs/fields/overview) and [localization](https://payloadcms.com/docs/configuration/localization) · [Sanity validation](https://www.sanity.io/docs/studio/validation) · [Webflow create field](https://developers.webflow.com/data/reference/cms/collection-fields/create) · [Wix item limits](https://support.wix.com/en/article/cms-understanding-collection-storage-limits-and-quotas) · [Strapi field rename request](https://feedback.strapi.io/developer-experience/p/gracefully-handle-renaming-of-content-types-and-fields-in-the-ctb) · [OpenAI structured outputs](https://developers.openai.com/api/docs/guides/structured-outputs) · [Anthropic structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) · [Cloudflare D1 limits](https://developers.cloudflare.com/d1/platform/limits/)

## Context
Collections are first-class since ADR-0042. An entry's collection-specific values still go into `ContentEntry.fields` as untyped JSON. Agents guess the shape, people get no form, and themes cannot rely on any value. The prior art shows the hazards: Strapi and ACF lose data when a field is renamed in a builder, and Sanity checks values only in its Studio.

## Decision
1. **Contract:** optional `CollectionSettings.fields`, an ordered list of 1–30 `FieldDefinition`s. There is no `schemaVersion` bump.
- **Types:** a `oneOf` of nine closed variants: `text`, `richText` (Cloudhal Markdown v1), `number`, `boolean`, `date` (`YYYY-MM-DD`), `datetime` (`{at, timeZone}`, ADR-0023 §9.1), `url` (https, http, mailto, tel, relative), `image` (an asset id of the same site), and `select` (closed options, optional `multiple`).
- **Common properties:** every variant has `key`, `labels` and `localized`; all but `boolean` can have `requiredToPublish`.
- **Keys:** lower camelCase. Built-in entry properties and prototype names are reserved.
- **Not offered:** `pattern` and defaults.
2. **Discriminator `type`.** AGENTS.md rule 2 now reads "a one-value enum discriminator, `kind` unless the domain says otherwise"; field types say `type`, like every CMS and the props subset.
3. **Values** stay in `ContentEntry.fields`, up to 256 KB per entry.
4. **One checker, three callers.** `@cloudhal/core-ts/fields` is eval-free and built on ADR-0030's `validateProps`.
- The store, the API and MCP's precheck all call it, and it checks only the values that changed.
- Once a collection has definitions, new unknown keys are refused.
- It emits the JSON Schema agents receive, limited to what both OpenAI and Anthropic strict modes accept.
5. **Requirements:** `requiredToPublish` is checked when an entry is written in review or published, like `required_term`. Drafts save freely.
6. **Localization**, chosen per field:
- localized values live on each locale's entry and, when empty, fall back to the original's value (`fromOriginal`);
- non-localized values live only on the translation group's original.
7. **Definitions stay on the site node**, capped at 200 fields per site and 256 KB for `collections`. A `Collection` node kind is the fallback if the cap is reached.
8. **Definition changes:**
- A plain edit never loses a value: `updateSite` refuses a risky one (`field_change_needs_operation`).
- `rename_field`, `change_field_type` (a closed conversion table) and `set_field_localized` are explicit operations, and so is `remove_field`. Each has a dry run, a snapshot before it runs, and approval by a person holding `site:settings`, in every mode.
- Each runs as resumable phases of guarded atomic batches (rule 4), and touches only the draft graph.
9. **Surfaces:**
- **Studio:** a generated, lazy entry form and a fields builder in the collection's settings.
- **MCP:** `list_collections` returns `fields` and `fieldsSchema`, and `update_entry` gains `fields`.
- **Themes:** additive exports (`resolveFields`, `formatFieldValue`, `EntryFields.astro`).
- **Search:** a field is indexed only when it is marked `searchable` (default false).
- **Import and pull:** invalid values are reported, not refused.
10. **Later:** field columns and filters in Studio lists, JSON-LD presets, expiry automations, reference fields, and `localTime` on `datetime`.

## Consequences
- Positive:
- Agents write typed, checked values, and the person approving sees them.
- People get forms.
- Renames and type changes are safe and reversible.
- Themes get resolved values.
- Negative:
- The site node grows with definitions; the caps bound it.
- Field operations add four tools and a resumable runner.
- Rule 2 now has an exception.
- Risks:
- **Two checkers:** a write path that skips `@cloudhal/core-ts/fields` would let unchecked values in. Every write goes through one store helper.
- **Rust numbers:** the Rust mirror writes `0` as `0.0`. The Rust core needs canonical number output (RFC 8785) before it writes nodes.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ One file per decision, numbered, immutable once accepted (write a new ADR to sup
| [0040](0040-collection-permalinks.md) | Collection permalinks: `Site.collections.<key>.permalink` (default `/<collection>/{slug}`, v1 a fixed prefix plus `{slug}`), a closed token set, `entryPath`/`entryAt` in core-ts, redirects derived from the change, collisions refused at write (RFC-0038) |
| [0041](0041-site-archives.md) | Site archives: `cloudhal export`/`import`, a deterministic `.cloudhal` tar of NDJSON parts and media with per-file SHA-256, ids kept, bootstrap/`--replace` (snapshot first)/`--dry-run`, graph only and no built-in encryption (RFC-0039) |
| [0042](0042-first-class-collections.md) | First-class collections: `Site.collections` is a registry (declared keys, collections with entries, `posts`) with no data migration; per-locale labels, icon, feed, sitemap and sort; immutable keys; a new collection's prefix checked for overlaps; agents create collections only through approved `create_collection` (RFC-0040) |
| [0043](0043-typed-custom-fields.md) | Typed custom fields: nine closed types on `Site.collections.<key>.fields` (discriminator `type`), values in `ContentEntry.fields`, one eval-free checker (`@cloudhal/core-ts/fields`) for store, API and MCP that also emits the agents' JSON Schema, `requiredToPublish` at review and publish, per-field localization, definitions on the site node with caps, renames/type changes/removals only as snapshotted, approved operations (RFC-0041) |
Loading
Loading