Skip to content

Commit a24e088

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14749-inline-i18nlabel-not-unauthored
2 parents 0691987 + f50c394 commit a24e088

18 files changed

Lines changed: 1491 additions & 41 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Documentation: the analytics `where` contract and the `element:number` D3 entry now name the hop an array filter is lowered at.
6+
7+
Text only — no schema, accept-set, runtime or test behaviour changes. `AnalyticsQuerySchema.where` is still `FilterConditionSchema` and still refuses an array, which is the protocol working as `FilterArray`'s docblock (#5158 ruling C) declares it: a `FilterArray` is input-only authoring sugar, lowered to a `FilterCondition` at the single sink `parseFilterAST` (`@objectstack/spec/data`) the moment it arrives, and only the lowered `FilterCondition` travels any further.
8+
9+
- `AnalyticsQuerySchema.where`'s `.describe()` gains one sentence pointing array authors at that lowering: an authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire, and this field admits only the lowered `FilterCondition`. It lands in the generated `content/docs/references/{api,data}/analytics.mdx` prop tables, which is where an author reads it.
10+
- The `element-number-filter-rule-array` semantic migration entry recorded its runtime prerequisite one hop too late: "authored array → adapter lowering → filter AST → accepted by `lowerAnalyticsWhere`". `lowerAnalyticsWhere` (`service-analytics`) is the in-process door (#5334) for callers reaching `analyticsService.query` directly. The wire's door is the runtime route `POST /analytics/query`, which parses `where` with `AnalyticsQueryRequestSchema` before any service code runs, so an un-lowered array is refused there. The entry's reason clause now names that route hop and the `parseFilterAST` lowering the adapter owes before the wire (#15828; the adapter-side fix is objectui#7752).
11+
12+
The sibling entry `element-record-picker-filter-rule-array` was read for the same claim and does not make it — its measured path is `find()` / `convertQueryParams`, not the analytics wire — so it is unchanged.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
fix(spec): `defineStack`'s cross-reference refusal carries an ADR-0112 envelope, so the five REFUSED ADR-0130 item classes are machine-readable (#14552)
7+
8+
`validateCrossReferences` — reached through `defineStack` — refuses a stack whose items name an object the stack does not define. That refusal was `new Error(message)` with `code` and `status` both `undefined`, so all five REFUSED item classes of the ADR-0130 matrix (action `objectName`, view `data.object`, permission-set `objects`, seed dataset `object`, import mapping `targetObject`) plus the `hooks[].object` rule (#14122 §4 rule R4) were distinguishable only by MESSAGE TEXT. It now throws `StackCrossReferenceError`, carrying `code: 'STACK_CROSS_REFERENCE_INVALID'`, `status: 422`, and one entry per finding in `issues`. The message text is byte-for-byte unchanged: this adds fields rather than rewriting a sentence, and five message-substring pins in the tree read that prose.
9+
10+
ADR-0112 makes `code` / `status` the machine-readable half of every refusal. Without them `os validate`, `os build` and any AI author reading the refusal could only pattern-match prose — the fragile shape the envelope exists to remove, made worse here because the message had already become load-bearing for those pins.
11+
12+
Why ONE code rather than five: there is exactly one raise site. `validateCrossReferences` returns every finding as a `string[]` and `defineStack` throws the collected set at once, so a single refusal can carry findings from several classes together and a per-class code would have to pick one of several true answers. The classes stay machine-readable in `issues`. The family is also wider than "undefined object" — the same aggregate carries the duplicate-action-key, global-`update`-action and mapping `javascript`-transform findings — so a `…_UNDEFINED_OBJECT` spelling would have been false for those.
13+
14+
Not narrowed, not widened: no accept-set changes and no export changes. `defineStack` accepts and refuses exactly the inputs it did before, and `StackCrossReferenceError` is deliberately module-local — `packages/spec/src/index.ts` re-exports that module with `export *`, so exporting the class would widen the published api-surface of the contract package, and the ADR-0112 contract is the `code` / `status` fields, which every reader reads structurally rather than by `instanceof`. No ledger registration either, for the same reason its two precedents (`ObjectOwnershipConflictError` #14367, `NamespaceConflictError` #14474) carry none: no wire door raises it. `defineStack` runs at authoring and boot time, and no HTTP domain handler calls it.
15+
16+
`@objectstack/runtime` carries the classification row for the new code in the dispatcher error-code vocabulary (verdict `boot-refusal`, door `none` — the measured verdict, not the expected one).
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
automation: a subflow parent left STRANDED by a failed up-bubble is reported at `error`, not `warn`
6+
7+
When an approval (or any pause) sits inside a subflow child, resuming the child
8+
bubbles up to the parent. If the parent's own continuation then fails on the
9+
engine's stranded exit — its suspension consumed, a repair snapshot journalled,
10+
the run recorded `failed` — nothing but a `warn` said so, while the child's
11+
resumer (an approvals decision door, a wait timer) was told the resume
12+
succeeded. Persisted state and runtime state disagree and nothing looks broken
13+
from the outside, which is the durability class.
14+
15+
`bubbleToParent` now grades that record by the engine's own
16+
`AutomationResult.status` discriminator: `'stranded'` is reported at `error`,
17+
naming the parent run and the `restoreConsumedSuspension` verb that repairs it.
18+
Every other parent-resume failure — a concurrent resume, an unreachable store,
19+
a thrown resume — stays at `warn` unchanged, on a narrower ground: those exits
20+
carry no `'stranded'` discriminator. `'stranded'` is the one exit that journals
21+
a repair snapshot, so it is the one an operator can act on, and grading by the
22+
engine's own verdict is what keeps `error` readable.
23+
24+
⚠️ That is a statement about what this seam can KNOW, not a guarantee that
25+
every other exit left the parent healthy. Two exits are known not to be:
26+
27+
- a **thrown** parent resume carries no discriminator at all, and #15555
28+
documents a window in which a throw between the journal and the stamp hides a
29+
parent that IS stranded. Left at `warn` deliberately, for that card;
30+
- the **claim-path** store failure reports, in its own envelope text, that
31+
whether the suspension was consumed is UNKNOWN — it relies on a retry to
32+
settle it, and an up-bubble has no retrier. ("Not consumed" is the guarantee
33+
of the strict-load store failure only, not of every store failure.)
34+
35+
⚠️ This is the log half only. What the child's resumer is told is unchanged.

content/docs/references/api/analytics.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const result = AnalyticsEndpoint.parse(data);
8282
| **cube** | `string` || Target cube name |
8383
| **measures** | `string[]` || List of metrics to calculate |
8484
| **dimensions** | `string[]` | optional | List of dimensions to group by |
85-
| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition) |
85+
| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire; this field admits only the lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`). |
8686
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'second' \| 'minute' \| 'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: string \| string[] }[]` | optional | |
8787
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
8888
| **limit** | `number` | optional | |

content/docs/references/data/analytics.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const result = AggregationMetricType.parse(data);
5555
| **cube** | `string` | optional | Target cube name (optional when provided externally, e.g. in API request wrapper) |
5656
| **measures** | `string[]` || List of metrics to calculate |
5757
| **dimensions** | `string[]` | optional | List of dimensions to group by |
58-
| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition) |
58+
| **where** | `any` | optional | Filtering criteria (canonical Query DSL FilterCondition). An authored `FilterArray` is lowered by `parseFilterAST` on the client before the wire; this field admits only the lowered `FilterCondition` (see `FilterArray` in `data/filter.zod.ts`). |
5959
| **timeDimensions** | `{ dimension: string; granularity?: Enum<'second' \| 'minute' \| 'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; dateRange?: string \| string[] }[]` | optional | |
6060
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
6161
| **limit** | `number` | optional | |

packages/objectql/src/registry-cross-package-item-classes.test.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,24 @@
3535
* both, and the matrix's verdict is the EFFECTIVE one — refused at authoring
3636
* means the module cannot be written, whatever the registry would have done.
3737
*
38-
* ## ⚠️ The authoring gate throws a BARE `Error` — there is no ADR-0112 envelope
38+
* ## The authoring gate carries an ADR-0112 envelope (#14552)
3939
*
40-
* `defineStack` aggregates its cross-reference errors into `new Error(...)`.
41-
* There is no `code` and no `status` to assert, so these rows assert the
42-
* message — which IS the contract here, since the message is the only thing
43-
* that distinguishes one refusal from another — and then assert the ABSENCE of
44-
* the envelope explicitly, in one place, so the gap is pinned rather than
45-
* merely unmentioned. Same shape of gap as #14367 (`registerObject`'s bare
46-
* `Error`), one door over.
40+
* `defineStack` aggregates its cross-reference errors and — since #14552 —
41+
* raises them as `StackCrossReferenceError`: `code:
42+
* 'STACK_CROSS_REFERENCE_INVALID'`, `status: 422`, one entry per finding in
43+
* `issues`, with the message text byte-for-byte unchanged. The rows below
44+
* still assert the MESSAGE, because the message is what distinguishes one item
45+
* class from another (the code names the rule FAMILY — there is one raise site
46+
* for all of them, and a single refusal can carry findings from several
47+
* classes at once). `ENVELOPE PRESENCE` then asserts the envelope itself, in
48+
* one place. Repaired the same way as #14367 (`registerObject`'s bare `Error`)
49+
* and #14474 (`NamespaceConflictError`), one door over.
4750
*
48-
* ⛔ If `ENVELOPE ABSENCE` below goes red, an envelope has ARRIVED. That is an
49-
* improvement: update this pin and the #14122 §4 matrix row. Do not delete the
50-
* assertion to make it green.
51+
* ⛔ If `ENVELOPE PRESENCE` below goes red, the envelope has been REMOVED or
52+
* its code renamed — a regression, not a cleanup. Restore it rather than
53+
* relaxing the assertion; five message-substring pins in this tree read the
54+
* prose it fences, and the #14122 §4 matrix row records the envelope as
55+
* present.
5156
*
5257
* ## This file measures. It does not prescribe.
5358
*
@@ -248,14 +253,17 @@ describe('#14122 §4 continuity — the method reproduces an already-measured ru
248253
expect(authoringVerdict(hookItem, true)).toBeUndefined();
249254
});
250255

251-
it('ENVELOPE ABSENCE — the authoring gate carries no ADR-0112 `code` / `status`', () => {
256+
it('ENVELOPE PRESENCE — the authoring gate carries the ADR-0112 `code` / `status` (#14552)', () => {
252257
// Pinned once, here, rather than repeated on every refusing row. See the
253-
// file header: red here means an envelope ARRIVED (good) — update the pin
254-
// and the §4 matrix, do not delete the assertion.
258+
// file header: red here means the envelope was REMOVED or renamed — a
259+
// regression. Restore it, do not relax the assertion.
255260
const refused = authoringVerdict(hookItem);
256261
expect(refused).toBeInstanceOf(Error);
257-
expect(refused?.code).toBeUndefined();
258-
expect(refused?.status).toBeUndefined();
262+
expect(refused?.code).toBe('STACK_CROSS_REFERENCE_INVALID');
263+
expect(refused?.status).toBe(422);
264+
// The message text is unchanged by the envelope — this pin fences both
265+
// halves at once, which is what makes it a regression detector for the
266+
// five message-substring pins elsewhere in the tree.
259267
expect(refused?.message).toContain('defineStack cross-reference validation failed');
260268
});
261269
});

0 commit comments

Comments
 (0)