Skip to content

Commit ad4a319

Browse files
authored
Merge branch 'main' into claude/issue-17062-settings-route-ledger-conformance
2 parents 0102716 + 91f65c4 commit ad4a319

115 files changed

Lines changed: 8985 additions & 490 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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/cli": patch
3+
"create-objectstack": patch
4+
---
5+
6+
fix(cli): re-measure the `better-auth` > `better-sqlite3` peer record, correct what it credits, and pin the declaration it justifies (#16813)
7+
8+
A tree containing `@objectstack/cli` reports an unmet peer on every fresh
9+
resolve — `better-auth` peers `better-sqlite3@^12.0.0`, the CLI declares
10+
`^13.0.3` — and the reading that decides what to do about it lived only inside
11+
the scaffold generator's prose. No range moves here and no resolution moves:
12+
what changes is the recorded reason, which had two measured errors in it, plus
13+
a gate that now holds the declaration to that reason.
14+
15+
**The declaration is correct and stays at `^13`.** Three readings, taken rather
16+
than inherited:
17+
18+
- The peer is `optional`, and it governs exactly one configuration — a raw
19+
better-sqlite3 `Database` passed to better-auth's `database` option.
20+
`AuthManager.createDatabaseConfig()` returns an ObjectQL adapter factory, or
21+
`undefined` for better-auth's in-memory adapter. Never a `Database`.
22+
- better-auth cannot be incompatible with better-sqlite3 13, because it never
23+
touches it: of the 464 files in the published `better-auth@1.7.2` tarball,
24+
exactly one names better-sqlite3 — `package.json`, the peer declaration
25+
itself — and no code file references it (positive control: `kysely` names 9).
26+
It accepts a `Database` the caller constructs; its own sqlite test path uses
27+
node's built-in `node:sqlite`.
28+
- Pinning back to `^12` is not a neutral alternative. Measured on a bare
29+
project depending on `@objectstack/cli@17.3.0`, it clears the report only by
30+
resolving a **second** native better-sqlite3 (12.11.1 beside 13.0.3) that
31+
nothing loads. The scaffold's existing `allowedVersions` entry clears the
32+
same report with the lockfile byte-identical.
33+
34+
**Two corrections to the record.** It credited `@objectstack/driver-sql` for
35+
the 13.x copy; on the chain that actually reports
36+
(`cli``runtime``plugin-auth``better-auth`) the binding copy is the
37+
CLI's own `optionalDependencies` entry, which pnpm names in the warning itself.
38+
And it was measured on better-auth 1.7.1 while the family has been pinned at
39+
1.7.2 since — re-measured, with the empirical reading replaced by a structural
40+
one.
41+
42+
The scaffold's rendered `pnpm-workspace.yaml` comment changes wording in both
43+
producers (`objectstack init` and the `create-objectstack` blank template); the
44+
declarations, the widening entry and the resolution are untouched.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": minor
4+
---
5+
6+
`DataMigrationFlagSchema` gains `columns_moved_at`, and the `sys_migration` platform object gains the matching column: the deployment-level attestation that a migration's COLUMN MOVE ran here — the step that retypes the migrated columns and rewrites the values they hold into the new encoding.
7+
8+
**What it attests** is a fact the ledger could not previously express. `applied_at` says the backfill ran in apply mode; `verified_at` says the self-check passed. Neither says anything about the physical columns, because the backfill and the column move are separate acts and only the first of them had somewhere to be recorded. A deployment can therefore have applied AND verified a migration and still store the legacy encoding. `columns_moved_at` is that second fact, carried as its own member rather than as a widening of either existing one: folding it into `verified_at` would change what an already-verified row authorises on every deployment that has never heard of a column move.
9+
10+
**Absence is the contract, not a default.** The member is optional and nullable, and nothing in this change writes it. Null or absent means the columns still hold the legacy encoding — a real, expected steady state on any deployment that has run the backfill but not the move, and never an error state — so every row that exists in the world today, and any consumer that cannot read the member at all, lands on the legacy encoding with no extra logic. A required member, or a default value, would destroy the exact property the mechanism was chosen for.
11+
12+
**Nothing reads it yet, and the arbiter is untouched.** `isDataMigrationFlagVerified` — documented as the ONE arbiter for the existing consumers (reap gating, the strict value-shape flip) — is unchanged in this diff, and is now pinned to return the same verdict for a row that omits the new member as it returned before the member existed; `authorisesIrreversibleAction`, which composes it, is pinned the same way. The predicate that will require `columns_moved_at` non-null belongs to the driver work this change unblocks, and reads it in addition to the arbiter, never inside it.
13+
14+
This is an additive widening: `DataMigrationFlag` (`z.input` of the schema) gains one optional member, no existing member changes or moves, and no export is added or removed.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/metadata": minor
5+
"@objectstack/metadata-protocol": minor
6+
"@objectstack/plugin-auth": minor
7+
---
8+
9+
feat(engine)!: `findOne`, `update` and `delete` declare what they answer, and their hook seams are guarded (#16231)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves. No spec key, no authored metadata property, no config field, no accepted request shape and no stored artifact changes spelling or shape; `objectstack migrate meta` has nothing to rewrite, `spec-changes.json` has nothing to project and the upgrade guide has no row to gain. What moves is the declared RETURN TYPE of three TypeScript methods (`packages/spec/src/contracts/data-engine.ts`, its `scoped-context.ts` mirrors, and `ObjectQL` itself) plus three new registered ADR-0112 error codes. The rewrite this ships — add the null check the type now demands — is addressed to a TYPESCRIPT CONSUMER and is delivered by the compiler at their own call site, which is the audience the ADR-0087 ledger explicitly does not serve. `type-surface-only` is the category built for exactly this class and it is NOT claimed here, because its predicate 2 (`no-spec-diff`) is mechanically false for this PR: the surface the maintainer ruling names IS `packages/spec/src/contracts/**`. That gap is reported on the card rather than worked around, and the `**BREAKING**` banner below is carried rather than dropped. -->
12+
13+
**BREAKING** on three published `.d.ts` surfaces. `ObjectQL.findOne`, `ObjectQL.update` and `ObjectQL.delete` — and the `IDataEngine` / `IScopedObjectRepository` contracts they implement — declared `Promise<any>` and now declare the answers they have always given:
14+
15+
- `findOne``Promise<Record<string, any> | null>`
16+
- `update``Promise<Record<string, any> | number | null>`
17+
- `delete``Promise<boolean | number>`
18+
19+
`any` is assignable to everything and admits every property read, so TypeScript consumers of these three methods can stop compiling — most often on the null check the declaration now demands. Shipped as `minor` under the repo's launch-window convention, in which `major` is refused by `check-changeset-no-major` and breaking-ness is carried by this banner plus the ADR-0087 disposition rather than by the level. The governing text is the **WHICH LEVEL** maintainer ruling of 2026-09-04 (decision batch #35, on #15294) recorded at `.github/workflows/pr-automation.yml`; `AGENTS.md`'s "a bug fix in a released package takes a patch changeset — never none" is the floor against `none` and was rejected as the ceiling here, because this PR also widens `@objectstack/objectql`'s index with new exported symbols, which that ruling puts at `minor` on its own.
20+
21+
**Why.** `engine.ts` has four `return hookContext.result` sites, one per hook-bearing verb. #15823 closed the `find()` one — an `afterFind` handler that replaced the array made a method declared `Promise<any[]>` resolve to an envelope, silently — and recorded that it could close only that one: the other three declared `Promise<any>` and so carried no declaration a handler could break. A guard cannot exist before a declaration worth guarding does. The maintainer ruled the gap shut (option A, 2026-09-07, director seat summon #17, decision batch #2; option B "declare only, no enforcement" and option C "record `any` as intended" were refused).
22+
23+
The shapes are read off the driver contract each engine exit delegates to, not invented: `driver.findOne` and the by-id `driver.update` declare `Record<string, unknown> | null`, `driver.delete` declares `boolean`, and the predicate exits `driver.updateMany` / `driver.deleteMany` declare the affected-row `number` a bulk write resolves (#4639). Row FIELD values stay erased (`Record<string, any>`), which is #15823's precedent extended exactly rather than softened: `find()` declares `Promise<any[]>`, so the CONTAINER is the contract and the rows inside it are `any`. It is also the only spelling that can state "record or null" at all, since `any | null` collapses to `any`.
24+
25+
**What is enforced now.** Each seam re-checks `hookContext.result` against its declaration immediately after the `after*` dispatch and ahead of the consumers that already assume the shape, and refuses a value outside it with a registered ADR-0112 envelope — `FIND_ONE_HOOK_RESULT_NOT_RECORD`, `UPDATE_HOOK_RESULT_NOT_WRITE_SHAPE`, `DELETE_HOOK_RESULT_NOT_WRITE_SHAPE`, all `500`, all branchable on `error.code`. Shaping stays legal exactly as it does on `find()`: a handler may mutate what it is handed, drop keys, or assign a different value of a declared shape. The falsy answers are legal and deliberately so — `null` from `findOne`, `null` or a count from `update`, and `false` or `0` from `delete`, the two most ordinary answers that verb gives.
26+
27+
**Who has to change something, on the TYPE axis.** A TypeScript consumer that reads a field off `findOne`'s result without a null check, or off `update`'s result without separating the by-id record from the predicate count. In this repository that was measured before anything moved, at the maintainer's instruction: 18 files and 92 compile errors, all repaired here.
28+
29+
**What changes at RUNTIME, per door.** TWO things can put an off-declaration value at a seam, and every refusal's `developerMessage` names both: an `after*` handler that assigned one, and a DRIVER whose own exit answered off `IDataDriver`. Each door goes from returning that value silently to refusing it — one door, one registered code, all `500`:
30+
31+
- `findOne` — FROM: whatever the `afterFind` dispatch left in `ctx.result`, or whatever `driver.findOne` answered off its declared `Promise<Record<string, unknown> | null>`, returned to the caller as-is and walked first by `maskSecretFields` / `stripSearchCompanionFromRead`. TO: `500 FIND_ONE_HOOK_RESULT_NOT_RECORD`, raised at the seam when that value is neither a record nor `null`.
32+
- `update` — FROM: whatever the `afterUpdate` dispatch left in the batch `ctx.result`, or whatever `driver.update` / `driver.updateMany` answered off their declared `Promise<Record<string, unknown> | null>` / `Promise<number>`, returned as-is and read first by `stripSearchCompanion` and the realtime publish. TO: `500 UPDATE_HOOK_RESULT_NOT_WRITE_SHAPE`, raised when that value is outside record-or-count-or-`null`.
33+
- `delete` — FROM: whatever the `afterDelete` dispatch left in `ctx.result`, or whatever `driver.delete` / `driver.deleteMany` answered off their declared `Promise<boolean>` / `Promise<number>`, returned as-is to a caller such as `metadata-protocol`'s `deleteData`, which turns `false` into a 404. TO: `500 DELETE_HOOK_RESULT_NOT_WRITE_SHAPE`, raised when that value is neither a boolean nor a number — never on `false` or `0`, which are declared answers.
34+
35+
The driver half of each line is not hypothetical: the seven off-contract test doubles this PR repairs are exactly that source, and they are why the refusal sentence names the SEAM instead of accusing the handler.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): project a union branch-by-branch, so five filter operators reach a published reference page
6+
7+
`z.toJSONSchema()` refuses a whole schema the moment ONE node in it has no JSON
8+
form, and `build-schemas.ts` applied that refusal per SCHEMA. `orderingComparandSchema`
9+
is `z.union([z.number(), z.date(), z.string(), FieldReferenceSchema])`, so four
10+
`data/filter.zod.ts` exports emitted nothing at all — and `$gt`, `$gte`, `$lt`,
11+
`$lte` and `$between` reached no reference row. Not a blank Description cell: no
12+
section. The ~2000 characters of `.describe()` on those slots — the #5685 comparand
13+
contract, the #6571 endpoint contract, and the `{ "$gte": "2026-01-01" }` shape the
14+
platform's own date-macro resolver produces — reached no reader.
15+
16+
The generator now makes a third attempt when both strict directions refuse: it
17+
projects with Zod's `unrepresentable: 'any'`, marks every node that came back with
18+
no structural keyword, and DROPS the marked ones that are direct members of an
19+
`anyOf` / `oneOf`. That is not a narrowing. These artifacts describe JSON
20+
documents, a JSON document cannot carry a `Date` INSTANCE, so the set of JSON
21+
documents that union accepts is unchanged by the drop.
22+
23+
⛔ A marked node anywhere else — an object property, a record value, an array item
24+
— refuses the projection and the export is skipped with the message Zod threw, so
25+
this cannot change WHY anything is skipped. Five exports leave
26+
`unemitted-schemas.baseline.json` (23 → 18): the four filter exports, plus
27+
`data/Hook`, whose only unprojectable member was the deprecated inline-function
28+
handler branch — that puts 22 `data/Hook:` authorable keys under the key ratchet
29+
for the first time.
30+
31+
Published artifacts gain `json-schema/data/{ComparisonOperator,FieldOperators,
32+
NormalizedFilter,RangeOperator,Hook}.json`, each carrying an
33+
`x-unprojectable-branches` record naming exactly which branch the projection
34+
dropped and where.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`id_field` now gets a named answer instead of a bare refusal: `FIELD_KEY_GUIDANCE` declares it a retirement with **no successor**, which is the spec-side fact objectui's ingestion choke point needs before it can canonicalise the key (objectui#7650 ruling A — retired spellings are folded once, at ingestion, never at the consumer).
6+
7+
The direction was a factual finding, not a preference, and it went the way the cheaper branch happens to point — so here is the evidence rather than the verdict alone. A lookup stores the referenced record's id, and which field holds that value is not an authored per-field choice: the picker resolves record identity itself. Nothing on `FieldSchema` names it, nothing in `objectql` / `runtime` / `metadata-protocol` reads a per-field id key, and the two places the platform does let a reference be stored by something other than an id are declared elsewhere — `APPROVER_VALUE_BINDINGS.valueField` (per approver type, e.g. `position` routing by `sys_position.name`) and a seed dataset's `externalId`, the channel lookup references already resolve through. So there is no member to fold onto, and the prescription says what to reach for instead: `displayField` for the candidate's label, a dataset `externalId` for a portable natural key.
8+
9+
**The entry is keyed `id_field`, in snake_case, and that is deliberate.** The two channels this table feeds disagree about the key face. A `to` becomes a `strictObject` alias, matched through `aliasProbe` — case folded, separators stripped — so one camelCase row covers every spelling. A `why` becomes strict guidance, matched exactly and case-sensitively on the authored spelling. A camelCase row would therefore never be reached by the key authors write, and every existing test in the file would still pass, because none of them asks whether an entry is ever consulted.
10+
11+
That gap is closed too. Three assertions read the channel that actually answers an authored field key — `FieldSchema.safeParse`, since the schema is strict and the authoring-key walker stays silent on a strict surface by its own posture rule — and pin that the refusal carries this table's sentence verbatim, that a retirement suppresses the rename channel, and that the same-named `idField` on the `inlineColumns` GridColumn mirror is a different schema that stays live.

0 commit comments

Comments
 (0)