Skip to content

Commit 8b8a6cf

Browse files
os-devclaude
andcommitted
Merge origin/main (4d1d9d9) into claude/issue-19133-governed-tiers — the north-star row keeps main's position with its tier; PD #14 keeps the tiered paragraph
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
2 parents d3bb7e0 + 4d1d9d9 commit 8b8a6cf

60 files changed

Lines changed: 3214 additions & 664 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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — retire the CEL predicate arms of `ServiceLevelIndicator.successCriteria`
6+
and `TraceSamplingConfig.composite[].condition`, the two observability predicates nothing
7+
ever evaluated.
8+
9+
Both slots were `z.union([<a structured arm>, <the evaluated expression schema>])`. The
10+
expression arm parsed, normalized a bare string to `{ dialect: 'cel', source }`,
11+
registered, and was served back — and **nothing anywhere evaluated it**. An identity scan
12+
over the whole tree finds every hit for `successCriteria`, `ServiceLevelIndicatorSchema`
13+
and `TraceSamplingConfigSchema` outside `packages/spec/src` to be a generated artefact or
14+
prose; inside it the only readers are the schemas' own unit tests and the two census tests
15+
that enumerate expression slots. No service, plugin, runtime or CLI path reads either key.
16+
So an author — very often an AI reading the generated reference page (ADR-0033) — who
17+
wrote `successCriteria: 'p95 < 300ms'` got a green parse and no signal, indistinguishable
18+
from a predicate that ran and answered.
19+
20+
ADR-0049 enforce-or-remove; maintainer ruling 2026-09-18 (director decision batch #160
21+
item 3, letter A). By the standing criterion that a declared-but-unread capability is kept
22+
only when mainstream platforms in the domain have it: application platforms do not carry
23+
SLI success criteria or trace-sampling conditions as authorable application metadata —
24+
that lives in observability infrastructure (SLO products, OTel sampling policy) and is
25+
structured there, not a free expression. The `cron-declared-unwired` family was retired
26+
outright under the same ADR after the same measurement.
27+
28+
## FROM → TO
29+
30+
| you wrote (17.4 and earlier) | write instead |
31+
| --- | --- |
32+
| `successCriteria: 'p95 < 300ms'` | `successCriteria: { threshold: 300, operator: 'lt', percentile: 0.95 }` — the structured rule this slot has always carried |
33+
| `successCriteria: { dialect: 'cel', source: 'p95 < 300ms' }` | the same structured rule; the envelope spelling goes with the bare-string one |
34+
| `condition: 'record.amount > 10'` on a composite sampling branch | `condition: { service: 'api', attributes: { 'http.route': '/v1/orders' } }` — a structured filter object carrying no `dialect` key |
35+
| `condition: { dialect: 'cel', source: 'record.amount > 10' }` | the same structured filter; an object carrying `dialect` is refused as an expression attempt |
36+
37+
**The one-line fix:** delete the predicate and write the structured shape the slot already
38+
carried. A criterion or a sampling rule the structured shape cannot express has no home in
39+
application metadata at all — it belongs in the SLO product or the OpenTelemetry sampler
40+
configuration that actually evaluates it. ⛔ Do not translate a predicate into a threshold
41+
by guessing the number: nothing was evaluating it, so there is no behaviour to preserve and
42+
a wrong number is worse than an absent one.
43+
44+
## The retirement kit
45+
46+
- **Neither KEY is retired — one ARM of each key's union is.** `successCriteria` and
47+
`condition` both survive with their structured arm intact, so `retiredKey()` and an
48+
ADR-0087 D2 strip are both the wrong tool: they retire a key. The prescription hangs on
49+
the surviving schema's own `error` map, dispatched on `issue.input` — the
50+
`HookBodyCapability` / `object.managedBy: 'system'` pattern for a narrowing a key
51+
survives.
52+
- **Where the prescription reaches, measured on zod 4.4.** A schema's `error` map is
53+
consulted for the top-level `invalid_type` a NON-OBJECT raises, and not for the child
54+
issues a wrong-shaped OBJECT raises. So on `successCriteria` the bare-string spelling
55+
carries the prescription and the `{ dialect, source }` envelope is refused by the
56+
structured arm's own missing-key issues (`threshold`, `operator`). On `condition` both
57+
spellings carry it, because the structured arm is a record whose aborting `dialect`
58+
refine sees the object itself. Pinned both ways in the schemas' unit tests, the negative
59+
included: a value refused for a reason that is NOT the retirement must not borrow its
60+
sentence.
61+
- **ADR-0087 disposition: a D3 SEMANTIC entry**, `observability-cel-predicates-retired`,
62+
not a D2 conversion. A predicate is an intent that no threshold/operator pair or
63+
attribute filter records; a mechanical strip would delete what the author meant and leave
64+
no trace of which SLI or which sampling branch lost it — and it would not even be lossless
65+
in the weak sense, because `successCriteria` is REQUIRED (a strip leaves an SLI that no
66+
longer parses) and a composite branch stripped of its `condition` declares no condition at
67+
all. That is the one place this retirement parts company with the two precedents it copies
68+
its MECHANISM from: `crypto.hash` on `HookBodyCapability` and `managedBy: 'system'` both
69+
ALSO registered a D2 conversion, because for each of them a mechanical rewrite existed.
70+
Here none does, which is what makes D3 the right disposition rather than merely an
71+
available one. The prescriptions therefore carry **no** `os migrate meta` sentence — that
72+
sentence is owed only where a conversion covers the surface.
73+
- **The same-major D3 record is absorbed, per the playbook's 「同 major 记账」.** The
74+
`evaluated-expression-slots-source-required` entry landed into this same unpublished step,
75+
and it enumerated these two slots among its 36 declaring positions while instructing the
76+
upgrader to give a sampling `condition` a dialect and a non-blank `source` — the exact
77+
envelope this head now refuses. Both entries first ship together, so the composite of the
78+
two changes is the retirement alone: that entry now reads 34 positions, names the two
79+
absentees and why, and routes them to this retirement instead of to its own repair.
80+
- **The surviving accept sets are pinned beside the refusals.** `successCriteria` still
81+
takes `{ threshold, operator, percentile? }`; a composite `condition` still takes any
82+
filter object carrying no `dialect` key — `{ source: 'x' }` included, because `source`
83+
alone is an ordinary filter key and the retirement narrowed the `dialect` door only.
84+
- **FOUR published JSON Schemas change projection direction**, and it is mechanical rather
85+
than chosen: the retired arm held the last `.transform()` in each of these subtrees, so
86+
each def now projects in output mode instead of falling back to the input shape. All four
87+
lose `x-io: input`, and what each gains differs:
88+
89+
| published schema | gains |
90+
| --- | --- |
91+
| `system/MetricsConfig` | `default: []` on `slis`, plus 8 `required` members |
92+
| `system/TracingConfig` | `default: {"type":"always_on","rules":[]}` on `sampling`, plus 4 `required` members |
93+
| `system/ServiceLevelIndicator` | one `required` member, `enabled` |
94+
| `system/TraceSamplingConfig` | one `required` member, `rules` |
95+
96+
Only the first two carry a `default` move, so only those two are declarable in
97+
`DEFAULT_CHANGES_BY_MAJOR` — the nested pair's `required` growth has no ratchet row to
98+
live in and is stated here instead. A `required` that lists defaulted keys is this repo's
99+
existing output-mode convention, not a new one, and the same-category control
100+
`system/CacheConfig` is untouched. The reference pages show the same signature: the nested
101+
type cells of both pages lose the `?` from their default-bearing keys. **No runtime default
102+
moves** — measured twice, by byte-identity of the untouched `.default(…)` and by parsing a
103+
minimal config on the built package.
104+
105+
## What is deliberately NOT in this change
106+
107+
- **The structured arms.** `{ threshold, operator, percentile }` and the sampling filter
108+
record are equally unread today. The ruling says so and leaves them to their own card:
109+
they carry no dialect and are outside the expression ledger's remit.
110+
- **`skills/objectstack-formula/SKILL.md`**, which still lists `metrics` / `tracing` under
111+
`structured | cel`. The ruling assigns that correction to the skills lane, at tier, and
112+
this diff does not touch it.
113+
- **`packages/spec/src/shared/expression.zod.ts`.** `EvaluatedExpressionInputSchema` is
114+
untouched and stays the schema of every remaining evaluated slot; what left is two
115+
references to it.
116+
117+
Shipped as `minor` under the repo's launch-window convention, in which `major` is refused
118+
by `check-changeset-no-major` and breaking-ness is carried by the banner above plus the
119+
ADR-0087 disposition rather than by the level.
120+
121+
Clause-②: yes (narrowing)
122+
123+
<!-- adr-0087: registered observability-cel-predicates-retired -->
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/client": minor
4+
"@objectstack/plugin-auth": minor
5+
---
6+
7+
The identity read routes now serve what `@objectstack/spec/identity` declares: `metadata` arrives DECODED on every organization route that reads the row back, and `updatedAt` is declared optional on `Organization` / `Member` / `Invitation` — the shape better-auth's own serializer documents (#18728).
8+
9+
Clause-②: yes (widening) — `updatedAt` moves from required to optional on three published schemas, so the set a consumer may hand to `OrganizationSchema` / `MemberSchema` / `InvitationSchema` grows by exactly one shape: the key being absent. Nothing previously admitted is refused, nothing is renamed, and no producer is required to write it. Contract-review tier.
10+
11+
Three published schemas could not parse a served response. `OrganizationSchema` declared `updatedAt` required and `metadata` an object; the four organization read routes (`setActive`, `get`, `delete`, `list`) carried no `updatedAt` at all and served `metadata` as the stored JSON text. `@objectstack/client` had recorded that as three 「not relayed」 notes rather than as a defect, and with zero in-repo consumers nothing went red — the audience was entirely external. Maintainer ruling C (batch #158 item 4) fixed the producer and made the one remaining key conditional on a measurement, which is what decided each half:
12+
13+
- **`metadata` is decoded at the producer, unconditionally** — it is our column. plugin-auth's data adapter decodes `sys_organization.metadata` out of its stored JSON text on its READ verbs, so all four routes serve the object the spec declares, and an unset column is OMITTED rather than sent as `null`. ⛔ The write verbs are deliberately untouched: better-auth's own organization adapter decodes the `create` / `update` echoes itself and discriminates on the value still being a string, so decoding there would fold the create echo's `metadata` to `undefined`. Both directions are pinned.
14+
- **`updatedAt` aligns to the documented wire** — ruling C's own fallback A, and its two conditions were measured against the installed better-auth 1.7.3 rather than assumed. The routes are better-auth's endpoints mounted through a single catch-all, each answering `ctx.json(...)` with no ObjectStack post-processing; and the vendor's `organization`, `member` and `invitation` models declare no `updatedAt` field, while its adapter factory's output transform iterates the declared fields only, so an undeclared column is dropped before any route sees it. Control, in the same file: the vendor's `team` and `organizationRole` models DO declare `updatedAt`, so the absence is a reading. For `member` and `invitation` there is additionally no column to serve — `sys_member` and `sys_invitation` are `managedBy: 'better-auth'`, the one disposition under which the platform injects no audit family, and neither declares `updated_at` itself.
15+
- **`@objectstack/client` relays the schemas.** `OrganizationWire` is the spec's `Organization`, `OrganizationMemberWire` is `Member`, and `OrganizationInvitationWire` is `Invitation` with `status` narrowed per route plus the three members the platform adds on top (`teamId` and the two ADR-0105 D8 placement fields, which the non-strict schema strips). The three 「not relayed」 notes are gone.
16+
- **The negative controls are the point.** "The client relays the spec schemas" and "the client stopped validating" look identical from a green positive test, so every accepted body is paired with a refused one — a required field genuinely missing, `metadata` still arriving as the stored JSON TEXT, and a `createdAt` or `updatedAt` present but not a datetime. `.optional()` widened the accept set by absence ONLY; a value that is there is still held to `z.string().datetime()`.
17+
18+
**Not declared breaking, and the reason is the repo's own criterion** rather than the level being convenient. AGENTS.md binds the breaking class to removing or renaming something an author can write, and to the `(narrowing)` arm of the clause-② pair. Neither holds here: nothing is removed, renamed or retired; the one `packages/spec` edit only widens an accept set; and the `metadata` half is a producer brought into line with a contract this package has published all along — `OrganizationSchema.metadata` has declared an object since it was written, and the client's own comment called the served text 「not relayed」 rather than a shape anyone was promised. No ADR-0087 disposition is claimed because no breaking change is declared: no authored metadata moves, so `objectstack migrate meta` has nothing to visit, `spec-changes.json` has nothing to project and the upgrade guide has no row to gain. These three schemas are not metadata types — not in `DEFAULT_METADATA_TYPE_REGISTRY`, no authorable surface. ⚠️ Stated here rather than assumed silently, because it is the one judgement in this diff that the contract review the `Clause-②: yes` declaration commissions should confirm.
19+
20+
**What a consumer notices**, and where it is delivered: `organization.metadata` was the stored JSON text and is now the decoded object, so a caller that decoded it itself drops that step.
21+
22+
```ts
23+
// before — the caller decoded what the route sent
24+
const meta = JSON.parse(org.metadata ?? '{}');
25+
// after — the producer decoded it; the key is ABSENT when unset
26+
const meta = org.metadata ?? {};
27+
```
28+
29+
The channel that reaches that caller is the compiler, on the line that used to work: `JSON.parse` no longer accepts the value. `updatedAt` needs nothing in either direction — it was never on this family's wire, so no caller can have been reading a value, and the declaration now says so out loud instead of promising one.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/objectql': patch
4+
---
5+
6+
Correct six `edit distance cannot reach` citations that are measurably false, and pin the role each alias entry actually plays.
7+
8+
`aliases` has two jobs, not one: filling a gap the distance fallback leaves empty, and overruling a hit the fallback reaches and gets wrong. The lookup is `aliases[aliasProbe(key)] ?? findClosestMatches(key, knownKeys, budget, 1)[0]` — the table is consulted first and wins outright — and the budget is `Math.max(2, Math.floor(key.length / 3))`. A sentence saying distance "cannot reach" the cited case denies the second job, and in three places the cited case is itself an example of it.
9+
10+
- **`latitude` → `lat` is an OVERRULE, not a gap** (`data/field-value.zod.ts`, `data/default-value-shape.ts`, `data/field-value.test.ts`, `data/default-value-shape.test.ts`, objectql `validation/record-validator.ts`). `latitude` is 8 characters, so the budget is 2; `lat` is 5 edits away and out of reach, but the declared `altitude` is exactly 2 — so without the curated entry the bare fallback answers `latitude` → `altitude` and points an author who wrote a GPS latitude at the elevation member. Four docblocks cited this pair as proof that aliases exist only where distance reaches nothing.
11+
- **`postal_code` → `postalCode` never involved an alias at all** (`data/default-value-shape.ts`). Scoring folds case and separators on both sides, so it is 1 edit against a budget of 3 — the worked example rendered in that docblock is the fallback's own answer, not the `AddressValueSchema` table's.
12+
- **`uri` → `url` is reachable and agreeing** (`data/driver/turso.zod.ts`). The block was headed "the spellings edit distance cannot reach"; that is true of five of its six rows and false of `uri`, which is 1 edit from `url` against a budget of 2. The row is a pin on an answer the fallback already gets right, not a gap-filler.
13+
14+
Prose plus new pins. No alias is added or removed, no schema, key list, strictness, suggestion or error message changes: `Clause-②: no`. The three roles are now asserted — `longitude` (gap), `latitude` (overrule, with the negative half), `altitud` (a plain typo still riding the fallback) in `data/field-value.test.ts`, and `dsn` (gap) beside `uri` (reachable) in `data/driver/turso.test.ts`.

0 commit comments

Comments
 (0)