Skip to content

Commit c500a43

Browse files
committed
Merge origin/main into claude/issue-5553-file-description-rendering
生成物 `content/docs/references/**` 一律取 origin/main 版本(#4675 第二步: 生成树用 checkout 定向取一侧,不手工解冲突),使本合并提交成为确定性基线 —— 两侧各自从不同源码状态生成过,文本自动合并虽无冲突,得到的却是「陈旧组合」。 主线带入 #6211(#5340,内联长枚举省略,落点 `lib/format-type.ts`); 本分支落点 `lib/file-description.ts`,两者互不重叠。 注:本提交刻意以 `--no-verify` 落下 —— os-regen 钩子(正确地)指出生成物相对 源码已陈旧。#4675 要求合并与重跑分成两个提交以便分别 review,故此处保留陈旧 状态,由紧随其后的提交整体重跑 `gen:schema && gen:docs` 修正。PR head 上的 CI 校验的是最终树,不是这个中间提交。
2 parents ac1e930 + 83df2fd commit c500a43

243 files changed

Lines changed: 10415 additions & 972 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.

.changeset/cel-parse-fault-kind.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/formula': patch
3+
---
4+
5+
fix(formula): 括号/引号/转义等 parse 期错误不再被误报为 `runtime`
6+
7+
`celEngine` 的错误分类此前完全靠**错误文案关键词**判定,而 cel-js 8.0.0 的 parse 期错误有约 19 种措辞,只有 3 种含 `parse` / `unexpected` / `syntax`。其余整类 —— 最典型的括号/方括号/花括号不配对(`Expected RPAREN, got EOF`)、未闭合字符串、非法转义、保留字 —— 全部落到默认值 `runtime`
8+
9+
`kind` 不是内部字段:它被原样拼进作者可见的写入拒绝文案(`@objectstack/objectql``rule-validator` / `cel-fault`)与 REST 错误响应体的 `reason`。少写一个右括号的校验规则,作者读到的是 `(runtime: …)` —— 指向数据与求值期,而真正该改的是表达式本身,与 ADR-0032 D1d 的"消息面向自纠"相悖。
10+
11+
改为按 cel-js 抛出的**错误类**判定:`ParseError``parse`(其中 `code: 'limit_exceeded'` 仍 → `bounds`,cel-js 的越界一律由 parser 抛出)。这一层不再读文案,因此也修掉了关键词方案无法修的一格:cel-js 会把**作者自己的源码行**嵌进 `message`(`formatErrorWithHighlight`),于是字段名能决定错误分类 —— 实测 `((record.type_id)` 这条普通的括号不配对,此前被判为 `type`,只因回显的源码里含子串 "type"。
12+
13+
`type` / `runtime` 两支暂仍走原关键词表:cel-js`TypeChecker`**阶段**而非按故障选择错误类(`isEvaluating ? evaluationError : typeError`),同一个 `unknown_variable` 在 check 期是 `TypeError`、在 eval 期是 `EvaluationError`,整体结构化会改变这些既有判定。审计见 #6133
14+
15+
kind 词表本身(`parse` / `type` / `runtime` / `bounds` / `dialect`)未变,消费方未改。
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): `compareTo` 在「日期维度本身就是网格维度」时把比较桶键平移回当期 (#6007)
6+
7+
趋势图 + 同比是 `compareTo` 最常见的形状:日期维度既写进 `selection.dimensions`
8+
(它就是图表的时间轴),又被 `compareTo` 用作锚点。这个形状下比较趟从来没有对齐过。
9+
10+
比较趟查询的是**平移后**的窗口,所以它的行按平移后的桶键落地;而
11+
`mergeByDimensions``selection.dimensions` 元组建键 —— `2025-01` 不等于
12+
`2026-01`,于是**没有一条**比较行合并得进去,全部作为新行追加。两趟各自只报告了自己
13+
那一半,`fillEmptyGroups` 把另一半填成自信的 `0`,再加上平移后的桶键坐在网格里,而它们
14+
落在调用方筛选窗口之外。一个 2 桶窗口的「今年 vs 去年同期」回来是这样的:
15+
16+
```
17+
[{"close_date":"2025-01","opp_count__compare":5,"opp_count":0},
18+
{"close_date":"2025-02","opp_count__compare":7,"opp_count":0},
19+
{"close_date":"2026-01","opp_count":1,"opp_count__compare":0},
20+
{"close_date":"2026-02","opp_count":2,"opp_count__compare":0}]
21+
```
22+
23+
四行、每行一个 0、两行在窗口外;期望是 2 行 × 2 列。
24+
25+
**修法(维护者裁决 2026-08-07,方向 1):合并之前,把每个比较桶键用当期的说法重述一遍。**
26+
上例现在返回 `[{close_date:'2026-01',opp_count:1,opp_count__compare:5},
27+
{close_date:'2026-02',opp_count:2,opp_count__compare:7}]`。
28+
29+
- `previousYear` —— 窗口是按日历年平移的,所以逆运算就是按日历年往前推一年:对桶自己的
30+
首日做平移再重新分桶。`2025-01``2026-01``2025-Q1``2026-Q1`
31+
`2025-W03``2026-W03`。它刻意是 `shiftRange` 那套年运算的精确逆运算(含
32+
`setUTCFullYear` 的溢出行为),窗口与桶键因此不可能对「一年」有两种理解。
33+
- `previousPeriod` —— 任意天数窗口没有日历对应物,所以按**桶序(bucket ordinal)**对齐:
34+
上一窗口的第 n 个桶对上本窗口的第 n 个桶,n 各自从自己窗口的起点数起。序号由**日历**算出
35+
而不是数组下标,所以本期网格里某个桶没有数据(存在空档)不会让其后每个桶都错位一格。
36+
37+
**响应形状不变** —— 仍然是 `<measure>__compare` 列,行仍然是网格维度元组,所以消费端
38+
(objectui#3337 正在收敛的那条契约)不受影响。
39+
40+
不确定时一律**保持原样**(即改动前的行为),而不是猜:空桶(两条聚合路径上键都是 `null`,
41+
两趟本来就互相合并)、未分桶的日期维度(分组的是原始时间戳,不是桶键)、以及平移回来落在
42+
当期窗口之外的桶(两个等长的天数窗口可以切出不同的桶数)。
43+
44+
范围严格限定在坏掉的那个形状:锚点必须是**网格维度**(仅作窗口的锚点两趟都不是列,#5688
45+
之后本来就对齐)且必须**被分桶**。两趟通过同一个 `granularityOf` 读取桶大小,所以这里重述
46+
的桶大小按构造就是查询分组用的桶大小。
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the reference generator elides an over-wide enum inside an inline shape summary, and says how many members it hid (#5340)
6+
7+
`formatType()` capped how many **keys** an inline object summary prints
8+
(`INLINE_KEY_LIMIT = 4`) but never capped how wide a single key's **type** could
9+
be. One long enum reached through a summary therefore printed every member into
10+
one table cell. The issue was filed on `BulkActionDef.params` at ~900
11+
characters; measuring the whole corpus found that is not close to the worst —
12+
the 261-member error-code vocabulary is inlined into the `error` shape of 80
13+
rows across 13 `api/*.mdx` pages, at **6242 characters in a single cell**.
14+
15+
An `Enum` body rendered below a summary's `{ … }` is now cut to 80 characters
16+
and the count of what was cut is printed in its place:
17+
18+
```
19+
type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | … +42 more>
20+
```
21+
22+
The count is the safety property, not decoration. A silent prefix would leave
23+
the page looking complete while it was not — a reader cannot tell a 7-member
24+
vocabulary from the first 7 of 49 — and these pages are the authoritative input
25+
for AI authors (ADR-0033), so a page that lies by omission is a worse defect
26+
than a wide cell.
27+
28+
**Nothing that owns its vocabulary is elided.** The cut applies only below an
29+
inline shape summary, which is by construction a *second* copy: a schema's own
30+
row (`BulkActionParam.type`, `ErrorResponse.code`), a union variant on its own
31+
row (`Enum< … > | string`, the `PageComponent.type` shape), a top-level
32+
`Record< string, Enum< … > >` and an array of a top-level enum all still print
33+
every member. For 457 of the corpus's 805 in-shape occurrences the elided
34+
copy's full list is still on the same page for that reason; for the remaining
35+
348 the count carries it, and the JSON Schema under `json-schema/` remains the
36+
authority it always was.
37+
38+
**The 80 is measured, not chosen.** Across 216 pages / 8541 type cells / 1768
39+
`Enum` occurrences, the 805 in-shape ones are bimodal and their density per
40+
character collapses at 80 (3.6 occurrences/char over `(64,80]`, 1.6 over
41+
`(80,100]`, 0.5 over `(100,200]`). Below it sit the ordinary short vocabularies
42+
a reader wants spelled out; above it sit listings. A tighter budget buys almost
43+
nothing and costs real information — budget 24 would elide 79% of them to save
44+
4% more characters — and a fixed member cap is worse at every setting.
45+
46+
An elision must also **pay for its own marker**: a body only a member or two
47+
over budget gives back less than `… +N more` costs to print, so it is left
48+
whole. That is why 31 in-shape enums between 81 and 107 characters are
49+
unchanged, and why the limit is not a cliff at exactly 81.
50+
51+
42 reference pages change, 144 rows, all in one direction: every changed row is
52+
shorter (462,140 characters removed in total, largest single row -6266), and
53+
every one carries a `… +N more` marker — no row is silently truncated and none
54+
grew. Cells over 900 characters go from 76 to 4, over 200 from 246 to 145;
55+
the p95 cell width is unchanged at 145, i.e. ordinary cells do not move.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os migrate --json` no longer exits with its own runtime as the status code (#4873)
6+
7+
A **successful** `os migrate recorded-by --json` returned a different non-zero
8+
exit code on every invocation — 208, 171, 176, 163, 62, 19, 48, 57 — while
9+
printing correct JSON, printing `✅ Graceful shutdown complete`, and leaving
10+
stderr completely empty. `os migrate resume --json` had it too. Nothing that an
11+
author reads was wrong; the only thing that was wrong is the only thing a CI
12+
step, a `set -e` script, a Makefile, or a container entrypoint reads. `--json`
13+
exists for programs, and the first thing a program consumes is the exit status.
14+
15+
**Root cause.** `emitJson(payload, exitCode, opts)` takes its exit code as the
16+
second positional argument, and both commands were passing `timer.elapsed()`
17+
there — a duration in milliseconds. So a run that took 531 ms set
18+
`process.exitCode = 531`, and the shell saw `531 & 0xFF` = 19. The codes looked
19+
random because they *were* the run's duration, and no two runs take the same
20+
number of milliseconds.
21+
22+
It was not what it looked like from the outside: no native `abort` during
23+
teardown, no libsql/sqlite handle, no `safeExit`, and not a leftover of #4813
24+
(whose 120-second hang is fixed and unrelated — the random codes predate and
25+
survive it).
26+
27+
**What changed.**
28+
29+
- Both commands now report their duration where every other `--json` command in
30+
this CLI already reports it — inside the payload, as `duration`. A successful
31+
run exits `0`; a failing one still exits `1`, unchanged.
32+
- `emitJson` / `emitText` narrow that parameter from `number` to
33+
`CliExitCode = 0 | 1`, so handing a duration (or any other stray number) to
34+
the exit-code slot is now a compile error instead of a silent false failure.
35+
36+
**Payload change.** `os migrate recorded-by --json` and `os migrate resume
37+
--json` gained a `duration` key (milliseconds). Consumers that were reading the
38+
exit status of these two commands should note that a zero now means what it
39+
says.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/plugin-auth": minor
3+
"@objectstack/runtime": patch
4+
"@objectstack/service-sms": patch
5+
---
6+
7+
feat(plugin-auth): the fixed-window counter gets its own `./rate-limit-storage` entry (#6040)
8+
9+
`rate-limit-storage.ts` is the repo's ONE fixed-window counter —
10+
`incrementFixedWindow` / `createLazyCounterStore` / `InProcessCounterStore`,
11+
ADR-0069 D2 — and #4790's cross-reference asks later arrivals to reuse it
12+
rather than write a third copy. They did, and from outside auth:
13+
`@objectstack/runtime` counts inbound requests and endpoint policy through it,
14+
and `@objectstack/service-sms` counts its daily SMS budget through it (#2814).
15+
16+
`@objectstack/plugin-auth` published exactly one entry, `"."`, whose `export *`
17+
chain takes **value** imports on `better-auth/adapters`
18+
(`objectql-adapter.ts`) and `@better-auth/core/db` (`backfill-account-issuer.ts`).
19+
Value imports are evaluated eagerly, so reaching those ~90 lines of counting
20+
loaded `better-auth` + `@better-auth/{core,oauth-provider,scim,sso}` + `jose` +
21+
`@noble/hashes` + `@objectstack/rest` + `@objectstack/platform-objects` first.
22+
Measured against the built package: `require('@objectstack/plugin-auth')` puts
23+
109 modules in `require.cache`; the counter needs one.
24+
25+
So the counter is now published on its own:
26+
27+
```ts
28+
// before — 109 modules, the whole better-auth family
29+
import { incrementFixedWindow } from '@objectstack/plugin-auth';
30+
// after — 1 module, 3.7 KB
31+
import { incrementFixedWindow } from '@objectstack/plugin-auth/rate-limit-storage';
32+
```
33+
34+
`tsup` emits the second entry with `splitting: false`, so it is a self-contained
35+
bundle rather than a nominal split: `dist/rate-limit-storage.mjs` is 3.71 KB
36+
against `dist/index.mjs`'s 330.28 KB, contains zero top-level imports and zero
37+
occurrences of the string `better-auth`. The one better-auth reference that
38+
survives is `import type { BetterAuthRateLimitStorage }`, which is erased at
39+
build and costs a consumer nothing at runtime.
40+
41+
**Nothing is removed.** The root still re-exports every one of these symbols, so
42+
existing `@objectstack/plugin-auth` imports keep working unchanged — this is a
43+
new entry point, which is why it is `minor` rather than breaking. The `patch` on
44+
`runtime` and `service-sms` is the import-specifier switch in those packages;
45+
their behaviour is identical.
46+
47+
`src/rate-limit-storage-isolation.test.ts` pins the invariant from both sides,
48+
in the shape `packages/types/src/node-isolation.test.ts` (#4700) established for
49+
the `./node` split: it walks the real import graph from the subpath entry and
50+
fails on any better-auth **value** import or any undeclared external package,
51+
it fails if a consumer reaches the counter through the package root again, and
52+
it fails if the root ever *stops* pulling better-auth eagerly — because at that
53+
point the split stopped buying anything and deserves re-measuring rather than a
54+
suite that passes for the wrong reason.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): REST 面的执行上下文补齐 ADR-0090 D9/D10 的 principal 分类(#6071)
6+
7+
`resolveAuthzContext`(`@objectstack/core`)被提取出来,正是为了让两个 HTTP 入口
8+
不再在**授权**上漂移。但它之后的一步 —— 把授权信封组装成 `ExecutionContext` ——
9+
仍是两份手写副本,而两份的字段集已经不一致:runtime / dispatcher 那份
10+
(`packages/runtime/src/security/resolve-execution-context.ts`)按 ADR-0090 D9/D10
11+
设置 `principalKind`(必要时连同 `onBehalfOf`),`rest-server.ts``computeExecCtx`
12+
两个都不设。
13+
14+
后果不在装饰面而在 enforcement 面:`plugin-security/explain-engine.ts`
15+
posture 下限、`security-plugin.ts` 的 agent 基线、`observability/perf-timing.ts`
16+
的披露闸门都读 `principalKind`,于是同一个请求走 dispatcher 与走 REST 会拿到不同
17+
的上下文,读这个字段的判断在 `os serve` / `dev` 的数据与元数据路由上**从不成立**
18+
问题由 #5859 实施时的 dogfood 全栈 boot 插桩测得:到达消费方的键集里 `__kernel`
19+
在(自证是 rest-server 这条组装路径)、`principalKind` 不在。
20+
21+
本次改动只补这一个传输上缺的字段,口径与 runtime 侧完全一致:
22+
23+
- 会话(cookie)或 API key 背书的主体 ⇒ `principalKind: 'human'` —— 与 runtime
24+
侧「an authenticated (API-key) request resolves as a human principal, never
25+
guest」的钉子同一判定。
26+
- `'agent'` 与随之而来的 `onBehalfOf` **在本传输上不可表达**:它需要一个指明已授权
27+
客户端的 OAuth access token,而该凭据只在 dispatcher 的 `/mcp` 门上被接受
28+
(`acceptOAuthAccessToken`),正是为了不让粗粒度的工具族 scope 溜进 REST。
29+
- `'guest'` 同样不可表达:`computeExecCtx` 在信封没有 `userId` 时就返回
30+
`undefined`,匿名 REST 调用者本来就拿不到任何上下文(随后被 `enforceAuth` 401)。
31+
**匿名面零变化** —— 不给匿名调用者凭空发一个 guest 上下文。
32+
33+
行为差量(逐条核过,无一条改变授权结果):`explain-engine.ts` 的 guest ⇒ `EXTERNAL`
34+
`security-plugin.ts` 的 agent 分支在 REST 面仍不成立(前者的 `!context?.userId`
35+
前肢本就恒真,后者读 `'agent'` 标签、且真正的兜底是委托 LINK);`perf-timing.ts`
36+
只认 `'service'` / `'system'`,`'human'` 不开闸。唯一可观测的新增是 explain 输出里
37+
多回显一个 `principalKind: 'human'`(该字段在 explain schema 中本就是 optional)。
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/service-settings": patch
3+
---
4+
5+
fix(service-settings): 写入路径与 env 路径执行 settings 声明的 min / max / minLength / maxLength (#5932)
6+
7+
`SpecifierSchema` 从存在起就声明了五类值约束 —— `pattern` / `min` / `max` /
8+
`minLength` / `maxLength` —— 而 `SettingsService.validatePatch` 只读其中一类。
9+
另外四个在整个写入路径上**没有任何读取点**:已发布的 manifest 里 42 个
10+
specifier 声明了取值窗口,每一个都只是装饰。
11+
12+
落点最重的是 `auth.password_min_length`。它声明 `min: 6`,控制台的数字框也按这个
13+
下限渲染,而 `PUT /api/settings/auth` 会接受 `1`(以及负数)并存下来,better-auth
14+
的口令策略随后照这个值执行。也就是说,声明是唯一一个宣称「存在下限」的东西,却没有
15+
任何一层在守它 —— 正是 Prime Directive #10 的正面形状。`ai.manifest.ts` 的六项
16+
(temperature / max_tokens / timeout 等)同理。
17+
18+
**修法与 #5131(options 表)同形,是同一族的第三个成员:**
19+
20+
- `validatePatch` 补一个取值窗口分支,发既有码表里的 `FieldError`(ADR-0114 D2):
21+
`min_value` / `max_value` / `min_length` / `max_length` —— 与
22+
`record-validator.ts` 对同一类越界发出的码一致。`constraint`**完整窗口**
23+
(`{ min, max }`,长度类再带 `actual`),客户端据此自行组织文案,不必解析我方
24+
英文句子。⛔ `packages/spec` 未改动:约束早已声明,码表现有即够用。
25+
- 沿用 #5131**TOUCH 闸门**:只校验本次 patch 触及的键。取值窗口在产品生命周期里
26+
会被**收紧**(口令下限从 6 提到 8),窗口下方的老工作区必须仍能编辑它无关的设置,
27+
只在重写该键时才被告知。
28+
- env 侧走 `effectiveEnvOverride`**一个**判定点,与 options 表同处,复用同一组
29+
比较函数 —— #5204 的成因就是同一比较有两份实现并各自漂移。因此
30+
`OS_AUTH_PASSWORD_MIN_LENGTH=1` 与写入路径得到同一个裁决:该 override 不生效、
31+
不贡献 cascade 条目、不锁定该键,并在注册时打出一条(且仅一条)`error` 日志。
32+
33+
**刻意不做的判断:** 取值窗口只裁决**可比较的值** —— `min`/`max` 只看数字(含经
34+
JSON / 表单往返变成字符串的数字),`minLength`/`maxLength` 只看字符串。布尔、数组、
35+
对象不做强制转换(`Number(true)` 是 1、`Number([])` 是 0):值的**形状**
36+
`invalid_type`,属于另一个约束、另一个负责人,在这里发明裁决会拒掉本检查从未被要求
37+
过问的写入。空值仍归 `required` 管。
38+
39+
约束的读取以**声明**为准,而不是以 specifier 的 `type` 为准 —— 与旁边按类型收口的
40+
options 检查不同,这个差异是 spec 定的:`SpecifierSchema` 的 superRefine 把 options
41+
**绑定**`select`/`radio`/`multiselect` 三型,却没有把四个窗口键绑定到任何类型。
42+
在这里自拟一份类型清单,正是 options 注释警告的「第三份会漂移的清单」,并且会把本
43+
issue 原样复制到下一层:窗口键声明在清单外的类型上,照样解析、照样渲染、照样不执行。
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/driver-sql': patch
3+
'@objectstack/driver-turso': patch
4+
---
5+
6+
drivers(sql,turso): 聚合函数拒收带上 ADR-0112 信封,并把两类条件分开措辞
7+
8+
`SqlDriver.mapAggregateFunc()``RemoteTransport.aggregate()` 此前对同一条件各抛一个裸
9+
`Error`(`code`/`status``undefined`),`mapDataError` 因此落默认分支——一条本该 4xx 的
10+
调用方错误以不透明 500 到达客户端。两处同时改,同一信封体例、首句逐字一致(#5240):
11+
12+
- **协议未声明的函数名**(如 `median`)→ `INVALID_QUERY` / 400。这正是协议门
13+
(`metadata-protocol``invalidQueryError`,#4254)对同一条件已经给出的码,于是
14+
进程内调用方与 REST 调用方读到同一个答案。
15+
- **协议已声明、本后端编不出**(`count_distinct` / `array_agg` / `string_agg`)→
16+
`NOT_IMPLEMENTED` / 501。这是能力缺口而不是调用方的错(`driver-mongodb` 编得出这三个),
17+
措辞明确说明查询拼写无误,不把作者说成打错字。
18+
19+
两面都只改拒收的身份:编得出的五个函数生成的 SQL 逐字节不变。

0 commit comments

Comments
 (0)