Skip to content

Commit 7f2b963

Browse files
committed
Merge origin/main into claude/issue-15600-readme-exports-exit3-prereq
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
2 parents 147dacd + a55efc6 commit 7f2b963

40 files changed

Lines changed: 3151 additions & 1596 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`IApprovalService.recall`'s contract prose names every actor who may recall, and scopes each one by status (#14670)
6+
7+
**Documentation only — no key, no accepted value, no runtime behaviour moves.** The implementation has been correct since #12775; only the contract's description of it was stale.
8+
9+
The docstring said *"Only the submitter (or a system context) may recall"*, then widened to `returned` requests in a second paragraph. Both halves were wrong, in opposite directions:
10+
11+
- **The list was not exhaustive.** A #3424 override actor — a platform or tenant admin holding no approver slot — may recall a `pending` request. That is the in-product recovery path for an approval routed to an unstaffed position, and this same file already documented it 387 lines above the sentence denying it: the docblock on `ApprovalRequestRow.viewer.can_override` spells the override's levers as `(approve / reject / reassign / recall it)`. One file, two contradicting sentences about the same verb.
12+
- **The ADR-0044 widening read as though it applied to that whole list.** It does not. The override and system arms are ANDed with `status === 'pending'` where they are computed, so neither reaches a `returned` request; an override actor is refused there exactly as any other non-submitter (#12775, maintainer ruling 2026-09-02). Abandoning a revision window is the submitter's alone.
13+
14+
The rewrite makes **status** the axis instead of appending a caveat, so the second defect cannot come back on a re-read: each status carries its own admitted set, and the `returned` bullet says outright that the submitter is alone in it.
15+
16+
`ApprovalRecallInput.actorId` carried the same stale sentence (*"Must be the request's submitter (or a system context)"*) and is corrected with it. Fixing only the method docstring would have left the contradiction alive on the very input type the corrected method takes.
17+
18+
The two sibling docstrings sharing that phrasing are **correct and unchanged**: `ApprovalSendBackInput.actorId` and `ApprovalResubmitInput.actorId`. `isOverrideActor` is called from exactly five places in `plugin-approvals``decideNode`, `reassign`, `recall`, `attachViewers` and `visibleRequestIds` — and neither `sendBack` nor `resubmit` is among them, so no override actor reaches either.
19+
20+
The published prose already described the corrected rule (`content/docs/automation/approvals.mdx`: an admin "may act on any `pending` request — approve, reject, reassign it to a real approver, or recall it"). This docstring was the one surface that had not kept up.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
feat(objectql,spec): `Field.valueDomain` binds at the write seam — a non-member is refused with `value_domain` (maintainer ruling 2026-09-02 on #14168, engine half)
7+
8+
**BREAKING** accept-set narrowing on the ObjectQL record write path, shipped as
9+
`minor` under the repo's launch-window convention for breaking changes.
10+
11+
The key is **already published, and published unenforced**. The version-packages
12+
cut `8a1bad8b8` (2026-09-04 10:20Z) consumed the spec half's changeset
13+
`field-value-domain-slot.md` and released `@objectstack/spec@17.3.0`, which
14+
declares `Field.valueDomain`, parses it, and refuses it on any type other than
15+
`text` — and never reads it when a record is written. The 17.3.0 liveness ledger
16+
states the gap in its own words: "a non-member WRITTEN to a `text` field
17+
declaring a domain is accepted today". That write is accepted on 17.3.0 and is
18+
refused from this release on.
19+
20+
**Refused shape**, precisely: a record write that supplies a value for a `text`
21+
field whose definition declares `valueDomain`, where the WRITTEN value is not a
22+
member of the named standard. It fails with the field error code `value_domain`,
23+
carrying `constraint: { valueDomain }` and a message that names the standard in
24+
all four platform locales. Nothing else narrows — a field that declares no
25+
`valueDomain` is untouched, and so is every other field type, because the schema
26+
accepts the key on `text` alone and the validator judges exactly that set.
27+
28+
**Remedy: write a member of the declared standard.** `iana_time_zone` admits
29+
`UTC` and refuses `Mars/Olympus`; `iso_4217_currency` admits `CHF` and refuses
30+
`chf`; `iso_3166_alpha2` admits `CH` and refuses `ZZ`. Dropping the
31+
`valueDomain` declaration from the field lifts the refusal entirely, for an
32+
author who declared a domain they did not mean.
33+
34+
**No stored row is touched, and none becomes invalid.** This is the `min` /
35+
`max` / `maxLength` transition-gate class: a value stored before the domain was
36+
declared — or before this release — is never re-read, and it survives an edit of
37+
another field on the same record. An absent or empty value follows the field's
38+
`required` handling, not this check.
39+
40+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is
41+
renamed, retired or tombstoned. `Field.valueDomain` keeps its name, its type and
42+
its position; this release only makes the declaration the key already carries
43+
bind at the write seam, so `objectstack migrate meta` has no metadata to
44+
rewrite — a document that declares a domain is already in its final spelling,
45+
and one that declares none is untouched. ⚠️ This disposition does NOT rest on
46+
the key being unpublished, and must not be read that way: 17.3.0 shipped
47+
`Field.valueDomain` declared, parsed and UNENFORCED, which is exactly why this
48+
changeset carries the BREAKING banner above. It rests on the stored side
49+
instead. A stored value outside a declared domain is never re-read, so no stored
50+
row is invalidated here and none is reachable by a ledger entry at all. And
51+
which member a stored non-member SHOULD have been is authoring intent no ledger
52+
entry can decide: the stored string carries no evidence of whether the author
53+
meant a different member of that standard, a different standard, or no
54+
declaration at all. The channel that reaches the author is the refusal itself,
55+
raised at the write, naming the standard — the same ground the sibling
56+
accept-set narrowing #15319 stands its own `no-migration-prescription`
57+
disposition on. -->
58+
59+
- The membership test is the spec's shared `isValueDomainMember` — the same
60+
predicate, over the same closed vocabulary, that a settings specifier's
61+
`valueDomain` uses. A time zone accepted in Settings is the time zone
62+
accepted in a field.
63+
- The two authoring forms (`fieldForm`, `objectForm`) gain a `valueDomain`
64+
control, shown on exactly the types the schema accepts the key on. The
65+
object-form control's choices are derived from the vocabulary, not re-typed.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
A `map` node inside a `loop` body now runs its collection on every iteration, not just the first.
6+
7+
`map` tracks its progress through the collection in the flow variable `<nodeId>.$mapState`, and wrote it into the flow's **shared** variable scope without ever removing it. A `loop` body region runs in that same scope by construction — that is what makes the iterator variable and the body's mutations visible to the rest of the flow — so the state written by iteration 1 was still there when iteration 2 entered the map. It read back `started === collection.length`, correctly concluded there was nothing left to start, and returned.
8+
9+
The result was silent partial work reported as success: measured on the engine, **5 iterations x 2 items produced 2 child runs instead of 10**, the map step reported `success` on all five iterations, and the run finished `completed`. Nothing threw and nothing was caught, so `FlowRunSummary.failed` — the run-level counter that exists to expose contained failures — reported `failed = 0` over it. An operator reading that counter was told the run was clean while it had done a fifth of its work.
10+
11+
The fix is a lifetime correction, not a new key: `$mapState` is now removed once the collection is exhausted, so its lifetime is one execution of the collection rather than the enclosing scope's.
12+
13+
**The durable-pause path is deliberately unchanged.** A `map` whose per-item subflow pauses still writes its progress before suspending, and still reads it back when the engine re-enters the node — that write is the mechanism resume depends on, because a resume rebuilds the variable scope from the snapshot taken at the suspend and so can never see any later write. Only the node's terminal path clears the key. A `map` resumed mid-collection continues where it left off, exactly as before, and no item is re-run.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The `PluginSchema` describe strings for `staticPath`, `slug` and `default` now name `ui`, the plugin type the enum actually accepts.
6+
7+
`PluginSchema.type` is `z.enum(['standard', ...CORE_PLUGIN_TYPES])`, and `CORE_PLUGIN_TYPES` spells the frontend member `ui`. The three describe strings beside it still named `ui-plugin` — a value the same schema refuses two lines above. They are not merely stale: they read as instructions ("Required for `type="ui-plugin"`"), so an author or an agent following the field's own documentation writes a value that is then rejected, with the correct spelling nowhere in the sentence that sent them there.
8+
9+
The strings now read `(Required for type="ui")`, `(Required for type="ui")` and `(Only one "ui" plugin can be default)`. Because these describes compile into the published JSON Schema and into the generated reference page, the correction reaches every consumer that reads field documentation out of the spec rather than out of the source file — the generated `content/docs/references/kernel/plugin.mdx` table now agrees with the `type` row printed directly above it, which previously listed `'ui'` among the accepted members while the three rows underneath told the reader to write `ui-plugin`.
10+
11+
No accept/reject behaviour moves: `type: 'ui-plugin'` is refused before and after, `type: 'ui'` is accepted before and after, and no key is added, renamed or removed. The closed-set pin tests that name `ui-plugin` as a non-member are deliberately unchanged — they are the reason this correction is provable.

0 commit comments

Comments
 (0)