Skip to content

Commit d362803

Browse files
committed
Merge origin/main into claude/issue-19244-pure-regen-keeps-review-record
Claude-Session: https://claude.ai/code/session_01Wnstp2kTth7sGXfr8fXypc Co-authored-by: Claude <noreply@anthropic.com>
2 parents 62c76fe + 49d5069 commit d362803

17 files changed

Lines changed: 464 additions & 25 deletions

‎.changeset/17108-element-text-variant-published-nine.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ Measured on the 17.3.0 declaration, per value, through `ElementTextPropsSchema.s
1414
- **⛔ Nothing is retired.** `heading` and `subheading` become named refusals carrying migration hints in **release 2**, which is a separate card and is blocked on a value-level retirement mechanism that does not exist yet: `retiredKey()` and ADR-0087 D2 retire a *key*, not a *value*. Authors who want to move early can write `h2` for `heading` and `h3` for `subheading`; neither spelling stops working in this release.
1515
- **No renderer changes here.** `element:text`'s renderer, its designer inspector options and its i18n rows are objectui's, on the released pin, and land on objectui's side of the sequence.
1616

17-
Generated projections follow the declaration: `api-surface-declarations/ui.txt` gains the seven members on `ElementTextPropsSchema` and on `ComponentPropsMap['element:text']`, and the `content/docs/references/ui/component.mdx` property table widens. `check:api-surface` reports nothing removed or narrowed.
17+
Generated projections follow the declaration: the `content/docs/references/ui/component.mdx` property table widens. `check:api-surface` reports nothing removed or narrowed.

‎.changeset/17667-packages-query-contract.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ with the reads, per the maintainer-approved ruling of 2026-09-13 (decision batch
1212
**BREAKING** — `limit` and `cursor` no longer parse on
1313
`ListInstalledPackagesRequestSchema`, and `limit`'s `.default(50)` is gone with
1414
them. Both were declared here and read by nothing: the serving door filters on
15-
`status` / `type` and then returns every remaining row, so no page was ever
16-
withheld and no continuation token was ever minted. The response half's
15+
`status` / `type` / `enabled` and then returns every remaining row, so no page
16+
was ever withheld and no continuation token was ever minted. The response half's
1717
`nextCursor` has never been emitted, so a caller looping "until the cursor runs
1818
out" re-read the first and only page forever, with no error and no `400`.
1919

‎.changeset/18605-enable-on-install-one-authority.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ It stays, and its published description says what it is: the marketplace channel
2424

2525
**What does not move**
2626

27-
No key is added, removed, renamed or retyped, and no default changes: the accept set of all three schemas is byte-for-byte what it was, and `api-surface`, `api-surface-declarations`, `authorable-surface` and `authorable-defaults` are all unchanged. What moves is the published description text of three keys and the reference pages generated from it. The `Clause-②` declaration is `yes` as the conservative arm, because three published declarations' stated meaning moves.
27+
No key is added, removed, renamed or retyped, and no default changes: the accept set of all three schemas is byte-for-byte what it was, and `api-surface`, `authorable-surface` and `authorable-defaults` are all unchanged. What moves is the published description text of three keys and the reference pages generated from it. The `Clause-②` declaration is `yes` as the conservative arm, because three published declarations' stated meaning moves.

‎.changeset/18991-user-export-slot-is-a-real-optin-grant.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ docs(data): `ResolveApiOptions.userExportAllowed` no longer documents itself as
66

77
`Clause-②: no`
88

9-
⛔ **No behaviour change.** `isLegacyDerivable`, `computeOperations` and `resolveEffectiveApiMethods` are byte-identical; the omitted-option default is still `true` (`opts?.userExportAllowed !== false`), and not one assertion in `api-derivation.test.ts` moved. What changes is two docblocks in `packages/spec/src/data/api-derivation.ts` that made a **false present-tense claim**, and the generated declaration baseline that reproduces one of them.
9+
⛔ **No behaviour change.** `isLegacyDerivable`, `computeOperations` and `resolveEffectiveApiMethods` are byte-identical; the omitted-option default is still `true` (`opts?.userExportAllowed !== false`), and not one assertion in `api-derivation.test.ts` moved. What changes is two docblocks in `packages/spec/src/data/api-derivation.ts` that made a **false present-tense claim**.
1010

1111
Both carriers said the same untrue thing, and they said it in a direction that invites reintroducing a defect:
1212

@@ -17,4 +17,4 @@ The bit exists. `PermissionSetSchema.allowExport` (`src/security/permission.zod.
1717

1818
An author who trusted the old text would read the parameter as inert and could legitimately simplify it away as dead weight — which is the same defect one level upstream of where it was last found, with no consumer left to notice. Both docblocks now state the axis as it is, name `PermissionSetSchema`'s `allowExport` as the authority on its semantics, and keep the one thing that *is* still true distinct from the one that is not: omitting the option resolves to `true` because a resolve carrying no permission context must not narrow the object's own exposure — that is what lets `apiExposureDenialReason` remain a pure function of `enable` — while a caller holding permission context passes the resolved bit explicitly.
1919

20-
**Why this publishes rather than taking `skip-changeset`.** Two entries of this package's `files[]` move. `api-surface-declarations/` ships, and the member docblock sits *inside* the `ResolveApiOptions` interface body, so it is part of the declaration text that artifact records (leading TSDoc is excluded; an interior member's is not) — `check:api-surface-declarations` reported the shard stale as `~ ResolveApiOptions (interface) (declaration text changed)`, 0 removed, 0 added, 1 reshaped, and the regenerated `data.txt` carries the new text. `dist/` ships too, and the packed `dist/data/index.d.ts` carries it. A consumer reading either one reads different bytes after this change, so the corrected sentence is what reaches them.
20+
**Why this publishes rather than taking `skip-changeset`.** One entry of this package's `files[]` moves. `dist/` ships, and the emitted declaration reproduces both corrected docblocks: the packed `dist/data/index.d.ts` carries the `ResolveApiOptions.userExportAllowed` member text and the `API_METHOD_DERIVATION` table text verbatim. A consumer reading it reads different bytes after this change, so the corrected sentence is what reaches them.

‎.changeset/19085-metadata-form-declared-rows.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ Measured on the tree before the change: zero rows for either key across every `*
1414

1515
A new pin (`metadata-form-declared-rows.pin.test.ts`) keeps both rows and both faces, and adds a registry-wide assertion — every row of every form, at every depth — that **no** form routes a `FilterCondition`-typed key to the rule-array builder, with a lit control proving the walk reaches both keys before it reports an empty misrouted set.
1616

17-
⛔ **No wire byte moves and no export changes.** `check:api-surface` and `check:api-surface-declarations` are green with no regeneration: `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so the row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks — hence the regenerated `platform-objects` metadata-form bundles (44 additive lines; the new `en` entries are source text, the translated locales still need translating).
17+
⛔ **No wire byte moves and no export changes.** `check:api-surface` is green with no regeneration: `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so the row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks — hence the regenerated `platform-objects` metadata-form bundles (44 additive lines; the new `en` entries are source text, the translated locales still need translating).
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): the protocol declares what an ABSENT `scale` means per field type — `percent` ⇒ 0 (#19579)
6+
7+
**Clause-②: yes (widening)** — two new exported symbols on the `@objectstack/spec/data` index (`resolveFieldScale`, `FieldScaleMeta`), so a published public surface grows purely additively. Graded `minor` for that act, per the repo's level rule. ⛔ Nothing narrows: no key is added, removed or retyped on any `z.object`, no `.default()` is introduced, and a field's parse output is byte-identical to before — see "Why a resolver" below for why that last point is deliberate rather than incidental.
8+
9+
`FieldSchema.scale` is optional with **no declared meaning for its absence**, so every face that renders a decimal width invented one. Measured on the pinned sibling checkout: the read-only percent cell, the grid summary footer, the detail summary chip and the dashboard metric widget each resolved an absent `scale` to `0`, while the percent EDIT widget resolved it to `2`. One stored `0.25` therefore read **`25%`** on one face and **`25.00%`** on another — two magnitudes for one record, out of a single empty declaration, and a difference users report as a data bug rather than a formatting one.
10+
11+
Maintainer ruling (director seat, summon 25, batch 194 item 1, letter A′, 「同意」), quoted rather than paraphrased:
12+
13+
> `@objectstack/spec` declares the default decimal places for an **absent** `scale` per field type, and consumers read it from the protocol — ⛔ no `?? N` in any consumer. **percent ⇒ 0** in this card.
14+
15+
**What lands.** `resolveFieldScale(field)` in `data/field-scale.ts` answers the effective decimal width: the field's declared `scale` when it has a well-formed one, the platform's declared value for an absent `scale` on that type otherwise. `percent` is the one type with a declared value, and it is `0`. `FieldSchema.scale`'s `.describe()` now states the rule in words an author can read and names the resolver as the single source, so the generated field reference page carries it too.
16+
17+
**Nothing to migrate.** The key keeps its type, its optionality and its bounds; an authored `scale` round-trips unchanged; a field that declares none parses to output that still omits it. Adopting the resolver is what removes a consumer's private fallback, and the consumer half of that is a separate landing in the sibling repo.
18+
19+
**Why a resolver, and not a Zod default — measured, ⛔ not assumed.** `FieldSchema` is a flat `strictObject`, so a key-level `.default(0)` cannot see `type` and would land on every numeric type at once: that is the plain letter the ruling refused by name, because an undeclared currency would fall from `$25.00` to `$25`. A type-conditional materialization in the schema's `.overwrite()` tail — the instrument `unique` and `deleteBehavior` use, which CAN see `type` — is wrong for a second reason, outside presentation entirely: `packages/objectql`'s record validator arms its write-time `max_scale` REFUSAL only when `def.scale !== undefined`. Materializing a `0` would start refusing writes the platform accepts today, on every percent field whose author declared nothing — a stored-data change bought for a display ruling, and one no author could read off their own metadata. The absent value therefore stays absent on the parsed field and is resolved at the moment of display; a pin asserts a bare `percent` field parses to output carrying no `scale` key.
20+
21+
**`number` and `currency` are deliberately NOT declared here.** The ruling scoped them to a consumer census, and the census came back inconsistent for both, so under its own instruction each takes its own card with the readings instead of a guessed default. `number`'s faces disagree by design — the cell renderer resolves absence to "no fixed width" while the summary footer and the metric widget resolve it to `0` — and the display-grouping policy keys on the very distinction a default would erase: a DECLARED `scale: 0` marks a discrete integer (a year, a fiscal period, an ordinal) and renders ungrouped, while an absent `scale` means "decimals unknown" and keeps its separators, so declaring `number ⇒ 0` would print `2026` where the platform shows `2,026`. `currency`'s money faces do not read this key at all: they resolve fraction digits from the currency's own ISO 4217 minor-unit count, with a different surface's `precision` as the authored override.

‎.changeset/scoped-packages-dispatcher-door.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fix(runtime): mount the scoped `/api/v1/environments/:id/packages*` door, and re
1010

1111
**The wire.** Two responses gained the key their own declared schema requires (contract review of #16628, finding F2). Both additions are **additive** — no key left either payload:
1212

13-
- `GET /packages` now sends **`hasMore`** (`ListInstalledPackagesResponseSchema`). It is `false`: this door applies its `status` / `type` filters and returns every remaining row, reading no `limit` and no `cursor`, so there is no next page to announce.
13+
- `GET /packages` now sends **`hasMore`** (`ListInstalledPackagesResponseSchema`). It is `false`: this door applies its `status` / `type` / `enabled` filters and returns every remaining row, reading no `limit` and no `cursor`, so there is no next page to announce.
1414
- `DELETE /packages/:id` now sends **`packageId`** (`UninstallPackageApiResponseSchema`). `registryRemoved` and `persisted` stay on the wire unchanged.
1515

1616
A client that reads only the keys it read before is unaffected; a client parsing either payload against the published schema stops being refused.

‎content/docs/references/data/field.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const result = CurrencyConfigSchema.parse(data);
6868
| **valueDomain** | `Enum<'iana_time_zone' \| 'iso_4217_currency' \| 'iso_3166_alpha2'>` | optional | Standard value domain the WRITTEN value must be a member of: `iana_time_zone` (an IANA/tzdb zone identifier such as `UTC`, `Asia/Kolkata`, `Europe/Kyiv` — membership is the `Intl.DateTimeFormat` probe, never the `Intl.supportedValuesOf` enumeration, which omits `UTC`), `iso_4217_currency` (an ISO 4217 alphabetic currency code, uppercase, e.g. `CHF`) or `iso_3166_alpha2` (an ISO 3166-1 alpha-2 country code, uppercase, e.g. `CH`). The same closed vocabulary and the same membership predicate as a settings specifier's `valueDomain`. Only authorable on `text` — the one type whose stored value is a single plain string naming the member. Checked on the WRITTEN value only (the `min`/`max`/`maxLength` transition-gate class): a stored value outside a domain declared later is never re-read and survives unrelated edits — only a write carrying a non-member is refused, with the field error code `value_domain`. Reach for it precisely where a pattern cannot help: `^[A-Z]{2}$` admits `ZZ`, and `Mars/Olympus` is a shape-valid zone that does not exist. |
6969
| **rows** | `integer` | optional | Height of the INLINE multiline editor, in text rows (positive integer — the HTML textarea `rows` attribute; fullscreen/dialog editor surfaces size themselves and ignore it). Only authorable on multiline editor types: textarea, markdown, html, richtext. Omit it for the widget default height. |
7070
| **precision** | `integer` | optional | Total digits (non-negative integer) |
71-
| **scale** | `integer` | optional | Decimal places (integer 0-100). On a `percent` field this is the number of decimal places of the PERCENTAGE-POINT value as displayed and entered — `scale: 2` means 12.34% — and the STORED precision derives from the field's storage scale rather than being declared again: a fraction-stored percent (no `max`, or a `max` at or below 1) stores 12.34% as 0.1234 and is allowed `scale + 2` decimal places at the write seam, while a whole-percent field (`max` above 1) stores the displayed number itself and is allowed exactly `scale`. Every other numeric type is allowed exactly `scale`. The upper bound is the platform's, not a policy: renderers turn `scale` into fraction digits through `toFixed` and `Intl.NumberFormat`'s `maximumFractionDigits`, both of which throw a RangeError above 100 — so a larger declaration is unrenderable by any conforming consumer. |
71+
| **scale** | `integer` | optional | Decimal places (integer 0-100). OMITTED on a `percent` field ⇒ 0 decimal places, so a stored 0.25 reads `25%` on every face; omitted on any OTHER numeric type declares NO fixed width — the value keeps its natural precision, and a DECLARED `scale: 0` (a year, a fiscal period, an ordinal) stays distinguishable from having declared nothing, so nothing is defaulted there. Consumers resolve the effective width by calling `resolveFieldScale` from `@objectstack/spec/data`, the single source for an absent `scale`: a renderer that spells its own fallback is a width no other face can see, and that is how one stored 0.25 came to read `25%` on the read-only cell and `25.00%` in the edit widget. On a `percent` field this is the number of decimal places of the PERCENTAGE-POINT value as displayed and entered — `scale: 2` means 12.34% — and the STORED precision derives from the field's storage scale rather than being declared again: a fraction-stored percent (no `max`, or a `max` at or below 1) stores 12.34% as 0.1234 and is allowed `scale + 2` decimal places at the write seam, while a whole-percent field (`max` above 1) stores the displayed number itself and is allowed exactly `scale`. Every other numeric type is allowed exactly `scale`. The upper bound is the platform's, not a policy: renderers turn `scale` into fraction digits through `toFixed` and `Intl.NumberFormat`'s `maximumFractionDigits`, both of which throw a RangeError above 100 — so a larger declaration is unrenderable by any conforming consumer. |
7272
| **min** | `number` | optional | Minimum value. Checked on the WRITTEN value only — the same transition-gate class as `requiredWhen`: an UPDATE validates just the fields the payload carries, so a stored value below a bound declared later is never re-read and survives unrelated edits; only a write that carries an out-of-bound value is refused, and a repairing write is accepted. For an invariant re-checked on every write, declare a `validations[]` `script` rule instead. |
7373
| **max** | `number` | optional | Maximum value. Checked on the WRITTEN value only — the same transition-gate class as `min`: a stored value above a bound declared later is never re-read and survives unrelated edits; only a write that carries an out-of-bound value is refused. For an invariant re-checked on every write, declare a `validations[]` `script` rule instead. |
7474
| **useGrouping** | `boolean` | optional | Digit-grouping presentation hint for `number` fields — maps to `Intl.NumberFormat`'s `useGrouping`. Absent = renderer decides (interim heuristic today, locale default eventually); `false` = author opts out of grouping (e.g. a year or other ordinal/identifier integer); `true` = author pins grouping on. |

0 commit comments

Comments
 (0)