Skip to content

Commit 25e89e9

Browse files
committed
Merge origin/main into claude/issue-18058-package-install-door-enforce
Resolves the one conflict by hand: packages/spec/dropped-refinements.baseline.json is deliberately left out of .gitattributes' merge=os-regen routing and has no gen: script, so its two sides are unioned entry by entry. The two sides are disjoint: this branch adds api/PackageInstallBody; main (#18704) adds data/QueryTransportParams and data/QueryWithTransport and moves the site list of api/FindDataRequest. No schema key moved on both sides, so no site list had to be chosen between. The header is derived from the merged body and adjudicated by packages/spec/scripts/build-schemas.ts in the follow-up regeneration commit. content/docs/references/index.mdx was routed to the merge driver, which resolved it to this branch's bytes and silently dropped main's side (measured: the merged blob equalled the branch blob exactly). main's side is restored here so the regeneration in the follow-up commit runs on a known-good base. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
2 parents 26805a1 + 0b31d90 commit 25e89e9

107 files changed

Lines changed: 8271 additions & 768 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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata-protocol": patch
4+
---
5+
6+
The query TRANSPORT dialect is declared — keys AND values — and the `findData` fold now derives from that one declaration.
7+
8+
`FindDataRequestSchema.query` declared `QuerySchema` — the canonical QueryAST — while the shipped `findData` door also accepted a second spelling of the same query through the same slot: `$filter` / `$top` / `$skip` / `$orderby` / `$select` / `$expand` and the plural `filters`. `@objectstack/metadata-protocol` folded them from a module-private table whose own comment called them "the wire-only spellings no schema declares". Two dialects, one slot, one of them declared — so every caller speaking the second was unverifiable at build time and unrejected at runtime.
9+
10+
**New in `@objectstack/spec/data`** (9 exports, 0 removed):
11+
12+
- `QueryTransportParamsSchema` / `QueryTransportParams` / `QueryTransportParamsParsed` — the transport parameters, each carrying the value of the canonical slot it folds onto.
13+
- `QUERY_TRANSPORT_ALIAS_SLOTS` — `RPC_QUERY_ALIAS_SLOTS` extended with the transport-only spellings (`filters` / `$filter` onto `where`, `$expand` onto `expand`).
14+
- `QUERY_TRANSPORT_DOLLAR_ALIASES` — the `$`-to-bare pairs that fold in two hops (`$top` onto `top` onto `limit`).
15+
- `QUERY_TRANSPORT_DOLLAR_PARAMS` — the `$` spellings a boundary quotes when it refuses an undeclared one.
16+
- `QueryWithTransportSchema` / `QueryWithTransport` / `QueryWithTransportParsed` — the query slot whose declared input is the AST or its transport spelling and whose parsed output is the AST plus the `count` flag.
17+
18+
**`FindDataRequestSchema.query` is that slot now.** Its `z.input` admits the canonical AST, the transport spelling, or a bag carrying both. Its `z.output` is `QueryAST & { count?: boolean }` — the canonical AST, plus the response total-count flag, which rides inside this slot on the wire and is read off it by `findData` rather than passed to the engine. The output is CONSTRUCTED: the fold's result is parsed by the AST schema and that parse's result is what leaves the transform, so a transport key or a non-AST value cannot reach a consumer. The transport form is the FLATTENED SPELLING of the canonical AST with a 1:1 alias table — never a second semantics — so `QuerySchema` itself is untouched and still drops a `$` key as unknown.
19+
20+
**One semantics means one set of VALUES, not only one set of keys, and that is what this declaration now enforces.** Every spelling of a slot accepts the same value shapes; each is lowered to the canonical member's declared shape, or refused. What lowers: a stringly-typed `$top` / `$skip` (`'50'` becomes `50`), a comma list on `$select` / `$searchFields` / `$expand`, a `{field: direction}` sort record, a relation-name list on `populate`, `'true'` / `'false'` on `$count`, and the input-only `FilterArray` sugar (`['status', '=', 'open']`) on every spelling of the filter slot — `where` included — lowered through `parseFilterAST`, the one declared sink (#5158 ruling C; `QuerySchema.where` still refuses the array).
21+
22+
**What is REFUSED at the parse**, because lowering it would mean parsing the spec must not do, and because emitting it would put a value under the AST type that the AST does not declare:
23+
24+
- a non-numeric `$top` / `$skip` (`$top: 'abc'`, `$top: ''`) — `400` instead of an engine call with `limit: null`, i.e. an UNBOUNDED read under a `200`, or `limit: 0`;
25+
- a JSON-encoded `$filter` string (`'{"status":"open"}'`);
26+
- an OData sort EXPRESSION on `$orderby` / `sort` (`'name desc'`, `'-created_at'`, `['name']`) — the record and `SortNode[]` forms are unaffected;
27+
- a filter array no lowering can express, such as the INFIX join `[condA, 'and', condB]` — the prefix form `['and', condA, condB]` is the one the platform reads, and the engine already answered `400` for the infix one;
28+
- a `$count` that is neither the boolean nor `'true'` / `'false'`;
29+
- two spellings of one slot carrying different values — reported at the canonical path, quoting the spelling the caller actually wrote (`$orderby`, not `orderBy`).
30+
31+
These refusals narrow no DECLARED surface: none of these value shapes was ever declared — `FindDataRequestSchema.query` was `QuerySchema`, which STRIPPED every one of these keys rather than declaring it.
32+
33+
**Five of them were nonetheless SERVED, and now answer `400 VALIDATION_FAILED` at the ingress.** The route forwards the ORIGINAL body, not the parse output, so a key the old schema stripped still reached the door, which read it and answered `200`. A `POST /data/:object/query` body written one of these five ways stops working; each has a declared spelling that means the same thing:
34+
35+
| body that now answers `400` | what the door served it as | write instead |
36+
|---|---|---|
37+
| `{ $orderby: 'name desc' }` | `orderBy: [{ field: 'name', order: 'desc' }]` | `{ $orderby: { name: 'desc' } }` — or `{ orderBy: [{ field: 'name', order: 'desc' }] }` |
38+
| `{ sort: '-created_at' }` | `orderBy: [{ field: 'created_at', order: 'desc' }]` | `{ sort: { created_at: 'desc' } }` — or `{ sort: [{ field: 'created_at', order: 'desc' }] }` |
39+
| `{ $orderby: ['name'] }` | `orderBy: [{ field: 'name', order: 'asc' }]` | `{ $orderby: { name: 'asc' } }` — or the `SortNode[]` form |
40+
| `{ $filter: '{"status":"open"}' }` | `where: { status: 'open' }` | `{ $filter: { status: 'open' } }` |
41+
| that same JSON string on `filters` or `filter` | `where: { status: 'open' }` | the object form on whichever of the two keys you write |
42+
43+
**`GET /data/:object` still serves every one of those shapes.** The querystring path does not parse through this schema at all — `FindDataRequestSchema` is parsed at exactly one call site, the POST handler — so `?$orderby=name desc`, `?sort=-created_at` and `?$filter={"status":"open"}` answer exactly as before. What narrowed is the POST body alone — the platform has not stopped accepting these spellings everywhere.
44+
45+
The remaining refusals in the list narrow nothing that was served correctly; they move an unservable body's refusal earlier — from the engine, or from a wrong answer under a `200`, to the ingress that can name the parameter to fix.
46+
47+
**`@objectstack/metadata-protocol` folds by the spec export** instead of its own table, and both resolved tables — plus the `$`-parameter list its `UNSUPPORTED_QUERY_PARAM` refusal quotes — are pinned byte-equal to their pre-change values. An undeclared `$` spelling is still refused loudly with the same `400 UNSUPPORTED_QUERY_PARAM`; the sentence now quotes `QUERY_TRANSPORT_DOLLAR_PARAMS` rather than a hand-copied list, so a spelling added to the table cannot leave the refusal naming a set the door no longer has.
48+
49+
Measured and unchanged: `getData` takes `select` / `expand` directly and carries no `query` slot, and `updateManyData` / `deleteManyData` take `records[]` / `ids[]` — none of the three has a transport-dialect split to declare.
50+
51+
Clause-②: yes (widening)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: the ADR-0090 D3 vocabulary freeze visits `objects[].fieldGroups[]` (#18306)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) an authoring-time lint rule reports a word on one more declaration surface; no key, symbol, enum member or stored value moves, so a stored metadata row is structurally identical before and after and `objectstack migrate meta` has nothing to rewrite -->
8+
9+
**BREAKING** in the accept-set sense — a declaration that passes today can fail tomorrow.
10+
Landing in the launch window as `minor` (the lockstep convention: `major` is refused by
11+
`check-changeset-no-major`, and breaking-ness is carried by this banner plus the ADR-0087
12+
disposition above).
13+
14+
**Clause-②: no (narrowing)** — the rule refuses more than it did; no key is added to any
15+
published payload and no public surface grows, so `lanes/spec.md`'s widening test is not
16+
met. Narrowing is still a semantic-surface change, which is why it is declared here rather
17+
than shipped silently.
18+
19+
`security-role-word` (ADR-0090 D3) judged an object's name, field names and labels, action
20+
names and labels, permission sets, positions, apps and books — and not the field-group
21+
heading that renders directly above the fields it was already judging. So on one record page
22+
a field labelled `Role Of Record` was refused while the group header above it,
23+
`Account & Role`, was admitted: the author renames the field and the heading keeps the word.
24+
That is the exact "refused on one surface, admitted on another" shape (#7220) that this
25+
rule's own split was made to avoid, one grain finer.
26+
27+
Both halves of the group declaration are judged, as on every other surface: `key` is an
28+
identifier (`Field.group` assigns membership by it, and a layout section's `group` inherits
29+
the group by it, ADR-0085 §5), `label` is the header an admin reads. ADR-0090 D3 bans the
30+
word in "identifiers, UI copy, and documentation", and a field group declares both.
31+
32+
Pages, views and components stay out, unchanged: `role` there is the HTML/ARIA attribute — a
33+
machine word with a fixed foreign meaning, not a word the author picked. `listViews`,
34+
`recordTypes` and the other label-bearing surfaces are deliberately not swept in with this;
35+
each needs its own reading first.
36+
37+
**What an author does.** Nothing is renamed for you and nothing is auto-rewritten: the
38+
platform vocabulary is `permission_set` (capability), `position` (distribution),
39+
`business_unit` (hierarchy), and the refusal itself names it at the exact path
40+
(`objects[i].fieldGroups[j].key` / `.label`). A group heading reading `Account & Role`
41+
becomes `Account & Assignment`; a group keyed `role_info` becomes `assignment`, and the
42+
member fields' `group` pointers move with it.
43+
44+
Unaffected: a system object (`sys_*` / `isSystem: true`) keeps the better-auth exemption on
45+
its field groups exactly as it keeps it on its fields, and a group carrying no reserved word
46+
is silent.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/client": patch
3+
---
4+
5+
fix(client): `normalizeSessionResponse`'s JSDoc records the `data.user.image` gap as closed, not as tracked (#18510)
6+
7+
Clause-②: no
8+
9+
No declaration, accept set, export or runtime behaviour moves. What moves is one
10+
sentence of developer commentary and the pin that now holds it honest.
11+
12+
The block above `normalizeSessionResponse` says what the lift compensates for,
13+
so it names the cards that opened and closed each compensation. One clause was
14+
still in the present tense:
15+
16+
> … with one gap that is NOT this: `data.user.image` served `null` against a
17+
> declared `string | undefined` (#17235, tracked separately).
18+
19+
Both halves went false when `SessionUserSchema.image` widened to
20+
`z.string().nullish()` and #17235 closed — the sentence described a live gap
21+
that no longer existed and pointed the next reader at a closed card as somewhere
22+
to go look. It now reads in the past tense, naming the widening that closed it
23+
and the residue list in `auth-login-register-envelope.test.ts` that is pinned
24+
empty. Nothing else in the block moves.
25+
26+
**Why this is a `patch` and not `skip-changeset`, measured rather than
27+
assumed.** "Only comments changed" is not "nothing published moves", and on this
28+
package the two answers differ. `@objectstack/client` ships `dist`, `README.md`
29+
and `CHANGELOG.md`; `dist` is six files (`index.js`, `index.mjs`, `index.d.ts`,
30+
`index.d.mts` and a `.map` beside each of the two bundles — this package emits no
31+
`*.cjs` and no `*.d.cts`). Built from the same tree before and after the change:
32+
33+
- the comment text reaches **none** of the six (`no longer residue`,
34+
`data.user.image` and `tracked separately` each 0 hits), while the positive
35+
controls land — `{@link normalizeSessionResponse}` appears 3× in each bundle
36+
and 3× in each `.d.ts`, carried there by the JSDoc of the **exported**
37+
`auth.login` / `auth.register` / `auth.me` that link to it, and `set-auth-token`
38+
4× / 3×. So comment text from this file does reach the published types; this
39+
block's own text does not, because the function it documents is not exported;
40+
- `index.js`, `index.mjs`, `index.d.ts` and `index.d.mts` are **byte-identical**
41+
across the change (sha256, same build, reproducibility control re-run);
42+
- both `.map` files **differ**. Neither carries `sourcesContent`, so no comment
43+
text ships inside them either — the position table shifts because the rewritten
44+
comment is two lines longer than the one it replaced.
45+
46+
So the published tarball's bytes do move, and a released package whose shipped
47+
bytes move takes a changeset.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): label every producer claim in the `build-progress` docblock — measured, ruled, or inferred (#18552)
6+
7+
Clause-②: no
8+
9+
The module docblock on `ai/build-progress.zod.ts` stated three producer claims
10+
as MEASUREMENTS. It ships in this tarball (the published `files[]` carries the
11+
`.zod.ts` sources) and is rendered verbatim into the generated reference page,
12+
and for a CLOSED vocabulary it is the audit trail the "re-measure before you
13+
move the array" discipline reads. One of the three was false, and a reader
14+
deciding whether a fifth phase is warranted would have read all three as
15+
readings.
16+
17+
Each producer claim now carries exactly one of three labels, defined at the top
18+
of the module: **measured on a named reachable source**, **declared by ruling**,
19+
or **inferred**.
20+
21+
- Membership is no longer described as uniformly measured. `structure`, `data`
22+
and `done` stay **measured** — the objectui reader's own union and coercion
23+
default, cited with the tree they were read against. `verify` is **declared by
24+
ruling** (cloud#2172, objectui#7388): at the read tree the chat panel has zero
25+
occurrences of `'verify'` against a control of four files for `'structure'`,
26+
and this repository emits no frame at all. That is a good reason for the
27+
member; it is not an observation, and the docblock no longer says it is.
28+
- The cloud#1838 window — "111 seconds and 9 tool calls", "one of them
29+
`verify_build`" — is **inferred**: that record is not reachable from this
30+
repository, so the figure is carried, not measured, and which tools those
31+
calls were is recorded nowhere reachable. What is measured is narrower and
32+
stated as such: `verify_build` is a registered platform tool.
33+
- "A turn that seeds no sample data never reports `data`" and "`apply_edit`
34+
turns need not report `structure`" are **inferred**. The consumer guidance
35+
around them is unchanged and does not rest on them: treat every phase as
36+
optional and compare by value.
37+
38+
A new `## Liveness watch` section records that `verify`, `hop` and `tool` are
39+
declared ahead of any code that uses them, that cloud#2172 and objectui#7388
40+
block 2 are the named carriers meant to close that, and that no gate watches it
41+
— `BuildProgressFrame` is not a registered metadata type, so the ADR-0049
42+
liveness ledger never sees it.
43+
44+
No schema, export or parse behaviour moves: `BUILD_PROGRESS_PHASES`,
45+
`BuildProgressPhaseSchema` and `BuildProgressFrameSchema` accept and refuse
46+
exactly what they did before.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
fix(lint): `list-view-field-unknown` walks `kanban.titleField` — the one item-titled face the position table never listed (#18565)
6+
7+
Clause-②: no
8+
9+
`POSITIONS` in `validate-list-view-field-refs.ts` declares, per view face, which field-reference keys are walked and at what level, and `kanban` was the only item-titled face with no `titleField` row. From #16894 the key is authorable on `KanbanConfigSchema`, so from that release a misspelt field name cleared the schema door, was walked by nothing, and the board fell back to the ADR-0079 record display name — a title the author did not ask for, on a board that renders correctly, with no gate reporting the miss. The byte-identical typo one block away on `calendar` or `timeline` was reported.
10+
11+
Measured on this branch, one list view carrying every walked position, one mutation at a time:
12+
13+
| probe | before | after |
14+
|:--|:--|:--|
15+
| `kanban.titleField` naming a field that does not exist | silent | `warning` `list-view-field-unknown` at `views[0].list.kanban.titleField` |
16+
| `kanban.titleField` naming a real field | silent | silent |
17+
| the other 51 walked positions | 51 reported, 1 silent (this one) | the same 51, each at its same severity |
18+
19+
Over the repo's own example apps (`app-crm`, `app-todo`, `app-multi-package`, `app-showcase`) the findings count is **0 before and 0 after**: five kanban blocks are authored there and none carries `titleField`, so nothing existing starts reporting. Injecting `titleField: 'zz_no_such_field'` into those same boards flips 0 → 1 warning in `app-crm` and `app-showcase`.
20+
21+
**`warning`, the level `calendar` takes — not the level of the two siblings that spell the key required.** `KanbanConfigSchema` declares `titleField` OPTIONAL (#16894 copied `CalendarConfigSchema` for this exact key and names `TimelineConfigSchema` / `GanttConfigSchema`, the two that spell it required, as the siblings it deliberately does not copy), and the board resolves an unresolvable name through the ADR-0079 display-name chain: measured in objectui `dda8f3815`, `resolveKanbanTitleField` returns the written name, the card reads `rec[titleField]`, finds nothing and falls to `getRecordDisplayName`. Every card still renders — the warning tier's own case in this rule's module note ("the renderer drops one decoration and renders the rest: an optional colour / title / tooltip / cover binding"), where `kanban.groupByField` is the error tier's, collapsing every card into one uncolumned lane.
22+
23+
No rule id, no severity and no message shape changes for any other position; `list-view-field-unknown` gains one more place it can be reported from.

0 commit comments

Comments
 (0)