Skip to content

Commit ad11217

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20466-gantt-timezone-describe
2 parents a6aeb19 + 9801da1 commit ad11217

19 files changed

Lines changed: 1612 additions & 68 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: a `progress` field's declared `min` / `max` are enforced on writes — a value outside them is refused with `min_value` / `max_value`, exactly as on `number` (#20386)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — a narrowing of the write accept set on `@objectstack/objectql`, shipped as `minor` under the repo's launch-window convention (`check-changeset-no-major` refuses `major` until GA); the breaking-ness is carried by this banner and the ADR-0087 disposition, never by the level. Nothing an author writes changes spelling: `min` and `max` keep their keys, their type and their legality on every field type.
10+
11+
`FieldSchema.min` / `max` declare a check ("Checked on the WRITTEN value only") with no type exclusion, but the record validator returned for a `progress` field right after its finite-number check, above the bounds. So a `progress` field declaring `max: 100` stored `150`, and one declaring `min: 0` stored `-5`, with `201` on memory and SQLite, while a `number` field with the same bounds refused both. The bounds now bind on `progress` at the one place a write is judged.
12+
13+
**What a caller sees, before → after.** A `progress` write outside a declared bound: `201`, stored as sent → `400 VALIDATION_FAILED` with field code `max_value` (`constraint: { max }`) or `min_value` (`constraint: { min }`), nothing stored. That is the `number` field's answer, envelope for envelope, in all four locales. The REST create, batch, update and updateMany routes all answer it, and `validate` (the dry run) predicts it. A value inside the bounds, or on either bound (both are inclusive), writes exactly as before. Only a write that CARRIES the field is judged: a stored value outside a bound is never re-read and survives an update that does not send it.
14+
15+
The fix, when a write is refused: send a value inside the bounds, or widen or delete the field's `min` / `max` to match what it really holds.
16+
17+
⛔ Only the bounds. `scale` and `precision` stay unread on `progress`: each key's own contract names the types it binds on, and `progress` is in neither set, so `33.5` still writes into a `progress` field that declares `scale: 0`.
18+
19+
**Who is affected, measured** on `origin/main` `dc0ab6a2e`: the two example-app `progress` fields (`examples/app-showcase` `showcase_task.progress` and the field zoo's `f_progress`) both declare `min: 0, max: 100`, and every value their seeds and actions write (12 seed rows, one `progress: 100` action) is inside. The console's `progress` editor, objectui's `SliderField`, drives a Radix slider bounded by the field's declared `min` / `max`, so it cannot emit a value outside them.
20+
21+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: `min` and `max` keep their keys, their type (`z.number().optional()`) and their legality on every field type, `packages/spec` is untouched, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What narrows is the record validator's write accept set for values under bounds the field already declares, which is runtime behaviour, not an authored shape. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id is minted here and none covers this (not `registered` / `already-registered`); and runtime behaviour changes, not only a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: `object-field-ref-unknown` judges the field-name lists on a field — `relatedListColumns`, `lookupColumns`, `lookupFilters[].field`, `dependsOn` — and an object's `indexes[].fields`
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) an authoring-time lint rule judges names on five more declaration positions; no key, symbol, enum member or stored value moves, so a stored metadata row is structurally identical before and after and `objectstack migrate meta` has nothing to rewrite -->
8+
9+
**BREAKING** in the accept-set sense — a declaration that passes today can fail tomorrow.
10+
Landing in the launch window as `minor` (the lockstep convention: `major` is refused by
11+
`check-changeset-no-major`, and breaking-ness is carried by this banner plus the ADR-0087
12+
disposition above).
13+
14+
**Clause-②: no (narrowing)** — the rule refuses more than it did; no key is added to any
15+
published payload and no public surface grows. Narrowing is still a semantic-surface change,
16+
which is why it is declared here rather than shipped silently.
17+
18+
Each of these five lists holds bare field names that the schema cannot judge, and until now no
19+
authoring door read them for existence, so a misspelling surfaced only when a user opened the
20+
view or the picker — or never:
21+
22+
- a misspelt `relatedListColumns` entry asked the child object for a column it does not have,
23+
when the parent's detail page opened;
24+
- a misspelt `lookupColumns` entry rendered an empty picker column;
25+
- a misspelt `lookupFilters[].field` filtered the picker's query by a field the referenced
26+
object lacks;
27+
- a misspelt `dependsOn` name kept its field gated for good;
28+
- a misspelt `indexes[].fields` column made the SQL driver skip the WHOLE index at sync, with a
29+
warning, and drift dropped it too — so a `unique` index was silently unenforced while
30+
everything looked normal.
31+
32+
`os validate`, `os build` and `os lint` now refuse each of them at `error` (exit 1), under the
33+
existing rule id `object-field-ref-unknown`, and so does the runtime publish door on an object
34+
write (`422`), exactly as they already did for `highlightFields` and
35+
`publicSharing.redactFields`. The finding sits at the exact path —
36+
`objects[i].fields.<field>.lookupColumns[j].field`, `objects[i].indexes[j].fields[k]`, and so
37+
on — names the string that was written and the object it was judged against, offers the
38+
nearest name when one is close, and lists that object's fields.
39+
40+
**Which object a name is judged against** — read off each key's runtime reader, not assumed:
41+
42+
| Position | Judged against |
43+
|:---|:---|
44+
| `relatedListColumns[]` | the object that owns the field — the related list shows that (child) object's rows |
45+
| `lookupColumns[]`, both arms | the referenced object — the picker lists its records |
46+
| `lookupFilters[].field` | the referenced object — the picker's query runs on it |
47+
| `dependsOn[]` name, or `{ field }` | the object that owns the field — the form gate reads this record |
48+
| `dependsOn[]` `param` (or the bare name, on a picker) | the referenced object — the picker filters its candidates by that key |
49+
| `indexes[].fields[]` | the object itself, including the columns the platform injects (`created_at`, `organization_id`, …) |
50+
51+
The referenced-object positions are judged on `lookup`, `master_detail` and `user` fields (a
52+
`user` field references `sys_user`), and only when the referenced object is in the stack being
53+
checked. `lookupColumns`, `dependsOn` and index columns are read verbatim by their readers, so a
54+
dotted name there is refused as a name that is not a field. The family's three skips hold
55+
unchanged: an object outside the stack, an object with no readable field map (ADR-0015
56+
`external`), and a registry-injected column resolved per object.
57+
58+
**What an author does.** Nothing is renamed or rewritten for you. Fix the name the finding
59+
points at, or drop the entry. On a lookup whose `dependsOn` field is spelled differently on the
60+
two records, write the entry with its `param` naming the referenced object's field. An existing
61+
object carrying one of these misspellings is refused when it is next republished through the
62+
publish door, and `os validate` reports it on the next run.
63+
64+
Unchanged: the object schema's own parse still admits these names, so a draft save does not
65+
judge them. An index column that resolves to a real but virtual field (a `formula`) passes this
66+
rule; whether the column is materialized stays the SQL driver's question at sync.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the console's round-trip keys on a stored `view` row are declared on the wire, so a parse keeps them (#20456)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authored moves: no key an authoring door accepts changes its spelling, type or legality, because the four keys are console state the authoring doors refuse by name. The only producer, objectui's console, writes none of the values now refused (census at the `.objectui-sha` pin: a boolean pin, an integer reorder index, the marker as `true`, and `visibility` only carried forward from a stored value). So `objectstack migrate meta` has no mechanical rewrite to perform; a stored row holding a refused value is repaired by correcting it or deleting the key. The other categories are closed on facts: `@objectstack/spec` publishes (not `unpublished`); no ADR-0087 id is minted here or pre-dates the base to cover this (not `registered` / `already-registered`); and a Zod schema's accept set changes, so neither `runtime-interface-only` nor `type-surface-only` applies. -->
10+
11+
**BREAKING** accept-set narrowing on the `view` write door (`PUT /api/v1/meta/view/:name`, the Studio and MCP save) and on every door that parses `ViewMetadataSchema`, shipped as `minor` under the repo's launch-window convention. The newly declared keys are typed, so a non-boolean `isPinned`, a non-integer `sortOrder`, a `visibility` outside `private` / `team` / `organization` / `public`, or an `_isOverride` other than `true` is now refused at the parse (`422 INVALID_METADATA` at the save door), where the strip used to swallow the key and the save stored the body as sent. To fix a refused body, correct the value or delete the key. The console writes none of these values: its pin toggle writes a boolean, its reorder an integer index, and it stamps the marker as `true`. The diff also widens: the keys are now declared, and `VIEW_CONSOLE_ROUND_TRIP_KEYS` is a new export.
12+
13+
`saveMetaItem` stores a `view` body exactly as it was sent (ADR-0005 appendix (c)), and the members of `ViewMetadataSchema` that judge a stored row `.strip()` every key they do not declare. So the keys the console writes onto a stored view and reads back were in the store and nowhere in the contract. A census of objectui's console (at the `.objectui-sha` pin) measured which ones the parse dropped:
14+
15+
- `isPinned` and `sortOrder` on a flattened list overlay (they were already declared on the ViewItem record);
16+
- `visibility`, on both the flattened list overlay and the ViewItem record;
17+
- `_isOverride`, the marker that tells the console a row is the settings overlay of a code-defined view and not a saved view of its own.
18+
19+
## What it does now
20+
21+
- The ViewItem wire member (`ViewItemWireSchema`) and the flattened list overlay (`VIEW_METADATA_MEMBERS.listOverlay`) declare `isPinned`, `sortOrder` and `visibility` from one shared declaration, each with its meaning. The flattened list overlay also declares `_isOverride: true`, and its existing `isDefault` now carries its meaning. A parse of a console-written row keeps every one of them.
22+
- **New export `VIEW_CONSOLE_ROUND_TRIP_KEYS`** (`@objectstack/spec/ui`): each round-trip key, mapped to the members whose rows the console writes it on (`isDefault`, `isPinned`, `sortOrder`, `visibility`, `columnState`, `_isOverride`).
23+
- `visibility` is display grouping only (`private` / `team` / `organization` / `public` in the view switcher). It restricts nobody, and its declared meaning says so.
24+
- None of these keys is authorable. `defineViewItem` still refuses each of them by name, and `visibility` now gets a prescription that says what it is.
25+
26+
## What does not change
27+
28+
- **What is persisted.** The save still stores the request body verbatim. Storing the parsed body is a later, separate change.
29+
- The alias spellings the census found keep their declared spellings: `objectName` is `object`, and a top-level `id` is `name`. The console's filter / sort builder row ids stay `VIEW_CONSOLE_ROW_DECORATIONS`, removed before the parse.

‎packages/lint/src/index.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,9 @@ export type {
497497

498498
// [#15254] The object-level half of the same sweep: the field-name LISTS an
499499
// object carries about its own fields (`highlightFields`,
500-
// `publicSharing.redactFields`). `error`, and on the runtime publish door as
500+
// `publicSharing.redactFields`, and since #20432 `indexes[].fields` and the
501+
// field-level lists `relatedListColumns` / `lookupColumns` /
502+
// `lookupFilters[].field` / `dependsOn`). `error`, and on the runtime publish door as
501503
// well as the three commands — Studio's app builder mints no `view` items, so
502504
// the list-view members above have nothing to inspect on the only artifacts
503505
// the click path authors, and a dangling `highlightFields` reference produced

‎packages/lint/src/reference-integrity-suite.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,12 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
326326
// authors is the OBJECT. The crossing carries the #9313 property that makes
327327
// it safe — this member resolves only against `stack.objects`, the
328328
// collection the per-write snapshot does carry, so it has no
329-
// missing-collection false-positive channel; and it resolves each name
330-
// against the object's OWN field map, so a one-object snapshot is not
331-
// merely sufficient, it is the whole universe the question has.
329+
// missing-collection false-positive channel. [#20432] Most of its names
330+
// resolve against the object's OWN field map, but the picker keys a lookup
331+
// field carries (`lookupColumns`, `lookupFilters[].field`, the filter half
332+
// of `dependsOn`) address the REFERENCED object: that object is judged when
333+
// the snapshot's `objects` carries it, and an object it does not carry is
334+
// `unknowable` — never a miss — so the channel stays closed.
332335
//
333336
// It names `flow` because EVERY member of this suite does — the #4463 P1
334337
// surface is the floor the member axis was never meant to narrow, and

0 commit comments

Comments
 (0)