Skip to content

Commit 6f47156

Browse files
authored
Merge branch 'main' into claude/issue-11819-adr-0125-record-drift
2 parents 8b422a7 + 494279c commit 6f47156

8 files changed

Lines changed: 990 additions & 37 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
feat(cli): `os build --json` carries the computed advisory lists on every failure exit, not the success payload alone (#11772)
6+
7+
**Machine-contract widening on the `--json` failure payloads.** A consumer that
8+
today branches on `warnings` being ABSENT from an `os build --json` failure
9+
payload sees a different shape after this change.
10+
11+
## What was wrong
12+
13+
The text face prints its advisory blocks before the gates that can stop the
14+
run — the #11529 author-time advisories at step 3b, the #3786 undeclared
15+
authoring-key findings at 3d — and both end in `— re-run with --json for the
16+
full list`. But `warnings` lived on the TERMINAL SUCCESS payload only (plus,
17+
for `ruleAdvisories` alone, the author-time-rules failure). On a tree with 60
18+
undeclared authoring keys *and* a package-docs error:
19+
20+
```
21+
os build Undeclared authoring keys (60) … 50 rows …
22+
… and 10 more … — re-run with --json for the full list
23+
os build --json {"success":false,"error":"docs validation failed","issues":[…]}
24+
^ the 60 keys nowhere
25+
```
26+
27+
The remedy the notice named returned a payload that did not contain the list,
28+
and the author could not reach the withheld entries by any route until an
29+
unrelated later failure was fixed — the "the remedy named is unreachable"
30+
shape of #11643 and #11391.
31+
32+
## Which exits gain the field
33+
34+
All nine failure exits of `os build --json`. Six already had a payload of their
35+
own; three more were found while enumerating (the filing card's table listed
36+
six). `warnings` is now present on every one, alongside each exit's existing
37+
keys, which are unchanged:
38+
39+
| exit (step) | existing keys | `warnings` before | after |
40+
| --- | --- | --- | --- |
41+
| `strict-body: missing body` (2b) | `issues` | absent | `[]` |
42+
| protocol parse failure (3) | `errors` | absent | `[]` |
43+
| `author-time rules failed` (3b) | `issues` | `ruleAdvisories` | unchanged |
44+
| `capability provider preflight failed` (3c) | `issues` | absent | rule + capability |
45+
| `access matrix drift` (3e) | `changes` | absent | rule + key + capability |
46+
| `docs validation failed` (3f) | `issues` | absent | all four lists |
47+
| `--no-runtime-bundle` refusal (4b) | `error` | absent | all four lists |
48+
| `runtime bundle failed` (4b) | `error` | absent | all four lists |
49+
| thrown / caught (bottom) | `error` | absent | what the run had computed |
50+
51+
The success payload is unchanged in content: its
52+
`[...ruleAdvisories, ...docWarnings, ...unknownKeyWarnings, ...capProviderWarnings]`
53+
spread — `os validate --json`'s order minus its trailing `structuralWarnings`
54+
— moved to a single `warningsSoFar()` site that every exit now reads, so the
55+
member order cannot drift between exits.
56+
57+
## What a consumer keying off its absence should do instead
58+
59+
`warnings` is no longer a signal of which exit produced the payload. Read
60+
`success` (and `error` / `errors`) for that; a consumer that inferred "this is
61+
a failure payload" from a missing `warnings` must switch to `success === false`.
62+
63+
`warnings: []` on a failure payload does NOT mean "this tree raises no
64+
advisories". It means **this run stopped before those advisories were
65+
computed** — the two early exits above (`strict-body`, protocol parse) run
66+
before any advisory step, so their list is empty by construction. A consumer
67+
that needs the full advisory set for a tree must read it from a run that
68+
reaches at least the gate that computes it, or from `os validate --json`.
69+
70+
`warnings` is always an array on every `os build --json` payload, success or
71+
failure, so it can be read unconditionally — that shape constancy is the point
72+
of the change (maintainer ruling 2026-08-25, option 1 of three; option 2,
73+
"carry them only where the text face printed them", was rejected as the hardest
74+
contract to declare).
75+
76+
Advisories stay CARRIED, never recomputed: each list is still computed at
77+
exactly the step that owns it, so an exit upstream of a step legitimately
78+
reports that list empty and no failure path pays for a computation it did not
79+
already do.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
fix(client): bind the three verifiable methods of the unannotated return-type erasure population to their spec contracts (#11925)
6+
7+
**Return-type narrowing on a published SDK.** No runtime change — the value each
8+
method resolves to is byte-identical before and after. Only the DECLARED type
9+
moved, off `any`, which is precisely why a runtime test cannot observe it and
10+
the pins for it are type-level.
11+
12+
`any` is assignable to everything and admits every property read, so for each
13+
method below a consumer's code could stop compiling where it previously did
14+
not: assigning the result to an unrelated annotation, reading a property the
15+
bound type does not declare, or forwarding the value to a differently-typed
16+
parameter.
17+
18+
## What changed, per family
19+
20+
**`client.packages.list``{ packages: InstalledPackage[]; total: number }`**
21+
(was `{ packages: any[]; total: number }`). A consumer stops compiling if it
22+
reads any key off a row that `InstalledPackage` does not declare. Note in
23+
particular `source` (`'database' | 'registry' | 'both'`): the REST surface
24+
spreads it onto each row, the dispatcher surface does not, and it is therefore
25+
deliberately NOT declared — code reading `pkg.source` off this result compiles
26+
today and will not after. `total` and the array envelope are unchanged.
27+
28+
**`client.packages.update``InstalledPackage`** (was `any`). This method
29+
declared no envelope before, so nothing about the shape claim changed; a
30+
consumer stops compiling if it reads an undeclared key off the returned row, or
31+
assigns the result somewhere `InstalledPackage` does not fit.
32+
33+
**`ScopedProjectClient.packages.get``{ package: InstalledPackage }`** (was
34+
`{ package: any }`). The `{ package }` envelope is unchanged; only the member
35+
narrowed. A consumer stops compiling if it reads a key off `.package` that
36+
`InstalledPackage` does not declare — again including `source`, which this
37+
route does send and which stays undeclared for consistency with its already
38+
bound `list` sibling.
39+
40+
## What deliberately did NOT change
41+
42+
The other 36 methods in the measured population keep their erased `any`, each
43+
with a docblock stating why and pointing at the issue that carries it:
44+
`meta.*` history/diagnostics (9) and eight `packages.*` routes have no published
45+
response contract to bind to (#12038); `client.packages.get` has two mounted
46+
surfaces that emit different envelopes and `install`/`enable`/`disable` declare
47+
an envelope no surface emits (#12034); the 15 cloud `projects.*` methods call a
48+
control plane that speaks snake_case while the `@objectstack/spec/cloud` rows are
49+
camelCase, so binding to them would compile and be false (#12036).
50+
51+
No consumer loses anything by those staying `any` — they are exactly as
52+
permissive as before.

0 commit comments

Comments
 (0)