Skip to content

Commit 9726089

Browse files
committed
Merge origin/main into claude/issue-6479-patch-path-id-wins
2 parents f3d4d82 + bb7cb41 commit 9726089

151 files changed

Lines changed: 9529 additions & 2938 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
Remove `step: 0.1` from the `ai.temperature` specifier (#6550). Since #6199 a declared `step` binds as a value constraint on both doors, and temperature's true domain is continuous on [0, 2]: the 0.1 grid refused legal values — `PUT /api/settings/ai` with `temperature: 0.15` was rejected and `OS_AI_TEMPERATURE=0.15` was loudly ignored. Both now work; `min: 0` / `max: 2` stay and keep binding (out-of-window values are still refused in the min/max vocabulary). #6199's grid machinery is untouched and still enforces any key that declares `step`.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata-protocol": patch
4+
---
5+
6+
feat(spec): `Field.autonumber` declares the field `readonly: true` (#5628)
7+
8+
`FieldSchema.readonly` is a **two-part** contract: "never editable in forms"
9+
AND server-enforced on both write paths. #5503 closed the server half for
10+
`autonumber` **by type** — a caller-supplied record number is stripped before
11+
any driver sees it, flag or no flag. The form half is keyed on the **flag**, and
12+
`Field.autonumber` never set it. So an authoring/rendering layer that decides
13+
editability from `field.readonly` drew an editable "record number" input whose
14+
value the server was already guaranteed to discard: the user types one, the
15+
create succeeds, and the record comes back carrying the number the sequence
16+
issued instead. Data was never at risk (that half has been enforced since
17+
#5503/#5627); what was wrong is what the form told the user.
18+
19+
`Field.autonumber(...)` now emits `readonly: true`. The injection is applied
20+
**after** the author's config, so it cannot be spread away, and the authoring
21+
type rejects the one config that contradicts it — `Field.autonumber({ readonly:
22+
false })` is a **compile error** rather than a silently coerced value, because
23+
an "editable record number" is not a state the runtime can deliver. Restating
24+
`readonly: true` stays legal. A hand-written `{ type: 'autonumber' }` literal
25+
(YAML/JSON metadata, or a plain object in TS) is unchanged and unaffected: it is
26+
covered by the by-type server enforcement, which never depended on the flag.
27+
28+
Two consequences worth knowing:
29+
30+
- **A flow that writes an autonumber field is now caught at `os validate`.**
31+
`flow-update-readonly-field` reads the static flag, so an `update_record` node
32+
writing a builder-authored record number — already a silent no-op at run time
33+
— is now reported at design time instead of in server WARN logs.
34+
- **The historical-import exemption is unchanged**, and stays that way by
35+
construction. The DataProtocol create ingress (`stripReadonlyForInsert`,
36+
#3043) knows only the `isSystem` exemption, while the engine's runtime-owned
37+
strip also honours `preserveAudit` (#3493 — a migration reinstating legacy
38+
record numbers). Now that the field carries the flag, the ingress would have
39+
deleted that value *before* the engine could keep it, so the ingress skips
40+
runtime-owned field types outright and leaves them to the engine strip, which
41+
runs on every insert path (including the direct `engine.insert` callers the
42+
ingress never sees). Author-declared `readonly` on every other field type is
43+
stripped at the ingress exactly as wide as before.
44+
45+
The set backing "which types the runtime owns" is now declared once in the
46+
protocol — `RUNTIME_OWNED_FIELD_TYPES`, exported from `@objectstack/spec/data`
47+
— and read by both consumers (objectql's write-path strips, the DataProtocol
48+
ingress) instead of each carrying its own literal.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/lint": patch
4+
---
5+
6+
feat(spec): `ChartAggregateSchema` / `ChartGroupBySchema` reject unknown keys instead of dropping them (#5583, #4001 批 15's last two sites)
7+
8+
`<ObjectChart aggregate={{ … }}>` is the react tier's object-bound chart binding,
9+
and until now a key it did not declare was **silently stripped by the parse**.
10+
`groupby` for `groupBy` degraded the chart to a single ungrouped point, `fn` for
11+
`function` fell back to the default, `dateGranularty` for `dateGranularity`
12+
turned off date bucketing — each with `os build` / `os validate` fully green.
13+
That is #4001's founding failure mode, on the surface an AI page author is most
14+
likely to write.
15+
16+
Both object shapes are `strictObject` now, so an undeclared key is a named
17+
rejection carrying the surface, the offending key and a rename:
18+
19+
```
20+
Unrecognized key(s) on this chart aggregate: `groupby`.
21+
Did you mean `groupby` → `groupBy`? Until #5583 an undeclared aggregate key was
22+
dropped at parse — …
23+
```
24+
25+
Curated beyond edit distance where the near-miss is semantic rather than a typo:
26+
`fn` / `agg` / `aggregation``function`, `measure``field`, and the ADR-0021
27+
dataset vocabulary an author carries over from the other binding mode
28+
(`dimension` / `category``groupBy`). Wrong-LAYER keys get a prescription
29+
instead of a rename — `dateGranularity` written *beside* `groupBy` did nothing at
30+
all and now says where it belongs; `alias`, `filter`, `objectName` and a
31+
`measures` array are pointed at the surface that owns them.
32+
33+
**Why this took two issues.** `.strict()` is a property of a PARSE, and until
34+
#5020 nothing parsed these schemas: the react-page publish gate re-derived the
35+
vocabulary by hand. Closing them first would have shipped a precisely-validated
36+
door with nothing behind it (#4583). #5020 wired the parse; this is the posture.
37+
38+
**The zod-4 union collapse is load-bearing here.** `groupBy` is a union, so the
39+
`unrecognized_keys` its strict arm raises never reaches `error.issues` — zod
40+
reports one `invalid_union` whose own message is the bare string `"Invalid
41+
input"`. What carries the named rejection to the author is `packages/lint`'s
42+
`describeIssue` arm unpacking, pinned end to end on both sides.
43+
44+
**`groupBy` stays REQUIRED — the product question this pair raised is answered,
45+
and the answer does not move the schema.** An ungrouped single-value chart is
46+
not a supported `<ObjectChart>` shape: the single-value need is served by the
47+
separate `object-metric` block, the example corpus authors zero ungrouped
48+
`<ObjectChart>` aggregates, and objectui's `schema.aggregate?.groupBy ||
49+
schema.xAxisKey` reads are optional-chained on `aggregate` itself — they serve
50+
charts with **no aggregate at all**, not ungrouped ones. #5020's `warning`-level
51+
tolerance for an absent `groupBy` therefore stays a tolerance rather than
52+
becoming a blessing; its hint now states the ruling.
53+
54+
**Upgrading:** if a chart aggregate carried a key this schema does not declare,
55+
it was already being ignored — the rejection names it and prescribes the fix. No
56+
legal declaration changes meaning.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): make the dashboard date-range preset names a single vocabulary and check a date filter's `defaultValue` against it (#4614)
6+
7+
A dashboard's built-in `dateRange` validated its preset name and a
8+
`globalFilters` entry of `type: 'date'` did not, so the same typo was an
9+
author-time error on one surface and a silent wrong answer on the other.
10+
11+
`GlobalFilterSchema.defaultValue` is `string | number | boolean`, which makes a
12+
bare preset name the only spelling available for a date filter's default —
13+
and nothing checked it. An unrecognised name cannot be lifted to a range, so it
14+
fell through to "a bare string date means equality on that day" and reached the
15+
backend as `created_at = 'last_7_dayz'`: a condition no row matches, answered
16+
`200 OK` with a zero. Every tile read `0` while the filter bar showed
17+
"All time", so the dashboard looked deliberately empty rather than
18+
misconfigured — the failure mode that costs the most time to diagnose, and the
19+
one an AI author reads as a correct answer and builds on.
20+
21+
- **`DATE_RANGE_PRESETS`** (+ the `DateRangePreset` type) is new in
22+
`@objectstack/spec/ui` and is now the vocabulary's single source of truth.
23+
The thirteen names existed three times before this: inline in
24+
`dateRange.defaultRange`, as `PRESET_RANGES` in objectui's
25+
`dashboard-filters` (the module that maps each name to its date-macro
26+
bounds), and as a hand-written table in the dashboard docs.
27+
- **`DATE_RANGE_DEFAULT_RANGES`** (+ `DateRangeDefaultRange`) is the presets
28+
plus the `custom` sentinel, and is what `dateRange.defaultRange` now reads.
29+
`custom` is deliberately not a preset — it names no window, it opens the
30+
picker — so it stays legal there and is rejected as a bare filter default,
31+
which has no `from`/`to` for it to hand over. `defaultRange`'s accepted set
32+
is otherwise unchanged by the extraction, and a test asserts that member for
33+
member.
34+
- **`GlobalFilterSchema` gained a `superRefine`**: on `type: 'date'`, a
35+
declared `defaultValue` must be a preset name, an ISO date (`2026-01-15`,
36+
optionally with an instant), or a known date-macro token (`{today}`,
37+
`{30_days_ago}`). The macro half asks `isDateMacroToken` rather than
38+
restating its grammar, so there is one token vocabulary and no second dialect
39+
to drift. The rejection quotes the offending value back and lists all three
40+
legal spellings, because a dashboard with several date filters otherwise
41+
gives no clue which one is wrong. Every other filter type is untouched — a
42+
`select` filter's values are the author's own vocabulary.
43+
44+
**Existing metadata is unaffected.** The tree's only date-filter default is
45+
`system_overview.dashboard.ts`'s `last_7_days`, which is a valid preset and is
46+
pinned by a test; a corpus scan of the three example apps and the docs found no
47+
misspelled preset name, so no ADR-0087 conversion is required. The accepted set
48+
is a strict superset of what objectui's renderer resolves today, so no
49+
declaration that used to render can stop parsing.
50+
51+
The new exports and the `.describe()` on `defaultRange` are additive; the only
52+
authorable behaviour that changes is that a value which previously parsed and
53+
then silently resolved to nothing is now an author-time error.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os doctor` stops guessing the installed-package ledger directory when the authority export is missing (#5996)
6+
7+
Hardens a diagnosis boundary; not a live defect. `DEFAULT_INSTALLED_PACKAGES_DIR``@objectstack/cloud-connection`'s export, the single authority on what the ledger directory is called — exists in every version ever shipped, so the consumer-side `??` fallback this change deletes had never fired. It sat two lines above the #5413 comment forbidding exactly that tolerant read (Prime Directive #12), and it answered the wrong state: a reader that LOADS without declaring the export would have doctor silently reading a hard-coded path while the runtime keeps reading wherever the package decides — two reports, potentially two directories, no line saying so.
8+
9+
Three changes, one authority:
10+
11+
- The `??` fallback is gone. The export is type-checked before `path.join()` ever sees it, which also retires the old misreport where a non-string export was absorbed by the config `catch` and surfaced as "Could not load config for analysis".
12+
- A reader that loads without declaring the directory (as a string) is now its own named report row — "The installed-package ledger reader does not declare the ledger directory (installed packages NOT checked)" — the last cell of the edge #5644 carved: that issue split "present but unloadable" out of absence's silence; this row is "loaded but unrecognizable". While it shows, doctor reads no directory at all — guessed or otherwise — and the ADR-0120 D5e advisory withholds its `✓ Unique scope` line, because its ledger half never ran.
13+
- `installedPackageLedgerSkippedEntriesCheck`'s fix now quotes the directory doctor actually read — resolved from the real export and carried on the reading — instead of re-hardcoding the ``Under `.objectstack/installed-packages/`:`` literal, which was the same guess in prose.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-settings': patch
3+
---
4+
5+
The settings env door now enforces declared `pattern` constraints (#6580). An
6+
`OS_*` override whose value the specifier's `pattern` rejects is loudly
7+
reported (`error` log, once per var+value) and ignored — the key resolves from
8+
the next cascade layer and is not locked — exactly the #5204 contract the
9+
option-table, value-window/step and valueDomain families already honor. The
10+
write gate's judgment is hoisted into shared helpers (`declaredPattern` /
11+
`firstPatternMiss`) called by both doors, so `PUT /api/settings/:ns` behavior
12+
is unchanged byte-for-byte (same `invalid_format` envelope, same tolerance for
13+
uncompilable pattern declarations) and the two doors can no longer drift.
14+
Family ordering agrees between doors: options → pattern → valueDomain → bounds.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/runtime": patch
4+
---
5+
6+
fix(spec,runtime): `functions: [{ name, handler }]` survives `objectstack build` (#6238)
7+
8+
The array form of the top-level `functions` collection could not pass its own
9+
build. `lowerCallables` has lowered the array branch the whole time — it rewrites
10+
both `handler` and `name` to the emitted ref — but the array member of the
11+
`functions` union in `stack.zod.ts` still demanded `handler: z.function()`. So
12+
`objectstack build` produced
13+
`[{ name: 'syncBilling', handler: 'syncBilling', effect: 'writes' }]` and then
14+
rejected it, with `invalid_union: Invalid input` and a path stopping at
15+
`functions`: no entry named, no key named, no reason given.
16+
17+
This is the third time the same seam has parted, and the first two fixes are why
18+
this one only looks small. #4343 taught the union the bare lowered ref; #4976
19+
taught it the lowered *declaration*. Both only ever touched the **map** member —
20+
the array member is a separate inline record (an array entry names itself, so it
21+
carries `name` and an optional `packageId` and cannot be `FlowFunctionEntrySchema`
22+
in a list), and widening one never widened the other.
23+
24+
**The fix.** The array member's `handler` now accepts the lowered string ref
25+
beside the authored callable. One widening covers both array spellings at once,
26+
unlike the map form's two separate members: `effect` is already optional on an
27+
array entry, so the bare and the declared entry differ only in whether that key
28+
is present. All four cells of map/array × bare/declared now round-trip.
29+
30+
**The load seam, which the fix made reachable.** `mergeRuntimeModule` re-attaches
31+
each callable from the sibling ESM module to the declaration the JSON carried.
32+
Its array branch fell through to a map rebuild — `existing` was `{}` whenever
33+
`bundle.functions` was an array — so the merged bundle came back as a bare
34+
`{ name: callable }` map with `effect: 'writes'` dropped on the floor. The
35+
function still registered and still ran, and its writes were counted as none:
36+
#4396's silent un-declaring arriving by the other door, and exactly the state
37+
that keeps #4354's broken-sweep alert quiet on the one run that needed it. Since
38+
the parse rejected the array form until now, no built artifact had ever reached
39+
that branch; it is fixed in the same change rather than shipped as a live trap.
40+
The array shape is preserved, callables are attached per entry `name`, and a
41+
module function the artifact declared no entry for still registers — the map
42+
branch keeps those, and the array branch must not ship fewer functions than the
43+
bundle was built with.
44+
45+
Authoring is unchanged and nothing narrows: this widens what the artifact form
46+
accepts. The map form is still the preferred spelling.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
'@objectstack/service-automation': major
3+
'@objectstack/spec': major
4+
'@objectstack/runtime': patch
5+
---
6+
7+
feat(automation)!: 未声明 `resumeAuthority` 的暂停节点改为 fail-closed —— 通用 resume 路由从「默认开门」变成「显式 `'any'` 才开门」(#5561 第二步)
8+
9+
<!-- adr-0087: registered action-descriptor-resume-authority-default-flip -->
10+
11+
**BREAKING**(仅影响注册了暂停型节点、且描述符未声明 `resumeAuthority` 的执行器 ——
12+
本仓内为零)。`AutomationEngine.resolveResumeAuthority` 对缺省值的解析由 `'any'` 翻成
13+
`'service'`:一个从未声明「谁可以续跑它产生的暂停」的节点类型,其暂停在通用路由
14+
`POST /automation/:name/runs/:runId/resume` 上被拒绝(`PERMISSION_DENIED` / 403),
15+
直到它的描述符把话说出来。通用 resume 门从此是描述符**主动 opt-in** 的一扇门,不是每个
16+
暂停节点**继承**来的默认。
17+
18+
这是 ADR-0044 2026-07-28 修正案里「记录但刻意不在此建造」的第一项,分两步落地。
19+
第一步(#5561 / PR #5725,非 breaking)把 `ActionDescriptorSchema.resumeAuthority`
20+
的 Zod `.default('any')` 摘成 `.optional()`。那个默认值的问题不只是取值不对,而是它
21+
**抹掉了事实**:`defineActionDescriptor` 在任何消费者看到对象之前就把 key 填上了,于是
22+
「作者选了 `'any'`」和「作者从没考虑过」parse 出逐字节相同的描述符,遗漏根本无法被观测。
23+
默认值摘掉之后「缺省」才重新可见,注册告警与 `check:resume-authority-declared` CI 门也
24+
才写得出来。第二步就是本次改动:让缺省真正意味着 fail-closed。
25+
26+
### 为什么往「拒绝」这个方向猜
27+
28+
两种猜错的代价不对称,这就是全部理由。猜 `'any'`,会让一次 resume 走过一个**没有任何
29+
记录的决策**,而且悄无声息 —— #3823 就是这么发生的:ADR-0044 把审批的 `revise` 边指向
30+
了通用 `wait`,`wait` 本身声明 `'any'` 完全正确,而站在「服务持有」位置上的那个暂停
31+
继承了一个没人选过的 fail-open 值;实测代价是一次未经审计的重新提交,外加一个被销毁的
32+
远程 run。猜 `'service'`,则是返回一次拒绝,并把修好它的那一行原样交回作者手里。
33+
两种错误里只有一种能被犯错的人自己发现。
34+
35+
### 迁移:`resumeAuthority` 未声明 → 显式声明(一行)
36+
37+
只有**注册暂停型节点的插件作者**需要动手,处方是在描述符上加一行:
38+
39+
```ts
40+
// FROM —— 依赖旧默认值,暂停可被通用路由续跑
41+
defineActionDescriptor({
42+
type: 'my_pause', version: '1.0.0', name: 'My Pause',
43+
supportsPause: true,
44+
});
45+
46+
// TO —— 通用路由确实是这个暂停的正门时(screen 式收集输入、signal wait 式外部生产者)
47+
defineActionDescriptor({
48+
type: 'my_pause', version: '1.0.0', name: 'My Pause',
49+
supportsPause: true, resumeAuthority: 'any',
50+
});
51+
52+
// TO —— 续跑是「某个服务必须先授权并记录的决策」的尾巴时
53+
defineActionDescriptor({
54+
type: 'my_pause', version: '1.0.0', name: 'My Pause',
55+
supportsPause: true, resumeAuthority: 'service',
56+
});
57+
```
58+
59+
两个值都被接受,**只有沉默改变了含义**。三条运行时通道会指着同一件事说话:注册时按类型
60+
去重的一次告警、resume 被拒时那条点名缺省字段并给出处方的错误消息,以及本仓自有执行器的
61+
`check:resume-authority-declared` CI 门。
62+
63+
⚠️ `supportsPause` 本身是一个没有任何执行路径强制的声明(#5703)—— run 会暂停是因为
64+
`execute()` 返回了 `suspend: true`。所以一个「会暂停但把 `supportsPause` 留成 false」
65+
的执行器,注册告警与 CI 门**都看不见它**,只有 resume 时的拒绝消息会带上同一份处方。
66+
请按同一条规则手工核一遍这类执行器。
67+
68+
### 仓内零行为变化
69+
70+
在册的六个暂停类型全部已显式声明:`screen` / `wait` / `subflow` / `map` 声明 `'any'`
71+
(第一步补齐),`approval` / `approval_revise` 声明 `'service'`。解析器测试与端到端测试
72+
都把这份清单和它们的解析结果一起断言 —— 一个只靠「什么都没注册」而变绿的零点名,和真的
73+
零点名是两回事。
74+
75+
`@objectstack/runtime` 只是注释与路由账本(`route-ledger`)的记述同步,无行为改动。

0 commit comments

Comments
 (0)