Skip to content

Commit 951f75c

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17080-per-release-spec-changes
2 parents 4f1724d + 89c6ec5 commit 951f75c

39 files changed

Lines changed: 2405 additions & 190 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`collectFlowGraphs` no longer hands out a `FlowGraph` whose `edges` can hold a non-record — the sibling list #16752's repair did not reach (#18102).
6+
7+
`FlowGraph.edges` is declared `readonly FlowEdgeParsed[]`. The walk forwarded it untouched, four lines from the node-side member filter the same walk has carried since #16752, and a nested region's edge list is admitted on `Array.isArray` alone — which proves the LIST and never its MEMBERS. A YAML `edges:` list item left empty deserialises to `null`, and a region its own schema refused is left RAW for `validateControlFlow` to name, so the producer handed out an array holding a member its own declared element type excludes. Measured on `main`:
8+
9+
```
10+
collectFlowGraphs({ nodes: [start, loop{ body: { nodes: [], edges: [null] } }], edges: [] })
11+
graph[1] scope="loop 'lp' body" edges=[null] declared readonly FlowEdgeParsed[]
12+
```
13+
14+
- **The junk member is DROPPED, per list**, through the same one predicate the node side uses (`isRegionDict`), so the two lists the walk hands out cannot drift from each other. Copy-on-write per list: a well-formed flow is handed back the very same arrays.
15+
- ⭐ **The real edge beside it is still HANDED OUT**, and so is the node list. "No non-record members" is half a contract — a filter that emptied `edges`, or reached into `nodes`, would satisfy it. Both are pinned.
16+
- **This is a drop in the producer, not a refusal.** No authoring door's accept set moves: `FlowSchema.safeParse` still returns an envelope rather than throwing, the region `safeParse` refusal in `validateControlFlow` still owns and still reports the malformed region, and `FlowGraph.path` still indexes the RAW node list so a Zod issue stays anchored where the author wrote it. ⛔ Not a looser signature either — the declared element type is unchanged and is now true.
17+
- **Latent, not live — measured, and not for the reason the filing gave.** There are THREE `graph.edges` consumers on the tree, not two. The two in `packages/lint` coerce through `recordsOf` (#16910). The third is `packages/services/service-automation`'s registration pass, which reads `.id` / `.source` / `.target` straight off each member with no guard, and is shielded only by call ORDER — `validateControlFlow` refuses the malformed region a few frames earlier in `registerFlow`. So no throw is reachable today, by one belt more than was counted. After this change the declared type carries it, and the next consumer needs neither a coercion nor a call-order argument.
18+
- **`analyzeRegion` is not one of those consumers.** It throws a `TypeError` on a `null` / `undefined` edge member (measured), but nothing routes producer output into it: its in-repo callers hand it post-`safeParse` region data. It reads an edge list, it does not read `FlowGraph.edges`.
19+
- **No behaviour changes on well-formed metadata.** The only input whose handling moves is input whose declared type already said it could not exist.
20+
21+
Clause-②: no
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`ListMapConfigSchema` now declares `style` — optional `z.string()`, the map style URL the renderer already reads and the schema refused by name (#18406). In the same stroke `object-map`'s `map` prop points at `ListMapConfigSchema` again, retracting the `z.unknown()` that the missing key had forced.
6+
7+
`ListMapConfigSchema` is a `strictObject`, and `style` was the one member of the renderer's own documented config surface it omitted. Measured at the `.objectui-sha` pin `53ded82b`: objectui's `ObjectMapConfigSchema` (`packages/types/src/zod/objectql.zod.ts:562`) declares all eight keys, `getMapConfig` reads `schema.mapStyle || schema.map?.style` (`packages/plugin-map/src/ObjectMap.tsx:365`), and objectui's own `content/docs/plugins/plugin-map.mdx:131` documents `style` inside the block. `ListMapConfigSchema.safeParse({ style: 'https://tiles.example/style.json' })` answered `success: false`, so a map style could not be declared through the spec's list-view face at all. Declared here under the director seat's decision batch #153 item 4 letter 1, confirmed by the maintainer verbatim 「其他同意」.
8+
9+
**Clause-②: yes (widening)** — one new declared key on a published, strict accept set, so the set a consumer writes against grows. Nothing previously admitted is refused, and nothing is retired. Contract-review tier.
10+
11+
- **`style`, not `mapStyle`, and not both.** Mapbox and MapLibre both call a style URL `style`, and that is the name the renderer reads inside the config block. The competing spelling — objectui#5017's dev warning teaching `map: { mapStyle }` — is corrected on the objectui side rather than learned here, and no alias is declared: an alias would be a permanent obligation for a key nobody has written yet.
12+
- **Not the node-level `style`.** A component node's `style` is `BaseSchema.style`, an inline CSS record; the renderer stopped reading a top-level `style` as a map style at objectui#5017. The component-level `mapStyle` prop is unchanged and still wins when both are present.
13+
- **`object-map.map` stops being `z.unknown()`.** That posture existed only because pointing the door at a schema missing `style` would have refused a value the renderer honours. With the gap closed, the door takes the spec's own block — so a misspelling inside an authored `map` block is now refused at `map`, by name, instead of passing through an open value. The two pins that recorded the divergence are inverted in the same change.
14+
- **The generated projections move with it** — `authorable-surface/ui.json` gains `ui/ListMapConfig:style`, and `content/docs/references/ui/view.mdx` plus `content/docs/references/ui/component.mdx` gain the key; the `object-map.map` row in the component reference changes from `any` to the block's real shape and gains a nested-shape table.

‎.changeset/client-get-session-envelope-and-refresh-read.md‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,21 @@ inert as it was.
5757
`token.signature` form interchangeably, so a client that held the signed form
5858
stays signed in across the call.
5959

60-
Two answers stay outside the declared type and are **not** addressed here: the
61-
anonymous `null`, which would need the published return annotation to widen, and
62-
`SessionUser.image`, declared `z.string().optional()` against a route that
63-
serves `null` (#17235). The sibling `auth.login` / `auth.register`, which
64-
normalize into `data` but set no `success`, are #17234.
60+
Three answers sat outside the declared type when this change was written and
61+
are **not** addressed by it. Each has since been answered on its own card, so a
62+
caller reading this entry does not have to code around any of them:
63+
64+
- the **anonymous** `/get-session` answer, recorded above as `200 null`. It no
65+
longer needs the published return annotation to widen, because the producer
66+
moved instead: since #17881 `plugin-auth`'s `refuseAnonymousSession` converts
67+
better-auth's `200` plus the literal JSON `null` into the declared ADR-0112
68+
refusal — HTTP `401` with `code: UNAUTHENTICATED` — before it leaves the
69+
process. The SDK's shared `fetch` wrapper throws on any non-2xx, so an
70+
anonymous `auth.me()` **rejects** rather than resolving outside its own type.
71+
Ruled by #17238: the producer moved and `SessionResponseSchema` is untouched.
72+
- `SessionUser.image`, then declared `z.string().optional()` against a route
73+
that serves `null` (#17235). It is now declared `z.string().nullish()`, so
74+
the `"image": null` every `/auth/*` session body carries parses.
75+
- the sibling `auth.login` / `auth.register`, which then normalized into `data`
76+
but set no `success` (#17234). They now run this entry's own lift, which
77+
fills `success` as well as `data`.

‎content/docs/data-modeling/analytics.mdx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,12 @@ dataset-bound reports — so the numbers match everywhere.
207207
The query result is presentation-ready — authors do not format dimension or
208208
measure values by hand:
209209

210-
- **Dimensions** — a `select` dimension returns its option **label** (not the
211-
stored value), a **reference** dimension (any field whose stored value is
212-
another record's id, a `user` person axis included) returns the related
213-
record's **display name** (not the FK id), and a `date` dimension with a
210+
- **Dimensions** — a dimension **whose field carries `options`** returns the
211+
option **label** (not the stored value; resolution keys on the `options` list
212+
being there, not on the field's type, so this is not limited to `select`), a
213+
**reference** dimension (any field whose stored value is another record's id,
214+
a `user` person axis included) returns the related record's **display name**
215+
(not the FK id), and a `date` dimension with a
214216
`dateGranularity` returns a human bucket label (`month` → `2026-04`,
215217
`quarter` → `2026-Q2`, `year` → `2026`). Unresolved values pass through
216218
unchanged, never blank.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ OS_MCP_STDIO_ENABLED=true OS_MCP_STDIO_API_KEY=osk_... os start # start the lo
257257
|:---|:---|:---|:---|
258258
| `OS_MCP_SERVER_ENABLED` | boolean | `true` | The MCP **HTTP** surface (`/api/v1/mcp`) is a core capability and defaults **on**. Set `false` to disable it (endpoint 404s, the Connect-an-Agent page disappears). |
259259
| `OS_MCP_STDIO_ENABLED` | boolean | `false` | Auto-start the long-lived **stdio** transport at boot. Opt-in and **stricter** than the HTTP surface. **Requires `OS_MCP_STDIO_API_KEY`** — stdio runs as that key's identity with RLS/FLS/tenant applied; if the key is missing or invalid, boot **fails closed** (stdio refuses to start). See ADR-0101. |
260-
| `OS_MCP_STDIO_API_KEY` | string | — | The `osk_...` API key the **stdio** transport runs as. Resolved through the same verify chain as the HTTP/REST surfaces, so reads are scoped to that identity's permissions. Mint one from **Setup → Connect an Agent** (or `POST /api/v1/keys`). For full authority, mint a key on a platform-admin or dedicated **service** identity — there is deliberately no `system`/unscoped bypass. |
260+
| `OS_MCP_STDIO_API_KEY` | string | — | The `osk_...` API key the **stdio** transport runs as. Resolved through the same verify chain as the HTTP/REST surfaces, so reads are scoped to that identity's permissions. Mint one from the **Connect an Agent** page — **Account → Developer** for any signed-in user, **Setup → Connect an Agent** for platform admins — or `POST /api/v1/keys`. For full authority, mint a key on a platform-admin or dedicated **service** identity — there is deliberately no `system`/unscoped bypass. |
261261
| `OS_MCP_SERVER_NAME` | string | `objectstack` | Server name advertised to MCP clients. |
262262
| `OS_MCP_SERVER_TRANSPORT` | enum | `stdio` | `stdio` \| `http`. Use `http` (Streamable HTTP) for a remote client; `stdio` for a local one. |
263263

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,27 @@ cannot read, and they are neither in nor out.
217217
| object name is an `object: string` parameter | 19 |
218218
| object name is some other run-time expression | 57 |
219219

220+
### Subtractions the census could NOT defend — enforced
221+
222+
A same-named call on something that is not a data engine is subtracted, and the
223+
subtraction is DEFENSIBLE when this census can name why: the receiver is a `node:`
224+
builtin, a value it watched being constructed, a language global, a type THIS
225+
corpus declares and the door rule rejected, or an `UNTYPED_RECEIVERS` row.
226+
227+
⚠️ Counted below are the subtractions it can name no such fact for — the
228+
receiver carries a declared type the engine type index does not hold, and that
229+
index is built from TRACKED sources only, deliberately. An untracked, generated
230+
or dependency-owned declaration is one this census never saw, and «never saw it»
231+
must not be spelled the same way as «read it, not an engine».
232+
233+
| what | count |
234+
| :--- | ---: |
235+
| write calls subtracted with no defensible reason | **3** |
236+
| …whose declared type text states an engine door anyway | **2** |
237+
238+
Every one of them is listed, by receiver and by the type text that could not be
239+
placed, in [`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`](https://github.com/objectstack-ai/objectstack/blob/main/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md).
240+
220241
The corpus walked is every tracked non-test source under `packages/services/`
221242
and `packages/plugins/`; calls to a same-named method on something that is not
222243
a data engine were subtracted. Every site is listed in
@@ -232,11 +253,11 @@ holds still. They are required to be HERE and to say WHEN they were true;
232253
their values are not compared. The reasoning, and the measurement behind it,
233254
are in `scripts/check-tenant-audit-census.mjs`.
234255

235-
Measured on 2026-09-16 at `11daf7f69`.
256+
Measured on 2026-09-18 at `02bdeaaf2`.
236257

237258
| corpus scale (not enforced) | count |
238259
| :--- | ---: |
239-
| tracked non-test sources scanned | 570 |
260+
| tracked non-test sources scanned | 573 |
240261
| engine-shaped types recognised | 63 |
241262
| declared objects in the registry | 117 |
242263
| same-named calls subtracted as non-engine | 146 |

‎content/docs/protocol/kernel/plugin-spec.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default definePlugin({
6868

6969
// PEER DEPENDENCIES (consumer must install)
7070
peerDependencies: {
71-
'@objectstack/ui': '^2.0.0',
71+
'@vendor/ui': '^2.0.0',
7272
},
7373

7474
// METADATA REGISTRATION
@@ -459,7 +459,7 @@ Plugin expects **consumer** to install these (not bundled).
459459

460460
```typescript
461461
peerDependencies: {
462-
'@objectstack/ui': '^2.0.0',
462+
'@vendor/ui': '^2.0.0',
463463
}
464464
```
465465

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ View filter rule
640640
| **staticData** | `any[]` | optional | Inline records — read SECOND by `getDataConfig`, wrapped into a `{ provider: 'value' }` config |
641641
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` |
642642
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Marker order for the fetched records — 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` |
643-
| **map** | `any` | optional | Map field config, the author face: `{ latitudeField, longitudeField, locationField?, titleField?, descriptionField?, zoom?, center?, style? }`. Taken WHOLE when present — the flat top-level spelling beside it is ignored |
643+
| **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map field config, the author face — the same block `ListViewSchema.map` declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored |
644644
| **mapStyle** | `string` | optional | MapLibre style URL or spec, overriding the public demo tiles. Read before `map.style`; NOT the base node `style`, which is an inline CSS record |
645645
| **navigation** | `any` | optional | Marker-click navigation config (`{ mode: page \| drawer \| modal \| split \| popover \| none }`) |
646646
| **enableClustering** | `boolean` | optional | Group nearby markers into clusters. Absent, the renderer clusters only above 100 markers |
@@ -694,6 +694,19 @@ Sort field and direction pair
694694
| **field** | `string` | ✅ | Field name to sort by |
695695
| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction |
696696

697+
### Nested Shape: `ObjectMapProps.map`
698+
699+
| Property | Type | Required | Description |
700+
| :--- | :--- | :--- | :--- |
701+
| **latitudeField** | `string` | optional | Field providing the marker latitude (used with longitudeField) |
702+
| **longitudeField** | `string` | optional | Field providing the marker longitude (used with latitudeField) |
703+
| **locationField** | `string` | optional | Field providing a combined location — a "lat,lng" string or a `{ lat, lng }` object — as the alternative to the latitudeField/longitudeField pair |
704+
| **titleField** | `string` | optional | Field displayed as the marker title (popup heading, mobile record card, and what the map search box matches on) |
705+
| **descriptionField** | `string` | optional | Field displayed as the marker description |
706+
| **zoom** | `number` | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records |
707+
| **center** | `[number, number]` | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records |
708+
| **style** | `string` | optional | Map style URL — the MapLibre style document the renderer loads in place of the public demo tiles. The component-level `mapStyle` is read FIRST and wins when both are present; this is NOT the inline CSS `style` record a component node carries |
709+
697710

698711
---
699712

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,7 @@ Map view configuration
775775
| **descriptionField** | `string` | optional | Field displayed as the marker description |
776776
| **zoom** | `number` | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records |
777777
| **center** | `[number, number]` | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records |
778+
| **style** | `string` | optional | Map style URL — the MapLibre style document the renderer loads in place of the public demo tiles. The component-level `mapStyle` is read FIRST and wins when both are present; this is NOT the inline CSS `style` record a component node carries |
778779

779780

780781
---
@@ -1023,6 +1024,7 @@ View filter rule
10231024
| **descriptionField** | `string` | optional | Field displayed as the marker description |
10241025
| **zoom** | `number` | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records |
10251026
| **center** | `[number, number]` | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records |
1027+
| **style** | `string` | optional | Map style URL — the MapLibre style document the renderer loads in place of the public demo tiles. The component-level `mapStyle` is read FIRST and wins when both are present; this is NOT the inline CSS `style` record a component node carries |
10261028

10271029
### Nested Shape: `ListView.tree`
10281030

@@ -1421,6 +1423,7 @@ View filter rule
14211423
| **descriptionField** | `string` | optional | Field displayed as the marker description |
14221424
| **zoom** | `number` | optional | Initial zoom level (1-20). Omit to let the renderer fit the camera to the queried records |
14231425
| **center** | `[number, number]` | optional | Initial camera center as [latitude, longitude]. Omit to let the renderer fit the camera to the queried records |
1426+
| **style** | `string` | optional | Map style URL — the MapLibre style document the renderer loads in place of the public demo tiles. The component-level `mapStyle` is read FIRST and wins when both are present; this is NOT the inline CSS `style` record a component node carries |
14241427

14251428
### Nested Shape: `ObjectListView.tree`
14261429

0 commit comments

Comments
 (0)