Skip to content

Commit 46af79f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5309-endpoint-envelope-body-split
2 parents 38bcd17 + a36db28 commit 46af79f

177 files changed

Lines changed: 10053 additions & 4409 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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/driver-sql": patch
4+
---
5+
6+
fix(objectql,driver-sql): 自增号播种按声明的 `suffix` 定位计数器,两侧收敛到同一答案 (#6468)
7+
8+
`autonumberFormat` 允许序号槽 `{0..0}` **后面**还有 token —— `renderAutonumber`
9+
专门返回 `suffix`,其契约就是 `prefix + zero-padded(seq) + suffix`。这类格式渲染
10+
出的值**序号不在串尾**:`{000}-{YYYY}` 渲染成 `001-2026`,是很常见的单号写法。
11+
12+
两侧的播种解析却都假定「串尾的数字就是计数器」,而且各错各的:
13+
14+
- 引擎兜底播种 `seedAutonumber()` 取整串的**最后一个**数字段 —— 读到的是年份。
15+
库里三行 `001-2026`/`002-2026`/`003-2026`(真实计数器 3)把计数器播种成 **2026**,
16+
下一个发出的号直接跳到 `2027-2026`;
17+
- driver-sql 的 `scanMaxNumericTail()` 把 tail 里**所有**数字拼接后 `parseInt` ——
18+
同样三行读成 **12026**,下一个号是 `12027-2026`
19+
20+
于是**同一份元数据、同一批行,换个驱动号段就不一样**;中间跳过的号已经烧掉,事后
21+
无法回收。只修一侧会把「两个不同的错误答案」变成「一个对一个错」,跨驱动仍不一致,
22+
所以两侧同 PR 修。
23+
24+
**修法:两侧解析器尊重已声明的 `prefix`/`suffix`** 两个字符串都由调用方从
25+
`renderAutonumber` 的返回值取得后传入 —— 两侧都不再自行理解格式,driver-sql 只收
26+
参数(`getNextSequenceValue` 仅多转发一个位置参数,序列逻辑本身未动):
27+
28+
- **prefix / suffix 任一非空 ⇒ 计数器「有锚」**:取 prefix 之后的**首个**数字段,
29+
并在该行确实带有声明的 suffix 时先把它去掉;
30+
- **两者皆空 ⇒ 「无锚」**:各自的既有读法**逐字保留**(引擎取整串最后一个数字段,
31+
driver-sql 拼接全部数字)—— 无 `{0..0}` 槽的格式渲染的就是串尾裸计数器,而早于
32+
格式存在的历史值根本没有锚可依。
33+
34+
**suffix 只在匹配时剥离,绝不要求匹配。** `{000}-{YYYY}` 的计数器 scope 是渲染后的
35+
**prefix**(此处为空),即全局一个计数器、只有显示的年份在变,所以去年的 `007-2025`
36+
持有计数器 7,必须计入。把 suffix 下推成 `like '%-2026'` 会把这些行整批漏掉、播种
37+
**低于**真实 max —— 那正是 #6249 修掉的重复单号伤害,自己再造一遍。因此 SQL 谓词
38+
保持 `like 'prefix%'`,suffix 只在 JS 侧逐行使用。
39+
40+
无后缀格式(`D-{0000}``{0000}`)两侧本来就正确,行为不变并已 pin 住;#6467
41+
播种扫描结构未触碰。
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata-protocol": patch
4+
---
5+
6+
fix(spec,metadata-protocol): `capability` 补齐三处注册 —— 授权面不再接受任意 JSON (#5961)
7+
8+
`capability` 是「enforced but undeclared」——#5271`api` 关掉的那个
9+
`declared ≠ enforced` 的镜像。平台早就把它当成一个 metadata kind 在用:
10+
`PLURAL_TO_SINGULAR`#5870 起就有 `capabilities``capability`,
11+
`AppPlugin` 用这个名字注册 stack 声明的 capability,
12+
`bootstrapDeclaredCapabilities` 再读回来 seed `sys_capability`。但三处注册表
13+
里都没有它:`MetadataTypeSchema`(kind 枚举)、`BUILTIN_METADATA_TYPE_SCHEMAS`
14+
(schema 解析)、`DEFAULT_METADATA_TYPE_REGISTRY`(谁可以写、怎么加载)。
15+
16+
后果有两条,第二条才是这个 issue 属于授权缺陷而非整洁度问题的原因:
17+
18+
- `getMetadataTypeSchema('capability')` 返回 `undefined`,于是 `saveMetaItem`
19+
走了它自己文档化的「未注册类型 → 不校验直接存」分支,
20+
`PUT /api/v1/meta/capability/:name` 接受**任意 JSON** 落进 `sys_metadata`
21+
capability 是靠**名字字符串**被解析的——授予侧 `systemPermissions`
22+
要求侧 `requiredPermissions` 都是——所以一行任意 JSON 直接落在活的授权命名
23+
空间里。
24+
- `isRuntimeCreateAllowed` 镜像 `getMetaTypes()` 的合成规则:没有静态注册表条目
25+
的类型被当作可运行时创建。所以缺的那一行不只是「没关上门」,它**把门打开了**
26+
`/meta/types` 同步发布了这个虚构:`allowRuntimeCreate: true` + 无 schema,
27+
metadata-admin 引擎据此渲染成一个 raw-JSON 文本框。
28+
29+
### 改了什么
30+
31+
- **`BUILTIN_METADATA_TYPE_SCHEMAS['capability'] = CapabilityDeclarationSchema`**
32+
既有的 422 `invalid_metadata` 路径就此覆盖 `capability`,`/meta/types` 发出真
33+
JSON Schema。
34+
- **`DEFAULT_METADATA_TYPE_REGISTRY` 新增 `capability` 条目,
35+
`allowRuntimeCreate: false` + `allowOrgOverride: false`**。ADR-0066 D1:包
36+
DEFINE capability,权限集 GRANT,资源 REQUIRE。管理员在运行时凭空造一个
37+
capability 在这个三分里没有位置——代码里不会有任何地方 require 那个名字,这行
38+
只是授权命名空间里一个无人引用的授予目标。这一对标志就是 #5086 的 CODE-ONLY
39+
声明,`saveMetaItem`**任何** kernel 上都以 403 `not_creatable` 拒绝,并从条
40+
目自己的 `filePatterns[0]` 读回「该去哪儿声明」。`supportsOverlay: false`——
41+
capability 只是名字/标签/scope,没有 merge 语义,而允许租户 overlay 一个包发布
42+
的声明等于允许把 `scope``org` 抬成 `platform``loadOrder: 12` 早于
43+
`permission`/`position`(15),使权限集的 `systemPermissions` 解析时 capability
44+
已经存在。
45+
- **`MetadataTypeSchema` 枚举补 `'capability'`**
46+
- **`CapabilityDeclarationSchema` 声明 ADR-0010 保护信封并收紧为 `.strict()`**
47+
信封是必须的:loader 对每个已注册类型都调 `applyProtection`,不声明就会 422 掉
48+
loader 自己的输出(#4001`permission`/`position` 上补过同一个洞)。收紧则与
49+
`api` 不同——`ApiEndpointSchema` 同时是**存储行**的解析器,所以它留在
50+
`STILL_STRIP`;而没有任何地方拿这个 schema 重新解析 `sys_capability`
51+
(`bootstrapDeclaredCapabilities` 通过 `capabilityRowFields` 按名读字段),
52+
所以收紧零成本,买到的是一个授权面本就该有的 declared = enforced 姿态。
53+
改用 `strictObject` 书写,已知键从 shape 派生,不新增手抄键表。
54+
55+
**包声明通道完全没动。** `AppPlugin` 通过 `registerInMemory` 注册 stack 的
56+
`capabilities[]`,文件系统 loader 按 `filePatterns` glob——两条都不经过
57+
`saveMetaItem`,所以 `bootstrapDeclaredCapabilities` 依旧照常 seed。
58+
`OS_METADATA_WRITABLE=capability` 仍是 ADR-0005 那唯一一道运维逃生门,而在它后面
59+
写入现在由 `CapabilityDeclarationSchema` 判定(422),不再原样落盘。
60+
61+
`role` / `profile` / `policy` **不搭车**:它们没有 `PLURAL_TO_SINGULAR` 映射、
62+
没有声明 schema、没有读回接缝,是另一个问题,另开单。这条以断言形式钉在
63+
`capability-metadata-kind.test.ts` 里,因为「capability 有了条目,邻居也该有」
64+
正是下一个显而易见却错误的改动。
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/metadata-protocol": patch
4+
"@objectstack/runtime": patch
5+
---
6+
7+
fix(spec,metadata-protocol,runtime): one place decides what an unset `NODE_ENV` advertises (#5936)
8+
9+
A deployment whose operator never exported `NODE_ENV` must not describe itself as
10+
`development` on `/discovery`: `environment` is a machine-readable field, a client
11+
reads it to answer "am I talking to production?", and it may skip production warnings
12+
or loosen a destructive action's confirmation on the answer. #5673 ruled that in and
13+
fixed it — but only for one of the two producers, because that dispatch put
14+
`packages/spec` out of scope. The other one, `MetadataProtocol.getDiscovery()` (served
15+
by `@objectstack/rest`), went on answering `development` for exactly that input.
16+
17+
The default now lives in the shared mapper, `resolveDiscoveryEnvironment`: an absent —
18+
or blank — value resolves to `production`, and both producers pass the operator's value
19+
through as they read it, neither carrying a default of its own. That is what makes it
20+
one decision instead of two copies, and it means the next discovery producer inherits
21+
the right answer without anyone remembering to copy a line. Patching only
22+
metadata-protocol would have left a second copy of the default — precisely the drift the
23+
shared table was created to prevent (#4828).
24+
25+
"Unset" includes a blank value: `NODE_ENV=` exports an empty string, the runtime's
26+
`getEnv` has always folded that into its default, and had the mapper treated blank as
27+
"anything else" the two producers would have drifted again on that one input.
28+
29+
**#4828's rule is untouched, and it points the other way on purpose.** A value that IS
30+
set but is not a spelling this repo recognises (`qa`, `preview`) still degrades to
31+
`development`, so nothing ever claims `production` on a guess. Absence is not a guess —
32+
it is the host declining to say.
33+
34+
Behaviour change to expect: a host that exports no `NODE_ENV` and serves `/discovery`
35+
through `@objectstack/rest` now advertises `environment: "production"` where it
36+
previously advertised `"development"`. A deployment that genuinely is development should
37+
say so — `NODE_ENV=development` — which is what the runtime dispatcher has already
38+
required since #5673.
39+
40+
The mapping table above `NODE_ENV_TO_DISCOVERY_ENVIRONMENT` is corrected in the same
41+
pass: its `unset / anything else -> development` row had been false for the runtime
42+
caller since #5673 and is now two rows, one per rule.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: retire the L2 ETL layer — `automation/etl.zod.ts` had no executor, and the sync architecture doc was recommending it (#6414)
6+
7+
`ETLPipeline`, `ETLPipelineRun`, `ETLSource`, `ETLDestination`, `ETLTransformation`,
8+
the `ETLEndpointType` / `ETLTransformationType` / `ETLSyncMode` / `ETLRunStatus`
9+
enums and the `ETL` factory are REMOVED under ADR-0049 enforce-or-remove. The whole
10+
file goes, on the same reading #4738 used to retire L1 `DataSyncConfig` one layer up:
11+
**narrative-only**. No engine ever parsed, scheduled or executed an `ETLPipeline`.
12+
13+
Measured on `origin/main` immediately before the removal: the only non-spec
14+
references in this repo are two fumadocs-generated documentation sources
15+
(`apps/docs/.source/*.ts`), not executors; objectui has no reference at all; and
16+
there is no `packages/spec/liveness/etl.json`, so no ADR-0049 gate ever had a reading
17+
on the surface — while the same file family's EXECUTED half does have one
18+
(`liveness/mapping.json`), which is what makes that absence meaningful rather than an
19+
oversight.
20+
21+
FROM → TO, layer by layer — with one gap stated plainly instead of redirected:
22+
23+
| removed | use instead |
24+
|---|---|
25+
| `ETLPipeline.source` + `syncMode` + `schedule` (scheduled extraction from an external system) | `ConnectorSchema.syncConfig` (`integration/connector.zod.ts`) — the live, parsed sync surface: strategy, direction, cron schedule, `conflictResolution`, batching, delete mode |
26+
| `ETLTransformation` of type `map` / `cast`-like per-field work | `mapping.fieldMapping[].transform` (`data/mapping.zod.ts`) — `none`/`constant`/`map`/`split`/`join`/`lookup`, applied row by row by the REST import path |
27+
| `ETLPipeline.schedule` alone | `system/job.zod.ts` |
28+
| `ETLTransformation` of type `join` / `aggregate` / `script` / `merge` / `deduplicate` / … | **nothing.** There is no replacement because there was never an implementation — those ten transformation types named capabilities no runtime had. Do the work where it runs (the destination warehouse's ELT, a `flow`, a scheduled job), and let multi-stage movement return through ADR-0049's ENFORCE route: the engine first, the vocabulary second |
29+
30+
**The fix:** delete the import. Nothing was ever deployed under an `ETLPipeline`
31+
that is the finding, not a consolation — so there is no data migration; `tsc` reports
32+
TS2724/TS2305 at every import of a retired name.
33+
34+
**`packages/spec/docs/SYNC_ARCHITECTURE.md` is rewritten in the same change**, and
35+
that is not incidental. It named `ETLPipeline` as the recommended destination for
36+
authors displaced by the L1 retirement and tabulated ten transformation types with
37+
copyable examples down to `script | Custom JavaScript/Python`. Retiring the schema
38+
while the doc still recommended it would have been self-contradictory, and
39+
forwarding L1's authors to a second layer with no executor was the defect compounding
40+
rather than closing.
41+
42+
**Absorbed:** the #4962 `etl-retry-converged-onto-retry-policy` entry (`retry.maxAttempts`
43+
`maxRetries`, default 3 → 0) — both land in the unreleased protocol 17, so composed,
44+
a rename on a shape that does not survive the major has no observable effect, and its
45+
`retiredKey()` tombstone goes with the shape that carried it.
46+
47+
The retirement kit — route 3: no tombstone, no D2 conversion.
48+
`RETIRED_DEFS_BY_MAJOR[17]` (9 defs) plus the D3 `SemanticMigration`
49+
`etl-pipeline-layer-retired` are the declaration.
50+
51+
<!-- adr-0087: registered etl-pipeline-layer-retired -->
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: retire `system/http-server.zod.ts`'s runtime vocabulary — the event, capability and status shapes nothing ever emitted (#5295)
6+
7+
`ServerEventType`, `ServerEventSchema` / `ServerEvent`, `ServerCapabilitiesSchema` /
8+
`ServerCapabilities` / `ServerCapabilitiesParsed` and `ServerStatusSchema` /
9+
`ServerStatus` are REMOVED under ADR-0049 enforce-or-remove. This is the second and
10+
final pass over the file: #4938 removed its CONFIG half (`HttpServerConfigSchema`,
11+
nine keys, zero readers, zero authoring entry), and this removes the RUNTIME half —
12+
a 7-member lifecycle event union, an eight-boolean capability report and a
13+
five-state status record with connection and request counters. Nothing ever emitted,
14+
consumed or parsed any of them.
15+
16+
FROM → TO:
17+
18+
| removed | what actually decides it |
19+
|---|---|
20+
| `ServerEventType` / `ServerEvent(Schema)` | nothing emits a server event feed. Lifecycle is the transport plugin's own start/stop seam; observability is `system/metrics.zod.ts` + `system/logging.zod.ts`, and `OS_SERVER_TIMING` for timings |
21+
| `ServerCapabilities(Schema/Parsed)` | a transport plugin declares what it provides by implementing the kernel plugin contract — the seams it registers ARE the capability statement |
22+
| `ServerStatus(Schema)` | `/health` for liveness, the metrics surface for counters |
23+
24+
**The fix:** delete the import. There is no replacement key, because there was
25+
never a key — none of the four was authorable on any shape. Server-level
26+
configuration that IS authorable is untouched: `defineStack({ server: { trustProxy,
27+
security } })` / `StackServerConfigSchema` (#5006) parses exactly as it did in 16.x,
28+
as does the route-registration half of the same module (`RouteHandlerMetadata`,
29+
`MiddlewareType`, `MiddlewareConfig`).
30+
31+
**Why now, and what unblocked it.** The card was held rather than queued on a real
32+
doubt: a response/capability vocabulary can legitimately be a REFERENCE surface for
33+
host implementers, so "zero consumers in this repo" is weaker evidence for one of
34+
those than for an authorable key. It was lifted by measuring the reference reader
35+
itself — `plugin-hono-server`, the one in-tree host implementation, neither
36+
implements nor reports any of the three: it names no capability record, no status
37+
shape and no event union, and what it registers is routes and middleware. The
38+
control passed in the same sweep (`MiddlewareConfig`, twelve lines away, resolves to
39+
`packages/runtime/src/middleware.ts`).
40+
41+
The retirement kit — route 3 of the retirement playbook, as #4938 was in this same
42+
file: **no `retiredKey()` tombstone and no D2 conversion**, because a prescription
43+
nobody can receive is noise and there is no authored document to rewrite.
44+
`RETIRED_DEFS_BY_MAJOR[17]` (4 defs) plus the D3 `SemanticMigration`
45+
`http-server-runtime-vocabulary-retired` are the declaration; the generated
46+
baselines (`json-schema.manifest/system.json`, `authorable-surface/system.json`,
47+
`api-surface/system.json`) lose their entries in the same change, deliberately.
48+
49+
If host-implementer conformance becomes a real requirement it returns through the
50+
ENFORCE route: an adapter contract with a checker behind it, vocabulary second.
51+
52+
<!-- adr-0087: registered http-server-runtime-vocabulary-retired -->
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
i18n label contract: inline locale maps are authorized, and filter-preset tabs get a translation slot
7+
8+
**`I18nLabelSchema` accepts two forms, not one (#5728).** It declared a plain
9+
`z.string()` while three published platform pages authored 31 inline
10+
`{ en, 'zh-CN', 'ja-JP', 'es-ES' }` maps that objectui resolves through
11+
`pickLocalized` — so the authoritative document was the wrong one, and the
12+
#5068 component-props gate reported 42 findings against the platform's own
13+
pages. The schema is now a union of the default-language string and an inline
14+
locale map. `ElementTextPropsSchema.content` was declared a bare `z.string()`
15+
and therefore out of that union's reach; it moves onto `I18nLabelSchema` in the
16+
same change, which is the other 8 of the 42. The gate now reports **0**.
17+
18+
This does not reverse #4667 / #5055. What those retired was the *key-reference*
19+
dialect (`{ key, defaultValue }`) — a shape with **no resolver**, whose label
20+
reached the screen as a raw key or not at all. What is authorized here is the
21+
inline locale map, which has a live resolver and which the CLI's `i18n-extract`
22+
already understands. Same "declared = enforced" principle, applied in both
23+
directions: the map's keys are constrained to BCP-47 tags (plus `default`), so
24+
`{ key, defaultValue }` stays a parse error rather than becoming "a locale map
25+
whose locales are named `key` and `defaultValue`".
26+
27+
Zero breaking: every previously-valid label is still valid. The
28+
translation-bundle channel remains the direction that scales and is unchanged.
29+
30+
**Filter-preset tab labels are translatable (#5377).** `ObjectTranslationData`
31+
gains `_tabs`, addressed by `ViewTabSchema.name`, and `resolveTabLabel` reads
32+
it — explicit `_tabs` translation, then the referenced view's `_views.*.label`
33+
for a tab that carries `view` (the path that already worked, preserved), then
34+
the authored literal. A tab carrying only a `filter` referenced nothing to
35+
inherit from and had no key of its own, so its label rendered in the source
36+
language above a fully localized grid with no authoring workaround. `os i18n
37+
extract` scaffolds the new keys, so the slot, the resolver and the extractor
38+
land together.
39+
40+
`I18nLabelSchema`'s description no longer claims "i18n keys are auto-generated
41+
by the framework" — none are. `AriaPropsSchema.ariaLabel` now states that no
42+
translation-bundle slot addresses it.

0 commit comments

Comments
 (0)