Skip to content

Commit 0f4a80e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14256-declarative-job-outcome
2 parents 339d7d3 + 655b106 commit 0f4a80e

36 files changed

Lines changed: 2569 additions & 110 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
"@objectstack/metadata": patch
3+
"@objectstack/core": minor
4+
"@objectstack/objectql": patch
5+
"@objectstack/runtime": patch
6+
---
7+
8+
fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)
9+
10+
A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
11+
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
12+
iterated the **flattened top level** and stamped every item with the artifact's
13+
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
14+
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
15+
pick — so a two-package artifact registered the **module's** object under the
16+
**App** package's identity, while the ObjectQL load path, reading the same
17+
artifact's `packages[]`, owned it under the module's.
18+
19+
The platform then held two answers to "who owns this object", and which one a
20+
consumer saw depended on the door it went through. Measured on a real boot of
21+
`examples/app-multi-package`:
22+
23+
- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
24+
slots by `${packageId}${name}`, so the two differently-attributed copies
25+
landed in two slots;
26+
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
27+
object, because the App-stamped copy was re-ingested into the registry as that
28+
package's contribution;
29+
- the layers door named the App package while the item door and
30+
`GET /api/v1/packages` named the module;
31+
- Studio's Data pillar for the App package listed the module's object — ADR-0130
32+
Consequences §1.3a ("Studio's scope is the package") did not hold.
33+
34+
**The door now reads both shapes, and attributes every item to the body it was
35+
found in.** `packages` present → each assembled package body's collections are
36+
registered stamped with **that body's** id; `packages` absent → the single
37+
`manifest` branch runs exactly as before (D7). The owner is read off the body an
38+
item was found in — never reverse-derived by matching a top-level item's name
39+
against a name-to-package index, which would be the second metadata-identity
40+
resolution path #14512's triage rejected by name.
41+
42+
**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
43+
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
44+
readers of one `packages[]` cannot disagree about the registration order **or**
45+
about which artifacts are loadable at all.
46+
47+
⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
48+
`@objectstack/core`** — hence the `minor` there. They were in
49+
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
50+
metadata door could not import them from where they lived; `@objectstack/core`
51+
already owns `resolvePluginOrder` and is already a dependency of both readers,
52+
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
53+
re-exports both under their existing names — its published surface is unchanged,
54+
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
55+
dispatcher error vocabulary's `file:` anchors, repointed at the new path.
56+
57+
**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
58+
than asserted: the whole `manager.register` sequence for a single-`manifest`
59+
artifact — every call, in order, with the id and version each item was stamped
60+
with — is pinned as a literal in
61+
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
62+
recorded identically on both legs of the ablation. A real boot of
63+
`examples/app-todo` answers every door identically before and after.
64+
65+
**Nothing a booted instance can see today disappears.** Every live
66+
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
67+
(measured, not assumed), so iterating bodies loses no collection; and because
68+
`packages` composes by `concat`, an artifact whose top level carries a
69+
definition no package body repeats keeps it — registered once, attributed to the
70+
artifact's own identity, and logged, because it means the artifact's two halves
71+
disagree about what it ships.
72+
73+
⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
74+
emitting the flattened top level alongside `packages[]`. Whether they should is
75+
#14512's decision, not this door's.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/plugin-auth": minor
3+
---
4+
5+
feat(plugin-auth): auth mail follows the caller's `Accept-Language`, deployment default second (#14319)
6+
7+
Request-triggered auth email — signup verification, password reset, magic link,
8+
and the change-email notice — now picks its `sys_email_template` row from the
9+
requesting caller's `Accept-Language`, falling back to the deployment default
10+
(`localization.locale`, then `i18n.defaultLocale`) and finally to
11+
`EmailService`'s documented `en-US`.
12+
13+
The motivating case is the one no deployment default can answer: at cloud
14+
self-service signup there is no workspace yet, so nothing on the server
15+
represents that person's language — a Chinese browser reached a Chinese signup
16+
screen and received an English verification email.
17+
18+
The header is parsed by the platform's existing `preferredLocaleFromHeader`,
19+
the same function REST uses for metadata translation and the runtime dispatcher
20+
uses for `ExecutionContext.requestLocale`, so the mail cannot disagree with the
21+
screen that triggered it. A requested locale takes effect only when it names one
22+
of `AUTH_EMAIL_TEMPLATE_LOCALES` (`en-US`, `zh-CN`, `ja-JP`, `es-ES`); anything
23+
else falls through rather than naming a row that does not exist.
24+
25+
Two deliberate exclusions. **Invitations keep the deployment default**:
26+
better-auth hands that callback a request too, but it is the *inviter's*, and
27+
stamping their browser language onto the invitee's mail would reproduce this
28+
same defect one seat over. **Per-user language stays deferred**`sys_user`
29+
grows no locale column here.
30+
31+
This ships as `minor` because it changes which template row an existing
32+
deployment sends: a workspace whose users' browsers ask for a different language
33+
than the workspace declares will now send in the browser's language.
34+
35+
**Ruling:** maintainer, 2026-09-02, superseding the 2026-08-13 ruling that had
36+
rejected `Accept-Language` outright. Both are recorded, with the older one
37+
marked superseded, on `AuthManager.setDefaultEmailLocale`.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): name the terminally-failed-but-repairable run on `AutomationResult.status``'stranded'` (#14384, contract half of #13937)
6+
7+
`AutomationResult.status` (`contracts/automation-service.ts`) gains a fourth
8+
member beside `'completed' | 'paused' | 'failed'`: **`'stranded'`** — the run
9+
whose resume CONSUMED its suspension and then had a downstream node throw, so
10+
the run is recorded as failed and can be re-armed only by an explicit operator
11+
verb (#13909's condition; the #13937 shape-4 ruling, maintainer 2026-09-01
12+
「命名同批定」). The wire mirror `TriggerFlowResponseSchema.data.status`
13+
(`api/automation-api.zod.ts`) carries the same four, and a pin test binds the
14+
two at the type level and the value level.
15+
16+
Additive: no existing literal changes meaning, `'failed'` still says "the run
17+
ran and was rejected", and no engine, route or client behaviour moves in this
18+
change — the engine begins stamping `'stranded'` when #13937's services half
19+
(the operator re-arm verb) lands. A consumer that switches exhaustively over
20+
`status` needs a `'stranded'` arm; the measured count of such switches in this
21+
repo is zero. plugin-approvals' report-only `StrandedRunState`
22+
(`'missing' | 'failed'`) is deliberately not promoted (same ruling).
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
flows: warn on a `loop` body with a fallible node and no containment, and on a `try_catch` with no `catch` (#14394)
6+
7+
Two authoring-time rules in the flow anti-pattern family, both `warning`:
8+
9+
- **`flow-loop-body-uncontained`** — a `loop` whose `body` region runs a node
10+
that can end the run (a record read/write, `http`, `notify`,
11+
`connector_action`, `script`, `subflow`, `map`, `approval`) with no
12+
`try_catch` between the loop and that node. The `loop` executor iterates with
13+
a bare `await` and has no `try`/`catch` at all, so the first failing item ends
14+
the whole run: later items are never processed, and the work already done is
15+
not even reported. The finding names the loop, the node, and the prescribed
16+
spelling.
17+
- **`flow-try-catch-without-catch`** — the near-miss, and the first target
18+
rather than an extra: `catch` is optional in the schema, and omitting it makes
19+
the container fail through, so an author who wrapped the node and stopped
20+
there gets **zero** containment and previously got no diagnostic either.
21+
Measured, the no-`catch` run and the unwrapped control produce identical
22+
output; a `retry` policy only delays that.
23+
24+
Both stay warnings under the family's severity bar: a loop deliberately allowed
25+
to stop at the first failure, and a retry-then-fail `try_catch`, are legitimate
26+
readings the rule cannot disprove.
27+
28+
`content/docs/automation/flows.mdx` documents `loop { try_catch { … } }` as the
29+
per-iteration containment spelling, with the measured minimal handler — one bare
30+
`assignment` node, `edges` and `errorVariable` omitted — and the three `catch`
31+
spellings the schema refuses (`catch` omitted gives no containment; `catch: {}`
32+
and `catch: { nodes: [] }` are rejected, the region's `nodes` being `.min(1)`).
33+
34+
No spec, engine or runtime change: the containment capability already exists and
35+
was measured working (5 of 5 iterations, items 4-5 processed, run completes).
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)
6+
7+
The `liveness/` ledgers ship inside this package's npm tarball (they are named in
8+
`files`), so this is a published-data change even though no runtime behaviour
9+
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
10+
is not edited at all.
11+
12+
Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
13+
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
14+
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
15+
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
16+
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
17+
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
18+
`query` / `qa` / `manifest` already take: server configuration is neither a
19+
metadata item nor a request body nor a manifest, so no registry has ever held it
20+
and no ratchet rooted in one could ask who reads it.
21+
22+
Seventeen properties are `live` with a symbol-anchored consumer and a producer
23+
pointer at the normalizer that threads the authored value into `this.config`.
24+
Fifteen are `dead` — the ten keys the census filed with this card measured, with
25+
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
26+
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
27+
nothing and `nameTransform: 'plural'` still mounts every route under the raw
28+
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
29+
switches for routes that were never built — no path ending in `/schema` is
30+
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
31+
counterpart to its three sibling batch methods.
32+
33+
What this records, and what it deliberately does not. #11984 made
34+
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
35+
casting them, so an out-of-enum or out-of-range value is now refused at
36+
construction. That settles accept/reject and nothing else: executing a declared
37+
contract does not give a key a consumer. No key is removed, enforced, deprecated
38+
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
39+
follow-up on the human floor — the enforce route is a feature per key, and
40+
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
41+
`@example`, which makes its removal a capability retirement rather than a cleanup.
42+
43+
Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
44+
which is measurement rather than taste: the ledger walk drills exactly ONE level,
45+
so with the whole config as the root the sub-objects would BE the drilled level
46+
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
47+
of their own — their container's blanket `live` (three of four members gate a real
48+
route mount) silently covering a dead key, which is the #4956 shape in the file
49+
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
50+
enrolled: its consumption seam is still validate-only and is the subject of its
51+
own card, so a census of it would record a half that is about to move.
52+
53+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->

