Skip to content

Commit ac0d3be

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-5928-hook-exclude-objects
2 parents d1ab7df + 57aa0bb commit ac0d3be

127 files changed

Lines changed: 6836 additions & 3767 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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/rest": patch
3+
"@objectstack/objectql": patch
4+
---
5+
6+
fix(rest,objectql): the import dry run asks the engine for its verdict instead of predicting it (#4633 ruling D)
7+
8+
`POST /api/v1/data/:object/import?dryRun=true` green-lit rows the very same
9+
endpoint then rejected. Measured on 17.0.0-rc.1: a CSV cell aimed at a
10+
structured `address` field reported `{ ok: 1, created: 1 }` on the dry run and
11+
`{ errors: 1, code: 'VALIDATION_FAILED' }` on the real write.
12+
13+
The dry run predicted the write's verdict with a hand-copied mirror of a slice
14+
of the engine's rules (`import-coerce.ts`'s `firstMissingRequiredField` and
15+
`firstConstraintViolation`). A copy cannot structurally keep up with the family
16+
it mirrors: ADR-0104 value shapes (`address` / `location` / references / media),
17+
`format` checks, object-level `validations` and the state machine had no
18+
counterpart, and `coerceFieldValue` routes structured shapes through its
19+
pass-through catch-all, so no verdict was formed at all.
20+
21+
**The mirror is retired.** The dry run now calls `DataProtocol.validateData`
22+
(#6037), which runs the same `validateRecord` / `evaluateValidationRules` that
23+
`insert()` runs, under the deployment's own ADR-0104 posture — so a bad value
24+
shape is an error on a self-certified deployment and an admitted warning on a
25+
warn-first one, exactly as on the write. Agreement is by construction, not by a
26+
copy kept in step by hand.
27+
28+
Also in this change:
29+
30+
- **`engine.validate()` now resolves `defaultValue`s and seeds owned roll-up
31+
`summary` fields before validating, on `insert` mode**, because `insert()`
32+
does. Without it a required-but-defaulted column left unmapped was previewed
33+
`failed` and written `created` — a false alarm on the row a preview is meant
34+
to reassure you about. `update` mode still does not default (#2706).
35+
- **A row report failed by validation now names the offending column.** The
36+
engine's `ValidationError` carries `fields[]`, so the row's `field` is set and
37+
its `code` is the field-level code (`required`, `min_value`, `max_length`,
38+
`invalid_type`, …) rather than the wrapper's `VALIDATION_FAILED`. This is the
39+
same vocabulary the dry run and the per-cell coercion failures already spoke;
40+
before, a `min: 0` violation was `min_value` on the dry run and
41+
`VALIDATION_FAILED` on the write.
42+
- **Dry-run rows may carry `warnings[]`** — findings this deployment admits
43+
rather than rejects (ADR-0104 warn-first). The row is `ok`, and the complaint
44+
is visible instead of living only in a server log line.
45+
46+
A protocol that does not implement `validateData` (plugin-auth's identity
47+
import, whose write is better-auth rather than the engine) is not handed a
48+
substitute: its dry run reports coercion and create/update/skip resolution only.
49+
An engine-derived preview of a non-engine write would report findings that write
50+
never produces.

0 commit comments

Comments
 (0)