Skip to content

Commit 6c29573

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15478-stale-repository-directory
2 parents 06409ff + f7db8f4 commit 6c29573

10 files changed

Lines changed: 982 additions & 27 deletions
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.

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)