Skip to content

Commit c38c18a

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20323-retire-action-aria
Conflicts resolved by stacking both intents: CONVERSIONS_BY_MAJOR[18] and step18.conversionIds keep main's currency-config-precision-removed and permission-rls-tags-removed, then this branch's action-aria-removed; step18's hand-written rationale carries main's two paragraphs, then this branch's action.aria paragraph. Generated regions are regenerated in the next commit. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
2 parents 8dd3a2c + 15bf186 commit c38c18a

112 files changed

Lines changed: 6236 additions & 775 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.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
A sharing rule whose `condition` compares a field with a `json` or `multiple` field is refused when it is authored, at `os validate` / `os build` / `os lint`, instead of being seeded and then granting nothing (#19886).
6+
7+
**BREAKING** — an accept-set narrowing, shipped by `@objectstack/lint` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is already registered under protocol major 18 as `cel-predicate-one-value-comparand-refused`, whose surface names `sharingRules[].condition` and this class.
8+
9+
Clause-②: no (narrowing)
10+
11+
`record.status != record.tags`, with `tags` a `json` field or a `multiple` lookup, lowers to a legal filter shape, because the CEL lowering sees the condition's text and not the object's field types, so the seeder seeds the rule. Measured before this change: 72 conditions (`==`, `!=`, `!(==)`, `>`, `>=`, `<`, `<=`; a `json`, `address`, `multiselect`, `multiple` lookup and `multiple` user field; both operand orders; plus a list-against-list and a compound spelling) were all accepted by the real `os validate`. The runtime refused every one of them, measured through the real plugin-sharing on driver-sql and driver-sqlite-wasm: the rule was seeded into `sys_sharing_rule`, every criteria query it ran answered `INVALID_FILTER` / 400, `SharingRuleService` read that as matching no record, and no `sys_record_share` grant was written, at boot or on a later insert or update. The recipient read nothing. The only signal was one WARN line per rule in the server log.
12+
13+
What changes:
14+
15+
- `@objectstack/lint`: `validateSharingRuleEnforceability` reports `sharing-rule-unlowerable-condition` for a condition that lowers but compares two fields (`==`, `!=`, `>`, `>=`, `<`, `<=`, on either side, under `!` too) where either column is DECLARED to hold a list or an object. It uses the same classification as the row-level-security rule's arm for this class (`listHoldingComparisons`, now exported from `validate-rls-predicate-enforceability.ts`), which reads the spec's value-shape classes, the same two driver-sql refuses such a comparison by: a structured JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), or a multi-valued field (`multiselect`, `checkboxes`, `tags`, or `select` / `radio` / `lookup` / `user` / `file` / `image` with `multiple: true`). The finding names each comparison and the declaration behind it, and states the run-time consequence. It keeps the unlowerable id because the fix is the same rewrite of the condition, and the literal spelling of the same class (`record.status == ['a', 'b']`) is already reported under that id.
16+
- Inactive rules are judged too, as the rule already does for every condition: the seeder seeds them regardless of `active`.
17+
18+
Not changed: a field compared with a single-valued field (`record.status != record.owner_name`, `record.amount > record.budget`), a `json` or `multiple` field compared with a literal or tested against `null`, and any column the stack does not declare (an anchor object from another package, an object with no field map, an undeclared name), which the rule does not judge. No row-level-security verdict changes. The rule still runs only at the CLI doors; the metadata save door for a `sharing_rule` does not run it, as before.
19+
20+
No shipped condition moves: the 3 declared sharing-rule conditions in this repository's packages and examples compare a field with a literal, the cloud repository declares none, and the real `os validate` over `app-crm`, `app-multi-package`, `app-showcase` and `app-todo` reports no new `sharing-rule-*` finding.
21+
22+
**What to change.** A field compared with a `json` or `multiple` field has no row-filter form: compare with a single-valued column, or with a literal ("one of these values" is `record.status in ['open', 'pending']`), or keep the value the rule keys on in a single-valued field and compare with that.
23+
24+
<!-- adr-0087: not-required (already-registered cel-predicate-one-value-comparand-refused) The entry's surface already names sharingRules[].condition and this exact class ("a field compared with another field (==, !=, or an ordering operator) where either column holds a list or an object on the record, as a json column or a multiple lookup does"), and its replacement carries the prescription ("A field compared with a json or multiple field has no pushdown form: compare with a single-valued column"). This change moves where that registered class is refused on a sharing rule, from a silent zero-share at run time to authoring time; it adds no class and no prescription the entry does not already hold, and it rewrites no stored metadata. -->
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
**BREAKING** — retire `currencyConfig.precision`: a currency's decimal places are its currency's (#19992).
7+
8+
`currencyConfig.precision` was declared, validated against ISO 4217, and baked to `2`
9+
into parse output — and **no renderer or runtime ever read it**. objectui's
10+
`CurrencyField` derives an amount's decimal places from the currency's ISO 4217
11+
minor unit (2 for USD, 0 for JPY, 3 for KWD) and never looked at the key, so an
12+
author who wrote `precision: 4` saw the same two decimals as everyone else. Its
13+
only reader was its own contradiction check. ADR-0049 enforce-or-remove; triage
14+
direction REMOVE under ruling 乙 on #19910 — 「a currency's decimal places are the
15+
currency's, not a setting」.
16+
17+
Clause-②: no
18+
19+
## FROM → TO
20+
21+
| you wrote (17.4 and earlier) | write instead |
22+
| --- | --- |
23+
| `currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' }` | `currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' }` |
24+
| `currencyConfig.decimals` / `currencyConfig.scale` (always refused, with a suggestion to write `precision`) | nothing — delete the key; the refusal now says why instead of suggesting `precision` |
25+
| a field whose amounts need a different number of decimals | a different currency: the width is the currency's minor unit and is declared nowhere |
26+
27+
**The one-line fix:** delete `precision` from every `currencyConfig`. ⛔ Do not move
28+
the number to the field-level `precision`: that key is the amount's TOTAL digit count
29+
(a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places, and it is
30+
unchanged by this release.
31+
32+
`os migrate meta --from 17` lists the mechanical edits for existing sources; apply
33+
them by hand.
34+
35+
## The retirement kit
36+
37+
- **`CurrencyConfigSchema.precision`** — removed from the shape. The schema is a
38+
`strictObject`, so the route is strict deletion plus a `guidance` entry: an
39+
authored key is refused as `unrecognized_keys` at `currencyConfig`, and the message
40+
carries the prescription (``currencyConfig.precision` was removed in
41+
@objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer or runtime ever
42+
read it: …``). `tsc` refuses a literal in a typed position too — the key is off
43+
`CurrencyConfig`'s input type.
44+
- **The `decimals` / `scale` aliases** — gone with their target. Each is now answered
45+
with the same reason (`` `currencyConfig.scale` is not a currency configuration key,
46+
and nothing replaces it: … ``) and no rename suggestion.
47+
- **The ISO 4217 contradiction check** (the `.superRefine`) and **the
48+
default-materializing `.overwrite()`** — both existed only for this key and are
49+
removed. `CurrencyConfigSchema.parse({})` now returns exactly
50+
`{ currencyMode: 'dynamic', defaultCurrency: 'CNY' }`; `CurrencyConfigParsed` no
51+
longer declares `precision`. The internal helpers `currencyPrecisionContradiction`
52+
and `currencyFractionDigits` (never exported from a public entry) are removed; the
53+
CLDR table they read stays, because the `iso_4217_currency` value domain reads its
54+
key set.
55+
- **The field designer form** — the field-level `precision` row's help text read
56+
"Decimal places (e.g., 2 for $10.50)", the one reading the contract refuses. It now
57+
reads "Total digits", matching the key's describe and the object designer's row;
58+
the zh-CN / ja-JP / es-ES translations follow (`@objectstack/platform-objects`).
59+
- **Registry** — `RETIRED_KEYS_BY_MAJOR[18]` gains `data/CurrencyConfig:precision`;
60+
the protocol-18 step gains the D2 conversion `currency-config-precision-removed` and
61+
its D3 entry `currency-config-precision-retired`, which states the two judgments the
62+
strip cannot make: a width declared where the old check never looked (a `dynamic`
63+
field, or a code with no known ISO 4217 minor unit) never applied, and code of your
64+
own that read the served key must derive the width from the field's currency.
65+
66+
## What an operator with STORED metadata sees
67+
68+
Nearly every stored currency field carries this key without anyone having written it:
69+
the old `.overwrite()` baked `precision: 2` into parse output, so `sys_metadata`
70+
object rows and built artifacts hold it. Nothing breaks at read: the conversion
71+
`currency-config-precision-removed` is retired from the load path but replayed by the
72+
stored-row and artifact seams, which strip the key from every field's
73+
`currencyConfig` on objects and object extensions and serve the row canonical. The
74+
strip is lossless — the key never had an effect — and the field-level `precision` is
75+
never touched. `os migrate meta --stored --apply` rewrites the stored rows so the
76+
per-row notice stops.
77+
78+
<!-- adr-0087: registered currency-config-precision-removed -->

‎.changeset/20215-generate-scaffolds-reach-stack.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ fix(cli): what `os generate` writes now reaches the stack, or the command says i
1010

1111
- `objectstack.config.ts` imports every directory `os generate` writes into (`src/objects`, `src/views`, `src/actions`, `src/flows`, `src/dashboards`, `src/apps`, `src/skills`) and hands each barrel's exports to `defineStack` under its key (`objects`, `views`, …). A file `os g` writes there is part of the stack with no edit to the config. The keys read the barrels through a small `exportsOf` helper declared in the config, because `Object.values` on an empty barrel does not type-check against `defineStack`'s collection types.
1212
- An `index.ts` containing only `export {};` for each directory the template puts nothing in. An `index.ts` that already exists is kept as it is and never overwritten.
13-
- `requires: ['automation', 'triggers']`. A flow that starts on a record change is fired by `triggers` and run by `automation`. Without `triggers`, `defineStack` refuses the config as soon as it holds such a flow. Without `automation`, the server loads the flow and never runs it.
13+
- `requires: ['automation', 'triggers']`. A flow that starts on a record change is fired by `triggers` and run by `automation`. If either one is missing, `defineStack` refuses the config as soon as it holds such a flow.
1414

1515
**What `os generate` now does:**
1616

17-
- After writing, it loads the project's config again and reports on the new item. Either the stack carries it, or it is **not wired** (the file is written, the config is left untouched, and the command prints the import and `defineStack` key to add), or it **cannot run** (a flow in a stack whose `requires` lacks `automation`: the command prints the whole `requires` list to use). It never edits the config.
18-
- It refuses a write that makes a config that loaded stop loading, for example an action or app bound to an object nobody declared, or a flow in a stack without `triggers`. It removes what it wrote, exits 1, and prints the stack's own reason. Generate the object first (`os g object customer`), then what binds to it. `dashboard` and `skill` now read the config too, so they can report, and they still generate when the config does not load.
17+
- After writing, it loads the project's config again and reports on the new item. Either the stack carries it, or it is **not wired** (the file is written, the config is left untouched, and the command prints the import and `defineStack` key to add). It never edits the config.
18+
- It refuses a write that makes a config that loaded stop loading, for example an action or app bound to an object nobody declared, or a flow in a stack without `triggers` or without `automation`. It removes what it wrote, exits 1, and prints the stack's own reason. Generate the object first (`os g object customer`), then what binds to it. `dashboard` and `skill` now read the config too, so they can report, and they still generate when the config does not load.
1919
- A view's own `name` is now the object it binds to, prefix included (`my_app_order_line`, not `order_line`). The server registers a view under its object and refused, at boot, a scaffold whose `name` disagreed. That never showed while the views barrel was not loaded.
2020
- The barrel step asks the compiler whether the barrel already exports the name, instead of searching the file's text. `os g view order` after `os g view order_line` had found `order` inside `orderLine` and exported nothing.
2121
- The `flow` scaffold's header states the `requires` it needs.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: a number, currency, percent, rating, slider or progress field refuses an array, a boolean or an object with `invalid_number` (#20309)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING**: shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner and the ADR-0087 disposition below, never by the level). The
12+
narrowing: an array, a boolean or an object whose `Number()` is finite, such as
13+
`[500]`, `[]`, `true` or `false`, written to one of those fields is now refused
14+
with `400 VALIDATION_FAILED` / `invalid_number`. It used to be accepted and
15+
stored as sent.
16+
17+
## What was wrong
18+
19+
The record validator judged `Number(value)` on a number-typed field, but the
20+
write carried `value` itself. Every value that JavaScript coerces to a finite
21+
number therefore passed the check and reached the driver unchanged:
22+
23+
- **SQLite** stored `[500]` as the TEXT `'[500]'`, which a read returned as the
24+
string `"[500]"`; `[]` as the TEXT `'[]'`; and `true` / `false` as `1` / `0`.
25+
- **memory** stored the array or the boolean itself.
26+
27+
`[5, 7]` and `{}` were already refused, because `Number()` of each is `NaN`.
28+
29+
## What changes
30+
31+
- On `number`, `currency`, `percent`, `rating`, `slider` and `progress`, a value
32+
that is neither a number nor a string is refused with `invalid_number`: an
33+
array, a boolean, a plain object, a `Date`. This holds on every engine, REST,
34+
batch and import write door, because they all write through the same
35+
validator.
36+
- A number is judged and stored exactly as before, and so are the `min`, `max`
37+
and `scale` checks and their messages.
38+
- A string is also unchanged. It is still judged by `Number()` and stored as
39+
sent. Which strings a number field accepts is a separate change.
40+
- `summary` is still not judged by this check (it is in the spec's
41+
`COMPUTED_VALUE_TYPES`). A blank still becomes `null` before the check runs.
42+
43+
## Rows already stored
44+
45+
This refuses new writes only; a stored value is never re-read by the check.
46+
Rows written earlier on SQLite may hold such a value as TEXT in a numeric
47+
column. To find them, run this once per number-typed column:
48+
49+
```sql
50+
SELECT id, "FIELD" FROM "OBJECT" WHERE typeof("FIELD") = 'text';
51+
```
52+
53+
OBJECT is the object name and FIELD is the field name. A match is a cell that
54+
SQLite could not store as a number: an array written as TEXT, or a string such
55+
as `'0x10'`. Decide its number by hand; nothing here rewrites it.
56+
57+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authored moves: `packages/spec` is untouched and no metadata key is added, removed or reshaped, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What is refused is a caller-written VALUE (an array, a boolean or an object on a number-typed field) at the write door; stored rows are never re-read by the check, and a caller that sends a number, a string or a blank is unaffected. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a write-door value check (not `registered` / `already-registered`); and the change is runtime behaviour, not a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->
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)!: retire `rowLevelSecurity[].tags` — no mainstream platform tags a row-level policy, and nothing here ever read one (#20321)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING** — shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below).
12+
13+
`tags` is removed from the row-level security policy (`RowLevelSecurityPolicySchema`,
14+
the entries of a permission set's `rowLevelSecurity`). ADR-0049
15+
enforce-or-remove, graded RETIRE by the maintainer's criterion for
16+
declared-but-unenforced families — does a mainstream platform have the
17+
capability? None does: Salesforce sharing rules, Dataverse security roles and
18+
PostgreSQL RLS policies carry no tag attribute, and compliance reporting there
19+
keys on the rule itself.
20+
21+
The key promised "categorization and reporting" for governance and compliance.
22+
Nothing ever read it. Measured before removal, each against a lit control: the
23+
RLS compiler reads a policy's `name`, `object`, `operation`, `positions`,
24+
`enabled` and predicates, never `tags`; objectui's permission preview renders
25+
the policy COUNT and its policy editor neither seeds nor reads the key; cloud
26+
has no reader. No example, default permission set or cloud source wrote it.
27+
28+
### FROM → TO
29+
30+
| removed | what to write instead |
31+
| --- | --- |
32+
| `rowLevelSecurity[].tags` | delete the key. To limit whom a policy applies to, list the positions in `positions` — a tag never did that. To say why a policy exists, use `description`. |
33+
34+
**The one-line fix: delete `tags:` from every row-level security policy.**
35+
`os migrate meta --from 17` lists the mechanical edits for existing sources;
36+
apply them by hand.
37+
38+
⚠️ Runtime behaviour is deliberately **unchanged**. No access decision ever
39+
depended on a tag, so removing the key removes no behaviour. What changes is the
40+
answer an author gets: a policy carrying `tags` is now refused at parse, with the
41+
prescription, instead of being stored with no effect. An author who wrote a tag
42+
such as `managers_only` believing it scoped the policy now learns that only
43+
`positions` does.
44+
45+
### The retirement kit
46+
47+
- **A `retiredKey()` tombstone** on `RowLevelSecurityPolicySchema` (the
48+
`priority` posture one key over): `tsc` types the key `never`, and every parse
49+
raises the prescription rather than a bare unknown-key verdict. The shape's
50+
did-you-mean never offers it: a near-miss `tag` is refused as unknown.
51+
- **D2 conversion `permission-rls-tags-removed`** (step 18, retired from the load
52+
path): a lossless delete over `permissions[].rowLevelSecurity[]`, so a stored
53+
permission row that still carries the key replays clean through the
54+
rehydration seam, while a live author is refused rather than rewritten.
55+
- **`RETIRED_KEYS_BY_MAJOR[18]`**: `security/RowLevelSecurityPolicy:tags`, and
56+
the family's D3 entry `permission-rls-tags-retired`, which states what the
57+
strip cannot decide — any report, audit filter or review process built on the
58+
belief that policy tags were read needs another path.
59+
- **The liveness row stays**, `dead`, under its tombstone (the key is still in
60+
the walked shape); `authorable-surface/security.json` carries it as
61+
`security/RowLevelSecurityPolicy:tags [RETIRED]`, and the generated reference
62+
pages print the prescription in place of the old describe.
63+
- **No deprecation window**, per the project's startup-stage posture.
64+
65+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
66+
is published, so this is breaking for consumers no telemetry was consulted for.
67+
68+
<!-- adr-0087: registered permission-rls-tags-removed, permission-rls-tags-retired -->

0 commit comments

Comments
 (0)