Skip to content

Commit c25d320

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14062-plugins-test-typecheck
# Conflicts: # packages/plugins/plugin-hono-server/package.json
2 parents 76b5c50 + 79b6a22 commit c25d320

30 files changed

Lines changed: 2187 additions & 188 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `ComponentPropsMap['element:number'].filter` converges onto the `ViewFilterRule` array form (#12039, objectui#6206 Option B)
6+
7+
<!-- adr-0087: registered element-number-filter-rule-array -->
8+
9+
**BREAKING** accept-set change on one props-map entry, shipped as `minor` under
10+
the repo's launch-window convention for breaking changes; the migration
11+
prescription is registered under protocol major 18.
12+
13+
One filter orthography platform-wide (maintainer batch adjudication
14+
2026-08-25, verbatim 「同意」, Option B): `element:number`'s `filter` was the
15+
one `filter` input in `ComponentPropsMap` declared as the MongoDB-style record
16+
(`FilterConditionSchema`) while every sibling (`record:related_list` and its
17+
Add-affordance picker) declared `z.array(ViewFilterRuleSchema)` — so the
18+
filter a list view stores and renders was refused by the KPI element beside
19+
it. The entry now declares the same array form its siblings do.
20+
21+
Sequenced consumer-first (the 2026-08-25 Option-A ordering ruling):
22+
objectui#6828 made `ObjectStackAdapter.aggregate()` lower a rule array through
23+
the same `translateFilterArray` its `find()` path runs before the analytics
24+
wire; the objectui pin carrying it (`d8ec8d6d`) was re-measured before this
25+
declaration moved, and the end-to-end chain is authored array → adapter
26+
lowering → filter AST → accepted by `lowerAnalyticsWhere` (which still refuses
27+
a RAW rule-object array — by design, and unchanged here).
28+
29+
Migration — FROM → TO, one rule per key (several keys AND):
30+
31+
```ts
32+
// before (refused now — `invalid_type` at `filter`, expected array)
33+
filter: { status: 'won' }
34+
filter: { amount: { $gt: 100 } }
35+
// after
36+
filter: [{ field: 'status', operator: 'equals', value: 'won' }]
37+
filter: [{ field: 'amount', operator: 'greater_than', value: 100 }]
38+
```
39+
40+
The ruled migration check ran with the change: the sweep of first-party
41+
corpora (examples/, skills/, create-objectstack, content/docs/, packages/apps/,
42+
spec fixtures) found one `element:number` author writing a record-form
43+
`filter` — a spec test fixture, rewritten to the array form here — and zero
44+
outside the spec package. `ElementNumberPropsParsed` is declared
45+
(ADR-0122: the parsed state now differs on `filter`, whose `operator` is
46+
normalized on parse). `element:record_picker.filter` is not changed here.

‎.changeset/lucky-pots-attend.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/metadata': patch
3+
---
4+
5+
Artifact/HMR loader: read a view container's own top-level `object` field
6+
7+
`MetadataPlugin`'s artifact/HMR registrar derived which object a `defineView`
8+
container binds to by walking `list.data.object` then `form.data.object`, and
9+
never consulted the container's own top-level `object` — the field
10+
`ViewSchema.object` documents as "how a stack-level `views: [...]` entry says
11+
which object its views belong to; read by `getViewsByObject()` /
12+
`GET /meta/view?object=`".
13+
14+
A package-shipped `defineView({ object: 'crm_lead', list: { columns: [...] } })`
15+
therefore registered nothing at all through this path: the container was dropped
16+
before registration, so no expanded `crm_lead.<key>` ViewItems were produced and
17+
`getViewsByObject('crm_lead')` / `GET /meta/view?object=crm_lead` answered empty
18+
for it. Both derivation sites now call the package's single spelling of that
19+
derivation (`deriveViewContainerObject`), which consults the container's own
20+
`object` first and keeps the existing `list.data.object` → `form.data.object` →
21+
row-name fallback unchanged for every container written before that field was
22+
read here. This is the same order #13407 settled at the runtime door.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
`AutomationEngine.resume(runId)` called with no signal object is now held to the suspended `screen` node's declared field contract exactly like a signal-carrying resume: a run paused on a screen with an unconditional `required` field is refused with `INVALID_SCREEN_INPUT` and stays paused, instead of proceeding with that variable unbound. The bare `if (!signal) return null` early return in `refuseInvalidScreenInput` is gone — an absent signal is an empty submission, the same shape the HTTP resume route has always assembled for an empty body — and the engine's own continuations (subflow output mapping, `map` item handoff) remain exempt only through the existing engine-built-signal mechanism. Pauses that declare no input contract are unaffected: `wait` and `approval` nodes, message-only and object-form screens, and screens whose fields are all optional or hidden resume without a signal exactly as before.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/driver-turso': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
drivers(turso): an unsafe identifier on the remote transport answers `400 INVALID_REQUEST`, not an opaque 500
7+
8+
`RemoteTransport.assertSafeIdentifier` is the one gate for every position where
9+
an identifier is inlined into SQL — the `object`, `field` and `groupBy`
10+
field/output-key positions of `aggregate`, the table and column names in
11+
`syncSchema` / `syncSchemasBatch`, and the index name and columns in unique-index
12+
sync — plus the free function of the same name in the remote canonical backfill.
13+
All of them threw a bare `Error` with no `code` and no `status`, so `mapDataError`
14+
reached none of its classifying branches and served a sanitised **500**: a caller
15+
whose own identifier was refused was told the server had faulted, and an SDK
16+
reading a 5xx retries a request that can never succeed.
17+
18+
Every one of those refusals now carries the ADR-0112 envelope `code:
19+
'INVALID_REQUEST'`, `status: 400`, built by one constructor so the positions
20+
cannot answer three ways. `@objectstack/spec` gains only the error-code ledger's
21+
provenance row registering this driver as an emitter of a code seven packages
22+
already register — the registered-code union is byte-identical (248 codes before
23+
and after) and no new code is minted.
24+
25+
**Nothing about which identifiers are refused changed.** `SAFE_IDENTIFIER` and
26+
every refusal message are untouched; exactly the inputs refused before are
27+
refused after, with byte-identical prose. Only `code` and `status` are new.

‎content/docs/protocol/backward-compatibility.mdx‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ When a feature, schema property, or API is deprecated, ObjectStack follows a str
5555
- Runtime warning is emitted on first use (once per session)
5656
- Migration path is documented in the CHANGELOG
5757

58-
### Phase 2: Migration Period (minimum 2 MINOR releases)
58+
### Phase 2: Migration Period
5959

6060
- Deprecated feature continues to function without behavior changes
6161
- Documentation is updated with migration guides
@@ -75,13 +75,12 @@ Shipped examples: `17.2.0` retired `http_request_errors_total` and `sys_position
7575

7676
```mermaid
7777
flowchart TD
78-
A["v3.2.0 — feature deprecated (warning emitted)"] --> B["v3.3.0 — migration period continues"]
79-
B --> C["v3.4.0 — migration continues (minimum 2 minor releases)"]
80-
C --> D["v3.5.0 — feature removed (earliest possible removal, MINOR)"]
78+
A["Deprecation notice — feature marked deprecated (warning emitted)"] --> B["Migration period — deprecated feature keeps working, no behavior change"]
79+
B --> C["Removal — retired at the boundary the ADR-0087 registry records (MINOR release, during the launch window)"]
8180
```
8281

8382
<Callout type="warn">
84-
**Minimum guarantee:** Deprecated features survive for at least **2 MINOR releases** before they can be removed. During the launch window the removal itself lands in a MINOR release, so a MINOR bump is where you should expect a removal to arrive.
83+
**No minimum survival window.** ObjectStack does not guarantee a deprecated feature survives for a fixed number of releases — [ADR-0049](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0049-no-unenforced-security-properties.md)'s dispositions for a flagged property are enforce, `experimental`, or remove, and none of them carries a dwell time. A deprecated feature is retired at the boundary the ADR-0087 registry records for it; during the launch window that removal itself lands in a MINOR release, so a MINOR bump is where you should expect a removal to arrive.
8584
</Callout>
8685

8786
---
@@ -109,7 +108,7 @@ Read the **Breaking?** column, not the version number: during the launch window
109108

110109
### Breaking Change Process
111110

112-
1. **RFC (Request for Comments)** — Breaking changes are proposed as GitHub issues with the `breaking-change` label.
111+
1. **RFC (Request for Comments)** — Breaking changes are proposed as GitHub issues with the `protocol:breaking` label.
113112
2. **Deprecation** — The old behavior is deprecated in a MINOR release (see timeline above).
114113
3. **Migration Guide** — A detailed migration guide is published before the removal lands, in the release notes for the version that carries it.
115114
4. **Release** — During the launch window the breaking change ships in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`. `scripts/check-changeset-no-major.mjs` fails any pull request that declares a `major` bump, because under lockstep one `major` would promote all 69 published packages.
@@ -204,7 +203,7 @@ MAJOR releases do still happen — 17.0.0 was cut precisely because its breaking
204203

205204
### When it stops applying
206205

207-
The tables and deprecation timeline above now state this rule directly, so the page no longer contradicts itself. Classic SemVer — where a breaking change once again requires a MAJOR — is the settled form the project returns to once the launch window closes; the condition that closes the window is tracked separately and is deliberately not stated here. Until it closes, **this section is the operative rule wherever any part of this page disagrees.**
206+
The tables and deprecation timeline above now state this rule directly, so the page no longer contradicts itself. Classic SemVer — where a breaking change once again requires a MAJOR — is the settled form the project returns to once the launch window closes: the window closes at GA, and strict SemVer resumes from that point. Until it closes, **this section is the operative rule wherever any part of this page disagrees.**
208207

209208
---
210209

@@ -213,6 +212,6 @@ The tables and deprecation timeline above now state this rule directly, so the p
213212
If you encounter an unintended breaking change:
214213

215214
1. **Check the CHANGELOG** — Verify the change was not documented as intentional.
216-
2. **Open an issue** — File a GitHub issue with the `compatibility` label.
215+
2. **Open an issue** — File a GitHub issue describing the unintended change.
217216
3. **Include a reproduction** — Provide a minimal code sample showing the breakage.
218217
4. **Reference the version** — Specify the exact versions where behavior changed.

‎content/docs/references/ui/component.mdx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,22 @@ const result = AIChatWindowProps.parse(data);
175175
| **object** | `string` | ✅ | Source object |
176176
| **field** | `string` | optional | Field to aggregate |
177177
| **aggregate** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>` | ✅ | Aggregation function |
178-
| **filter** | `any` | optional | Filter criteria |
178+
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter rules narrowing the aggregate — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` input in this map shares. The MongoDB-style record form is refused — see migration `element-number-filter-rule-array` |
179179
| **format** | `Enum<'number' \| 'currency' \| 'percent'>` | optional | Number display format |
180180
| **prefix** | `string` | optional | Prefix text (e.g. "$") |
181181
| **suffix** | `string` | optional | Suffix text (e.g. "%") |
182182
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
183183

184+
### Nested Shape: `ElementNumberProps.filter[number]`
185+
186+
View filter rule
187+
188+
| Property | Type | Required | Description |
189+
| :--- | :--- | :--- | :--- |
190+
| **field** | `string` | ✅ | Field name to filter on |
191+
| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator |
192+
| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. |
193+
184194
### Nested Shape: `ElementNumberProps.aria`
185195

186196
| Property | Type | Required | Description |

‎packages/drivers/driver-memory/objectstack.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { ObjectStackManifest } from '@objectstack/spec/system';
3+
import type { ObjectStackManifest } from '@objectstack/spec/kernel';
44

55
/**
66
* In-Memory Driver Plugin Manifest

‎packages/drivers/driver-memory/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
1717
"dev": "tsc -w",
1818
"test": "vitest run",
19-
"typecheck": "tsc --noEmit"
19+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.typecheck.json"
2020
},
2121
"dependencies": {
2222
"@objectstack/core": "workspace:*",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// The MANIFEST-layer type-check program for @objectstack/driver-memory (#13284).
2+
//
3+
// `objectstack.config.ts` sits at the PACKAGE ROOT and declares this driver's
4+
// plugin manifest, annotated `ObjectStackManifest`. Until this file existed no
5+
// tsc program read a line of it: `tsconfig.json` selects `src/**/*`, and that
6+
// glob cannot match a root-level file. The annotation was therefore decorative
7+
// -- the file imported the type from `@objectstack/spec/system`, an entry that
8+
// does not export it, and `pnpm --filter @objectstack/driver-memory typecheck`
9+
// still exited 0. A wrong key or a wrong import here cost nothing at author
10+
// time, which is the whole defect: the ADR-0049 manifest retirements
11+
// (#11332 / #10724 / #4914) lean on the `retiredKey()` DOUBLE channel -- tsc
12+
// (input typed `never`) plus parse -- and the tsc half was blind at the only
13+
// two places in this repo where a plugin manifest is authored in TypeScript.
14+
//
15+
// A SIBLING rather than a wider `include` on `tsconfig.json`, the distinction
16+
// #5475 drew for `packages/spec` and #10756 for `packages/objectql/scripts`:
17+
// that config EMITS (`rootDir: "./src"`, `outDir: "./dist"`, and `dev` runs
18+
// `tsc -w` through it), so widening it to reach the package root would put the
19+
// manifest in front of the emit -- measured, that is exactly TS6059 ("File
20+
// ... is not under 'rootDir' ... 'rootDir' is expected to contain all source
21+
// files"), and it fires under `--noEmit` too. This program emits nothing, so
22+
// it can neutralise `rootDir` without touching what ships. `tsup` builds
23+
// `src/index.ts` and `files` publishes `dist` only, so nothing about the
24+
// published package moves.
25+
//
26+
// STRICTNESS IS INHERITED and deliberately not relaxed: `strict`,
27+
// `esModuleInterop`, `forceConsistentCasingInFileNames` and the rest come from
28+
// the root config through `tsconfig.json`. The manifest type-checks clean
29+
// under them -- it enters with ZERO recorded debt, and there is no ledger here
30+
// to record any in.
31+
{
32+
"extends": "./tsconfig.json",
33+
"compilerOptions": {
34+
"noEmit": true,
35+
// `.` rather than the inherited `src`, because the file this program checks
36+
// is the one outside `src`. Safe precisely because nothing is emitted from
37+
// here -- see the header.
38+
"rootDir": "."
39+
},
40+
"include": ["objectstack.config.ts"]
41+
}

‎packages/drivers/driver-turso/src/remote-canonical-backfill.ts‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@
9393
* twin's contract calls out, kept across the transport boundary.
9494
*/
9595

96+
// [#14287] The unsafe-identifier refusal's ADR-0112 envelope, from the one
97+
// place that decides it. Imported rather than re-spelled so this module and
98+
// `RemoteTransport` cannot answer one condition two ways; `turso-driver.ts`
99+
// already loads both modules together, so the edge costs nothing at runtime.
100+
import { unsafeIdentifierError } from './remote-transport.js';
101+
96102
/** The `@libsql/client` surface this module uses — nothing more. */
97103
export interface RemoteBackfillClient {
98104
execute(stmt: { sql: string; args?: unknown[] } | string): Promise<{
@@ -218,9 +224,34 @@ export const REMOTE_BACKFILL_EPOCH_MS_MAX = 4_102_444_800_000;
218224
*/
219225
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
220226

221-
function assertSafeIdentifier(name: string): void {
227+
/**
228+
* [#14287] The second producer of the transport's unsafe-identifier refusal,
229+
* carrying the identical ADR-0112 envelope — `INVALID_REQUEST` / 400 — through
230+
* the one constructor that decides it ({@link unsafeIdentifierError}). One
231+
* condition, one wire answer, whichever of the two helpers refused it.
232+
*
233+
* ## Measured: on THIS module's paths the envelope is defence in depth, not a
234+
* ## wire answer — and saying so is the point
235+
*
236+
* Both callers flatten the throw by design: {@link probeRemoteCanonicalColumns}
237+
* turns it into `{ error: message }` and {@link backfillRemoteCanonicalColumn}
238+
* into `report.error`, because ADR-0053 D-B3 forbids a migration from taking a
239+
* boot down. So `code` and `status` reach no response envelope from here today
240+
* — only the MESSAGE survives, into the report a caller logs. The envelope is
241+
* still worth carrying, for two reasons and no third: the two producers of one
242+
* condition must not drift onto two spellings (the card's own "decide the code
243+
* once" argument), and a future caller that propagates instead of reporting
244+
* inherits the right answer rather than re-deriving one.
245+
*
246+
* ⛔ It is therefore NOT the wire-reachability evidence for this package's
247+
* ledger provenance row — `RemoteTransport`'s DDL and `aggregate` positions
248+
* are, and they answer over HTTP. Exported so the envelope has a real pin: no
249+
* exported path can observe it, and an unobservable assertion is the phantom
250+
* check `AGENTS.md` names rather than a test.
251+
*/
252+
export function assertSafeIdentifier(name: string): void {
222253
if (!SAFE_IDENTIFIER.test(name)) {
223-
throw new Error(`remote canonical backfill: unsafe identifier rejected: "${name}"`);
254+
throw unsafeIdentifierError(`remote canonical backfill: unsafe identifier rejected: "${name}"`);
224255
}
225256
}
226257

0 commit comments

Comments
 (0)