Skip to content

Add documentation for ObjectKanban, ObjectCalendar, ObjectGantt, and ObjectMap view controls - #196

Merged
hotlong merged 3 commits into
mainfrom
copilot/add-view-controls-documentation
Jan 25, 2026
Merged

hotlong merged 3 commits into
mainfrom
copilot/add-view-controls-documentation

Conversation

Copilot AI commented Jan 25, 2026 •

Copy link
Copy Markdown
Contributor

Completes documentation coverage for all view controls in @object-ui/views. Four components were previously undocumented.

Added Documentation

  • ObjectKanban - Drag-and-drop kanban board with auto-grouping by field, column summaries, card customization
  • ObjectCalendar - Multi-view calendar (month/week/day) for event scheduling and timeline visualization
  • ObjectGantt - Project timeline with task dependencies, progress tracking, critical path
  • ObjectMap - Geographic visualization with marker clustering, popups, flexible location formats

Each includes:

  • Interactive demo with InteractiveDemo component
  • JSON schema examples and TypeScript API reference
  • Real-world integration examples (5+ per component)
  • Data provider patterns (object/api/value)
  • Event handler usage

Example Usage

<ObjectKanban 
  schema={{
    type: 'object-grid',
    objectName: 'tasks',
    filter: {
      kanban: {
        groupByField: 'status',
        columns: ['name', 'assignee', 'priority'],
        summarizeField: 'estimated_hours'
      }
    }
  }}
  dataSource={dataSource}
  onCardMove={(id, from, to) => updateStatus(id, to)}
/>

Files Changed

  • content/docs/views/object-kanban.mdx (new, 428 lines)
  • content/docs/views/object-calendar.mdx (new, 536 lines)
  • content/docs/views/object-gantt.mdx (new, 567 lines)
  • content/docs/views/object-map.mdx (new, 652 lines)
  • content/docs/views/index.mdx (updated with component categorization)
  • content/docs/views/meta.json (registered new pages)

Documentation structure mirrors existing object-view.mdx and object-grid.mdx patterns for consistency.

Original prompt

在文档中补充所有view控件的说明并为关键功能设计演示demo效果


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel

vercel Bot commented Jan 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
objectui-docs Error Error Jan 25, 2026 3:31pm
objectui-site Ready Ready Preview, Comment Jan 25, 2026 3:31pm

Request Review

…ar, Gantt, Map)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add documentation for all view controls with demo designs Add documentation for ObjectKanban, ObjectCalendar, ObjectGantt, and ObjectMap view controls Jan 25, 2026
Copilot AI requested a review from hotlong January 25, 2026 15:31
@hotlong
hotlong marked this pull request as ready for review January 25, 2026 15:36
Copilot AI review requested due to automatic review settings January 25, 2026 15:36
@hotlong
hotlong merged commit 19e2ae3 into main Jan 25, 2026
5 of 6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive documentation for four previously undocumented view control components in the @object-ui/views package: ObjectKanban, ObjectCalendar, ObjectGantt, and ObjectMap. Each component receives detailed documentation including interactive demos, JSON schema examples, TypeScript API references, and multiple real-world integration examples.

Changes:

  • Added documentation for ObjectKanban (kanban board with drag-and-drop workflow visualization)
  • Added documentation for ObjectCalendar (multi-view calendar for event scheduling)
  • Added documentation for ObjectGantt (project timeline with task dependencies and progress tracking)
  • Added documentation for ObjectMap (geographic visualization with location markers)
  • Updated index.mdx to categorize components into "CRUD Components" and "Specialized Visualization Components"
  • Registered new documentation pages in meta.json

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
content/docs/views/object-kanban.mdx Complete documentation for kanban board component with workflow stage visualization, drag-and-drop, and column summaries
content/docs/views/object-calendar.mdx Complete documentation for calendar component with month/week/day views and event management
content/docs/views/object-gantt.mdx Complete documentation for Gantt chart component with timeline visualization, dependencies, and progress tracking
content/docs/views/object-map.mdx Documentation for map component with location markers - contains critical issues with non-existent features
content/docs/views/index.mdx Updated overview with component categorization and feature summaries
content/docs/views/meta.json Registered four new documentation pages

