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
7 changes: 7 additions & 0 deletions .changeset/5250-record-source-issue-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@object-ui/types': patch
---

A record-source refusal (`RECORD_SOURCE_REQUIRED`, raised by every arm that requires a record source) now reports the same path every time (objectui#5250).

The refusal's `path` was one array, built when the arm was defined and handed to every issue. zod copies an added issue shallowly and prefixes its `path` in place as the issue climbs out of an array slot or an object key, so the first refusal reported from inside a container rewrote the path of every later refusal of that arm for as long as the module stayed loaded. A process that judges many documents saw the drift: `objectui check` over a project, or `objectui validate` called repeatedly in one process, could report a root refusal at a path like `0 → 0 → 0 → 0`. Each issue now carries its own copy. The refusal, its message and its code are unchanged.
12 changes: 12 additions & 0 deletions .github/workflows/doc-snippet-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,15 @@ jobs:
# narrowing what the gate scans.
- name: Compile JSDoc `@example` blocks against the built types
run: node scripts/check-doc-example-types.mjs

# objectui#5250 (the maintainer ruling's JSON-fence slice). Every `json` /
# `jsonc` node document in `content/docs` is judged through the STRICT
# authoring face — `StrictAnyComponentSchema`, read from the
# `@object-ui/types` the build step above produced (the snippet gate's
# closure carries it) — the face `objectui validate` judges with. A fence
# that is deliberately not a complete node document is declared on the
# line above it, and the declaration is re-judged on every run. Last of
# the three, for the reason the step above gives: a newly wired blocking
# gate must not mask an established one.
- name: Judge JSON doc fences through the strict authoring face
run: node scripts/check-doc-json-fences.mjs
2 changes: 1 addition & 1 deletion content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ A single-record detail view with grouped fields, actions, and tabs.
"title": "Customer Info",
"fields": [
{ "name": "customer", "label": "Customer", "type": "text" },
{ "name": "email", "label": "Email", "type": "email" },
{ "name": "email", "label": "Email", "type": "text" },
{ "name": "created", "label": "Created", "type": "date", "format": "MMM d, yyyy" }
]
},
Expand Down
14 changes: 9 additions & 5 deletions content/docs/blocks/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,17 @@ behaviour. Common customisations:

### Wire Up Submission

A control that runs something is its own node type, `action:button`, and its
props go in the `properties` bag; `actionType: "api"` sends the request to
`target`:

```json
{
"type": "button",
"label": "Sign In",
"action": {
"type": "submit",
"endpoint": "/api/auth/login",
"type": "action:button",
"properties": {
"label": "Sign In",
"actionType": "api",
"target": "/api/auth/login",
"method": "POST"
}
}
Expand Down
26 changes: 16 additions & 10 deletions content/docs/blocks/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ Customize dashboard blocks for your needs:

### Add Real Data

A `card` fetches nothing. A live figure comes from a data-bound block: an
`element:number` takes its object from the per-element `dataSource` binding and
its aggregate from its `properties` bag (see the
[Data Source guide](/docs/guide/data-source)):

```json
{
"type": "card",
"dataSource": {
"api": "/api/stats/revenue",
"method": "GET"
}
"type": "element:number",
"dataSource": { "object": "order" },
"properties": { "aggregate": "count" }
}
```

Expand All @@ -55,13 +58,16 @@ Customize dashboard blocks for your needs:

### Add Actions

A control that runs something is its own node type, `action:button`, and its
props go in the `properties` bag; `actionType: "url"` navigates to `target`:

```json
{
"type": "button",
"label": "View Details",
"action": {
"type": "navigate",
"path": "/dashboard/details"
"type": "action:button",
"properties": {
"label": "View Details",
"actionType": "url",
"target": "/dashboard/details"
}
}
```
Expand Down
25 changes: 15 additions & 10 deletions content/docs/blocks/ecommerce.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,30 @@ Customize e-commerce blocks for your store:

### Add Product Data

A `card` fetches nothing. Records come from a data-bound block that names its
object in the per-element `dataSource` binding (see the
[Data Source guide](/docs/guide/data-source)):

```json
{
"type": "card",
"dataSource": {
"api": "/api/products",
"method": "GET"
}
"type": "object-grid",
"dataSource": { "object": "product", "limit": 20 }
}
```

### Add to Cart Action

A control that runs something is its own node type, `action:button`, and its
props go in the `properties` bag; `actionType: "api"` sends the request to
`target`:

```json
{
"type": "button",
"label": "Add to Cart",
"action": {
"type": "submit",
"endpoint": "/api/cart",
"type": "action:button",
"properties": {
"label": "Add to Cart",
"actionType": "api",
"target": "/api/cart",
"method": "POST"
}
}
Expand Down
14 changes: 9 additions & 5 deletions content/docs/blocks/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,17 @@ or in a TypeScript-authored schema pass the real thing —

### Add Submit Action

A control that runs something is its own node type, `action:button`, and its
props go in the `properties` bag; `actionType: "api"` sends the request to
`target`:

```json
{
"type": "button",
"label": "Submit",
"action": {
"type": "submit",
"endpoint": "/api/contact",
"type": "action:button",
"properties": {
"label": "Submit",
"actionType": "api",
"target": "/api/contact",
"method": "POST"
}
}
Expand Down
13 changes: 0 additions & 13 deletions content/docs/blocks/marketing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,6 @@ Customize marketing blocks for your campaigns:
}
```

### Add Analytics

```json
{
"type": "button",
"label": "Get Started",
"action": {
"type": "analytics",
"event": "cta_clicked"
}
}
```

### Customize Content

```json
Expand Down
1 change: 1 addition & 0 deletions content/docs/components/basic/icon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The glyph is named by `icon`:
`name` is the SDUI identity key every node carries, alongside `id`. It is **not**
read as a glyph name on `ui:icon`, and writing one there renders no icon:

{/* doc-json: fragment — a deliberately refused node: it shows the retired name-as-glyph shape the paragraph below describes */}
```json
{ "type": "icon", "id": "save_icon", "name": "save_icon" }
```
Expand Down
2 changes: 1 addition & 1 deletion content/docs/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ All ObjectUI components follow the same schema-based pattern:
"type": "button",
"label": "Click Me",
"variant": "default",
"size": "md",
"size": "default",
"className": "mt-4"
}
```
Expand Down
42 changes: 40 additions & 2 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ one has its own section below.
| `skill-examples.yml` | Skill Example Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a MARKED fenced example in a `skills/` or `.claude/skills/` guide no longer compiles against the packages' built types, no longer parses as JSON, uses a bare `any`, or carries a marker that opts nothing in |
| `skill-eval-tokens.yml` | Skill Eval Token Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when an eval assertion's `must_contain` token is not taught as a whole token anywhere in its own `skills/` bundle |
| `doc-component-types.yml` | Doc Component Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**.mdx` snippet teaches a `type` nothing registers |
| `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types, **or** when a covered JSDoc `@example` block does (two gates, one job — see below) |
| `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types, **or** when a covered JSDoc `@example` block does, **or** when a `json` / `jsonc` node document in `content/docs` is refused by the strict authoring face (three gates, one job — see below) |
| `doc-fence-languages.yml` | Doc Fence Language Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a TypeScript block sits under a fence the snippet gate does not read |
| `doc-example-ids.yml` | Doc Example Id Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**` page references a schema-catalog example id the registry does not carry, or when the gate's own population collapses |
| `pre-install-import-graph.yml` | Pre-Install Import Graph Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a gate a workflow runs *before* `pnpm install` reaches a package anywhere in its import graph |
Expand Down Expand Up @@ -1159,7 +1159,7 @@ names: `pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --b

### JSDoc `@example` Types — the second gate in this job (`doc-snippet-types.yml`)

The same job's **last** step runs `scripts/check-doc-example-types.mjs`
The same job's second blocking step runs `scripts/check-doc-example-types.mjs`
(`pnpm check:doc-examples`). It asks the snippet gate's question of a different corpus: every fenced
`ts` / `tsx` block inside a JSDoc `@example` on an **exported** declaration under
`packages/NAME/src/**` must compile `--strict` against the packages' built `dist/*.d.ts`, or be
Expand All @@ -1174,6 +1174,7 @@ copying it out of an IDE hover got TS2322, and nothing in this repository had ev
| `pnpm check:doc-snippets` (`scripts/check-doc-snippet-types.mjs`) | fenced `ts` / `tsx` blocks in the **documents** the `UNGATED_DOCS` ledger does not exempt | **Yes** |
| `pnpm check:doc-snippets --emit-census` | code this repository's generators emit from **template literals** | No — report-only |
| `pnpm check:doc-examples` (`scripts/check-doc-example-types.mjs`) | fenced `ts` / `tsx` blocks in JSDoc **`@example`** tags on exported declarations under `packages/NAME/src/**` | **Yes** |
| `pnpm check:doc-json` (`scripts/check-doc-json-fences.mjs`) | fenced `json` / `jsonc` **node documents** in `content/docs`, judged through the strict authoring face | **Yes** |

**Why it shares this job rather than getting a workflow.** It is a *sibling* of the gate above, not a
fork: it imports that script's compiler host, its built-`.d.ts` resolution and every one of its
Expand Down Expand Up @@ -1211,6 +1212,43 @@ example, or declare it. Run it locally exactly as this job does: build first
(`pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter)`), then
`pnpm check:doc-examples`; the gate prints that same recipe when it exits `2`.

### JSON Node Documents — the third gate in this job (`doc-snippet-types.yml`)

The job's **last** step runs `scripts/check-doc-json-fences.mjs` (`pnpm check:doc-json`). The two
gates above read `ts` / `tsx` only, so a `json` fence was checked by nothing:
[#5229](https://github.com/objectstack-ai/objectui/issues/5229) taught a `validation` bag on an
`input` node in `content/docs/blocks/forms.mdx` while every shipped tool was green. Every fenced
`json` / `jsonc` block under `content/docs` that parses into an object whose `type` names a node
schema is judged through the **strict authoring face** — `StrictAnyComponentSchema`, read from the
`@object-ui/types` the build step above produced, the face `objectui validate` and `objectui check`
judge an authored document with ([#5250](https://github.com/objectstack-ai/objectui/issues/5250)).
So a key no schema declares fails here exactly as it fails `objectui validate`; the tolerant face
keeps it, because `BaseSchemaCore` is `.passthrough()` for the renderer.

**What it does not judge, it counts.** A fence that is not JSON, not an object, carries no string
`type`, or names a `type` no node schema declares is not judged; the summary prints each bucket, and
`--list` prints every fence with its bucket. Pages outside `content/docs` are not read, and the
summary says so.

**Fragments are declared, and the declaration is re-judged.** A node document that is deliberately
not complete or not valid — a refused example that shows the error, an excerpt that elides required
keys, an object whose `type` only collides with a node type — carries
`{/* doc-json: fragment - why */}` (the HTML-comment form in `.md`) on the line above its fence. The
declared fence must be one the strict face refuses: a declaration on a fence the face accepts, or on
one that is not a node document at all, is reported as stale, so the declarations can only shrink.

**It proves itself before it judges the docs.** The face must resolve inside `packages/types/dist/`;
a document carrying an undeclared key must be refused by the strict face and accepted by the
tolerant one; a declared-only document must be accepted; and the walk must judge at least one node
document, including one from its anchor page. Its exit codes are the sibling gates' three: `0`
clean, `1` the gate ran and found errors, `2` it **could not run** — `@object-ui/types` unbuilt, or
a control or the enumeration failed — which is neither green nor red.

**If it fails:** each refusal names the page, the line of its fence, and each reason — an undeclared
key by name and path, anything else by the face's own message. Remove the key, or replace it with
the declared key its renderer reads; declare a deliberate fragment. Run it locally with
`pnpm --filter @object-ui/types build`, then `pnpm check:doc-json`.

## Fence Languages (`doc-fence-languages.yml`)

**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — **no path
Expand Down
1 change: 1 addition & 0 deletions content/docs/guide/dashboard-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ it refuses the same documents before they reach the platform.

Options can also be fetched from an object at runtime:

<!-- doc-json: fragment — not a component node: a `globalFilters[]` member, whose `type` is the filter's input type and only collides with the `select` node type -->
```json
{
"name": "industry",
Expand Down
61 changes: 27 additions & 34 deletions content/docs/guide/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,34 +216,16 @@ The scope the evaluator builds is what you published, plus two names the rendere
> is `record` (ADR-0089 D3) — rewrite such a gate to `record.*` rather than re-publishing a
> `data` key.

### Scoped Data
### Lists

Some components provide scoped data:

```jsonc
{
"type": "list",
"items": "${users}",
"itemTemplate": {
"type": "card",
"title": "${item.name}", // 'item' is scoped data
"description": "${item.email}"
}
}
```

### Index in Loops

Access the current index in loops:
`list` has no item template, and it provides no `item` or `index` scope for an expression to
read. It draws its `items` array — or the array its `bind` path resolves to — one row per entry:
a string, or an object whose `content` is a node.

```json
{
"type": "list",
"items": "${users}",
"itemTemplate": {
"type": "text",
"content": "#${index + 1}: ${item.name}"
}
"bind": "users"
}
```

Expand Down Expand Up @@ -406,7 +388,7 @@ Two neighbouring traps: `text` is not a `badge` key at all — the badge's text
{
"type": "empty",
"visibleOn": "${items.length === 0}",
"message": "No items to display",
"title": "No items to display",
"description": "Start by adding your first item"
}
```
Expand Down Expand Up @@ -453,39 +435,50 @@ and a fail-soft surface answers it by showing the field on every row.

### Dependent Fields

A form's fields are its `fields` array, and each field's `visibleOn` is evaluated against that
row:

```json
{
"type": "form",
"children": [
"fields": [
{
"type": "select",
"name": "country",
"type": "select",
"label": "Country",
"options": ["USA", "Canada", "Mexico"]
"options": [
{ "label": "USA", "value": "USA" },
{ "label": "Canada", "value": "Canada" },
{ "label": "Mexico", "value": "Mexico" }
]
},
{
"type": "select",
"name": "state",
"type": "select",
"label": "State/Province",
"visibleOn": "${record.country === 'USA'}",
"options": ["CA", "NY", "TX"]
"options": [
{ "label": "CA", "value": "CA" },
{ "label": "NY", "value": "NY" },
{ "label": "TX", "value": "TX" }
]
}
]
}
```

### Dynamic Validation
### Validation

An `input` declares no `validations` bag, and nothing reads one. `inputType: "email"` renders an
email input, which the browser validates, and `required` marks it required:

```json
{
"type": "input",
"name": "email",
"label": "Email",
"required": true,
"validations": {
"isEmail": true,
"errorMessage": "Please enter a valid email"
}
"inputType": "email"
}
```

Expand Down
Loading
Loading