Skip to content

Commit bf3f890

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15204-s2d-activation-door-package-less
2 parents db73adc + 5fc57b3 commit bf3f890

17 files changed

Lines changed: 653 additions & 71 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
fix(skills): the `objectstack-data` field-types rule names all five formula `returnType` members, `'currency'` included, and the `currencyConfig` a currency formula carries (#22743)
6+
7+
Clause-②: no
8+
9+
`skills/objectstack-data/rules/field-types.md` taught a formula's `returnType` as the four-member set `'number' | 'text' | 'boolean' | 'date'`, and its formula example said `(no 'currency')`. `@objectstack/spec`'s `FieldSchema.returnType` is the five-member enum `'number' | 'text' | 'boolean' | 'date' | 'currency'`, and a formula whose `returnType` is `'currency'` carries its currency in its own `currencyConfig` — `CurrencyConfigSchema`, the same shape a `currency` field declares; absent, the mode is `dynamic`, the tenant default currency. The `formula` table row and the formula example now both state the five-member set and name `currencyConfig` as the carrier. The example's `CEL — record. prefixes required` comment, which restated the table cell above it, is removed to pay the skill's token ratchet. Nothing is said about how a currency formula renders.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `RuntimeAuthoringIssueSchema.path` states the name-keying rule for every collection-resident write type, so a `dataset` write's name-keyed paths are no longer described as positional
6+
7+
Clause-②: no
8+
9+
The description of `path` on a runtime publish-gate finding (the 422 `issues[]` and the 2xx `advisories[]`) named `object`, `permission` and `book` as the only write types whose top-level collection entry is keyed by name, and called every other write type positional. A `dataset` write's findings have been name-keyed too (for example `datasets.acme_invoice_metrics.dimensions[0].field`), so a consumer that followed the text would have expected `datasets[N]` and never received it.
10+
11+
The description now states the rule instead of a list: a write type is collection-resident when the gate's per-write snapshot holds the tenant's other stored items of that type beside the written one, and there the top-level entry is keyed by name. Every other write type is the only member of its collection in that snapshot, so its `[0]` stays positional. The set is derived inside `@objectstack/lint`'s runtime gate, and the description names that derivation rather than copying its members. It also states the fallback the gate already applies: an entry whose name cannot be spliced into a dotted path keeps its index.
12+
13+
Description text only: the schema's shape, accepted values and types are unchanged. The generated `content/docs/references/api/protocol.mdx` is regenerated.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
`security/explain` answers a by-id write of a row the caller cannot read with the whole answer it gives for an id no row carries, layers included.
6+
7+
Clause-②: no
8+
9+
- The update, delete and transfer doors already answer such a row `404 RECORD_NOT_FOUND`, as they answer a nonexistent id, and explain's record verdict already took the missing-record shape. Every row-scoped layer's `record` block, and the tenant layer's verdict, now come from the same path a nonexistent id takes, so no layer names a status the door does not give.
10+
- Nothing you author changes. The doors and the record verdict answer exactly as before, and an explanation of a row the caller can read is unchanged.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): a new trigger-api service contract declares an optional, transport-neutral inbound-hook member (`ITriggerApiService.handleInboundHook`)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What is new.** `@objectstack/spec/contracts` exports a new contract, `ITriggerApiService`, for the `trigger-api` service slot that `@objectstack/trigger-api` fills. It has one optional member, `handleInboundHook?(request: Request, hook: { flowName, hookId }): Promise<Response>`. It serves the inbound hook, `POST /api/v1/automation/hooks/:flowName/:hookId`, the door a third-party sender posts a signed event through to start an `api` flow.
10+
- **What it serves.** The same answers as the trigger's self-hosted route, in the same order. An unknown flow or a wrong hook id answers `404 RESOURCE_NOT_FOUND`, identically. A hook secret that cannot be read answers `503 SERVICE_UNAVAILABLE`. A missing or wrong `x-objectstack-signature` answers `401 INVALID_SIGNATURE`. Only a verified body is read as JSON: one that is not valid JSON, or not a JSON object, answers `400 INVALID_REQUEST`. An enqueued post answers `202` with `{ accepted: true, messageId }`, and the flow runs on the queue consumer, never in-band (ADR-0041 §5). No queue answers `503 SERVICE_UNAVAILABLE`, and a failed publish answers `503 ENQUEUE_FAILED`.
11+
- **The member is the signature boundary.** The implementer verifies inside the member, with the hook's own secret, in constant time, and before anything is enqueued. It reads the raw body from the request itself. The caller forwards the request with its body unread, and never parses, re-serialises or pre-checks it.
12+
- **Transport-neutral.** A web-standard `Request` goes in and a `Response` comes out: there is no Hono context, no raw app and no `http.server` type, and no execution context, because the signature is the only credential. The two path parameters arrive as the second argument, as the caller's route matched them, so the member never re-parses the URL. A hosted tenant kernel has no raw app to mount the route on, and the runtime dispatcher's exact `POST /automation/hooks/:flowName/:hookId` domain is the member's one caller.
13+
- **Why a trigger-api slot, not the automation service's.** The automation service holds the trigger registry that the `api` trigger registers into. The door (the hook lookup, the secret, the signature check, the body parse, the enqueue and every answer) belongs to the trigger, so the door exists exactly where `@objectstack/trigger-api` is composed.
14+
- **Absence.** The member is optional, so a trigger-api service without the door still satisfies the contract. Its caller answers a typed 404 or 501 when the `trigger-api` service or this member is absent, never `ROUTE_NOT_FOUND`.
15+
- **Nothing changes for existing code.** No existing contract or member moved. Nothing registers the `trigger-api` slot or calls the member yet: the trigger's implementation and the dispatcher domain land separately, and the trigger's self-hosted route stays as it is.

