Skip to content

Commit d0003a1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20301-list-tabs-retired
# Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
2 parents a3f8054 + 70ce802 commit d0003a1

119 files changed

Lines changed: 6715 additions & 944 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/core': minor
4+
'@objectstack/spec': patch
5+
---
6+
7+
`os test` reports the suite and scenario names an author writes, and selects scenarios with `--tags` (#20289)
8+
9+
Clause-②: no
10+
11+
A Quality Protocol suite's `name`, each scenario's `name` and `description`, and scenario `tags` were parsed at load and then read by nothing: the report headed each suite with its file's basename, printed every scenario by its `id`, and `os test --tags critical` failed with `Nonexistent flag: --tags`.
12+
13+
- **Names in the report.** The suite heading is now the suite's `name` followed by its file — `📄 Running suite: Accounts smoke (accounts.test.json)` — and each scenario line is its `name` with the `id` in brackets — `✅ Scenario: An account can be created [acct-create] (12ms)` (the id alone when the two are equal). A failed scenario's `description` is printed under its line, before the error. A suite whose file fails to load is still headed by the file alone, since no name was parsed.
14+
- **`--tags TAG[,TAG...]`** runs only the scenarios carrying AT LEAST ONE of the listed tags (any-of, exact, case-sensitive) — the comma-list reading of Odoo's `--test-tags` and the everyday use of Playwright's `--grep @a|@b`. With the flag, an untagged scenario is left out. Left-out scenarios are **deselected**: not run, counted on the summary (`--tags smoke selected 1 of 4 scenarios; 3 deselected (not run, not counted as passed).`), never counted as passed. A requested tag that no loaded scenario carries is named on the summary. An empty entry (`--tags smoke,`) is refused before anything runs. Without the flag nothing changes: every scenario runs.
15+
- **Exit status.** A selection that matches no scenario takes the posture an empty pattern already has: exit `0` with `No scenario matched --tags …`, and exit `1` under `--fail-on-empty`, whose description now covers both cases. The `Found N test suites.` line and the `SUCCESS: All N scenarios passed.` / `FAILED: …` summary lines keep their spelling.
16+
- **`@objectstack/core`:** `QA.TestResult` gains `scenarioName` and `description` on every result, and `suiteName` on every result `runSuite` produces (absent only from a lone `runScenario` call, which has no suite).
17+
- **`@objectstack/spec`:** `TestScenario.requires` (`params`, `plugins`) is still checked by nothing — its describe() now says **NOT CHECKED** instead of reading as a guard, so a scenario that declares a plugin the target lacks still runs, and the unmet requirement surfaces only as whatever failure it causes, if any. The liveness ledger (`liveness/qa.json`) moves the four keys above to `live`, citing their readers.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/formula': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
Expression refusals now carry a stable `code` and typed `params` beside their English `message`, so a localized author surface can render its own words: `validateExpression` (every entry in `errors[]` and `warnings[]`), `collectCelRootIdentifiers` (its `ok: false` arm), `predicateSlotRefusal` and `structuralConditionRefusal` (#20291)
7+
8+
Clause-②: yes
9+
10+
Until now the only thing a refusal said was an English sentence, and a designer running in another locale could only show it verbatim, beside its own translated headings. Each refusal now also names its code — one of a closed, kebab-case set — and the values its sentence interpolates, so a consumer keys a catalogue row to the code and fills it from the params. The `message` is the same sentence, byte for byte; nothing is removed or renamed, so no existing reader changes.
11+
12+
- `@objectstack/formula` exports `EXPRESSION_REFUSAL_CODES` (the closed set as a frozen list) and the types `ExpressionRefusalCode`, `ExpressionRefusalParams` (code → params), `ExpressionRefusal`, `ExprValidationCode`, `CelRootsRefusalCode`, `CelFieldRole`, `ExpressionSourceKind` and `CelRootIdentifiersResult`. `ExprValidationError` gains `code` and `params`.
13+
- `@objectstack/spec/automation` exports `FLOW_SLOT_REFUSAL_CODES` (the closed set of both flow-slot refusal producers, as a frozen list) and the types `FlowSlotRefusalCode`, `FlowSlotRefusalParams` (code → params), `PredicateSlotRefusalCode`, `PredicateSlotRefusal`, `PredicateSlotValueKind`, `StructuralConditionRefusalCode`, `StructuralConditionRefusal` and `StructuralConditionValueKind`. `predicateSlotRefusal` returns `PredicateSlotRefusal` and `structuralConditionRefusal` returns `StructuralConditionRefusal`; each is its previous `{ message, source }` plus `code` and `params`.
14+
- Narrowing on `code` narrows `params`. A code never changes once published: a reworded message keeps its code, and a new refusal gets a new one.
15+
- A `detail` param is the CEL or template engine's own diagnostic, in English, passed through as the message carries it.
16+
- These are authoring diagnostics returned as values, not ADR-0112 request error codes, which is why they are kebab-case.
17+
- The `validate_expression` MCP tool forwards `validateExpression`'s `errors` and `warnings` as they are, so each entry in its answer now also carries `code` and `params`.

0 commit comments

Comments
 (0)