Skip to content

Commit fb280eb

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16354-dataset-measure-aggregate-field-type-lint
2 parents 775dfcd + b4b83b3 commit fb280eb

45 files changed

Lines changed: 2546 additions & 95 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/rest": minor
4+
"@objectstack/metadata-protocol": minor
5+
"@objectstack/lint": minor
6+
"@objectstack/verify": minor
7+
---
8+
9+
The remaining raw `FieldSchema.reference` readers now **REFUSE** a carrier they cannot read, instead of answering "no target" (#18550). The previous release routed the arbiter (`referenceCarrierOf`) and the lint target readers; these were the measured residue of the same ruling — every reader, not just the arbiter.
10+
11+
`FieldSchema.reference` is `z.string().optional()`, so `ObjectSchema.safeParse` refuses an object- or array-valued carrier at the contract door. These reads are the other door: the one a value reaches only when it never went through parse — a hand-built fixture, a raw `registerObject`, a stored row rehydrated past its schema.
12+
13+
**`@objectstack/objectql`** — both of the delete cascade's carrier reads (`planCascadeAtomicity` and `cascadeDeleteRelations`). This is the one with a measurable runtime consequence, and it is why the level is not `patch`:
14+
15+
```
16+
before acct=1 task=1
17+
delete RESOLVED true <- success reported to the caller
18+
after acct=0 task=1 <- an ORPHANED master_detail row
19+
```
20+
21+
An unreadable carrier made the relation invisible to the cascade, so the parent was deleted, the detail row stayed, and the caller was told the delete succeeded — no `restrict` refusal, no `set_null`, nothing logged. It now refuses before any row is touched.
22+
23+
**`@objectstack/rest`** — the public-form lookup picker's field-def fallback. The field def is also hoisted out of the metadata fetch's `catch {}`, so an unreadable carrier is no longer reported as `LOOKUP_TARGET_MISSING`: "no target is declared" and "the declared target cannot be read" want different fixes from whoever owns the metadata.
24+
25+
**`@objectstack/metadata-protocol`** — the seed dependency graph, which also retires an `as string` cast that asserted exactly what its truthiness guard had not checked.
26+
27+
**`@objectstack/lint`** — the four remaining target readers: `masterDetailCount` (`validate-expressions`), the `displayField` consumer edge (`validate-field-consumers`), the field and action-param targets (`validate-object-references`), and `masterOf` (`validate-sharing-rule-enforceability`).
28+
29+
**`@objectstack/verify`** — `relationTarget`, which no longer degrades an unreadable carrier to the generic "has no `reference` target" an object with no relationship metadata at all receives.
30+
31+
`null`, `undefined` and `''` are ABSENCE, not a wrong shape, and still answer `undefined` at every one of these sites — a field is allowed to name no target, and `StrictField` declares `reference` nullable. Each site's absence answer is pinned alongside its refusal.
32+
33+
Upgrading: nothing conformant changes. A non-string `reference` could not be authored, stored or parsed before this release either; what changes is that one now fails loudly at the read instead of being read as an absent target. If a test asserted the old silence, assert the refusal instead.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
docs(cloud-connection): cite the cloud control-plane decisions as `cloud ADR-NNNN` instead of bare numbers that resolve to this repo's own records (#18762)
6+
7+
AGENTS.md Prime Directive 13 is explicit — an ADR "lives in the repository whose
8+
code it governs", and a cloud decision is cited as `cloud ADR-NNNN`, "never as a
9+
bare number, which `scripts/check-adr-anchors.mjs` resolves against *this*
10+
registry (the two number independently)". The rule landed; the stock this
11+
package already carried was never swept.
12+
13+
Read against this repository's registry, the bare numbers pointed at real but
14+
unrelated records:
15+
16+
- `ADR-0008` → `docs/adr/0008-metadata-repository-and-change-log.md`, *Metadata
17+
Repository, Change Log & Subscription (M0 → M4)* — zero occurrences of
18+
"control plane", "cloud-connection" or "Phase 1"/"Phase 2".
19+
- `ADR-0007` → `docs/adr/0007-settings-manifest-and-kv-store.md`, *Settings —
20+
Manifest + K/V Store + Resolver*. The cloud ADR-0007 these lines mean is the
21+
one this repo's own ADR-0003 status line already names: the decision that
22+
redefined `sys_package_installation` as management-plane desired state and put
23+
runtime truth in the `LocalManifestSource` ledger.
24+
- `ADR-0009` → `docs/adr/0009-execution-pinned-metadata.md`, *Execution-Pinned
25+
Metadata* — not the marketplace Setup-navigation ownership decision the lines
26+
describe.
27+
28+
That is worse than citing a number nobody has. A dangling id stops a reader; an
29+
id that resolves lets them believe they read the right page and walk away with
30+
the wrong decision.
31+
32+
18 citations now carry the `cloud` qualifier, in the spelling this package
33+
already used elsewhere for the very same numbers — `cloud ADR-0008` in
34+
`connection-credential-store.ts`, `cloud ADR-0007 step ⑤` in
35+
`local-manifest-source.ts`, `cloud ADR-0009 P2a` in `marketplace-ui.ts`'s own
36+
header. All three numbers already carried both spellings inside this one
37+
package, and `marketplace-ui.ts` carried both inside a single file — qualified in
38+
its header on line 4, bare on lines 16 and 43.
39+
40+
What actually reaches a consumer of this package:
41+
42+
- The npm `description` field, which is the sentence shown on the package page.
43+
- `README.md`, including the closing pointer that already said "in the cloud
44+
repository" while writing the number bare.
45+
- The published `.d.ts`, which carries the module and plugin docblocks.
46+
47+
No behaviour moves. No type, export, route, schema or runtime path is touched —
48+
this is citation spelling and prose only, which is why it ships as a patch rather
49+
than silently. No ADR record is written or edited. `packages/cloud-connection/CHANGELOG.md`
50+
is deliberately untouched: it is published history, and a released entry is
51+
amended in a dedicated docs-only PR, never as a rider on code changes.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): `spec-changes.json`'s aggregate export diff declares the release pair it really spans (#18978)
6+
7+
Clause-②: yes (widening) — one new OPTIONAL key on a published artifact (`aggregate.surfaceScope`)
8+
and one new optional field on `SpecChangesSchema`. Nothing is renamed, retired or reshaped: the
9+
schema still ACCEPTS a record without it, every existing key keeps its spelling and meaning, and
10+
`perMajor` and the `release` section are byte-identical. Contract-review tier.
11+
12+
`aggregate.added` / `aggregate.removed` are not registry-derived. A release-time api-surface diff
13+
fills them by comparing the artifact being published against the previously **published** one, so
14+
they span **one release** — while the record they sit in is keyed by protocol major (`from: 10,
15+
to: 17`) and every entry carries only `since: 17` / `removedIn: 17`, with
16+
`perMajor[16 → 17].added` at `0` beside it. Nothing in the file distinguished one minor's slice
17+
from the whole major-boundary delta.
18+
19+
Measured on the published `@objectstack/spec@17.4.0` Release asset: `aggregate.added` = **225**,
20+
`aggregate.removed` = **51**, every entry `since`/`removedIn` = 17 — and set-identical to a
21+
recomputed `17.3.0 → 17.4.0` diff of the two tarballs' own `api-surface/` snapshots. It was the
22+
minor's delta wearing a major's label.
23+
24+
**What ships now.** A record whose export arrays are non-empty carries the version pair they were
25+
diffed between:
26+
27+
```bash
28+
jq '.aggregate | {from, to, surfaceScope, added: (.added | length), removed: (.removed | length)}' \
29+
node_modules/@objectstack/spec/spec-changes.json
30+
```
31+
32+
- `surfaceScope: { fromVersion, toVersion }` present ⇒ `added`/`removed` span exactly that
33+
published-version pair. ⛔ They are **not** the `from` → `to` major delta, and never were.
34+
- `surfaceScope` absent ⇒ the record carries no export diff at all and `added`/`removed` are
35+
empty. ⛔ Read that as "this record does not say", never as "nothing was added between `from`
36+
and `to`" — the same rule the `release` section already states for itself.
37+
- `from` / `to` still answer the major-boundary question for `converted` / `migrated`, which are
38+
registry-derived and unaffected.
39+
40+
**Refused at the producer and at the publish gate, in both directions.** The generator reads the
41+
previous version off the previous artifact's own `package.json`, omits the arrays loudly when it
42+
cannot read one, and refuses outright to write a non-empty unlabelled array.
43+
`scripts/check-release-spec-changes.mjs` — which until now checked the `release` section and not
44+
the aggregate — recomputes the aggregate's claim from the two tarballs and refuses an absent,
45+
mislabelled or untrue scope. Its self-test roster grows from 15 batteries to 23.
46+
47+
**Nothing previously honest moved.** The committed registry-only projection and every `perMajor`
48+
record carry no new key at all; the committed `spec-changes.json` changes on its `$comment` line
49+
and nowhere else. The published schema is deliberately not narrowed — every manifest published so
50+
far carries an unscoped diff and must keep parsing.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING for authored metadata** — the `object-grid` page-component door now refuses a page size of `0`, a negative page size and a non-integer page size, at all three of its spellings: `pagination.pageSize`, every `pagination.pageSizeOptions[]` entry, and the flat `pageSize` shorthand (#19046).
6+
7+
Clause-②: yes (narrowing)
8+
9+
The accept set shrinks to the one the VIEW arm has ruled all along. `PaginationConfigSchema` (`view.zod.ts`) declares `pageSize: z.number().int().positive()` and pins its refusals by name; `MetadataQuery` and the two marketplace request schemas say `z.number().int().min(1)`, each with its own throwing pin. The `object-grid` door said `pagination: z.unknown()` and `pageSize: z.number()` — the only page-size declaration in the package that accepted `0`, and the one renderers read.
10+
11+
**It was not theoretical.** Measured at objectui#9853: an authored `pagination.pageSize: 0` reached `ObjectGrid`, went out on the wire as `$top: 0` and rendered ZERO ROWS, with no grouping needed to trigger it — through this arm, with a `success: true` receipt from this schema. The view arm would have refused the same value. objectui#9896 repaired the consumer half (a resolver at every read point, fail-soft, one loud diagnostic); this is the declaration half and is not a prerequisite for it.
12+
13+
```
14+
✗ pagination.pageSize: Too small: expected number to be greater than 0
15+
✗ pageSize: Invalid input: expected int, received number
16+
```
17+
18+
### Migration — FROM → TO
19+
20+
| You wrote | Write instead |
21+
| --- | --- |
22+
| `pagination: { pageSize: 0 }` | `showPagination: false` and no `pagination` bag — the bag's PRESENCE is what enables paging, so `pageSize: 0` never meant "no paging" |
23+
| `pagination: { pageSize: 0 }` (meaning "all rows on one page") | the page size you actually want (`{ pageSize: 100 }`); `0` reached the wire as `$top: 0` and returned nothing |
24+
| `pagination: { pageSizeOptions: [0, 25, 50] }` | `{ pageSizeOptions: [25, 50] }` — drop the `0` entry; selecting it set the fetch window to zero rows |
25+
| `pageSize: 25.5` | `pageSize: 25` — a fractional page size was truncated or forwarded verbatim, depending on the read point |
26+
27+
The one-line fix is always the same: **write a positive integer, or delete the key and take the renderer's default.**
28+
29+
<!-- adr-0087: registered ui-object-grid-page-size-positive-integer-refused -->
30+
31+
**⛔ What this deliberately does NOT narrow: the `pagination` bag stays OPEN.** The card's defect is that the two arms disagreed about a page SIZE — not that the bag should become a closed shape. `pagination` is now a `z.looseObject` that validates the two members whose value is a page size and passes every other key through unvalidated, so a sibling key that parsed before still parses and still survives the parse byte-identically (pinned in `component-object-grid-pagination-accept-set.pin.test.ts` §3). Reusing the view arm's `PaginationConfigSchema` here would have refused every sibling key this door has accepted since it was written — the `…` in its own describe says authors write them — which is a wider narrowing than the measured defect and a different decision. `PaginationConfigSchema` itself is unchanged and stays closed; §4 of that pin states both the agreement and the deliberate asymmetry.
32+
33+
**One second axis, named rather than left to be discovered.** `pagination` moves from `z.unknown()` to an object type, so a non-object value (`pagination: true`) is refused where it used to parse. Measured before narrowing: zero non-object `pagination` values exist on an `object-grid` node in either repository's corpus, the objectui registry has published this input as `type: 'object'` all along (`plugin-grid/src/index.tsx`), so the html tier already answered `type-mismatch` on one, and the renderer reads the key for PRESENCE (`schema.pagination !== undefined`) — which means an authored `pagination: false` used to turn paging ON. That value now gets a located refusal instead of the opposite of what it says.

‎content/docs/references/ui/component.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,8 @@ Sort field and direction pair
514514
| **defaultFilters** | `any` | optional | Legacy base-filter fallback, read only when `filter` is absent. Prefer `filter` |
515515
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Initial row order — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` |
516516
| **defaultSort** | `never` | optional | [REMOVED] `object-grid` property `defaultSort` was removed in @objectstack/spec 17 (ADR-0049) — it was the legacy second spelling of `sort`: a single `{ field, order }` pair read only when `sort` was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` becomes `sort: [{ field, order }]`); the pair itself is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
517-
| **pagination** | `any` | optional | Pagination config (`{ pageSize, pageSizeOptions, … }`); its presence enables paging |
518-
| **pageSize** | `number` | optional | Flat page-size shorthand; `pagination.pageSize` wins when both are set |
517+
| **pagination** | `{ pageSize?: integer; pageSizeOptions?: integer[] } & Record<string, any>` | optional | Pagination config (`{ pageSize, pageSizeOptions, … }`); its presence enables paging. `pageSize` and every `pageSizeOptions` entry is a positive integer — the accept set the view arm's `PaginationConfigSchema` already rules; the bag stays open, so other keys pass through unvalidated |
518+
| **pageSize** | `integer` | optional | Flat page-size shorthand, a positive integer; `pagination.pageSize` wins when both are set |
519519
| **showPagination** | `boolean` | optional | Show the pager (read only when `pagination` is absent) |
520520
| **searchableFields** | `string[]` | optional | Fields the toolbar search queries; a non-empty list enables search |
521521
| **showSearch** | `boolean` | optional | Show the search box (read only when `searchableFields` is absent) |