‎.claude/skills/spec-property-retirement/SKILL.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,12 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
210210
「没有 alias 窗口,故意的」;对**默认值翻转**,只有确知输入早于翻转的 seam 才可重
211211
放,其余按 id 退订 `excludeConversionIds` —— `app-hidden-to-unpublished` 在 artifact
212212
门即如此。
213-
- [ ] **一步 D3 链**,在 `packages/spec/src/migrations/registry.ts`;`conversion.toMajor` **必须等于**该步的
214-
major。**18 步**:conversion 只进 `conversions/registry.ts` 的 `MAJOR_18_CONVERSIONS`,照其头注按
215-
标识符排序插入,本步 `conversionIds` 由它派生;`rationale` 只加一个 `STEP18_RATIONALE` 片段,
216-
按其头注插在你 D3 semantic id 的排序位;尾部追加被两处头注点名的 merge 测试拒收。
213+
- [ ] **一步 D3 链**,在 `packages/spec/src/migrations/registry.ts.template`(`registry.ts` 由
214+
`gen:migration-registry` 从它与 `entries/` 整文件生成并被 git 忽略,在 `registry.ts` 里手改不会
215+
进提交);`conversion.toMajor` **必须等于**该步的 major。**18 步**:conversion 只进
216+
`conversions/registry.ts` 的 `MAJOR_18_CONVERSIONS`,照其头注按标识符排序插入,本步 `conversionIds`
217+
由它派生;`rationale` 只加一个 `STEP18_RATIONALE` 片段,按其头注插在你 D3 semantic id 的排序
218+
位;尾部追加被两处头注点名的 merge 测试拒收。
217219
**更早的步**:id 加进 `MIGRATIONS_BY_MAJOR[N].conversionIds`,扩写该步 `rationale`。⚠ 没有东西
218220
直接断言「每个 conversion 都接进了某一步」,拼错的 id 在 replay 时被**静默跳过**;
219221
chain-replay 测试抓得到它,只因为没接线的 fixture 永远到不了自己的 `after`。
@@ -257,7 +259,7 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
257259
无墓碑删键 → 该行**消失**,这是门 (a) 的绊线,所以同一个 PR 里有意删掉它)与
258260
`json-schema.manifest/<category>.json`。#5837 起两者都按 category 分片 —— 门
259261
禁把整个目录读成一个集合,退役流程不变;变的只是那一行住在哪个文件。然后
260-
`gen:spec-changes`、`gen:upgrade-guide`、`gen:api-surface`、`gen:docs`。
262+
`gen:api-surface`、`gen:docs`;新条目跑 `gen:migration-registry`;D4 投影不提交,本地不重生成。
261263
「改了 X → 重新生成 Y」的表见 AGENTS.md。
262264
- [ ] **Forms** —— 从 `packages/spec/src/**/*.form.ts` 剪掉 `{ field: '<key>' }` 输
263265
入。未强制能力的表单输入是假合规的 UI 半边。原位留一行注释。
@@ -332,6 +334,4 @@ pnpm check:i18n && pnpm --filter @objectstack/spec test
332334
- **截断的 grep 漏掉编写者。** `| head -8` 藏掉了 `SKILL.md` 的 `defineSkill` 示
333335
例;`examples/` 有三处 `template: true`,不是一处。先按上下文找文件,再进文件里
334336
grep 键 —— 且把 `tsc` 和门禁当权威清扫器。
335-
- **只改 conversion 的 `summary` 也会过期。** 那个字符串被逐字抄进
336-
`spec-changes.json` 的 `to` 字段与 upgrade guide 的表行,所以纯散文改动也要
337-
`gen:spec-changes` + `gen:upgrade-guide`,和任何别的改动一样。
337+
- **只改 conversion 的 `summary` 不欠重生成。** 它逐字进两条 D4 投影,都不提交;PR 渲染其 diff。

‎AGENTS.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ Root also exports: `defineStack`, `composeStacks`, `defineView`, `defineApp`, `d
703703

704704
### Touched `packages/spec`? Regenerate its artifacts BEFORE pushing
705705

706-
`packages/spec` has **eight** checked-in generated artifacts, each with its own CI
706+
`packages/spec` has **six** checked-in generated artifacts, each with its own CI
707707
gate. All of them live in one job — `TypeScript Type Check` in `lint.yml`, which is
708708
required and has no paths filter, so no gate can go dormant on the PR that breaks it.
709709
That job runs its gates **sequentially**, so the first stale artifact masks every one
@@ -714,7 +714,7 @@ behind it. Match the change to the gate and regenerate up front:
714714
| A `.describe()` / TSDoc on any schema | `check:docs` | `gen:schema && gen:docs` |
715715
| A public export (added / removed / renamed) | `check:api-surface` | `gen:api-surface` |
716716
| An authorable key on a metadata schema | `check:authorable-surface` | `gen:schema` |
717-
| An ADR-0087 conversion / migration registry | `check:spec-changes`, `check:upgrade-guide` | `gen:spec-changes`, `gen:upgrade-guide` |
717+
| An ADR-0087 conversion / migration registry entry | `check:spec-changes`, `check:upgrade-guide` — in memory, red only when the registries do not project | nothing to commit — `gen:migration-registry` (install, build and test run it too) writes the gitignored `registry.ts` |
718718
| A `SKILL.md` (frontmatter or body) | `check:skill-docs`, `check:skill-refs` | `gen:skill-docs`, `gen:skill-refs` |
719719
| The react-blocks contract | `check:react-blocks` | `gen:react-blocks` |
720720

@@ -758,8 +758,9 @@ Principles the wrapper encodes (its own output is the authority on detail):
758758
a name, with accepted cases in the shrink-only, hand-edited
759759
`dual-source-exports.baseline.json`.
760760

761-
Two generators have **no** gate at all — `gen:openapi` and `gen:sbom`. Nothing verifies
762-
their output is current; the wrapper reports that each run rather than staying silent.
761+
Three generators have **no** gate at all. Nothing verifies `gen:openapi` and `gen:sbom`
762+
are current, and `gen:migration-registry` writes gitignored output that install, build and
763+
test regenerate; the wrapper reports all three each run rather than staying silent.
763764

764765
---
765766
## Context Routing — apply the right role per path

‎content/docs/references/api/protocol.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,7 +2368,7 @@ Installed package with runtime lifecycle state
23682368
| Property | Type | Required | Description |
23692369
| :--- | :--- | :--- | :--- |
23702370
| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
2371-
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. |
2371+
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example `objects.acme_invoice.sharingModel` (an `object` write) or `datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` write). Every other write type is the sole member of its own collection in that snapshot, so its `[0]` IS the written item and stays positional (for example `flows[0]...`). Which write types are collection-resident is derived, never listed: `@objectstack/lint`'s runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (`objects.acme_invoice.indexes[1]`): they index the author's own document. |
23722372
| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
23732373
| **message** | `string` | ✅ | What is wrong, in the rule author's own words. |
23742374
| **hint** | `string` | ✅ | How to fix it. |
@@ -2594,7 +2594,7 @@ Installed package with runtime lifecycle state
25942594
| Property | Type | Required | Description |
25952595
| :--- | :--- | :--- | :--- |
25962596
| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
2597-
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. |
2597+
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example `objects.acme_invoice.sharingModel` (an `object` write) or `datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` write). Every other write type is the sole member of its own collection in that snapshot, so its `[0]` IS the written item and stays positional (for example `flows[0]...`). Which write types are collection-resident is derived, never listed: `@objectstack/lint`'s runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (`objects.acme_invoice.indexes[1]`): they index the author's own document. |
25982598
| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
25992599
| **message** | `string` | ✅ | What is wrong, in the rule author's own words. |
26002600
| **hint** | `string` | ✅ | How to fix it. |
@@ -2648,7 +2648,7 @@ Installed package with runtime lifecycle state
26482648
| Property | Type | Required | Description |
26492649
| :--- | :--- | :--- | :--- |
26502650
| **rule** | `string` | ✅ | Stable diagnostic rule id (`flow-multi-write-unfiltered`, `approval-expression-invalid`, …). Machine-readable and stable across releases — the key a renderer groups or suppresses by. |
2651-
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. For the collection-resident write types (`object` / `permission` / `book`) the TOP-LEVEL collection entry is keyed by NAME (`objects.acme_invoice.sharingModel`), never by an array index — the gate evaluates against a private per-write snapshot whose indexes no caller can resolve. Every other write type is the sole member of its own collection, so its `[0]` is trivially stable and stays positional (`flows[0]...`), as do nested positions inside one named item (`objects.acme_invoice.indexes[1]`), which index the author's own document. |
2651+
| **path** | `string` | ✅ | Config path inside the SUBMITTED body (`flows[0].nodes[1].config.multi`), so an editor can jump to the offending key. May be empty when the finding is about the item as a whole. The TOP-LEVEL collection entry follows one rule. The gate evaluates against a private per-write snapshot whose indexes no caller can resolve; a collection-resident write type is one whose collection that snapshot fills with the tenant's other stored items beside the written one, and there the entry is keyed by NAME, never by an array index — for example `objects.acme_invoice.sharingModel` (an `object` write) or `datasets.acme_invoice_metrics.dimensions[0].field` (a `dataset` write). Every other write type is the sole member of its own collection in that snapshot, so its `[0]` IS the written item and stays positional (for example `flows[0]...`). Which write types are collection-resident is derived, never listed: `@objectstack/lint`'s runtime gate computes the set (`NAME_KEYED_STACK_KEYS`) as the collections it carries as resolution context that a gated write type also lands in, so it changes with the gate, not with this text. An entry whose name cannot be spliced into a dotted path keeps its index. Nested positions inside one named item stay positional (`objects.acme_invoice.indexes[1]`): they index the author's own document. |
26522652
| **where** | `string` | ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
26532653
| **message** | `string` | ✅ | What is wrong, in the rule author's own words. |
26542654
| **hint** | `string` | ✅ | How to fix it. |

0 commit comments

Comments
 (0)