Skip to content

Commit 0945eb2

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20294-openapi-info-overlay
2 parents 69b4d20 + 0bbe400 commit 0945eb2

104 files changed

Lines changed: 5594 additions & 637 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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `datasource-*`, `filter-*`, `action-*`, `data-*` and `element-*` migration entries states each lesson in words instead of citing tracker numbers
6+
7+
Clause-②: no
8+
9+
The ADR-0087 semantic entries of the `datasource-*` family (the publish-time credential,
10+
placeholder and URL refusals, and the bound-secret pairs a mongo datasource cannot use),
11+
the `filter-*` family (the retired `$regex`, the `$between` endpoint refusals and the
12+
comparand shapes the save door now refuses), the `action-*` family (the retired
13+
descriptor key, the `resumeAuthority` default flip, the action-session rename, the bulk
14+
dispatch contract and the engine facade's query envelope), the `data-*` family (the
15+
retired driver and engine contract members, the retired field-changed event and two
16+
duration keys renamed with their unit) and the `element-*` family (the filter rule array
17+
at the page binding and the element and block doors) are printed by `os migrate meta` as
18+
the header, `why:` and `verify:` lines of a manual change. Their text sent the reader to
19+
issue-tracker, decision-batch and ruling-record numbers — some of which no longer
20+
resolve, and some in other repositories — for what a ruling, measurement or fix had
21+
decided; it now says what was decided, in the sentence being read. ADR ids are kept.
22+
23+
Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes,
24+
and the chain rewrites exactly what it rewrote before. The generated migration registry,
25+
`spec-changes.json` and the protocol upgrade guide carry the same text.

‎.changeset/20289-os-test-names-tags.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ A Quality Protocol suite's `name`, each scenario's `name` and `description`, and
1414
- **`--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.
1515
- **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.
1616
- **`@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.
17+
- **`@objectstack/spec`:** the liveness ledger (`liveness/qa.json`) moves the four keys above to `live`, citing their readers. `TestScenario.requires`, the family's fifth key, is checked in this same release and has its own note: an unmet `params` or `services` entry skips the scenario with its reason, and `requires.plugins` is retired into `requires.services`.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/core': minor
4+
'@objectstack/cli': minor
5+
---
6+
7+
feat(spec,core,cli)!: a scenario's `requires` is checked before it runs — unmet `params` or `services` SKIP it with a reason; `requires.plugins` is retired into `requires.services` (#20289)
8+
9+
Clause-②: yes (narrowing)
10+
11+
**BREAKING** — shipped as `minor` under the launch-window convention
12+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
13+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below,
14+
never by the level).
15+
16+
A Quality Protocol scenario's `requires` block declared preconditions —
17+
`params` (environment variables) and `plugins` (plugins that must be loaded) —
18+
that nothing checked: measured on a stub target, a scenario naming a missing
19+
plugin and an unset variable reported PASSED exactly like its no-requirements
20+
control. ADR-0049 enforce-or-remove, verdict ENFORCE (the mainstream has
21+
declared preconditions: JUnit `@EnabledIfEnvironmentVariable`, pytest `skipif`),
22+
ruled B for the shape: each key is judged against something `os test` can
23+
actually observe.
24+
25+
- **`requires.params`** — each variable must be set to a non-empty value in the
26+
environment of the process running `os test` (not the target server's, which a
27+
suite cannot see). An empty value counts as unset: an unconfigured CI secret
28+
arrives as an empty string.
29+
- **`requires.services`** (new) — each entry is a discovery service key
30+
(`CoreServiceName`: `auth`, `automation`, `analytics`, `ai`, `storage`, …; a
31+
misspelling is refused when the suite loads) that the target must declare
32+
`enabled` with status `available` in its discovery document (ADR-0076 D12).
33+
It is read from the discovery request the HTTP adapter already makes once per
34+
run; a suite that requires no service issues no extra request.
35+
- **SKIPPED.** A scenario with an unmet entry runs no step — `setup` included —
36+
and `os test` prints it with its reason, naming every unmet entry and, for a
37+
service, the services the target does declare available:
38+
`Skipped: requires.services 'ai' is not available on the target (enabled: false, status: unavailable). The target declares available: auth, data, metadata.`
39+
It is counted on its own — `SUCCESS: 3 scenarios passed. 1 skipped (not run, not counted as passed).` —
40+
and never as passed. Skips alone exit `0`; a run in which EVERY selected
41+
scenario was skipped prints `No scenario ran: …` instead of `SUCCESS`, exits
42+
`0`, and exits `1` under `--fail-on-empty`. With nothing skipped, the summary
43+
lines keep their spelling.
44+
- **`@objectstack/core`:** `QA.TestResult` gains `status` (`'passed' | 'failed' | 'skipped'`)
45+
and, on a skipped result, `skipped` (`reason`, `unmet[]`, `availableServices`);
46+
`passed` stays and is `false` on a skip. `TestRunner` takes an optional
47+
`{ env }` (default: this process's environment), and `TestExecutionAdapter`
48+
gains an optional `readTargetServices()` — `HttpTestAdapter` answers it from
49+
its one discovery probe. An adapter without it skips a service requirement
50+
rather than running it.
51+
52+
```
53+
FROM { "id": "ai-summary", "requires": { "plugins": ["@objectstack/service-ai"] }, "steps": [...] }
54+
-> ran anyway; the missing plugin surfaced as whatever failure it caused, or passed
55+
TO -> os test refuses the suite at load:
56+
✗ scenarios.0.requires.plugins: `scenarios[].requires.plugins` was removed in
57+
@objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever checked it: …
58+
Delete the key and name the service the scenario needs in `requires.services`, …
59+
Plugin → service: @objectstack/service-analytics → analytics, @objectstack/plugin-auth → auth, …
60+
61+
FROM { "requires": { "services": ["ai"] }, … } (new key)
62+
TO -> against a target whose discovery does not declare `ai` enabled and available:
63+
⏭️ Scenario: Summarise an account [ai-summary] (skipped)
64+
Skipped: requires.services 'ai' is not available on the target (…). The target declares available: …
65+
```
66+
67+
**Fix.** `requires.plugins: ["<package>"]` → `requires.services: ["<service>"]`,
68+
using the mapping the refusal prints (derived from `CORE_SERVICE_PROVIDER`, the
69+
provider table discovery itself reports): `@objectstack/plugin-auth` → `auth`,
70+
`@objectstack/service-analytics` → `analytics`, `@objectstack/service-automation`
71+
→ `automation`, `@objectstack/service-storage` → `storage`, and so on; the `ai`
72+
service is provided by ObjectStack Cloud/Enterprise. A plugin that fills no
73+
discovery service slot has no service to require — gate that scenario with a
74+
`params` variable or select it with `--tags`. `tsc` refuses `plugins` at a typed
75+
authoring site (its input type is `never`). A `TestResult` consumer that counted
76+
`!passed` as a failure should read `status` — a skipped result is `passed: false`
77+
and is not a failure.
78+
79+
**What does not change.** A scenario without `requires` runs exactly as before,
80+
and a suite that requires no service issues no discovery request it did not
81+
already issue.
82+
83+
### The retirement kit
84+
85+
- **Schema.** `TestScenarioSchema.requires` is a non-strict `z.object()`, so
86+
`plugins` is a `retiredKey()` tombstone carrying its prescription (a bare
87+
deletion would have stripped it in silence); `services` is new, closed over
88+
`CoreServiceName`.
89+
- **ADR-0087.** `RETIRED_KEYS_BY_MAJOR[18]` gains `qa/TestScenario:requires.plugins`.
90+
No D2 conversion: a QA suite is a loose JSON file `os test` loads, never a
91+
stack collection member or a stored row. The family's D3 entry,
92+
`qa-scenario-requires-plugins-retired`, carries the prescription to
93+
`os migrate meta` and the upgrade guide.
94+
- **Ledger and docs.** `liveness/qa.json` moves `qa.scenarios.requires` from
95+
`dead` to `live`, citing the runner's judgement and the adapter as producer;
96+
`state-counts.md` moves `qa` to 9 live / 0 dead. The `os test` section of the
97+
CLI reference documents the check, the skip line and the exit posture, and the
98+
generated `qa/testing` reference page is regenerated.
99+
100+
<!-- adr-0087: registered qa-scenario-requires-plugins-retired -->
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: a string compared against a number field must be a number: a non-numeric one is refused with `INVALID_FILTER` / 400 on `where`, a per-aggregation `filter` and `having`, and a numeric one is narrowed to its number (#20351)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a filter COMPARAND at the engine's query door: no authorable key, spelling or stored shape moves, `packages/spec` is untouched (the grammar, the verdict and the words shipped with the spec contract), and no stored row is read or rewritten. What is refused is a string that names no number, compared against a declared numeric field or a numeric aggregated column; which number the caller meant is not something a ledger entry can decide. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a filter comparand (not `registered` / `already-registered`); and the change is runtime behaviour, not a declaration (not `runtime-interface-only` / `type-surface-only`). -->
10+
11+
**BREAKING**: this narrows what a filter may compare a number field with. A string the platform's numeric grammar does not read as a number used to answer 200 with no rows (every row under `$ne`) on memory and SQLite and a 500 on PostgreSQL; it now answers 400, before any read, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. `@objectstack/objectql`'s root exports are unchanged.
12+
13+
FROM a string that is not a JSON number spelling of a finite number (`"abc"`, `""`, `" 12 "`, `"0x10"`, `"1,000"`, `"+5"`, `"007"`, `"Infinity"`, a `{placeholder}`), compared against a `number`, `currency`, `percent`, `rating`, `slider`, `progress` or `summary` field (or a `count` / `sum` / `avg`, or a numeric `min` / `max` / groupBy column in `having`) at the implicit comparand, `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte`, or a member of `$in` / `$nin` / `$between` → TO `INVALID_FILTER` / 400, naming the field, its declared type, the comparand, its position and what is wrong with it. The fix is one line: send the number (`12`, `-3.5`, `1e3`) or a string of exactly that spelling (`"12"`).
14+
15+
Measured through `engine.find` / `engine.aggregate` and `POST /data/:object/query` (the two doors agree), three rows (5, 12, 30):
16+
17+
| position | comparand on a `number` field | before: memory · SQLite · PostgreSQL 16 | now, on all three |
18+
|:--|:--|:--|:--|
19+
| `where` | `$gt` / `$eq` / implicit / a `$in` member `"abc"`; `$eq ""` | no rows · no rows · `DATABASE_ERROR` (500) | `INVALID_FILTER` / 400 |
20+
| `where` | `$ne "abc"` | every row · every row · 500 | `INVALID_FILTER` / 400 |
21+
| `where`, over REST | `$gt "{current_user_id}"` (resolved to the user's id) | no rows · no rows · 500 | `INVALID_FILTER` / 400 |
22+
| per-aggregation `filter` | `$gt "abc"` (`$ne "abc"`) | count 0 (3), on all three | `INVALID_FILTER` / 400 |
23+
| `having` on `sum(amount)` | `$gt "abc"` (`$ne "abc"`) | no group (every group), on all three | `INVALID_FILTER` / 400 |
24+
| `where` | `$gt "12"` / `$eq "12"` | **no rows** · 1 row · 1 row | 1 row, the number's answer |
25+
26+
What changes:
27+
28+
- A new door at the engine's single filter collection point, after the temporal-comparand door. It reads `@objectstack/spec/data`'s published contract (`numberComparandDoorVerdict` over `NUMERIC_VALUE_TYPES`, the numeric grammar, and `numberComparandRefusalMessage` for the words); the engine carries no numeric grammar of its own.
29+
- It runs on `where` in both spellings (the filter object and the `FilterArray` sugar) on `find`, `findOne`, `count`, `aggregate`, `update` and `delete`, and on `IObjectQLEngine.judgeFilter`; on each per-aggregation `filter`, against the object's declared fields; and on `having`, over the columns the engine classes numeric.
30+
- A numeric string is rewritten to its number, copy-on-write, before any driver or in-memory evaluator reads it. InMemoryDriver used to compare `"12"` as a string and match nothing; it now matches what `12` matches, as SQLite and PostgreSQL already did.
31+
- A `{placeholder}` compared against a number field is refused unresolved: every filter token resolves to an id or a date, never a number.
32+
33+
**Who is affected.** A caller that compares a number field with a string that is not a plain number, through any door that reaches the engine: a REST query parameter (`?amount=abc`), a `where` / `$filter` / `filter` body of `POST /data/:object/query`, or an in-process engine call. A caller that sends a number, or a numeric string such as `"12"` or `"1e3"`, is unaffected, except that memory now answers it as the other drivers do.
34+
35+
**Unchanged.** A numeric comparand: the `$gt 10` controls at `where`, the per-aggregation `filter` and `having` answered identically before and after on memory, SQLite and PostgreSQL, through the engine and REST. Not judged by this door, so the filter reaches the driver as written (pinned per case in the engine suite): a string compared against a non-numeric field; `$null` / `$exists` / `$empty`; the text operators (a text operator over a number field keeps its own refusal); a `{ $field }` reference; a dotted key; a key that names no declared field. A `formula` field is still refused one door earlier with `INVALID_FIELD`. RLS, sharing and tenant predicates the security layer composes onto a query are not judged by this door; a policy predicate is judged at authoring where the host hands the rule the engine's `judgeFilter`. A boolean or a `Date` compared against a number field is outside this contract (it judges strings) and keeps its old answer, measured: `$gt true` no rows on memory, every row on SQLite and a 500 on PostgreSQL; a `Date` no rows on memory and SQLite and a 500 on PostgreSQL.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`hook.form.ts`'s `condition` row now declares `language: 'expression'`, matching the CEL predicate `HookSchema.condition` actually is.
6+
7+
Clause-②: no
8+
9+
The row previously declared `language: 'javascript'` — the same declared language as a real script row (`body.source`) — over a field that is `EvaluatedExpressionInputSchema`, a CEL predicate. A consumer keyed on the row's declared language could not tell the predicate apart from a script. The `helpText` moves from "Optional formula — skip the hook when this evaluates to false" to "CEL predicate — the hook runs only when TRUE", matching the phrasing every sibling predicate row (`field.form.ts` / `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the formula `expression` row) already uses.
10+
11+
No key is added, removed, narrowed or widened, and no parse verdict changes — `type: 'code'` and `language` are already-declared form-DSL vocabulary. `metadata-form-declared-rows.pin.test.ts` pins the new value, with a control against a sibling predicate row.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/service-analytics': minor
3+
---
4+
5+
fix(service-analytics): both analytics filter faces answer `$empty` by the field's declared type (#20445)
6+
7+
Clause-②: yes (widening)
8+
9+
`$empty: true | false` is declared by `@objectstack/spec` (`FieldOperatorsSchema`) with a per-type meaning: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. `$empty: false` is the exact complement. Both of this package's filter faces now answer it by that table, through the spec's one expansion (`expandEmptyOperator`), instead of refusing it:
10+
11+
- **The analytics `where`** (`/analytics/query`, `/analytics/sql`, dataset filters): `NativeSQLStrategy` and the `ObjectQLStrategy` SQL echo compile the field's declared row; the ObjectQL execute path hands `{ $empty }` to the data engine, which answers it once the engine's own arm lands (until then the engine refuses it, `INVALID_FILTER` / 400, as it does today).
12+
- **Row-level read scopes** compiled to SQL (`compileScopedFilterToSql`): same rows, in the read-scope envelope.
13+
14+
A multi-value field's empty list is tested with a JSON function per SQL dialect (`json_array_length` on SQLite, a `jsonb` comparison on Postgres, `JSON_LENGTH` on MySQL).
15+
16+
**Refused, never guessed** — `INVALID_FILTER` / 400 on the `where` face, `READ_SCOPE_COMPILE_FAILED` / 500 on a read scope — when the host cannot name the field's declared type (no `sourceFieldMeta` wired, or no such field), when a multi-value field's datasource dialect is unknown, and when the flag is not a boolean (`$empty: 'true'` is refused like a non-boolean `$null`).
17+
18+
Host API (two new optional members, hence `minor`): `AnalyticsServiceConfig.sourceFieldMeta` may now answer `multiple` beside `type`, and `AnalyticsServicePlugin` relays it from the field definition; `compileScopedFilterToSql` takes an optional `declaredValueShape` option. A host whose `sourceFieldMeta` answers `type` but not `multiple` has every multi-capable field it declared `multiple: true` (select / radio / lookup / user / file / image) read as single-valued, which is the null-only row. On such a field a read scope's `$empty: false` then admits rows holding `[]`, and `$empty: true` misses them. Relay the field's `multiple` from its definition to get the list row.
19+
20+
`$empty` stays staged: it is not in `FILTER_OPERATORS`, and the view operators `is_empty` / `is_not_empty` still lower to `$null`.

0 commit comments

Comments
 (0)