Skip to content

Commit b294219

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14366-consume-api-parse
2 parents d51d455 + 581d8f8 commit b294219

70 files changed

Lines changed: 5499 additions & 239 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: 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
Ratify `./hook-body` as a public subpath export — `extractHookBody`, `HookBodyExtractionError`, `HookBodyRefusalKind` and `ExtractedBody` were reachable as a deep `dist/utils/extract-hook-body.js` import until #13123 sealed the surface, and an app's hook-body fidelity harness (hotcrm's `test/helpers/action-sandbox.ts`) consumes them to run the SAME body-only lowering `os build` ships through the real QuickJS runner, so a test executes what production executes rather than a lookalike. The #13123 body names exactly this remedy for an out-of-repo consumer — ratify the subpath as public surface rather than read `dist/` paths — and 17.3.0 applied it to `./console` for cloud's `objectos-runtime`; this applies it to the second consumer (#15325). `@objectstack/cli/hook-body` is a dedicated entry that re-exports those four names and nothing else; the deep `dist/` path stays sealed. Also admits `./package.json`, so the ordinary tooling idiom of reading a dependency's own manifest resolves again.
6+
7+
`minor`, not `patch`: a new subpath on a published package's `exports` map is a purely additive widening of its public surface — a new accepted key — which takes at least `minor` under the maintainer's 2026-09-04 rule (decision batch #35, on #15294) in the Check Changeset step's "WHICH LEVEL" prose; the commit type never lowers it.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-automation": minor
4+
"@objectstack/lint": minor
5+
---
6+
7+
A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator.
8+
9+
A `predicate`-role expression slot holds **bare CEL text**`DecisionConditionSchema.expression` is declared `z.string()`, and so is a screen field's `visibleWhen`. An author who instead wrote the `{ dialect, source }` expression *envelope* there reached a shape nothing could see: a flow node's `config` is an open `z.record(z.unknown())` that no Zod schema is parsed against, the unknown-key walk exempts the schemaless node types on purpose (`decision` publishes no descriptor `configSchema`), and the expression ledger's `predicate` arm skipped every non-string as "a type violation for the schema pass to report" — a schema pass that, for those node types, does not exist. `registerFlow` accepted the flow, `objectstack validate` reported nothing, and the evaluator was the only layer that ever read the predicate.
10+
11+
- `resolveFlowNodeExpressions` now emits a non-string sitting in a `predicate` slot, and the new `predicateSlotRefusal` / `PREDICATE_SLOT_STRING_REFUSAL` say why it is refused — one notion, derived once, read by both validators so build time and author time cannot disagree about the shape. `flow-template` slots keep the old rule: no validator implements that dialect, so a finding there is one nobody could judge.
12+
- `registerFlow` throws, naming the node, the slot and the index, and attributing the finding to the envelope's own `source`. `objectstack validate` reports the same refusal as a located `error`.
13+
14+
**String predicates are untouched, deliberately.** A whitespace-only string still means "not authored" on both sides, exactly as before; what a non-empty string *says* is still judged by `validateExpression('predicate', …)`, brace trap and all. Only the shape moved.
15+
16+
An app that authored an envelope in one of these slots now fails to register with a message naming the slot; the fix is to write the predicate as bare CEL text (`record.rating >= 4`). The `{ dialect, source }` envelope remains the `value`-role spelling, on the `assignment` node's `assignments` map.
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os lint --eval` no longer scores a failed generation as a perfect one: a generator that throws now counts 0 toward `meanScore` instead of 100.
6+
7+
The harness has always handled a throwing `--generator` by substituting an empty stack and scoring that. An empty stack is **100 / grade `A` / `valid: true`** — it has nothing wrong with it because it has nothing in it. So a live eval in which every single generation failed reported the best possible headline number:
8+
9+
```
10+
os lint --eval --json --generator ./throws.mjs
11+
exit 1 · ok: false · passed: 0 · failed: 5 · meanScore: 100
12+
every case: score 100 · grade A · valid true · generationError "model unavailable"
13+
```
14+
15+
`meanScore` is the first number a human scanning that report reads, and it read perfect precisely when the model under test produced nothing.
16+
17+
**What was NOT wrong: `passed`.** It carries its own guard (`!generationError && …`), so the failed cases were reported as failed and `ok` was `false` throughout. A reader who cross-read `ok`/`passed` was safe; a reader who checked the mean and moved on got exactly the wrong impression. That is the whole defect, and nothing about `passed`, `ok`, `total`, `failed` or the exit code changes here.
18+
19+
The repair is the verdict the sibling failure path already used. A generator that *returns* a value nobody can walk was already scored `0 / F / valid: false`, with the reason written into the module: a stack that cannot be walked is not an empty stack, and `valid: true` for one that was never parsed is simply false. A stack that was never produced is not an empty stack either — so both now answer the same:
20+
21+
```json
22+
{ "id": "invoice_with_line_items",
23+
"generationError": "model unavailable",
24+
"passed": false,
25+
"score": { "score": 0, "grade": "F", "valid": false } }
26+
```
27+
28+
and the run above now reports `meanScore: 0`.
29+
30+
`meanScore`'s denominator is unchanged and is now stated in the payload's own documentation: the mean is over every case **attempted**, so a failed case contributes its 0 and is counted. The alternative — averaging only over cases that could be scored — is a different metric that would report the quality of the generations that arrived while staying silent about how many never did; a `meanScore` that switched denominators without saying so would be a worse defect than the one being fixed.
31+
32+
No key is added to or removed from the `--json` payload, and nothing a generator can return is newly accepted or rejected: an off-shape stack is still a **scored** case whose schema errors are why it fails, never a generation error.
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.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `TryCatchErrorValueSchema` declares the `code` key the `try_catch` engine binds (#14954)
6+
7+
`TryCatchErrorValue` — the ONE shape the catch region's author, the engine and the run log share for the value a `try_catch` binds to `errorVariable` (default `$error`) — gains an optional `code: string`: the platform-classified error code (ADR-0112) the failing node's own result carried, e.g. `create_record`'s `DUPLICATE_RECORD`. The engine has bound it since `@objectstack/service-automation`'s #14419 change; the schema was a plain `z.object` that did not declare it, so a round-trip through the declared shape silently STRIPPED the key the engine had put there, and the generated reference page documented four keys where the runtime binds five. The `errorVariable` description on `TryCatchConfig` names `code` too, so the authorable surface documents branching on `$error.code`.
8+
9+
Typed as an open `string`, deliberately not `StandardErrorCode` and not the ledger union: ADR-0112 D3/D4 with the #9106 amendment make the code vocabulary `StandardErrorCode` ∪ registered ledger codes ∪ tenant-authored codes, and `NodeExecutor` is third-party-registrable, so a closed type would be false the moment anyone registers an executor that throws its own code. The closed-at-every-door rule governs `ApiErrorSchema.code` at an HTTP door; this value is bound in-process and never crosses one.
10+
11+
Additive and optional: every value that parsed before parses byte-identically, and a binding without a classified code still carries no `code` key — absent means "no classified code", never "nothing failed". Semver: a new optional key on a published schema widens the accept set and the exported `TryCatchErrorValue` type without retiring or renaming anything ⇒ `minor`; no ADR-0087 entry is owed because there is nothing an upgrader must migrate.

0 commit comments

Comments
 (0)