Skip to content

Commit b48f957

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-13263-reexport-doc-block
# Conflicts: # skills/objectstack-ui/references/_index.md
2 parents 5f7ed02 + 56c093c commit b48f957

31 files changed

Lines changed: 3190 additions & 164 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
"@objectstack/driver-memory": minor
3+
"@objectstack/types": patch
4+
---
5+
6+
fix(driver-memory): enforce field-level `unique`, so a colliding write is refused instead of landing silently (#13197)
7+
8+
`InMemoryDriver` enforced **no uniqueness at all**. `create` was a
9+
`table.push()` and `syncSchema` allocated an array, so a `unique: true` field
10+
was declared-and-not-enforced — the ADR-0078 / Prime-Directive-#10 shape the
11+
platform refuses everywhere else. A colliding write did not fail; it landed, and
12+
a read returned both rows.
13+
14+
The motivating instance is the worst-shaped one. The engine's
15+
`createWithAutonumberResync` re-seeds the counter and re-issues a record number
16+
when the STORE rejects it as a duplicate, so on a store that rejected nothing
17+
the whole branch was unreachable: an autonumber allocated out of process
18+
duplicated an existing business identifier with **no error anywhere**. The
19+
remedy's location was already ruled in-tree at that method — «uniqueness
20+
enforcement in the driver, NOT a pre-issue existence probe here» — and this is
21+
that remedy. Nothing in the new code knows what an autonumber is; the defect was
22+
that the driver constrained nothing.
23+
24+
**The refusal** carries the ADR-0112 envelope the SQL family answers a conflict
25+
with: `code: 'UNIQUE_VIOLATION'`, `status: 409`, no `[driver-memory]` prefix. So
26+
a suite that swaps this driver for SQLite sees one envelope — the parity
27+
`memory-filter-refusal-envelope.test.ts` already states for the filter family,
28+
now held for the constraint family. It is checked before the row is written, so
29+
a refused write leaves the table exactly as it found it, and `updateMany`
30+
prepares and checks the whole batch before mutating any of it.
31+
32+
**The scoping is `driver-sql`'s, measured — not a simpler invention.** Read off
33+
`uniqueIndexesFromFields` (ADR-0120 D1/D3) and reproduced arm for arm:
34+
`unique: 'global'` is platform-wide; bare `true` and `'organization'` are
35+
per-organization (bare `true` is the POSITIONAL spelling of `'organization'` at
36+
FIELD level — reading it as `'global'` is the #4986 trap and would make two
37+
organizations' identical values collide on a constraint neither can see); both
38+
degrade to a single column when the object has no tenant column, and a `unique`
39+
declaration on the tenant column itself stays single-column. NULL values stay
40+
NULL-DISTINCT, exactly as under SQL `UNIQUE`. The D3 NULL-organization fold
41+
needs no `'__global__'` token here — that sentinel is a SQL-expression artefact,
42+
and a JavaScript key holds `null` directly.
43+
44+
**Not** widened into: object-level declared `indexes[]` (composite uniques),
45+
primary keys, or row-level tenant isolation. This driver still refuses to boot
46+
multi-tenant (#6915) and that guard is untouched.
47+
48+
`@objectstack/types` (`patch`): `isUniqueViolationError` now reads the
49+
platform's own registered `UNIQUE_VIOLATION` code on the `code` channel. Not
50+
cosmetic — a conflict that predicate does not recognise leaves the autonumber
51+
resync unable to re-seed, so the counter stays warm and every following insert
52+
collides too (#5495's PROBE3 storm), i.e. a silent duplicate traded for a
53+
non-converging insert loop. It is a tautology rather than a widened heuristic
54+
(the code already MEANS this condition), and no existing in-repo producer's
55+
classification changes: `@objectstack/rest`'s own response body is the only
56+
other site carrying that string, and it is downstream of the predicate.
57+
58+
**Grade.** `minor` for the driver, not `patch`: a write that previously
59+
succeeded is now refused (`409`), which is an accept-set narrowing under the
60+
repo's launch-window convention for breaking changes, and the package also gains
61+
public exports (`UNIQUE_VIOLATION_CODE`, `uniqueConstraintsFromFields`,
62+
`tenantFieldOf`, `uniqueKeyOf`, `assertNoUniqueViolation`,
63+
`uniqueViolationError`). `patch` for `@objectstack/types`: no API added or
64+
removed and no in-repo verdict changes — the limb exists to serve the new
65+
producer. Fixtures that relied on duplicates landing on a declared-unique field
66+
must stop declaring `unique`, or stop writing the duplicate; the repo's own
67+
suites were measured and none did.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/sdui-parser': minor
3+
---
4+
5+
sdui-parser: `interpretBrace` materializes the JS literal subset, in lockstep with objectui
6+
7+
The html tier's braced attribute values accepted strict JSON only, so the spelling every
8+
JSX author and every AI author writes — `columns={['name','amount']}` — compiled to the
9+
deferred `{ $expr }` marker that nothing downstream evaluates, and the author's data
10+
binding vanished at render. Under the maintainer's ruling on objectui#6614 (Q1-A,
11+
2026-08-28) `interpretBrace` now materializes the JS **literal subset**: exactly two
12+
widenings over JSON — single-quoted strings (value position and key position) and unquoted
13+
identifier object keys.
14+
15+
Everything else JSON refuses is still refused and still becomes `{ $expr }`: trailing
16+
commas, comments, array holes, spreads, `undefined` / `NaN` / `Infinity`, `+1` / `.5` /
17+
`1.` / `0x1f`, template literals, and every genuine expression. `JSON.parse` still runs
18+
first and untouched, so strict-JSON behaviour is invariant by construction, and the subset
19+
contains no identifier lookup and no operator — the widening moves habitual spellings onto
20+
the materialized side, it does not move the data/code boundary (ADR-0080: this tier parses,
21+
never executes).
22+
23+
An authored `__proto__` key is written as an own data property, the way `JSON.parse` gives
24+
it, never through the prototype setter — a plain assignment in the unquoted-key path would
25+
hand untrusted page source a prototype-pollution lever the strict-JSON path never had.
26+
27+
The `inert-expression` diagnostic message is reworded to match: the old text advised
28+
writing the value as JSON with double-quoted strings and keys, which now names a legal
29+
spelling as the wrong one. Diagnostic **codes** are unchanged.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
liveness ledger: re-close `tool.json` against the real cloud runtime, and repair a `_note` that was false three ways (#13042)
6+
7+
All six entries cited `packages/services/service-ai/…` — a path that exists in
8+
**neither** repo. The framework has no service-ai tree at all (`git ls-files`
9+
matches 0 paths containing it; `packages/services/` holds 16 members, none of
10+
them service-ai), and the cloud repo's real layout is `packages/service-ai/…`
11+
and `packages/service-ai-studio/…`. Six pointers into thin air, green for as
12+
long as they existed because `scripts/liveness/evidence.mts` hardcodes exactly
13+
the wrong spelling in `FOREIGN_PATH_PREFIXES` and so never resolved them.
14+
15+
Re-closed against cloud `origin/main@15f55df` and objectui `origin/main@26896c6`
16+
— the first pass run from a container with both checkouts in reach, which is the
17+
executor constraint that parked this card. All six entries are now dated.
18+
19+
- `name`, `description` — confirmed `live`, and both now carry **framework-local
20+
anchored** evidence (`packages/mcp/…#registerToolFromDefinition`, the MCP
21+
bridge that reads each `AIToolDefinition`) beside the cloud citation. CI can
22+
falsify them from this checkout for the first time; the old note's claim that
23+
"the OPEN framework edition does not consume them" was false for these two.
24+
- `parameters` — confirmed `live` on the cloud LLM path. Deliberately *not*
25+
co-cited to the MCP bridge: that bridge never forwards the key, while its own
26+
docblock says it does (filed as #13271).
27+
- `label` — confirmed `live` and given its **first evidence pointer ever**; it
28+
had carried a bare `live` with no `evidence` field since seeding, the one row
29+
the #13003 census could not even call stale.
30+
- `outputSchema` — stays `experimental`, with the negative half now measured
31+
rather than asserted: the key occurs on exactly four non-test lines in the
32+
whole cloud repo, and none of them validates anything.
33+
- `objectName` — stays `live`, but **on a completely different basis**, and the
34+
row now says so. Its stated basis is falsified: both cited sites read
35+
`action.objectName` (`action.json` carries the identical citation verbatim),
36+
and the same-named `AIToolDefinition.objectName` is written and read by
37+
nothing, so the key gates, binds and routes nothing. The consumer that does
38+
exist is objectui's registered metadata-admin preview, which reads it off the
39+
persisted record and renders it as the header's object pill — the #7131
40+
display-key rule, and pinned in `ToolPreview.test.tsx` over a comment reading
41+
"`objectName` is NOT residue". So: `live` as a **display** key, never as a
42+
binding, and explicitly not an ADR-0049 retirement candidate — retiring it
43+
would delete a key the renderer deliberately renders and tests.
44+
45+
The `_note`'s fourth false claim is corrected in the same pass: "tool metadata
46+
is WRITE-ONLY … not metadata read-back" is simply untrue, and it is the reason
47+
nobody had looked at the renderer. `ToolPreview` reads **all six** props off the
48+
stored record. The type therefore has two live bases — the `AIToolDefinition`
49+
surface (behavioural) and the metadata read-back (display) — and each row now
50+
names which one carries it.
51+
52+
Data-only: no schema, no runtime, no authoring surface changes, and no verdict
53+
moved, so `state-counts.md` is untouched and still current. `liveness/` is in
54+
this package's `files` array, so these ledgers ship in the npm tarball and this
55+
is published data.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): state the `view` name grammar per body spelling in the view module header (#13134)
6+
7+
`ViewMetadataSchema` is a union over three persisted `view` body spellings, and
8+
they do not share one `name` grammar. Nothing said so — the rule was
9+
reconstructible only by reading three schema factories:
10+
11+
| body spelling | `name` is declared as | flat (undotted) name |
12+
|:---|:---|:---|
13+
| standalone ViewItem record | `ViewItemNameSchema``QUALIFIED_ITEM_NAME_PATTERN`, dot REQUIRED | rejected, located at `["name"]` |
14+
| flattened runtime overlay | `z.string().optional()` — no grammar | accepted |
15+
| `defineView` container | `z.string().optional()` — no grammar | accepted, and normally IS flat |
16+
17+
Which grammar applies is decided by the body's shape, which an author never
18+
names explicitly, so neither failure direction is discoverable from the key
19+
being written: reading `ViewItemNameSchema` alone suggests the dot is mandatory
20+
everywhere (it is not — a container's own name is the bare object key under
21+
ADR-0017 §3.2's dual-read, and an overlay's name is stamped by the write path),
22+
while reading a flat-named overlay or container row suggests flat is fine
23+
generally (it is not — the same name on a standalone ViewItem record is
24+
refused).
25+
26+
**No schema change.** This is documentation: a family-level JSDoc block on
27+
`ui/view.zod.ts`, three pointer comments at the declaration sites, and the
28+
regenerated reference page. Every accept/reject decision is byte-for-byte what
29+
it was — all three spellings were already internally consistent and the
30+
flat-named rows already parse.
31+
32+
Side effect worth naming for readers of the generated reference: `ui/view.zod.ts`
33+
had no module description, so `content/docs/references/ui/view.mdx` and the two
34+
skill reference indexes opened with the doc comment attached to an unrelated
35+
`HttpRequest` re-export. They now open with the module's own description. The
36+
`HttpRequest` note is unchanged in the source, where it documents that
37+
re-export.

.github/workflows/lint.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4637,6 +4637,24 @@ jobs:
46374637
- name: Check docs object examples pass the os validate security posture
46384638
run: pnpm --filter @objectstack/lint run check:doc-security-posture
46394639

4640+
# The YAML half of the same surface (#13086, ruled 2026-08-29).
4641+
# check:skill-examples reads only ts/tsx fences — while metadata
4642+
# AUTHORING examples are written in YAML, so the format carrying most of
4643+
# what an author copies was in no gate's population at all
4644+
# (layout-dsl.mdx taught a phantom field-level `visible` map through two
4645+
# hand sweeps of the same page, #8251/#8306/#12935). Each ```yaml block
4646+
# tagged `os:check-yaml <schema>` is safeParsed against the LIVE spec
4647+
# schema it declares — resolved through the same registry
4648+
# `PUT /api/v1/meta/:type/:name` validates with — so a rejection here is
4649+
# the one a runtime save would print, rename hint included. Also prints
4650+
# the tagged/untagged fence census so opt-in adoption stays visible
4651+
# rather than becoming a permanent excuse. Reads src/ through tsx (no
4652+
# dist), so its place in this job is cohesion with the example-gate
4653+
# cluster above, not a build dependency. Self-tests first, like its
4654+
# neighbours.
4655+
- name: Check docs YAML examples parse against live spec schemas
4656+
run: pnpm --filter @objectstack/spec run check:yaml-examples
4657+
46404658
# Same anti-drift class as the gates above, for the generated translation
46414659
# bundles in packages/platform-objects/src/apps/translations/. Nothing
46424660
# regenerated them either, so they rotted three ways at once (#3670):

content/docs/data-modeling/drivers.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,8 +841,10 @@ pass `persistence: 'auto'` (or `'file'`) explicitly. See
841841
For tests, prefer in-memory **SQLite** — `SqlDriver` with
842842
`connection: { filename: ':memory:' }`, or `SqliteWasmDriver({ filename: ':memory:' })`
843843
when you want no native build. Both give the SQL semantics production runs on;
844-
mingo does not enforce primary keys, uniqueness, `NOT NULL` or column types, so a
845-
green run against the memory driver is weaker evidence than it looks. The
844+
the memory driver enforces **field-level `unique`** (with the same
845+
per-organization scoping the SQL family applies) and nothing elseno primary
846+
keys, no `NOT NULL`, no column types and no object-level composite `indexes[]`
847+
so a green run against it is still weaker evidence than it looks. The
846848
framework's own dogfood gate boots on WASM SQLite at `:memory:` for this reason.
847849

848850
The memory driver remains fine where you want no setup at all and the assertions

content/docs/protocol/objectui/layout-dsl.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ All layouts use a responsive grid that divides space into 12 columns.
224224

225225
### Basic Grid Layout
226226

227+
{/* os:check-yaml FormSectionSchema key=section */}
227228
```yaml
228229
section:
229230
label: Contact Information
@@ -279,6 +280,7 @@ section:
279280

280281
Grid automatically collapses on smaller screens:
281282

283+
{/* os:check-yaml FormSectionSchema key=section */}
282284
```yaml
283285
section:
284286
columns: 3 # Desktop: 3 columns, Tablet: 2 columns, Mobile: 1 column
@@ -328,6 +330,7 @@ Sections are collapsible containers for related fields.
328330

329331
### Basic Section
330332

333+
{/* os:check-yaml FormSectionSchema[] key=sections */}
331334
```yaml
332335
sections:
333336
- label: Contact Information
@@ -355,6 +358,7 @@ sections:
355358
Show sections based on field values. The key is **`visibleWhen`** and its value is a
356359
**CEL predicate string** — never a `{ field, value }` rule object:
357360

361+
{/* os:check-yaml FormSectionSchema[] key=sections */}
358362
```yaml
359363
sections:
360364
- label: Basic Info
@@ -468,6 +472,7 @@ strip.
468472

469473
### Basic Tabs
470474

475+
{/* os:check-yaml FormViewSchema */}
471476
```yaml
472477
type: tabbed
473478
tabPosition: top # top | bottom | left | right
@@ -550,6 +555,7 @@ Breakpoint-driven show/hide and per-breakpoint layout live one tier up, on a
550555
([ADR-0065](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0065-sdui-styling-model.md)) — desktop-first buckets compiled to id-scoped CSS at
551556
render:
552557

558+
{/* os:check-yaml ResponsiveStylesSchema key=responsiveStyles */}
553559
```yaml
554560
# On a page component (*.page.ts) — NOT on a form field or a form section
555561
responsiveStyles:

content/docs/references/ui/view.mdx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,51 @@ description: View protocol schemas
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
77

8-
HTTP Method Enum & HTTP Request Schema
9-
Migrated to [shared/http.zod.ts](/docs/references/shared/http). Re-exported here for backward compatibility.
8+
View protocol schemas — the `view` metadata type and its three persisted body spellings.
9+
10+
Covers the authoring surfaces (`defineView`, `defineViewItem`), the wire
11+
doors Studio and the REST layer write through, and
12+
`ViewMetadataSchema` — the union every persisted `view` body is judged
13+
by.
14+
15+
## Name grammar depends on the body spelling
16+
17+
`ViewMetadataSchema` is a union over three persisted body shapes, and
18+
they do not share one `name` grammar. Which grammar applies is decided by the
19+
shape of the body — something an author never names explicitly — so neither
20+
failure direction below is discoverable from the key being written:
21+
22+
| body spelling | recognised by | `name` is declared as | flat (undotted) name |
23+
|:---|:---|:---|:---|
24+
| standalone **ViewItem record** | a nested `config` | `ViewItemNameSchema``QUALIFIED_ITEM_NAME_PATTERN`, dot REQUIRED | **rejected**, located at `["name"]` |
25+
| flattened runtime **overlay** | an inline view config; no `config`, no container slot | `z.string().optional()` — no grammar at all | accepted |
26+
| `defineView` **container** | a container slot (`list` / `form` / `listViews` / `formViews`) | `z.string().optional()` on `ViewSchema` — no grammar at all | accepted, and normally IS flat |
27+
28+
The two permissive rows are deliberate, not gaps left to tighten later:
29+
30+
- A container's own name is the **bare object key**. ADR-0017 §3.2's
31+
dual-read loader registers the aggregated container under `<object>` and
32+
each expanded item under `<object>.<viewKey>`, so an object-scoped
33+
container is named `crm_lead` — a name with no dot to carry.
34+
- An overlay's name is **stamped by the write path**, not authored:
35+
`normalizeViewMetadata` puts it on every view body at the single write
36+
chokepoint, and a personalization PUT inherits the identity of the entry it
37+
shadows.
38+
39+
So both of the readings an author naturally forms are wrong:
40+
41+
- *"the dot is mandatory on every view row"* — read off
42+
`ViewItemNameSchema` alone. It is not: overlay and container rows
43+
accept flat names, and rows in this repo legitimately use them
44+
(`case_grid`, `cases`, the container row `crm_lead`). Those rows are
45+
correct as written, not defects awaiting a dotted rewrite.
46+
- *"flat names are fine generally"* — read off one of those flat-named rows.
47+
It is not: put the same name on a standalone ViewItem record and it is
48+
refused, on the one field the flat-named row told you to fill.
49+
50+
This describes what the three shapes already do; it widens and narrows
51+
nothing. The one item-name grammar itself lives in
52+
`shared/identifiers.zod.ts` — grammar changes belong there, not here.
1053

1154
<Callout type="info">
1255
**Source:** `packages/spec/src/ui/view.zod.ts`

packages/drivers/driver-memory/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ export {
2222
} from './memory-tenancy-guard.js';
2323
export type { TenancyAwareSchema } from './memory-tenancy-guard.js';
2424

25+
// [#13197] Field-level uniqueness — the refusal's wire identity and the
26+
// scoping helpers, exported so a consumer can assert the envelope (`code` AND
27+
// `status`, never merely "it threw") without string-matching the message.
28+
export {
29+
UNIQUE_VIOLATION_CODE,
30+
UNIQUE_VIOLATION_STATUS,
31+
assertNoUniqueViolation,
32+
tenantFieldOf,
33+
uniqueConstraintsFromFields,
34+
uniqueKeyOf,
35+
uniqueViolationError,
36+
} from './memory-unique-constraint.js';
37+
export type { MemoryUniqueConstraint, UniqueAwareSchema } from './memory-unique-constraint.js';
38+
2539
export default {
2640
id: 'com.objectstack.driver.memory',
2741
version: '1.0.0',

0 commit comments

Comments
 (0)