Comment on lines +412 to +420
renderPopup={(record) => (
<div>
<h3>{record.name}</h3>
<p>{record.address}</p>
<button onClick={() => navigate(record)}>
Get Directions
</button>
</div>
)}

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references a renderPopup prop that doesn't exist in the ObjectMapProps interface. The actual component only has these props: schema, dataSource, className, onMarkerClick, onEdit, and onDelete. This example code will not work as written.

Suggested change
renderPopup={(record) => (
<div>
<h3>{record.name}</h3>
<p>{record.address}</p>
<button onClick={() => navigate(record)}>
Get Directions
</button>
</div>
)}
onMarkerClick={(record) => {
openMapPopup(
<div>
<h3>{record.name}</h3>
<p>{record.address}</p>
<button onClick={() => navigate(record)}>
Get Directions
</button>
</div>
);
}}

Copilot uses AI. Check for mistakes.
Comment on lines +599 to +614
```json
{
"filter": {
"map": {
"latitudeField": "latitude",
"longitudeField": "longitude",
"titleField": "name",
"enableClustering": true,
"viewportLoading": true
}
},
"pagination": {
"pageSize": 1000
}
}
```

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows enableClustering and viewportLoading fields in the map configuration, but these fields don't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54). The MapConfig only supports: latitudeField, longitudeField, locationField, titleField, descriptionField, zoom, and center.

Copilot uses AI. Check for mistakes.
"longitudeField": "current_lng",
"titleField": "vehicle_id",
"descriptionField": "driver_name",
"markerIconField": "vehicle_type"

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows markerIconField in the map configuration example, but this field doesn't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54).

Suggested change
"markerIconField": "vehicle_type"
"markerColorField": "vehicle_type"

Copilot uses AI. Check for mistakes.
Comment on lines +498 to +514
### Real Estate Listings

```json
{
"type": "object-grid",
"objectName": "properties",
"filter": {
"map": {
"latitudeField": "property_lat",
"longitudeField": "property_lng",
"titleField": "property_address",
"descriptionField": "property_details",
"markerColorField": "property_type"
}
}
}
```

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows markerColorField in the map configuration example, but this field doesn't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54).

Copilot uses AI. Check for mistakes.
- Interactive map with markers
- Marker clustering
- Custom popups
- Auto-fit to bounds

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions "Auto-fit to bounds" as a feature of ObjectMap, but the autoFit configuration field doesn't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54). This feature is not currently implemented.

Suggested change
- Auto-fit to bounds

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +86
- Marker clustering
- Custom popups

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation mentions "Marker clustering" and "Custom popups" as features of ObjectMap, but the necessary configuration fields (enableClustering) and props (renderPopup) don't exist in the actual implementation. These features are not currently available.

Suggested change
- Marker clustering
- Custom popups

Copilot uses AI. Check for mistakes.
Comment on lines +337 to +349
```json
{
"filter": {
"map": {
"latitudeField": "latitude",
"longitudeField": "longitude",
"titleField": "name",
"markerIconField": "icon_url",
"markerColorField": "category"
}
}
}
```

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows markerIconField and markerColorField fields in the map configuration, but these fields don't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54). The MapConfig only supports: latitudeField, longitudeField, locationField, titleField, descriptionField, zoom, and center.

Copilot uses AI. Check for mistakes.
Comment on lines +373 to +387

Auto-fit map to show all markers:

```json
{
"filter": {
"map": {
"latitudeField": "latitude",
"longitudeField": "longitude",
"titleField": "name",
"autoFit": true
}
}
}
```

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows an autoFit field in the map configuration, but this field doesn't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54). The MapConfig only supports: latitudeField, longitudeField, locationField, titleField, descriptionField, zoom, and center.

Copilot uses AI. Check for mistakes.
Comment on lines +552 to +566

```json
{
"type": "object-grid",
"objectName": "customers",
"filter": {
"map": {
"locationField": "address_location",
"titleField": "company_name",
"descriptionField": "contact_info",
"markerColorField": "customer_tier"
}
}
}
```

