Skip to content

Commit 85434e5

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20361-shard-liveness-counts
# Conflicts: # packages/spec/liveness/state-counts.md
2 parents 1be2134 + fb194c7 commit 85434e5

81 files changed

Lines changed: 3503 additions & 597 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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: a view filter rule's `operator` is typed as the canonical `ViewFilterOperator`, not `unknown`
6+
7+
**BREAKING for TypeScript code that writes a view filter rule through a published type**: `ViewFilterRule`, and every carrier of it — `ListView.filter`, a view tab's `filter`, `InterfacePageConfig.filterBy`, and the related-list, record-picker and `object-*` block filter doors. A narrowing of a published TYPE, landing as `minor` (the bump level is not the carrier; this banner and the disposition below are). The runtime accept set does not move at all: no schema's parse, no value and no export changes.
8+
9+
`operator` is a `z.preprocess` over the alias fold, and zod types a preprocess's input from its function's parameter. That parameter was `unknown`, so `ViewFilterRule['operator']` was `unknown`: `{ field: 'status', operator: 42 }` compiled as a rule on every carrier, and was refused only when the schema parsed it. The input type is now `ViewFilterOperator`, the vocabulary the alias table's own contract says new producers emit, so an alias spelling or a non-string is refused by the compiler.
10+
11+
What does not change:
12+
13+
- **The runtime.** `ViewFilterRuleSchema` still folds every legacy spelling it folded before (`eq`, `gt`, `notIn`, `isNull`, …) to its canonical id, and still refuses a non-string at `operator` with the enum's own issue. Stored `sys_metadata` rows, YAML and JSON bodies and plain-JS producers that carry an alias parse exactly as before, and `os validate` answers as before.
14+
- **`normalizeFilterOperator`.** Its parameter stays `unknown`: it exists to fold untyped stored metadata, and its callers pass raw strings by design.
15+
- **The parsed type.** `ViewFilterRuleParsed['operator']` was already the canonical enum.
16+
17+
## FROM → TO
18+
19+
| Wrote (TypeScript) | Write instead |
20+
| --- | --- |
21+
| `{ field: 'status', operator: 'eq', value: 'open' }` | `{ field: 'status', operator: 'equals', value: 'open' }` |
22+
| `{ field: 'amount', operator: 'gte', value: 100 }` | `{ field: 'amount', operator: 'greater_than_or_equal', value: 100 }` |
23+
| `{ field: 'stage', operator: 'notIn', value: ['lost'] }` | `{ field: 'stage', operator: 'not_in', value: ['lost'] }` |
24+
| `operator: someString` (a value typed `string`) | type the unvalidated rule `unknown` and `ViewFilterRuleSchema.safeParse` it, or fold it with `normalizeFilterOperator` and check it against `VIEW_FILTER_OPERATORS` first |
25+
26+
The one-line fix: write the canonical id. Every alias maps to exactly one, and `VIEW_FILTER_OPERATOR_ALIASES` is that map; the rewritten rule selects the same rows, because the schema already folded the alias to that id.
27+
28+
Clause-②: no (narrowing)
29+
30+
<!-- adr-0087: registered view-filter-rule-operator-input-canonical -->
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/rest': minor
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(rest, runtime): the runtime dispatcher serves the layered view, `GET /meta/:type/:name/layers` and the deprecated `?layers=` flag, as `RestServer` serves it (#20478)
7+
8+
Clause-②: yes (widening) — `@objectstack/rest`'s root entry gains three value exports (`createMetaLayeredAnswer`, `wantsMetaItemLayers`, `metaItemLayersDeprecationHeaders`) and two type exports (`MetaLayeredAnswer`, `MetaLayeredRequest`). Nothing any published version exported is removed, renamed or narrowed. `@objectstack/runtime` publishes no new surface and stays a `patch`.
9+
10+
A host that mounts only the `${prefix}/*` catch-all (`createHonoApp`, and any
11+
adapter written on the public `HttpDispatcher` API) serves `/meta` through the
12+
runtime dispatcher. Until now, on such a host:
13+
14+
- **`GET /meta/:type/:name?layers=true` answered the plain read.** The body was
15+
`{ type, name, item }` with a `200`, so a client reading `code`, `overlay` or
16+
`effective` read `undefined`. There was no `Deprecation` header and no `Link`
17+
to the successor. An author (a caller the item's save door admits) was served
18+
the app pruned, where the layered view serves them every layer whole.
19+
- **`GET /meta/:type/:name/layers` was no route.** It answered a located
20+
`404 ROUTE_NOT_FOUND`.
21+
22+
Both spellings now answer what `RestServer` answers: the three layers, each
23+
judged by the per-caller read gate under the stored-version doors' policy
24+
(whole for a caller who may save the item, pruned as the plain read prunes it
25+
for everyone else), each projected through the object-schema field mask, and
26+
`private, no-store` when the caller's field visibility could not be determined.
27+
The read is scoped to the caller's vetted organization and to `?package=`. The
28+
flag's answers, refusals included, carry `Deprecation: true`, and a `Link` to
29+
`/layers` built from the request's own URL (every `createHonoApp` request
30+
carries one; a host that hands `dispatch()` no URL gets `Deprecation` alone). The route answers `501 NOT_IMPLEMENTED` where the protocol has no
31+
layered read, and the flag is then the plain read, on both transports.
32+
33+
**What changed.** Everything `RestServer`'s layered helper does after the store
34+
read moved, unchanged, into `createMetaLayeredAnswer`, and the flag's parse and
35+
headers into `wantsMetaItemLayers` and `metaItemLayersDeprecationHeaders`. The
36+
dispatcher's `/meta` domain calls all three. `RestServer`'s own answers are
37+
unchanged: every existing REST test passes unedited.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@objectstack/rest': minor
3+
---
4+
5+
fix(rest): `POST /api/v1/data/:object/import` reads a comma in a number cell only as a thousands group, and refuses every other comma instead of storing a different number (#20497)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for callers of the import door.** A cell for a numeric field
10+
(`number`, `currency`, `percent` and the other numeric value types) that
11+
carries a comma is now admitted only when the comma groups thousands: 1 to 3
12+
leading digits, then groups of exactly three digits, and only before any `.`
13+
(`1,000`, `12,345.67`, `-1,234,567.89`). Any other
14+
comma makes the cell that row's `invalid_number` error, the same code the
15+
plain write doors (create, update, batch) already answer for the cell. The
16+
reader used to strip every comma before parsing, so each of these was stored
17+
as a DIFFERENT number while the import reported `ok 1, errors 0`. For each
18+
shape, change the cell FROM the refused spelling TO the one that says what you
19+
mean:
20+
21+
- **A decimal comma.** FROM `3,14`, `1,5`, `0,5`, `1.000,5` (stored as `314`,
22+
`15`, `5`, `1.0005`) TO a `.` decimal point with no grouping, or with comma
23+
grouping: `3.14`, `1.5`, `0.5`, `1000.5` or `1,000.5`. A file exported with a
24+
decimal-comma locale has to be converted before import. No locale is
25+
guessed: `1,500` is always one thousand five hundred.
26+
- **A comma that does not group thousands.** FROM `1,2,3`, `1,23`, `1,0000`,
27+
`1234,567`, `,123`, `1,000,` or a comma after the `.` (`12,345.6,7`), each
28+
stored with its commas removed, TO the number with no separators, or with
29+
well-formed thousands grouping.
30+
- **Grouping by twos (`12,34,567`, `1,00,000`).** FROM that grouping TO
31+
`1234567` / `100000`, or `1,234,567` / `100,000`. These used to import as
32+
the number they denote. They are refused now because the reader cannot tell
33+
a two-digit group from a decimal comma, and it no longer guesses.
34+
35+
**What is not affected.** Every cell without a comma reads exactly as before.
36+
That is measured over the platform numeric grammar's 41 case rows: the only
37+
row whose import reading changed is `1.000,5`. A well-formed thousands grouping
38+
reads as before, with or without a leading currency symbol, a trailing `%`, a
39+
sign or accounting parentheses (`$1,000`, `1,234%`, `(1,234)`). A JSON number,
40+
and an xlsx cell Excel stores as a number, never pass through this reading.
41+
The dry run answers the same verdicts as the real write. A refused cell fails
42+
only its own row, and the rest of the batch imports as before.
43+
44+
**If you are refused.** The row's result carries `code: 'invalid_number'` and
45+
quotes the cell, so the file can be corrected and re-imported.
46+
47+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row. The import door's cell reader accepts fewer spellings of a number inside an imported file, which is data, not metadata, so `objectstack migrate meta` has nothing to reach. The row's invalid_number error quotes the refused cell, and the repair is to write the number with a `.` decimal point. -->

0 commit comments

Comments
 (0)