Skip to content

Commit ee39632

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17785-tracing-duration-units
# Conflicts: # packages/spec/src/migrations/registry.ts
2 parents efbf300 + ff1e771 commit ee39632

113 files changed

Lines changed: 7342 additions & 1453 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/16236-formula-return-type-measure-column.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
fix(service-analytics): a `min`/`max` over a `formula` field is typed from the formula's declared `returnType`, not described as `number` (#16236)
66

7+
> ⚠️ **Superseded within the same release window — ⛔ do not act on this entry.**
8+
> Everything below was accurate when it was written and is kept as the record of what
9+
> #16236 measured and built. It never reached a published version: **#17560** (director
10+
> ruling, decision batch #127, 2026-09-13) refuses `min` / `max` over a `formula` field
11+
> outright, on the compatibility table's own storage ground — a formula is VIRTUAL in SQL
12+
> storage, no column is emitted, so no aggregate can be lowered to it whatever
13+
> `returnType` says. At the version that compiles this entry such a measure answers
14+
> `DATASET_INVALID` / **400** at compile time instead of carrying any `fields[].type`, and
15+
> the `returnType?: string` member described at the foot of this entry is **not** on
16+
> `AnalyticsServiceConfig.sourceFieldMeta` — it was added and removed inside one release
17+
> window, so no published version ever carried it. ⇒ Read #17560's entry instead; the
18+
> FROM → TO below never became a shipped behaviour.
19+
720
**Behaviour change — read this if any dataset measure aggregates a `formula`
821
field.** `AnalyticsResult.fields[].type` for such a measure column was always
922
`number`, whatever the formula computes. It is now translated from the field's
@@ -47,3 +60,8 @@ a word outside the declared four: left alone, never guessed at.
4760
host that returns the three-member shape still satisfies the contract and gets
4861
exactly today's behaviour for every column. `AnalyticsServicePlugin` relays the
4962
key automatically, so a host on the plugin needs no change at all.
63+
64+
⚠️ **Superseded — see the banner at the top.** #17560 removed that member again in
65+
the same release window, so the shape a host writes against is the three-member one
66+
this paragraph calls today's. Nothing to do either way: a host that returns the
67+
fourth key is ignored, not refused.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/lint': patch
4+
---
5+
6+
`sys_user.manager_id` gains an admin write surface: `POST /api/v1/auth/admin/set-user-manager`
7+
8+
`{ type: 'manager' }` is the canonical first rung of a tiered approval ladder,
9+
and it resolves `sys_user.manager_id` — a column **no product surface could
10+
write**. Measured: the generic data path refuses it (the ADR-0092 D2
11+
managed-update whitelist for `sys_user` is `{name, image, locale}`), the admin
12+
bulk import does not carry it (`admin-import-users.ts` matches `manager_id` 0
13+
times, against a control of `phone_number` 8), and the column is `readonly` on
14+
the user form. So on any install without a directory sync the rung expanded to
15+
nobody, the request opened on a slate no one could act on, and under the
16+
default `lockRecord: true` the record stayed locked.
17+
18+
**The endpoint.** A platform admin posts `{ userId, managerId }`; `managerId:
19+
null` clears the link. It is an ObjectStack mount on the raw app ahead of the
20+
better-auth catch-all — the same family as `POST /api/v1/auth/admin/unlock-user`
21+
— platform-admin gated (ADR-0068) and ledgered in `auth-route-ledger.ts`.
22+
23+
**It is not a new editable profile column, and that is the design.** The
24+
handler runs under a **system context**, so it reaches the column by context
25+
rather than by a whitelist entry — the same way `admin-import-users` already
26+
reaches `phone_number` and `role`. `SYS_USER_PROFILE_EDIT_FIELDS` is
27+
untouched, `MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user` stays `{locale}`, and
28+
`sys_user.manager_id` keeps `readonly: true`, so ADR-0092 D4 still holds by
29+
construction. Since ADR-0092 D5's amendment made Tier-1 membership imply
30+
self-editability, admitting the column to Tier 1 would have handed every member
31+
their own first-rung approver and a widening of their own `own_and_reports`
32+
read scope; it is not admitted.
33+
34+
**Five refusals, every one enforced at the write** — the only manager-chain
35+
walkers in the open tree are single-hop, so nothing downstream catches a bad
36+
link: self-assignment; a link that closes a cycle (the walk is itself
37+
cycle-safe, so a pre-existing loop is reported rather than hung on); a chain
38+
past the depth cap that ADR-0057 D3's bounded rollups require; a manager
39+
provably outside every organization the user belongs to (beside, not instead
40+
of, the existing routing-time screen); and any identity whose `sys_user.source`
41+
is `idp_provisioned`, where the directory stays the one authoring surface.
42+
43+
**`@objectstack/lint`** keeps the `approval-approvers-may-resolve-empty`
44+
advisory and its `stackWiresManagerChain` silencer — the dead end it reports
45+
survives the write surface, because a static check still cannot read the
46+
column; only its *cause* became recoverable. What changed is the remedy text,
47+
which named a column with no route and now names the endpoint, its body, how to
48+
clear the link, and what it refuses. The Approvals guide carries the same
49+
rewrite in prose.
50+
51+
**Why `patch` and not `minor`.** No new exported symbol is reachable from
52+
either published entry: `admin-set-user-manager.ts` is deliberately not
53+
re-exported from `plugin-auth/src/index.ts` and is not named in the package's
54+
`exports` map, so none of `runSetUserManager`, `MAX_MANAGER_CHAIN_DEPTH`,
55+
`SetUserManagerDeps`, `SetUserManagerEngine`, `SetUserManagerResult` or
56+
`SetUserManagerRefusalReason` appears in the built `dist/index.d.ts`. No
57+
already-published payload gains a key — the endpoint's response is a new
58+
payload, not a new field on an old one. A new **route** is wire, and wire
59+
compatibility is not the grading floor.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: `ListViewSchema.sort` retires the bare string clause — the PRODUCER half of the sort seam, so the contract stops minting documents its own consumer refuses (#17053; objectui#8221, decision batch #77 option B)
6+
7+
<!-- adr-0087: registered list-view-sort-string-clause-to-array -->
8+
9+
**BREAKING** accept-set narrowing at `view.sort` — the list-view doors
10+
(`ListViewSchema`, and the `ObjectListViewSchema` copy behind `object.list` /
11+
`object.listViews.*`) — shipped as `minor` under this repo's launch-window
12+
convention for breaking changes, the same grade its sibling
13+
`object-block-sort-item-array` took for the two `ComponentPropsMap` doors. The
14+
mechanical prescription is registered under protocol major 18 as
15+
`list-view-sort-string-clause-to-array`.
16+
17+
**Why this is graded on the seam, not on the string.** objectui ruled one sort
18+
orthography platform-wide — the array (objectui#8221, decision batch #77,
19+
2026-09-07, option B) — and objectui PR #8758 executes it: `convertSortToQueryParams`
20+
refuses a runtime string and its diagnostic names the array form. `ListViewSchema`
21+
is the producer of exactly those documents: `object.list.sort` is what
22+
`deriveRelatedLists` reads. So until this release a view authored with
23+
`sort: 'created_at desc'` **validated here, cleanly, and then failed downstream** —
24+
the contract minting a shape its consumer rejects, with the author told off by
25+
the wrong layer. Re-measured on this tree before the change, with `bogusProp`
26+
refused by name on the same call as the firing control: `'name desc'`, `'-name'`
27+
and the array form all returned `success: true`, and only a bare number was
28+
refused (`sort/invalid_union`).
29+
30+
`sort` survives as a key, one union arm lighter, so this is a VALUE narrowing with
31+
no `retiredKey()` tombstone to hang a prescription on. The surviving array member's
32+
own `error` map carries it, keyed on `issue.input` being a string — the same shape
33+
`view.type`'s retired `'page'` value and `view.exportOptions`' retired `'pdf'` value
34+
already use in this schema. Every other invalid value (a number, an object, a
35+
string reaching a *descendant* such as a misspelled `order`) keeps zod's default
36+
report, so nobody is told a clause they never wrote "was removed".
37+
38+
**Migration** (`list-view-sort-string-clause-to-array`, a D2 conversion, not a
39+
semantic TODO — the rewrite is lossless and wholly mechanical):
40+
`sort: 'created_at desc'` becomes `sort: [{ field: 'created_at', order: 'desc' }]`;
41+
a bare field name meant ascending, so `sort: 'created_at'` becomes
42+
`sort: [{ field: 'created_at', order: 'asc' }]` — `order` is required on the entry
43+
and is written out rather than omitted; a comma-separated clause becomes one array
44+
entry per key, in the same order. `os migrate meta --from 17` lists these edits for
45+
author sources, and stored rows replay them through `applyConversionsToStoredItem`.
46+
47+
**The narrowing was not free, and the population was measured rather than assumed.**
48+
A tree-wide census over both the TS and JSON spellings of a string-valued `sort`,
49+
read as STRUCTURES rather than counted as tokens, found the clause authored on
50+
three live in-tree sites, all converted here: the shipped showcase list view
51+
`examples/app-showcase/src/ui/views/task.view.ts` (`'estimate_hours desc'`, carried
52+
since objectui#2601 as a deliberate live coverage fixture for the string form), the
53+
frozen `packages/lint` snapshot of that same shipped shape, and the published
54+
`skills/objectstack-ui` list-view rule. The census fired: it *found* documents, and
55+
`tsc` independently reds on the first two the moment the arm is removed. Sites
56+
deliberately NOT converted, having been read rather than grepped: ObjectQL
57+
`query.sort` and the wire `normalizeSortNodes` (different doors, different
58+
dialects), `packages/spec`'s `book`/`doc` field-mapping records whose `sort: 'order'`
59+
is an unrelated key of the same name, and the `packages/lint` rule fixtures, which
60+
feed the PRE-parse walker and never reach this schema.
61+
62+
**Not moved by this release.** `RecordRelatedListProps.sort` keeps its declared
63+
string arm. That string is the `'field'` / `'-field'` dialect normalised by
64+
objectui's own `RelatedList.normalizeSortSpec`; it never reaches
65+
`convertSortToQueryParams`, and retiring it was not ruled. For the same reason the
66+
conversion above declines any clause that does not parse as `<field> [asc|desc]`:
67+
guessing a direction for `'-name'` would invent an ordering the author never wrote,
68+
so on a list view it meets the door's prescription instead.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
---
5+
6+
A flow screen field can now express a numeric bound, help text and a lookup target — spelled with the object field's own key names
7+
8+
<!-- adr-0087: registered screen-field-lookup-reference-required -->
9+
10+
`ScreenFieldConfigSchema` was `.strict` over exactly
11+
`name`/`label`/`type`/`required`/`options`/`defaultValue`/`placeholder`/`visibleWhen`,
12+
so three ordinary authoring intents had **no expression at all**. They did not
13+
degrade quietly — `max`, `helpText` and every lookup-target spelling were
14+
refused BY NAME — but a loud refusal with no landing key is still a dead end,
15+
and the reference app worked around all three in prose: a discount ceiling
16+
interpolated into the `label` and the `placeholder` (with a comment explaining
17+
why there was no `max`), and a `type: 'lookup'` field whose `placeholder` asked
18+
a human to type a record id because the picker could not be pointed anywhere.
19+
20+
Four keys land, and **their names are derived from `FieldSchema`, not invented**
21+
— one platform, one field vocabulary, so a name learned on an object field means
22+
the same thing on a screen field:
23+
24+
| Key | Derived from | |
25+
|:---|:---|:---|
26+
| `min` / `max` | `FieldSchema.min` / `.max` | the bound pair |
27+
| `inlineHelpText` | `FieldSchema.inlineHelpText` | help under the input — `FieldSchema` renames `help`/`helpText`/`hint`/`tooltip` onto it, so a screen-local `helpText` would have been a second contract for one question |
28+
| `reference` | `FieldSchema.reference` | the object a `type: 'lookup'` field picks records from |
29+
30+
**The bound is enforced, not advisory.** It rides to the client on
31+
`ScreenFieldSpec` so the user is stopped at the input, **and**
32+
`validateScreenInputs` re-checks it when the run resumes (`min_value` /
33+
`max_value`, both already in the ADR-0114 D2 field-error catalog — no new error
34+
code). A screen field's declared contract is the only contract behind it, so a
35+
bound the dialog alone applied would be bypassed by any caller posting to
36+
`resume` directly — the gap #4477 closed for `required`.
37+
38+
That sentence needs no "when the value is a number" qualifier, because the
39+
value SHAPE is checked first: on a `type: 'number'` field a present value that
40+
is not a finite JSON number is refused with `invalid_type` (also already in the
41+
catalog — still no new code), ⛔ **not coerced**. Before this, a bound pass that
42+
compares numbers was satisfied by anything that never reached it, so `"25"`
43+
under a `max` of `20` was conformant. One member of the open `type` vocabulary
44+
is read as a value domain; every other widget hint stays open, and a bound on a
45+
non-numeric field still constrains nothing.
46+
47+
**Delivered with its rendering, not ahead of it.** The executor forwards all
48+
four onto the wire and the Studio designer form offers all four as repeater
49+
columns; `builtin-node-form-zod-ledger.test.ts` reconciles the two key sets
50+
against the Zod in both directions, so a key declared here and absent from the
51+
form fails that test rather than shipping as a field nobody can author.
52+
53+
**BREAKING** in the accept-set sense, in TWO places — landing as `minor` on
54+
both packages because the launch-window guard (`check-changeset-no-major`)
55+
keeps breaking changes off `major` outside pre-mode, not because the narrowing
56+
is small. Both were ruled (maintainer ruling A′, decision batch #130 item 1,
57+
2026-09-13); this release is **not** purely additive.
58+
59+
1. `reference` is **required** when `type` is `lookup`, as it is on an object
60+
field. A picker with no target object resolves nothing — ADR-0078's own
61+
example of silently-inert metadata — and a degraded shape that ships today
62+
is not a reason to bend the contract to it. A stored flow with a bare
63+
`lookup` screen field parsed before and does not now. There is **no lossless
64+
conversion**: nothing in the metadata says which object the author meant, so
65+
this is an ADR-0087 **semantic** migration entry — a structured TODO
66+
(`screen-field-lookup-reference-required`) that names the flow and the field
67+
for a human to answer — and ⛔ never a D2 conversion that would have to
68+
invent a target.
69+
2. A non-number submitted for a `type: 'number'` screen field is refused on
70+
resume (`invalid_type`) instead of passing silently. A resume bag that was
71+
accepted before can be refused now; it was never doing what its author
72+
declared.
73+
74+
Everything else is additive: the bound itself fires only on a field that
75+
declares one, which nothing did before this release.
76+
77+
The neighbouring spellings are refused **with their landing key** rather than
78+
with a bare key list: `help`/`helpText`/`hint`/`tooltip` name `inlineHelpText`,
79+
and `object`/`referenceTo`/`targetObject`/`lookupObject`/`relatedTo`/`target`
80+
name `reference`. ⚠️ `object` means different things one level apart — on the
81+
screen **node** it renames to `objectName`, on a screen **field** it can only
82+
mean the lookup target — so it earns its own row on both.
83+
84+
**One stale claim corrected in passing, because this change falsified it.** The
85+
flows translation surface documented `help`'s exclusion as *"`ScreenFieldConfig`
86+
declares nothing help-shaped at all"*, in `translation.zod.ts`'s guidance string
87+
(which enumerated the old key set verbatim), its doc block, and
88+
`i18n-resolver.ts`'s `FLOW_SCREEN_FIELD_COPY_KEYS`. The screen field now
89+
declares `inlineHelpText`, so the copy is real. The exclusion **stands** — the
90+
flows bundle still carries `label` and `placeholder` only, and growing that face
91+
is a ruled step against the #7646 enumeration, not a resolver-side accretion —
92+
but its reason is now stated as a not-yet instead of telling an author the field
93+
has no help copy when it has. ⛔ No translation key was added and no resolver
94+
behaviour moved.

0 commit comments

Comments
 (0)