Copilot AI Jan 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation shows markerColorField in the map configuration example, but this field doesn't exist in the actual MapConfig interface (packages/views/src/ObjectMap.tsx:39-54).

Copilot uses AI. Check for mistakes.
os-elon-musk pushed a commit that referenced this pull request Sep 21, 2026
… refusal names the operator instead of throwing (objectui#9050)

`toFilterNode` delegates to `convertFiltersToAST`, which refuses eleven authored
shapes with a `FilterOperatorError`. Four call sites reach it off the wire path:
three from a render-time `useMemo` (`RelatedList`, `LineItemsPanel`,
`ObjectGrid.schema.filter`), where a throw is a render error with no
`classifyLoadError` above it, and one inside `ObjectGrid`'s load effect
(`defaultFilters`), which already caught the refusal but reported it as a
generic load failure.

- `@object-ui/core` gains `toFilterNodeSafely`, returning a UNION so a refusal
  cannot be read as `undefined` — which means "no filter", i.e. every row, the
  silently unconstrained query objectui#9001 closed.
- `FilterOperatorError` carries `operator` / `field` as data; the eleven
  messages share no idiom, so scraping a token out of them would be a twelfth
  dialect.
- The three render-time readers send nothing and render a state naming the
  operator; `ObjectGrid`'s load-path refusal lands on the same branch.
- `view.malformedFilter` added to all ten locale packs and the three affected
  `createSafeTranslation` tables.

Ruling-ref: objectui#9050 comment 5749197961 (batch #196 item 2, letter C′).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr7APep6jm1Zta3KUzPzZf
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
… refusal names the operator instead of throwing (objectui#9050) (objectstack-ai#10201)

Fixes objectstack-ai#9050

Seat session, in prose because a footer does not survive an edit:
`session_01Xr7APep6jm1Zta3KUzPzZf`. ⚠️ The five `ObjectGrid.tsx`
coordinates this body cited were addresses in a tree this very diff
moves; they are now bound to the tree each was read from, per the
`changeset-claim-re-read` gate on this PR (⛔ correcting the number alone
re-stales on the next insertion). Edited by the reviewing `domain:ui`
seat from the dev's own re-taken readings; ⛔ no code changed and the
head did not move.

`Ruling-ref:` comment **5749197961** — batch objectstack-ai#196 item 2, letter **C′**,
maintainer 「同意」 2026-09-20T10:21Z. Steps 1 and 2 in one PR, as the
ruling directs. Rule 3 (no new save-time gate) is honoured: nothing here
adds a validator.

`Clause-②: no` — re-derived from the diff at the foot of this body.

---

## What changed

`convertFiltersToAST` refuses eleven authored shapes with a
`FilterOperatorError`. `toFilterNode` delegates to it from four call
sites that are not on the wire path, so three of them turned an
authoring mistake into a **render error** with no `classifyLoadError` to
translate it, and the fourth reported it as a network-ish load failure.

- **`@object-ui/core`** gains `toFilterNodeSafely`. Its result is a
UNION — `{ ok: true, node }` or `{ ok: false, refusal }` — so a refusal
is **not representable as `undefined`**. That is the point, not a style
choice: `undefined` means "no filter", i.e. every row, which is the
silently unconstrained query objectui#9001 eliminated, and the reason
the ruling refuses option A. TypeScript makes a caller narrow before it
can build a query.
- **`FilterOperatorError`** now carries `operator` and `field` as data,
and `filterRefusalSubject` picks the token a diagnostic names. The
eleven messages deliberately share no idiom (the file's own docblocks
argue at length that they must not), so recovering the token by pattern
would be a twelfth dialect that fails silently on the next rewording.
- **The three render-time readers** keep the refusal as a value, send
nothing, and render a state that names the operator.
- **`ObjectGrid`'s `defaultFilters` leg** keeps its existing throw —
measured, it is already inside the load effect's own `try` — and now
lands on the same render branch, so it is diagnosed instead of reported
under "Error loading grid".
- **`view.malformedFilter`** added to all ten locale packs and to the
three affected `createSafeTranslation` tables.

⛔ **Which shapes convert is byte-identical.** This is a delivery change,
not an acceptance one.

---

## Step 1 — the measurement

Re-runnable. Driven through `toFilterNodeSafely` (the render-time entry,
not `convertFiltersToAST` directly), against **installed
`@objectstack/spec` 17.4.0**, which is the artifact this repo builds and
ships against (`@object-ui/core` floor: `^17.3.0`).

The eleven throws span **two** authoring vocabularies, so "does the
protocol refuse the same input" has to be asked at the door that
actually gates each one. `ViewFilterRuleSchema` — the schema the ruling
names — gates throws 10 and 11; the object arm is `FilterCondition`'s
door. The two right-hand columns are the SLOT declarations, which decide
whether a parse happens at all.

| # | throw | arm | converter | protocol VALUE door |
`ObjectGridProps.filter` | `ObjectGridProps.defaultFilters` |
`ListViewSchema.filter` | `RecordRelatedListProps.filter` |
|---|---|---|---|---|---|---|---|---|
| 1 | L176 combinator value is not an ARRAY | obj | THROW (`$and`) |
FilterConditionSchema: **REFUSE** | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 2 | L191 combinator member is not a condition OBJ | obj | THROW
(`$and`) | FilterConditionSchema: **REFUSE** | ACCEPT | ACCEPT | REFUSE
| REFUSE |
| 3 | L389 `$icontains` empty / non-string comparand | obj | THROW
(`$icontains`) | FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT |
REFUSE | REFUSE |
| 4 | L500 `$not` combinator | obj | THROW (`$not`) |
FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 5 | L539 bare ARRAY as equality comparand | obj | THROW (`tags`) |
FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 6 | L641 exotic object in comparand position | obj | THROW (`created`)
| FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 7 | L669 `$regex` operator | obj | THROW (`$regex`) |
FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 8 | L715 retired lowercase alias | obj | THROW (`$startswith`) |
FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 9 | L726 unknown operator | obj | THROW (`$bogus`) |
FilterConditionSchema: ACCEPT | ACCEPT | ACCEPT | REFUSE | REFUSE |
| 10 | L1040 ARRAY on a single-value view operator | rule | THROW
(`equals`) | **ViewFilterRuleSchema: ACCEPT** | ACCEPT | ACCEPT | ACCEPT
| ACCEPT |
| 11 | L1171 `icontains` empty / non-string comparand | rule | THROW
(`icontains`) | **ViewFilterRuleSchema: ACCEPT** | ACCEPT | ACCEPT |
ACCEPT | ACCEPT |

**Envelope controls** — every REFUSE above is unreadable without these,
and my first pass produced phantom refusals until I added them:

```
ObjectGridPropsSchema  envelope, no filter   : ACCEPT
ListViewSchema         envelope, no filter   : ACCEPT
ListViewSchema         envelope, valid rule  : ACCEPT
RecordRelatedListProps envelope, no filter   : ACCEPT
RecordRelatedListProps envelope, valid rule  : ACCEPT
ViewFilterRuleSchema   valid rule            : ACCEPT
FilterConditionSchema  valid condition       : ACCEPT
```

**Negative controls** — without these the doors could be inert and every
ACCEPT would mean nothing:

```
ViewFilterRuleSchema   missing `field`       : REFUSE
ViewFilterRuleSchema   unknown operator      : REFUSE
ViewFilterRuleSchema   `in` with a SCALAR    : REFUSE
ViewFilterRuleSchema   `between` with 3      : REFUSE
FilterConditionSchema  a bare string         : REFUSE
ObjectGridPropsSchema  objectName: 42        : REFUSE
```

⭐ Those last four are what make row 10 a finding rather than a shrug.
`ViewFilterRuleSchema` **does** couple value shape to operator —
`superRefine(checkViewFilterRuleValueShape)` is present in the shipped
artifact and it refuses `in` with a scalar and `between` with three
members. It ends `if (!isPair) return;`, so **every other operator is
not checked**, while the same schema's own `value` description says
"every other operator takes a scalar". Row 10 is the converter enforcing
what the protocol declares in prose and does not enforce at parse.

Rows 3, 7 and 11 are the same shape one layer over:
`@objectstack/spec/data`'s `FILTER_TEXT_CASES` **declares** these
refusals, with `expectRejection: true` and `code: 'INVALID_FILTER'` —
for `$regex`, for a dangling `$options`, and for an empty or non-string
`$icontains` comparand. It is a backend conformance table, not a parse
rule, so `FilterConditionSchema.safeParse` accepts all three.

### The slot columns, and a correction worth stating plainly

On the **installed 17.4.0 artifact** this repo builds against:

```
ObjectGridPropsSchema:  objectName:     z.string().optional()            ← control, discriminates (42 REFUSED)
                        columns:        z.array(z.unknown()).optional()
                        filter:         z.unknown().optional()           ← unconstrained
                        defaultFilters: z.unknown().optional()           ← unconstrained
                        sort:           z.unknown().optional()
```

On the **`objectstack` working tree** (`04d639c659`,
`packages/spec/src/ui/component.zod.ts:2661`, version string also
`17.4.0`, **unreleased**), `filter` has since been narrowed to
`z.array(ViewFilterRuleSchema, { error: ruleArrayFilterError(…)
}).optional()`, while `defaultFilters` is still
`z.unknown().optional()`. The two builds also carry different `describe`
text for that key, which is the cheapest way to tell them apart.

⇒ the ruling's rule 3 premise — "the protocol's parse IS the save-time
gate" — **splits by key, and by artifact**. It does not fail wholesale:

- `ObjectGrid`'s `schema.filter` read — the render-time `useMemo` this
PR rewrites (`:1718` at `e686f4d9a`, `:1730` at this head) — is **pin
lag, not a contract gap**: narrowed upstream, absent from the floor this
repo pins, so nothing gates it on the tree that ships today. Remedy is a
release and a floor bump, not a spec change.
- `ObjectGrid`'s `schema.defaultFilters` read inside `loadSchemaAndData`
(`:2092` at `e686f4d9a`, `:2123` at this head) —
`z.unknown().optional()` in **both** trees. Nothing gates it and nothing
is scheduled to. **This one is the contract gap**, and it is the
strongest argument for why step 2's B is not redundant.
- `ListViewSchema.filter` and `RecordRelatedListProps.filter` are
`z.array(ViewFilterRuleSchema)` in the shipped artifact and already
refuse all nine object-arm shapes structurally.

**Everything in this section belongs to the protocol, not to this PR.**
The objectstack card carrying these readings is drafted and filed by the
seat at ACCEPT (rule 1).

---

## Boundary readings, re-taken on this branch

The card body says a render-time throw "takes the subtree down".
Measured, that is not what happens on the schema path — and the
difference decides what the pin must discriminate.

| reading | where |
|---|---|
| `SchemaRenderer` wraps **every** rendered node in a per-component
`SchemaErrorBoundary`, auto-recovering on `resetKey` |
`packages/react/src/SchemaRenderer.tsx` — class at `:624`, the wrap at
`:2067` |
| that boundary's fallback prints `error.message`, so the operator name
**does** survive into it | `SchemaRenderer.tsx:661` — the heading above
it is the generic `Component "TYPE" failed to render` |
| CONTROL — neither caller package has a boundary of its own |
`plugin-grid` **0** non-test `ErrorBoundary` hits, `plugin-form` **0** |
| all three components are exported from their package entry, so a host
can mount them with **no** `SchemaRenderer` above them |
`plugin-detail/src/index.tsx:37`, `plugin-form/src/index.tsx:66`,
`plugin-grid/src/index.tsx:23` |
| an in-repo direct mount exists today |
`apps/console/src/dev/DevRowActions.tsx:56` mounts `ObjectGrid` with
zero `SchemaRenderer` in the file |
| ⭐ the fourth call site is **not** render-time | the
`toFilterNode(schema.defaultFilters)` call inside `loadSchemaAndData`,
within the `try` whose `catch` calls `setError` — so it already rendered
a visible panel, under `t('grid.errorLoading')`. Coordinates, each bound
to the tree it was read from: `:2092`, try `:1809`, catch `:2258` at
`e686f4d9a`; `:2123`, `:1828`, `:2289` at this head `28cf61ab1` |

⇒ what B buys differs per site, and the PR says so rather than claiming
one story for four:

1. **Containment** where there is no boundary — a direct mount, or any
host embedding these packages outside the console.
2. **The named diagnostic** everywhere else, in place of "Component
failed to render" (schema path) or "Error loading grid" (the
`defaultFilters` leg).

A pin that only proved "the page did not blank" would have been green
before this change on the schema path. Neither of these is.

---

## Ablation

Mutated on disk by a refuse-on-zero-match writer, one leg at a time,
anchor matched exactly once, verified in both directions, restored from
`HEAD` under a `trap` armed with an absolute path, restore proven by
blob hash **and** an empty `git diff HEAD`. `vitest.config.mts:503`
aliases `@object-ui/core` to `packages/core/src`, so a mutation there
reaches the run with no rebuild.

| leg | mutation | predicted | observed |
|---|---|---|---|
| **A** — remove the catch | `toFilterNodeSafely`'s
`FilterOperatorError` branch rethrows instead of returning `{ ok: false
}` | all three caller pins RED, the eleven core rows RED, positive
controls GREEN | **matched** — 16 failed / 7 passed. The three positive
controls stayed green, and so did the `defaultFilters` leg, which does
not go through `toFilterNodeSafely` at all |
| **B** — remove the named operator | `filterRefusalSubject` returns
`undefined`, so the translated headline interpolates an empty subject |
the callers' `toContain(operator)` RED, their "filter is malformed" and
"no query sent" GREEN | ⚠️ **first run did NOT match** — only the eleven
core rows reddened; all three caller files passed |

⭐ **Leg B's miss was a real defect in the pin, and it is the most useful
thing this ablation produced.** The banner renders the converter's own
message under the headline, and that message repeats the operator token
— so a `toContain` over the whole banner stayed green with the name
removed from the translated headline. That is precisely the "names the
operator is decoration" failure this work was told not to ship. The
headline is now separately addressable (`*-malformed-filter-subject`)
and the operator assertions read it; **re-run, leg B reds 16 / passes
7**, with "sends no query at all" and the positive controls among the
survivors. The two halves are now separable, which is the property that
makes the pin worth having.

Both legs restored cleanly: `RESTORE PROVEN: blob
63228ed == HEAD blob, git diff HEAD
empty`.

---

## Gates

Hand-derived from objectui's own `package.json` scripts and
`.github/workflows/` — `dispatch-gates.mjs` refuses for this repo (it
derives families from the tree the process runs in, which is
`objectstack`). Every exit code captured to a file and read back, ⛔
never through a pipe.

All readings taken at `28cf61ab1`, this branch's head.

| gate | exit |
|---|---|
| `vitest run packages/core/ packages/plugin-detail/
packages/plugin-form/ packages/plugin-grid/ packages/i18n/` | **0** —
`Test Files 680 passed (680)`, `Tests 8926 passed / 1 skipped (8927)` |
| `type-check` on those five packages (`tsc --noEmit` + `tsc -p
tsconfig.test.json` each) | **0** — 0 occurrences of `error TS` |
| the four new pin files on their own | **0** — `Test Files 4 passed
(4)`, `Tests 23 passed (23)` |
| `pnpm exec eslint .` (whole repo, 5230 files) | **0** — 0 errors,
13449 warnings |
| `pnpm check:control-bytes` | 0 |
| `pnpm check:test-path-roots` | 0 |
| `pnpm check:new-line-citations` | 0 |
| `pnpm check:changeset-claims` | 0 |
| `pnpm check:vi-mock-specifiers` | 0 |
| `pnpm check:vi-mock-inherit` | 0 |
| `pnpm check:i18n-keys` | 0 |
| `pnpm check:i18n-drift` | 0 |
| `pnpm check:i18n-dead-keys` | 0 |
| `node scripts/check-changeset-presence.mjs` | 0 |
| `node scripts/check-changeset-no-major.mjs` | 0 |
| `node scripts/check-governed-queue-guard.mjs --test` over the six
touched paths | 0 — `NOT GOVERNED`, 6 paths checked against 5 governed
surfaces |

**No narrowing is declared for eslint** — the repo-wide run is the whole
population (5230 files), not a subset, and it is green at this branch's
head. For the record, type-aware linting is **not** enabled
(`eslint.config.js` sets no `project` / `projectService` /
`parserOptions`), so this diff cannot move the verdict on a file it does
not touch.

⚠️ One reading that will mislead anyone who repeats it: `eslint .
--no-inline-config` reports 97 errors in 80 files on this tree. That
flag disables the repo's own `eslint-disable-next-line` comments, which
are load-bearing here (`react-hooks/static-components` in particular),
and it is **not** what CI runs. It is green both before and after this
change with the flag off.

**A changeset is present** and declares `minor` for the five affected
packages — ⛔ never `major`, per the fixed-group rule.

---

## `Clause-②: no`, re-derived from the diff

The ruling's test: 「a render state on published components ⇒ expected
`no`; any converter change that **widens what converts** is `yes`」.

- The converter's **accept set is unchanged**. `convertFiltersToAST`'s
eleven refusal conditions are byte-identical; the only edit inside them
is a second constructor argument carrying `{ operator, field }`. The
positive control in the core pin executes both directions — a valid
object filter and a valid rule array still lower to exactly the nodes
they lowered to before.
- `toFilterNodeSafely` is a **new** entry point that wraps
`toFilterNode`; it converts nothing `toFilterNode` did not already
convert, and it narrows nothing either.
- The render states are on published components and publish no authoring
surface.
- Nothing in `packages/spec/**` or in the `objectstack` repo is touched.
The protocol-side deltas from step 1 are a card, not a diff.

⛔ The stop-and-report fork — "the alignment requires the converter to
**accept** a shape it refuses today" — was **not** reached. No refusal
was removed or relaxed.

---

## Scope notes

- **Fence extension, declared.** The dispatch fenced the converter, the
three caller files and their tests, and separately required
`check:i18n-keys` to pass with a real key rather than a hard-coded
English string. A real key cannot exist in one file:
`all-locales-key-parity.test.ts` requires every `en` key in all ten
packs, and `defaults-maps-mirror-en-pack` requires the
`createSafeTranslation` rows to be byte-identical to `en`. So this diff
also touches
`packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts` and
`packages/plugin-detail/src/useDetailTranslation.ts`. None of those is
on the held list (`packages/i18n/src/provider.tsx` is, and is
untouched).
- **Held paths respected.**
`packages/components/src/custom/filter-builder.tsx`,
`packages/app-shell/src/hooks|console|layout`,
`packages/i18n/src/provider.tsx`, `packages/plugin-tree/`,
`packages/react/` — all read, none edited.
- `packages/react/src/SchemaRenderer.tsx` was **read** for the boundary
measurement and is not in the diff.

## Acceptance notes

Noted, not filed — observations, out of the three filing classes:

- `ObjectGrid`'s `sort` slot is `z.unknown().optional()` on the
installed artifact for the same reason `filter` is, and the narrowing to
`z.array(SortItemSchema)` is in the objectstack tree unreleased. Same
mechanism as the `filter` row above, different key; it rides the same
floor bump, so it needs no card of its own. Carrier: the objectstack
card this PR's step 1 produces.
- `FilterOperatorError.operator` is absent on exactly two arms
(bare-array and exotic comparand), where the author wrote no operator.
`filterRefusalSubject` falls back to the field, so the sentence reads
"the `tags` condition cannot be applied", which is accurate but is a
field where the copy says condition. Carrier: none — it is a wording
judgement on new copy, not a defect.

The dependency closure for the packages above was built first under the
shared verify lock, so no `TS2307` / `TS6305` prerequisite failure is
hiding in these readings.

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch had an error being deployed

1 failed and 1 active deployments
Preview – objectui-site — f465e29c Deployed Jan 25, 2026 by vercel[bot]
Preview – objectui-docs — f465e29c Deployed Jan 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants