You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bf3f890
Browse filesBrowse the repository at this point in the historyBrowse files
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.
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.
`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.
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.
### Touched `packages/spec`? Regenerate its artifacts BEFORE pushing
705
705
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
707
707
gate. All of them live in one job — `TypeScript Type Check` in `lint.yml`, which is
708
708
required and has no paths filter, so no gate can go dormant on the PR that breaks it.
709
709
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:
714
714
| A `.describe()` / TSDoc on any schema |`check:docs`|`gen:schema && gen:docs`|
715
715
| A public export (added / removed / renamed) |`check:api-surface`|`gen:api-surface`|
716
716
| 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`|
718
718
| A `SKILL.md` (frontmatter or body) |`check:skill-docs`, `check:skill-refs`|`gen:skill-docs`, `gen:skill-refs`|
719
719
| The react-blocks contract |`check:react-blocks`|`gen:react-blocks`|
720
720
@@ -758,8 +758,9 @@ Principles the wrapper encodes (its own output is the authority on detail):
758
758
a name, with accepted cases in the shrink-only, hand-edited
759
759
`dual-source-exports.baseline.json`.
760
760
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.
763
764
764
765
---
765
766
## Context Routing — apply the right role per path
Copy file name to clipboardExpand all lines: content/docs/references/api/protocol.mdx
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2368,7 +2368,7 @@ Installed package with runtime lifecycle state
2368
2368
| Property | Type | Required | Description |
2369
2369
| :--- | :--- | :--- | :--- |
2370
2370
|**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. |
2372
2372
|**where**|`string`| ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
2373
2373
|**message**|`string`| ✅ | What is wrong, in the rule author's own words. |
2374
2374
|**hint**|`string`| ✅ | How to fix it. |
@@ -2594,7 +2594,7 @@ Installed package with runtime lifecycle state
2594
2594
| Property | Type | Required | Description |
2595
2595
| :--- | :--- | :--- | :--- |
2596
2596
|**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. |
2598
2598
|**where**|`string`| ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
2599
2599
|**message**|`string`| ✅ | What is wrong, in the rule author's own words. |
2600
2600
|**hint**|`string`| ✅ | How to fix it. |
@@ -2648,7 +2648,7 @@ Installed package with runtime lifecycle state
2648
2648
| Property | Type | Required | Description |
2649
2649
| :--- | :--- | :--- | :--- |
2650
2650
|**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. |
2652
2652
|**where**|`string`| ✅ | Human-readable location — `flow "leave_approval" · node "approve"`. Prose for a person; use `path` for anything mechanical. |
2653
2653
|**message**|`string`| ✅ | What is wrong, in the rule author's own words. |
0 commit comments