‎content/docs/ui/forms.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ sections: [{
257257
| `displayFields` | Fields projected into each result row (plus `id`); the visitor's `q` is `contains`-matched against the **first** entry. At most 5; omitted → `['name']`. |
258258
| `maxResults` | Rows per request, integer 1–50 (default 20). 50 is a hard server ceiling; there is **no pagination** on this surface (`offset` is pinned to 0), so a leaked endpoint cannot enumerate the table. |
259259
| `filter` | Static pre-filter rows (same `{ field, operator, value }` dialect as list-view filters), ANDed ahead of the visitor's search. |
260-
| `object` | The object to search. Optional — omit it and the server resolves the target from the field's own definition on the parent object: its `reference` key, and only that key. A stored row spelling the target `referenceTo` / `target` / `options.objectName` is **not** resolved — the route answers `500 LOOKUP_TARGET_MISSING` — because `FieldSchema` accepts no spelling but `reference`. Declare it only to search something other than what the field points at. |
260+
| `object` | The object to search. Optional — omit it and the server resolves the target from the field's own definition on the parent object: its `reference` key, and only that key. A stored row spelling the target `referenceTo` / `target` / `options.objectName` is **not** resolved — the route answers `500 LOOKUP_TARGET_MISSING` — because `FieldSchema` accepts no spelling but `reference`. That key is also **read through the one carrier accessor**, so a stored row whose `reference` holds something other than a string (an object, an array) is refused rather than searched — see the error table below. Declare it only to search something other than what the field points at. |
261261

262262
Those four keys are the whole block. It admits exactly what the route enforces
263263
— an unknown subkey, a 6th display field, or `maxResults: 51` is a **parse
@@ -285,6 +285,7 @@ Errors:
285285
| `403 LOOKUP_NOT_PUBLIC` | the field has no `publicPicker` block — the deliberate loud default (#3022); also any server-managed anchor (`owner_id`, `organization_id`, …), which never gets a picker even if one is declared |
286286
| `404 FORM_NOT_FOUND` | slug not registered on any `sharing.allowAnonymous: true` view |
287287
| `500 LOOKUP_TARGET_MISSING` | the referenced object could not be resolved from either `publicPicker.object` or the field definition — the field names no target object at all (or its object metadata is unreachable). Until #7486 this also fired for a perfectly well-formed field, because the fallback read only the legacy spellings and not the canonical `reference`; declaring `object` was the workaround and is no longer needed. |
288+
| `500 INTERNAL_ERROR` | the field def **declares** a target this route cannot READ — a stored `reference` holding an object or an array rather than the object name `FieldSchema` declares. ⚠️ Deliberately **not** `LOOKUP_TARGET_MISSING`: "nothing names the target" and "the named target is unreadable" want different fixes from whoever owns the metadata, so they get different answers. The unreadable carrier is named in full in the server log (it is withheld from the response body, as every fault's text is); the picker's search never runs. |
288289

289290
### Auth model
290291

‎content/docs/upgrading.mdx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,27 @@ jq '.release | {fromVersion, toVersion,
335335
named — `"./ai: AgentSchema (const)"`, the entry point followed by the export
336336
and its kind. `converted` and `migrated` are the ADR-0087 conversions and
337337
semantic migrations first registered in that release. The same file's
338-
`aggregate` and `perMajor` records are unchanged and still answer the
339-
major-boundary question.
338+
`perMajor` records are unchanged and still answer the major-boundary question,
339+
and so does `aggregate` — for its `converted` and `migrated`, which are derived
340+
from the ADR-0087 registries across the whole `from` → `to` range.
341+
342+
⛔ **But not for `aggregate.added` / `aggregate.removed`.** Those come from the
343+
same one-release export diff as the section above, not from the major range the
344+
record is keyed by, so when they are filled the `aggregate` record carries a
345+
`surfaceScope` naming the exact pair they span:
346+
347+
```bash
348+
jq '.aggregate | {from, to, surfaceScope,
349+
added: (.added | length), removed: (.removed | length)}' \
350+
node_modules/@objectstack/spec/spec-changes.json
351+
```
352+
353+
`surfaceScope` absent means that record claims no export diff at all and its
354+
`added` / `removed` are empty — the registry-only shape. ⛔ Read that as "this
355+
record does not say", never as "nothing was added between `from` and `to`",
356+
which is the same rule the `release` section states for itself below. A release
357+
whose `aggregate` arrays disagree with the two published tarballs, or carry no
358+
`surfaceScope`, does not publish.
340359

341360
The `os` CLI reads the same section, so a CI job does not have to know the file
342361
exists:

0 commit comments

Comments
 (0)