Skip to content

Commit cc2fc46

Browse files
committed
Merge remote-tracking branch 'origin/main' into wt-19658-merge2
2 parents 9abe79f + 8cbc3c0 commit cc2fc46

49 files changed

Lines changed: 2531 additions & 256 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`ListViewSchema` declares the `columns` x `hiddenFields` x `fieldOrder` composition instead of leaving it to be inferred from a renderer (#15184)
6+
7+
The three keys that together build a list view's field list now say how they compose, in their own `.describe()` text — the string that ships into `json-schema/` and into the generated `content/docs/references/ui/view.mdx`:
8+
9+
- `columns` is the **projection**: the candidate set and the baseline order, and neither other key can add a field it omits. An **empty** `columns` declares no projection, so neither other key applies: which columns show is left to the renderer (objectui's `ListView` grid derives the object's default columns).
10+
- `hiddenFields` **subtracts** from that projection, before any ordering runs. A name `columns` never projected subtracts nothing.
11+
- `fieldOrder` **orders what survives** and never adds a field. A surviving column absent from `fieldOrder` sorts **last**, after every listed one, keeping its `columns`-relative order; a name listed there that did not survive orders nothing.
12+
13+
**Why this is a declaration and not a precedence rule.** `fieldOrder` was proposed for retirement as a second spelling of `columns` with no contract deciding who wins. They never compete: one selects, the other sorts. Maintainer decision batch #115 (2026-09-11) kept the key and ruled the composition into the contract, which is what this change lands.
14+
15+
⛔ **No accept set moves.** No key is added, removed, narrowed or widened; no parse verdict changes; the four `@objectstack/lint` list-view validators are untouched. What changes is the published description of three keys that were already there, plus one ledger row's evidence.
16+
17+
**`packages/spec/liveness/view.json` — the `/props/list/children/fieldOrder` row is re-cited**, `verifiedAt: 2026-09-21`. The ledger ships in this package's `files[]`, so the pointers an upgrading reader follows are these, and both halves of the 2026-08-10 citation had rotted: its first path (`objectui packages/react/src/spec-bridge/bridges/list-view.ts`) no longer exists, and its second had drifted in range through three sets of line numbers. The row now anchors both pointers on symbols, splits the relay rung out into `producer`, and names the measurement it was taken at.
18+
19+
The declaration and the accept set are held together by `packages/spec/src/ui/view-field-order-composition.pin.test.ts`: it reads the three descriptions off the live schema and parses a document carrying all three keys through the page-list, object-views, `defineView` and registered-metadata doors, asserting the arrays come back verbatim — the spec declares the composition, it does not perform it.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): a failed `find` reports at `warn`, not `error` — the caller was already told (#17212)
6+
7+
`find` ends its `catch` with `throw e`, and one frame down `reportFindFailure`
8+
logged every failure it did not classify as a missing table at ERROR. AGENTS.md
9+
→ *Degradation log levels* names that exact shape and forbids it: "a failure
10+
handed to the CALLER is not a degradation at all … Do not bolt a `logger.error`
11+
onto such a site." It is the read-door twin of the write doors' move to `warn`
12+
(#17052).
13+
14+
**Nothing else about the entry moved.** Same message (`Find operation failed`),
15+
same `object` meta, and the message and stack still travel with it: the `Logger`
16+
contract gives an `Error` slot to `error`/`fatal` only, so the engine builds the
17+
`{ error: { message, stack } }` meta that slot used to build — handing the Error
18+
to `warn` as meta would have serialised `{}`, because those two fields are
19+
non-enumerable. The throw is unchanged, and so is the missing-table branch,
20+
which stays at `debug` without a stack. On the SQL read path the fault is also
21+
reported one frame down on the driver's own `warn` line, as before.
22+
23+
If you grep your logs for this message, keep the message and drop the level
24+
from the pattern. If you alert on error-level lines from `@objectstack/objectql`,
25+
a failed read no longer raises one — the read's exception still does.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/connector-rest': patch
4+
'@objectstack/connector-openapi': patch
5+
'@objectstack/connector-mcp': patch
6+
'@objectstack/connector-slack': patch
7+
'@objectstack/service-automation': patch
8+
---
9+
10+
feat(spec)!: retire `connector.connectionTimeoutMs` — declared, bounded, defaulted, served back, and never applied as a deadline
11+
12+
**BREAKING** — `connector.connectionTimeoutMs` is removed. ADR-0049
13+
enforce-or-remove; maintainer ruling 2026-09-22, letter A. It is the narrower
14+
**second** decision this key was owed: the earlier ruling that made its nine
15+
liveness siblings live (`retryConfig.*`, `requestTimeoutMs`) left this one dead
16+
on a stated reason rather than by oversight, and `packages/spec/liveness/connector.json`
17+
has been asking for this decision since.
18+
19+
The key was bounded (`min(1000).max(300000)`), defaulted (`30000`),
20+
`.describe()`d, authorable on both carriers and served back by
21+
`/meta/connector`. Every signal an authoring surface can give said it worked.
22+
23+
### FROM → TO
24+
25+
| removed | what to write instead |
26+
| --- | --- |
27+
| `connector.connectionTimeoutMs` (on `Connector` and on `DeclarativeConnectorEntry`, so `stack.connectors[]` and `PUT /meta/connector/:name`) | `requestTimeoutMs` — the deadline the platform keeps, applied as `resilientFetch`'s per-attempt timeout. For a connect-only bound, configure it at a connector provider or upstream gateway on a transport that can separate the phases. |
28+
| `ConnectorProviderContext.connectionTimeoutMs` (handed to every `ConnectorProviderFactory`) | `ctx.requestTimeoutMs`, or the factory's own `providerConfig` where the provider owns the vocabulary. |
29+
| The `ZodObject` combinators on `ConnectorSchema` and `DeclarativeConnectorEntrySchema` — `.extend()`, `.omit()`, `.pick()`, `.partial()`, `.merge()`, `.strict()`, `.keyof()`, `.safeExtend()` | Both exports are now `z.preprocess` **pipes** (the residue stage below), so those methods no longer exist on them. **Build on the object and re-wrap:** `acceptRetiredDefaultResidue(<your extended object>, { connectionTimeoutMs: 30000 })`, the `EffectiveObjectPermissionSchema` route. ⚠️ `.superRefine()` still *exists* on a pipe but returns a schema with no read-through `shape`, so refine before wrapping, not after. Parsing, `z.input` / `z.infer`, and the read-through `.shape` are unchanged. |
30+
31+
**The one-line fix: delete the key** — and, for a custom provider factory, stop
32+
reading `ctx.connectionTimeoutMs`. `os migrate meta --from 17` lists the
33+
mechanical edits for existing sources; apply them by hand.
34+
35+
⚠️ Runtime behaviour is **unchanged for every shipped provider**, because none
36+
ever applied the value: a connector that authored `connectionTimeoutMs: 1000`
37+
made exactly the same calls, with exactly the same deadlines, as one that did
38+
not. What does change is observable and intended: the def served by
39+
`GET /connectors` no longer echoes a connect deadline nobody keeps.
40+
41+
### ⭐ This is NOT the zero-mention retirement shape
42+
43+
Measured with `git grep -n connectionTimeoutMs SHA -- . ':!packages/spec'` at
44+
`origin/main`: **thirteen** non-test source occurrences over seven files in five
45+
packages — **six reads** (`openapi-connector.ts:242`, `openapi-provider.ts:193`,
46+
`rest-connector.ts:134`, `rest-provider.ts:64`, `plugin.ts:307`,
47+
`plugin.ts:1589`), **four type declarations**, and **three** surviving hardcoded
48+
`30000` writes. Reading the retirement as "nothing referenced it" loses the
49+
finding. Measured across all six reads, every one is a **pass-through**: the
50+
value's only termini were the def `GET /connectors` echoes and the fingerprint
51+
that decides whether to re-materialize. `connectorFetchOptions()` — the one
52+
mapping from authored policy onto the platform's outbound `fetch` — was handed
53+
`{ retryConfig, requestTimeoutMs }` only. Carrying a number is not honouring it,
54+
and ADR-0049 forbids the parsed-unmarked-unenforced state whether the inert
55+
value travels or sits still.
56+
57+
Nor was the `实现` arm available. A connector's outbound call is a WHATWG
58+
`fetch`, whose only cancellation surface is ONE `AbortSignal` covering the whole
59+
operation; nothing in that interface observes the connection phase. Bounding
60+
"time until the response arrives" with this key would kill a slow-but-connected
61+
upstream the author meant to allow with a large `requestTimeoutMs` — breaking
62+
the very promise the key makes. (undici's `connectTimeout` needs a custom
63+
dispatcher: Node-only, and a new subsystem underneath every connector, which the
64+
ruling that made the siblings live forbids.)
65+
66+
### The retirement kit
67+
68+
- The **authorable key** is a `retiredKey()` tombstone on `ConnectorSchema`,
69+
registered as `integration/Connector:connectionTimeoutMs` and
70+
`integration/DeclarativeConnectorEntry:connectionTimeoutMs` in
71+
`RETIRED_KEYS_BY_MAJOR[18]`. The schema is not `.strict()`, so a bare deletion
72+
would strip an authored key in silence (ADR-0104): the tombstone is audible in
73+
both channels — `tsc` (input type `never`) and the parse, which raises the
74+
prescription itself. `DeclarativeConnectorEntrySchema` carries it too — both
75+
published carriers wrap the same private `ConnectorBaseSchema` — so
76+
`stack.connectors[]` and the `/meta/connector` door refuse it too.
77+
- **A D2 conversion, `connector-connection-timeout-ms-removed`** — one strip per
78+
`connectors[]` entry, a pure lossless delete. ⭐ The ruling left whether one was
79+
owed to be **measured** ("a D2 conversion only if a stored connector row can
80+
carry the key"). It can, and both legs were measured before the tombstone
81+
landed: `getMetadataTypeSchema('connector')` — what `PUT /meta/connector/:name`
82+
validates against — parsed a body carrying the key and its output **retained**
83+
the authored value, so the number reached `sys_metadata`; and
84+
`applyConversionsToStoredItem('connector', …)` is live for this type. Rows
85+
written on 17.x therefore replay clean.
86+
- **A D3 semantic entry,
87+
`connector-provider-context-connection-timeout-ms-retired`**, for the withdrawn
88+
`ConnectorProviderContext` member. A provider factory is code: there is no
89+
authored source and no `sys_metadata` row for a conversion to rewrite, so the
90+
removal reaches a factory author as a `tsc` error and as that entry.
91+
- **No def leaves.** The key was a bare `z.number()`, never a `ConfigSchema`
92+
shape, so `RETIRED_DEFS_BY_MAJOR[18]` gains nothing — and `api-surface/` and
93+
`json-schema.manifest/` are byte-identical, which is the correct reading for a
94+
key-only tombstone rather than a missed regeneration.
95+
- `authorable-surface/integration.json` gains two `[RETIRED]` rows;
96+
`authorable-defaults/integration.json` loses the two `= 30000` rows.
97+
- The liveness row **stays** `dead` with a `REMOVED` note, because `retiredKey()`
98+
keeps the key in the walked shape. Its previous note claimed "every occurrence
99+
outside `packages/spec` is a WRITE". That reading was **correct at the SHA the
100+
card cited and dated** (`0870fb5418` — exactly five non-spec source hits, all
101+
five `connectionTimeoutMs: 30000,`) and was superseded by `b929e0a662`, the PR
102+
the card itself flagged as pending. It is **stale, not false**, and the row now
103+
carries both readings with their trees rather than one undated claim.
104+
- **An `acceptRetiredDefaultResidue` stage** (#12840), `{ connectionTimeoutMs: 30000 }`
105+
on both carriers. The key was `.optional().default(30000)`, so a 17.x parse
106+
materialized it into **every** connector — measured across two builds: the base
107+
build emits it for an entry that authored only `name`/`label`/`type`, and the
108+
tombstoned build refuses that exact object at `connectors.0.connectionTimeoutMs`.
109+
The D2 does **not** discharge the obligation, and the precedent shows it:
110+
`ObjectPermission:allowPurge` carries a D2 **and** the residue stage, for its
111+
own reason (a released toolchain materialized its default into every built
112+
artifact's entries). The reason *here* is a different one — this schema has a
113+
second door: `AutomationEngine.registerConnector` parses `ConnectorSchema` for
114+
a def a plugin or provider factory builds **in code**, where no conversion
115+
ever runs, and all four shipped connector packages put the materialized value
116+
straight into that def literal. So the emitted `30000`
117+
is accepted-and-stripped while `15000` keeps the tombstone's refusal, and
118+
nothing is un-retired: `z.input` stays `never` and the `[RETIRED]` row stays.
119+
- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」),
120+
and no staged retirement.
121+
122+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
123+
is published, so this is breaking for consumers no download, dependent or source
124+
telemetry was consulted for. The pinned sibling checkout **was** measured: zero
125+
occurrences of the name at objectui `87af769e`, against a lit control on the same
126+
command and scope, so no sibling fix or pin bump rides with this.
127+
128+
`Clause-②: yes (narrowing)` — a published authorable key is removed on two
129+
carriers and a published interface member leaves `ConnectorProviderContext`, so
130+
the accept set a consumer writes against narrows. Nothing is widened and nothing
131+
is renamed. Contract-review tier.
132+
133+
<!-- adr-0087: registered connector-connection-timeout-ms-removed, connector-provider-context-connection-timeout-ms-retired -->
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
---
4+
5+
docs(metadata-core): the `item-key-discriminators` module docblock quoted a spec sentence that no longer exists and that the contract denies ("best match") (#19592)
6+
7+
Clause-②: no — no accept set moves, no published payload key changes, no
8+
export is added or removed. The corrected prose ships as TSDoc in
9+
`@objectstack/metadata-core`'s `dist/index.d.ts` and `dist/index.d.cts` (the
10+
package publishes `dist`), which is why this is a changeset rather than
11+
`skip-changeset`.
12+
13+
The module docblock of `packages/metadata-core/src/item-key-discriminators.ts`
14+
put a sentence inside quotation marks and attributed it to
15+
`EmailTemplateDefinitionSchema` in `packages/spec/src/system/email-template.zod.ts`:
16+
that the service "picks the best match for the recipient's locale". That
17+
sentence occurs nowhere in `packages/spec/src` today, and it states the opposite
18+
of the contract: `SendTemplateInput.template` in
19+
`packages/spec/src/contracts/email-service.ts` says there is no "best match" and
20+
no language-subtag folding.
21+
22+
The docblock now cites the spec by file and symbol instead of quoting it. It
23+
says the `locale` key is the second half of the bundle key, that resolution is
24+
exact, and that `SendTemplateInput.locale` holds the ladder: the named tag matched
25+
exactly, then the literal `en-US`, then, only for a call that named no locale and
26+
only when the bundle has no `en-US` row, the bundle's lowest locale tag. The one
27+
quotation left in the docblock ("is resolved by `(name, locale)`", from the
28+
schema's header) still exists verbatim in the spec.
29+
30+
No behaviour changes: the edit is prose. `ITEM_KEY_DISCRIMINATORS`,
31+
`readDiscriminatorValue`, `itemDiscriminator` and the `en-US` canonical are
32+
untouched.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
`README.md` — the `TypeScriptSerializer` line now says what the serializer emits and what it is for, instead of claiming it exists "for `ObjectSchema.create()`, `defineView()`, etc." (#19724).
6+
7+
The serializer has never written a factory call. It writes a JSON document wrapped in a module — `export const metadata = { …JSON… };` then `export default metadata;`, with the `typescript` format adding an `import type { ServiceObject }` and annotating the constant with it — and it reads back only a JSON body (double-quoted keys and strings, no comments, no trailing commas), so an authored `ObjectSchema.create({ … })` file with ordinary unquoted keys is refused with `Failed to parse object literal as JSON`. It is the file format `FilesystemLoader` uses for the `typescript` / `javascript` formats: `MetadataManager.save('object', 'account', data)` routed to the filesystem loader writes `{rootDir}/object/account.ts`, never a `*.object.ts`.
8+
9+
- **No behaviour moves.** The emitter, the parser and every published export are byte-identical; only the README text shipped in this package's `files[]` changes.
10+
- ⚠️ **Not an authoring shape.** Authored metadata — a `*.object.ts` written `ObjectSchema.create({ … })`, a view written `defineView({ … })` — is not produced by, and in its usual TypeScript spelling not readable by, this serializer; do not point it at authored source files.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/driver-turso': minor
3+
---
4+
5+
fix(driver-turso): a REMOTE `TursoDriver` refuses to arm deferred schema DDL instead of accepting it and running the DDL anyway (#19823)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for callers that arm DDL deferral on a remote Turso datasource** — `TursoDriver.setDeferredDdl(true)` in `remote` transport mode (a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`) now throws a `NOT_IMPLEMENTED` / `501` error, where it used to be accepted and then ignored. The five `os migrate` commands that arm it — `plan`, `apply`, `duplicates`, `account-issuer` and `multi-value-columns` — therefore exit non-zero against a remote Turso database, where they used to exit 0 after changing it. Disarming (`setDeferredDdl(false)`) is accepted, and the `local` and `replica` modes defer exactly as before.
10+
11+
What the refusal replaces, measured on the transport's SQLite-backed test double: arming was accepted, but none of the remote schema doors reads the flag. The engine's boot sync (`syncSchemasBatch`) ran `CREATE TABLE` and `ALTER TABLE … ADD COLUMN` through `RemoteTransport`; the `syncSchema` / `initObjects` doors ran the same DDL plus the canonical temporal backfill, rewriting stored `datetime` / `time` values in place; and `previewDeferredSchemaWork()` and `flushDeferredSchemaDdl()` both answered `[]`. So `os migrate plan` changed the database and then reported no pending work, and `os migrate apply` asked for confirmation after the schema work had already run.
12+
13+
- **Refused at the setter.** Every deferring caller passes through `setDeferredDdl`, and it runs before any schema work: a refused arm sends nothing to the database and leaves the driver un-armed.
14+
- **The driver's message is what the operator reads.** The CLI prints it verbatim. It names the `remote` transport mode, says why the promise cannot be kept, and says what to do instead.
15+
- **No new error code.** `NOT_IMPLEMENTED` / `501` is a standard code, the envelope this transport already uses for its remote transaction and auto-number refusals.
16+
- **Ordinary boots are unchanged.** A boot that does not arm the deferral (`os serve`, `os start`, `os dev`) syncs a remote schema exactly as before.
17+
18+
**If you are refused:** to preview schema work, run the command against a local SQLite copy of the database (a `file:` URL); the local and embedded-replica faces defer DDL. To perform the additive schema work, let an ordinary boot against the remote datasource (`os serve` / `os start`) run it directly.
19+
20+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or reshaped: no spec key, no export, no stored row and no config key — `setDeferredDdl` keeps its name and its signature. There is no old spelling that maps to a new one: the refused call asked the remote transport for a capability it never delivered, and the refusal itself carries the remedy. -->

0 commit comments

Comments
 (0)