Skip to content

Commit c9058f4

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18375-skip-changeset-claude-rule
2 parents b1cf1e9 + c3a4c74 commit c9058f4

14 files changed

Lines changed: 1084 additions & 39 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`translation-target-unknown` no longer reports the locale keys a package ships for a view, page, action, app, dashboard or flow a SIBLING package of the same artifact declares. Six collection rungs built their universe from the top-level collection alone, while the same file already read `packages[].manifest.…` for `navigationContributions` (#18442), `objectExtensions` (#18441) and `objects` (#19064) — so the capability was present and these rungs did not use it (#19349).
6+
7+
`os build` runs the rule table per PACKAGE as well as over the union (`compile.ts` step 3b-ii): each package body is judged as its own stack with the artifact's `packages[]` beside it as resolution context (`packageBodyAsStack`, #16611). On that leg each `stack.<collection>` holds ONE package's declarations. Measured on a throwaway probe before anything was touched, each level produced exactly one `error`, at `translations[0]["zh-CN"].dashboards.crm_overview`, `….flows.lead_conversion`, `….globalActions.export_all`, `….objects.crm_order._views.board`, `….objects.crm_order._tabs.mine` and `….apps.crm_app` — each carrying a remedy (`or drop it`) that deletes a translation the runtime honours, at a severity that FAILS the run.
8+
9+
**The carrier is proven rather than assumed.** Every one of these keys carries disposition `concat` in `COMPOSE_KEY_DISPOSITIONS`, which is precisely what puts it inside `ASSEMBLED_PACKAGE_BODY_DISPOSITIONS` and so inside an ADR-0130 D4 entry's assembled body — the same proof the `objectExtensions` and `objects` folds rest on. ADR-0130 makes the release artifact the co-ownership boundary, so the miss is the RUN's blind spot and not the author's mistake.
10+
11+
**Records, not names — and for three different reasons, not one assumption applied six times.** `dashboards`, `flows` and `apps` are keyed by their own name and carry a sub-rung derived from the record (widget ids and header `actionUrl`s, screen node ids and their field names, navigation ids), so a name-only fold would resolve the top key and then judge that sub-rung against an empty set. The `actions` record is itself read downstream by `checkActionParams`, and it carries the owner that keeps an object-bound action under `objects.<obj>._actions` instead of making it globally addressable. `views` and `pages` have no bundle rung of their own at all — they contribute `_views`, `_sections` and `_tabs` facts under the object they bind to — so the record is the only thing carrying both the fact and its binding.
12+
13+
**`apps` is the half #18442 did not cover.** That change reads `navigationContributions`, so an app became addressable only where THIS package contributes into it; a sibling's `apps[]` declaration was invisible either way. With no contribution the app NAME was the orphan; with one, the name resolved through #18442 while the owner's own navigation ids were orphans — and were diagnosed "this stack contributes no such item", advising a move to a package sitting in the same artifact. Folding the records before the contributed-only pass closes both halves and restores the declared-app diagnosis, while an app owned OUTSIDE the artifact keeps #18442's wording.
14+
15+
**The controls, which are what make this a narrowing and not a hole.** Every rung pins both directions side by side: the same bundle judged ALONE still errors (so "no findings" cannot be confused with the rung going quiet); a name no entry of the artifact declares still errors with its rule id and a remedy that now enumerates what the artifact provides; every sub-rung stays judged against the sibling's declaration, so a typo under a resolved dashboard, flow, screen, app or object is still an `error`; an object-bound sibling action keyed under `globalActions` still errors with its routing message; where both packages declare the same name the declaration being judged keeps the slot, so a sibling's widget ids do not become addressable under this package's dashboard; an entry with no readable body (a segment reference) makes nothing addressable on any rung; and the single-`defineStack` shape is untouched, because all six are stack collections with no `stack.manifest` form to read.
16+
17+
No schema moved, no export moved, and no accept set moved: this is a lint rule's false-positive set narrowing. `Clause-②: no`
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.

‎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)