.claude/agents/os-dev.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -325,19 +325,19 @@ dispatch prompt 只携带每单增量(裁决引文、裁决 / PM-机制假设分
325325
内容的 PR 顺带」、2026-08-29 裁「筹行(为内容购买行数)⛔ vs 独立密度修复(无内容购买)允
326326
许」。分界只问折行有没有为新增内容买行 —— 门禁分不出两种 net-0;⛔ 不买内容的密度
327327
修复是修复,不要当筹行拒掉。删不出等量内容 ⇒ 报 `blocked`,⛔ 不抬 ceiling(人工地板)。
328-
- **`skip-changeset` 标签按仓库分流——先认清目标仓库有没有这个机制。** 判据:**不从任何包
329-
发布东西**;闭合清单:仅含 `docs/adr/**` · `.claude/**` · `scripts/pm/**` · tests/workflow · 注释 的
330-
diff**本仓库**:标签是真实机制,打标签是你的步骤、不是 CI 的,**PR 一开出就打**,⛔ 永不
331-
Check Changeset 转红再补。写入首选**加法端点**(REST `POST .../issues/<n>/labels`——不碰已有标
332-
),⚠️ 可达性是**会话**属性、不是端点或席位的(门先于端点应答、读写同拒,门开会话
333-
实测 200;读数住 platform-readings)⇒ 先探后用;被拒 ⇒ 走回退:MCP 读现值→并集→整组
334-
写→**必做对比式读回**(diff 现集对 union(读集, 目标),union 有而回读缺 = 被剥的并发标签,
335-
挂并写进报告;读回只**检测**剥除防不了,门语义标签被剥恰成绿灯),并申报换道。加法写
336-
同样必要不充分:size-labeler 整组 PUT 曾 ~1 秒内抹掉正确加法写——收尾一律读回、清单引
337-
进报告;标签消失读作被抹,**重新加上**,不是你的错。关此步骤的是读回不是写入;读回
338-
验「写落了」验不出「有门在读」——幻影门标签读回照样成功。**objectui:同名标签对象
339-
在(历史误挂铸出),零 workflow/脚本读它、豁免不了任何东西,pin 测试钉着**——那边用空
340-
frontmatter 的 changeset 声明,门禁判定行是权威;⛔ 永不在那边施加该标签。
328+
- **`skip-changeset` 标签按仓库分流——先认清目标仓库有没有这个机制。** 判据是规则:**不从
329+
任何包发布东西**;清单仅为示例(`docs/adr/**` · `.claude/**` · `scripts/pm/**` · 仓根工具配置 ·
330+
`examples/**` 等私有 workspace · tests/workflow · 注释)**本仓库**:标签是真实机制,打标签是你
331+
的步骤、不是 CI 的,**PR 一开出就打**,⛔ 永不等 Check Changeset 转红再补。写入首选**加法端
332+
**(REST `POST .../issues/<n>/labels`——不碰已有标签),⚠️ 可达性是**会话**属性(读写同拒;读
333+
数住 platform-readings)⇒ 先探后用;被拒 ⇒ 走回退:MCP 读现值→并集→整组写→**必做对比式
334+
读回**(diff 现集对 union(读集, 目标),union 有而回读缺 = 被剥的并发标签,重挂并写进报告;读
335+
回只**检测**剥除防不了,门语义标签被剥恰成绿灯),并申报换道。加法写同样必要不充
336+
:size-labeler 整组 PUT 曾 ~1 秒内抹掉正确加法写——收尾一律读回、清单引进报告;标签消
337+
失读作被抹,**重新加上**,不是你的错。关此步骤的是读回不是写入;读回验「写落了」验不
338+
出「有门在读」——幻影门标签读回照样成功。**objectui:同名标签对象在(历史误挂铸出),
339+
零 workflow/脚本读它、豁免不了任何东西,pin 测试钉着**——那边用空 frontmatter 的 changeset
340+
声明,门禁判定行是权威;⛔ 永不在那边施加该标签。
341341
- **报告在 draft PR 时点交付 —— CI 收敛等待归 PM,不归你**(维护者 2026-08-10 拍板)。分支一推
342342
上、draft PR 一开出,立刻交报告;门禁状态如实记录 —— `in_progress` 是诚实值。⛔ draft PR 开
343343
出后永不 sleep、定时等待或空转轮询 CI(实测:空转轮询烧掉的恰是一个红门禁需要的预

0 commit comments

Comments
 (0)