Skip to content

Commit 2b15196

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-9227-inline-columns-strict
2 parents fde7df9 + 83fe945 commit 2b15196

97 files changed

Lines changed: 9133 additions & 400 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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
fix(cli): `os start` / `os dev` stop writing `OS_ARTIFACT_PATH` into the child `serve` environment — the CLI's own plumbing moves to an internal channel (#8985)
6+
7+
`os start` and `os dev` are supervisors: each resolves an artifact, then spawns
8+
`os serve` to boot it. Both handed the resolved path down by writing
9+
**`OS_ARTIFACT_PATH`** into the child environment — the same variable an operator
10+
sets to name an artifact. `dev` wrote it unconditionally; `start` wrote it
11+
whenever it had resolved anything and no `OS_ARTIFACT_URL` was in play. Both
12+
writes happen **before** the downstream `objectstack.config.ts` is evaluated.
13+
14+
So inside any config, that variable was set on **every** boot, including boots
15+
where no operator had ever mentioned it — measured from the shipped EE image
16+
with its own `ENV` deliberately deleted: `os start` still printed
17+
`Artifact: dist/objectstack.json` and handed that path down. A config could not
18+
answer *"did a human ask for this, or did the CLI put it here?"*
19+
20+
The resolved path now travels on **`OS_INTERNAL_ARTIFACT_PATH`**, a channel the
21+
CLI owns both ends of (`packages/cli/src/utils/internal-artifact-channel.ts`),
22+
and the property downstream consumers need is restored:
23+
24+
> **the presence of `OS_ARTIFACT_PATH` in a config's environment means an
25+
> operator set it.**
26+
27+
**Nothing about resolution changed.** Each command's ladder resolves in the
28+
parent exactly as before, and `serve` reads the new channel strictly between the
29+
reference and the operator knob:
30+
31+
```
32+
--artifact > OS_ARTIFACT_URL > OS_INTERNAL_ARTIFACT_PATH > OS_ARTIFACT_PATH > <cwd>/dist/objectstack.json
33+
```
34+
35+
That position is what preserves today's answers in both directions. It beats
36+
`OS_ARTIFACT_PATH` because `os start --artifact X` run with an operator's
37+
`OS_ARTIFACT_PATH=Y` exported boots **X** today — the parent used to overwrite
38+
the variable on the way down, and now inherits it untouched. It loses to
39+
`OS_ARTIFACT_URL` because `os dev` writes the channel unconditionally, as it
40+
wrote the old variable unconditionally, and the reference has always outranked
41+
the path.
42+
43+
Two further behaviours are unchanged and now pinned rather than incidental:
44+
`start` still refuses to set `OS_BOOT_EMPTY` when a reference is driving the
45+
boot (an unreachable artifact host stays a loud refusal instead of a silently
46+
empty platform), and a resolved-but-missing artifact is still "named" to
47+
`resolveDefaultArtifactPath`, so it fails loudly rather than booting empty.
48+
49+
**If you depended on the old side effect** — a config reading
50+
`process.env.OS_ARTIFACT_PATH` and expecting the CLI to have populated it — set
51+
the variable yourself, or read the artifact from the config's own inputs.
52+
`OS_ARTIFACT_PATH` remains a fully supported operator knob on the exact rung it
53+
has always occupied; the CLI simply no longer manufactures it on your behalf.
54+
`OS_INTERNAL_ARTIFACT_PATH` is not a supported knob and is deliberately absent
55+
from the environment-variable reference.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/dogfood": patch
3+
---
4+
5+
test(qa): a wired realtime transport can no longer be signed off by the row that records the absence of authorization (#9083)
6+
7+
The ADR-0056 D10 authz conformance matrix carries a tripwire note promising
8+
that wiring an end-user realtime transport reds CI "until this row is upgraded
9+
with the enforcement site." The gate did not hold that out. `checkLedger`
10+
requires an `enforcement` site only when `state === 'enforced'`, while a row's
11+
`covers` keys classify a discovered surface **regardless of state** — so the
12+
shortest path from red back to green was to append the tripwire key to the
13+
`experimental` `realtime-delivery-authz` row, whose own summary records that
14+
realtime fan-out has **NO** per-recipient authorization.
15+
16+
Measured on `origin/main` before the fix, both legs reproduced from the filing:
17+
wiring `new EventSource('/api/v1/stream')` into `packages/client/src/realtime-api.ts`
18+
fails 2 of 15 cases as `UNCLASSIFIED surface — … realtime:client/realtime-api.ts:transport(TRANSPORT-WIRED)`;
19+
appending that one key to the experimental row — with the transport still wired
20+
and zero authorization written — returns 15/15 green. The `removed` state was
21+
measured to admit the identical exit, so the rule keys on **not `enforced`**
22+
rather than on `experimental`.
23+
24+
`checkTransportWiredAdmission` in `authz-conformance.test.ts` now refuses a
25+
`TRANSPORT-WIRED` key covered by any row that is not `enforced`, and
26+
`checkLedger`'s existing enforced-has-site invariant supplies the other half —
27+
the two compose into the promise the note makes, so flipping a row's state
28+
without writing the site is refused as well. The rule lives beside the probe
29+
table rather than in the shared ADR-0060 `checkLedger` helper on purpose:
30+
`TRANSPORT-WIRED` is this ledger's own vocabulary, and five other conformance
31+
ledgers share that helper without having transport tripwires. Tripwire keys are
32+
now minted through one `tripwireKey()` helper so the marker cannot drift out of
33+
the rule's sight (the keys themselves are byte-identical to before), and every
34+
assertion in the file drives the composed gate instead of `checkLedger`.
35+
36+
The matrix note, the `covers` field TSDoc and both file headers were corrected
37+
to describe the gate that actually ships — the declared-≠-enforced defect here
38+
was in the *note*, so leaving it in place would only have moved the
39+
discrepancy. Six cases pin the new rule, including both reverse-verification
40+
legs and a positive control proving an `enforced` row naming its site still
41+
admits the key; each refusal case also asserts that bare `checkLedger` accepts
42+
the same ledger, so none can pass for an unrelated reason. Gate behaviour only
43+
— no runtime, spec or product surface changes, and no matrix row changed state.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/client': minor
5+
---
6+
7+
**BREAKING**`POST /api/v1/automation/:name/runs/:runId/resume` answers real HTTP
8+
status codes for a failed run instead of HTTP 200 wrapping an inner `{success: false}`.
9+
10+
Until now a screen flow driven to a server-side node failure answered:
11+
12+
```
13+
HTTP 200
14+
{"success":true,"data":{"success":false,"error":"Node 'create_opportunity' failed: …"}}
15+
```
16+
17+
The run genuinely failed; the transport reported success. A scripted or integration
18+
caller that branches on the HTTP status alone read a failed run as a successful one.
19+
This applies to the resume route the ruling for `/actions` (business failures must not
20+
ride HTTP 200 inside a double envelope), which every other automation refusal on this
21+
route already followed.
22+
23+
What changes on the wire:
24+
25+
- **A run that resumed and then failed ⇒ `400` with `error.code: 'FLOW_FAILED'`.** The
26+
node failure stays the human-readable `error.message`. The flow author's own
27+
`errorMessage` travels in `error.details.errorMessage` — one documented location, the
28+
same one the console reads — and the run's per-node `summary` in
29+
`error.details.summary`. `durationMs` is no longer carried on this response.
30+
- **A stale suspension ⇒ `404`.** The flow the run belongs to was deregistered, or the
31+
node it was parked on was edited away under a live pause. Nothing ran and the pause can
32+
never continue, so this is reported as terminal rather than as a business rejection.
33+
The engine now classifies both cases as `RUN_NOT_FOUND`; the message names which one.
34+
- **Unchanged:** every refusal that leaves the suspension intact keeps its own code and
35+
stays retryable — `PERMISSION_DENIED` (403), `INVALID_SIGNAL` /
36+
`INVALID_SCREEN_INPUT` (400), `RESUME_IN_PROGRESS` (409), `STORE_UNAVAILABLE` (503) —
37+
and a resume that pauses again still answers 200 with the next screen.
38+
39+
**`@objectstack/client`:** `client.automation.resume()` and
40+
`client.project(id).automation.resume()` now **reject** on a failed run instead of
41+
resolving with `{success: false, error, summary}` — the SDK throws on every non-2xx
42+
before unwrapping. Callers that inspected the resolved value must move to a `catch`:
43+
44+
```ts
45+
try {
46+
await client.automation.resume(flow, runId, { inputs });
47+
} catch (err: any) {
48+
err.code; // 'FLOW_FAILED' (400) — the run ran and failed
49+
err.httpStatus; // 400 | 404 | 403 | 409 | 503
50+
err.message; // the node failure, verbatim
51+
err.details?.errorMessage; // the flow author's own message, when the flow declares one
52+
}
53+
```
54+
55+
Raw-HTTP callers that treated `2xx` as success and never opened the inner envelope now
56+
see the failure they were already being told about, one level up.
57+
58+
<!-- adr-0087: not-required (no-migration-prescription) retires no metadata surface: no Zod schema, no authorable key, no stored sys_metadata row changes shape, so `objectstack migrate meta` has nothing to rewrite and no ledger entry can be written for it. What changes is an HTTP status plus an SDK method's promise contract, and the only channel that reaches those consumers is this changeset itself. -->
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
feat(cli): `os migrate duplicates` — an operator-facing inventory of the business identifiers the tenancy split already minted twice (#8928)
6+
7+
Two producers of untenanted rows have been closed (#8686's seed loader plus its
8+
one-shot backfill, and #8844's runtime system-context write). Neither touches
9+
the **damage already done**, and both rulings say the same thing about it: a
10+
business identifier that has already been handed out — on an invoice, in a
11+
notification, in another system's idempotence key — is not the platform's to
12+
rewrite. What an operator needs instead is to know **which ones they are**.
13+
14+
```bash
15+
os migrate duplicates # the whole report, JSON on stdout
16+
os migrate duplicates > duplicates-2026-08-17.json
17+
os migrate duplicates --object crm_case # narrowed (and the report says so)
18+
```
19+
20+
**Run it BEFORE the #8686 backfill is applied.** The evidence is perishable:
21+
`organization_id = NULL` is the marker that says "this row came from the
22+
untenanted side", and it is exactly what that repair overwrites. The repair also
23+
merges and deletes the `__global__` counter, which is the report's only
24+
forward-looking line — an install that repairs before reporting can never
25+
produce it again. The command itself applies nothing: it boots read-only (no
26+
DDL, no seed, no database file brought into existence) and issues SELECTs only.
27+
28+
What the report contains, per the 2026-08-16 maintainer ruling on all five of
29+
the card's decision points:
30+
31+
- **one row per duplicated value, with its holders** — id, organization,
32+
partition and creation timestamp per row, so the operator can decide case by
33+
case rather than per value. JSON on stdout, no persistence and no new schema:
34+
the operator archives it;
35+
- **the narrow definition of duplicate** — a value held by rows in more than one
36+
of the partitions `COALESCE(organization_id, '__global__')` separates
37+
(ADR-0120 D3). A value repeated *inside* one partition is refused by the
38+
partitioned unique index and is not reported;
39+
- **the live condition too** — an object still running a `__global__` counter
40+
beside an organization-scoped one is about to mint more duplicates;
41+
- **a data-side probe**`GROUP BY <field> HAVING COUNT(*) > 1` over the
42+
object's own table, never an enumeration of `_objectstack_sequences`, so a
43+
duplicate whose counter was since merged is still found. The counter table is
44+
read for the live condition alone, because that fact lives nowhere else.
45+
46+
Scope is every registered object that is organization-scoped, and on it every
47+
`autonumber` or `unique` field. `sys_` / `cloud_` / `ai_` objects are **not**
48+
filtered out — that filter is correct for a repair (platform seeds stay global
49+
by design) and wrong for a report, which must not silently omit a real
50+
duplicate. Anything that could not be probed is listed in `skipped` with the
51+
driver's own message, so a target the command could not read never reads as a
52+
target with no findings; a driver with no raw-SQL seam refuses loudly and exits
53+
non-zero rather than reporting zero duplicates.
54+
55+
⛔ Reporting is all it does. Renumbering, deduplicating or otherwise rewriting an
56+
already-minted identifier stays out of scope per both rulings.

0 commit comments

Comments
 (0)