Skip to content

Commit 9bb46c7

Browse files
committed
Merge origin/main into claude/issue-16350-init-lint-script-parity
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
2 parents 89b96df + a5d4e28 commit 9bb46c7

42 files changed

Lines changed: 2341 additions & 131 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/driver-memory": minor
3+
---
4+
5+
fix(driver-memory)!: an array comparand is refused instead of compared by reference, and a `Date` comparand is compared by time value
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) An accept-set narrowing and an evaluation repair inside one driver's filter gate: no metadata key, spec symbol, Zod schema, object definition or stored representation is added, removed or renamed. `packages/spec`'s half of this diff is a docblock sentence and one test comment — `filter-comparand-type.ts`'s list of the cases its door deliberately does not rule, corrected to state what the drivers now do — and no schema member, accepted key or exported symbol moves with it, so `check:api-surface` and `check:authorable-surface` see an unchanged surface. What moves is which FILTERS `@objectstack/driver-memory` accepts, which is authored at a call site or in a stored `where`, never a key `objectstack migrate meta` could visit: there is no old spelling to rewrite into a new one, because the refused shape has no replacement spelling that means the same thing — an array comparand never had agreed semantics to preserve, which is the whole reason the cell is refused rather than converged. Measured on this tree, the authored corpus that could carry the shape holds none of it: a structural scan of every tracked JSON and YAML file with a `where` / `filters` / `filter` object (10 JSON candidates, 1 YAML candidate — the latter a GitHub Actions job named `filter`, not a filter) finds ZERO bare-array comparands, and across the repository's own suites exactly one case authored the shape (`{ code: [] }`, in this package's face-agreement table), moved in this diff to assert the refusal. Whether an out-of-repo host authors one is NOT measured and is not claimed to be zero. -->
8+
9+
`checkCondition` routed both `Date` and `Array` into `value == condition`, under a comment two lines above calling that "exact match" — and a comment seven lines below giving `==`'s reasons, all three of which are about primitives ("undefined/null mismatch", "string/number coercion", "convenient in weakly typed queries"). Between two objects `==` does no conversion at all: it compares REFERENCES. So the arm was neither exact nor a match, and the two adjacent comments described something the code could not do.
10+
11+
**BREAKING** accept-set narrowing on a published driver, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`): a purely additive widening takes at least `minor`, and a narrowing is not the additive bucket, so `patch` is refused here — the public surface really does move. The two halves take DIFFERENT dispositions, and the difference is the contract's rather than this driver's:
12+
13+
**An ARRAY comparand is refused.** `@objectstack/spec`'s comparand door names an array outside `$in`/`$nin`/`$between` as a position it deliberately does not rule, leaving it "to the layers that already answer it"; `ACCEPTED_FILTER_COMPARAND_TYPES` has no array member, and `driver-sql` refuses one with its own message. This driver now answers the same way, in the ADR-0112 envelope its every other filter refusal speaks (`code: 'INVALID_FILTER'`, `status: 400`), with a message naming the field, the received shape, the accepted set and the operators that DO take a list. The refusal is raised from `assertFilterConditionShape` — the one gate every face of this package runs before it evaluates anything — so the live query path, the reference matcher and the analytics face cannot disagree about it.
14+
15+
Why refused rather than made to work, measured on one row `{ tags: ['a','b'] }` with the filter `{ tags: ['a','b'] }` before this change: the live query path returned the row (mingo deep-equals arrays) and the reference matcher returned none (`==` compared references). One filter, one package, two answers, neither reported — the same shape as this package's `{ field: {} }`, malformed-`$between` and non-boolean-`$null` refusals. Converging UP to deep equality was available and is not what landed: the contract declines to rule the cell, and every sibling that has answered it declines the semantics.
16+
17+
**A `Date` comparand is EVALUATED, not refused** — the opposite disposition, because the contract does rule this one. `Date` is a member of `ACCEPTED_FILTER_COMPARAND_TYPES`, and `FILTER_COMPARAND_TYPE_CASES` requires that a Date comparand "must pass the door and execute everywhere". It is now compared by time value, arm for arm with `@objectstack/formula`'s `looseEq` — the sibling record-at-a-time matcher this face's conformance suites are held against — which also settles the case that actually reaches a stored row: a declared `datetime` is canonicalised to ISO TEXT on write (ADR-0053 D-B1), so a `Date` comparand meets a STRING, and `==` stringified the Date to `"Wed Jan 01 2026 …"`, which no ISO value equals. `$eq` / `$ne` take the same equality, so one predicate no longer answers two ways depending on which spelling the author used. An Invalid Date has no time value and so equals nothing, itself included.
18+
19+
**What is deliberately NOT changed**, and is now pinned so a later edit cannot take it away by accident: a SCALAR comparand against a stored ARRAY. `{ tags: 'a' }` against a row storing `['a','b']` still answers false, and `{ tags: 'a,b' }` against that row still answers true, because `==` stringifies the stored array to `"a,b"`. That is a third bad direction of the same operator, but it is on the VALUE side, and the comparand door judges comparands. It is recorded, not repaired. The list operators (`$in` / `$nin` / `$between`), for which an array is the declared comparand, are untouched; so is the text family, whose comparand disposition this package's refusal gate had already recorded as deliberately fail-closed.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`objectNavTargetExclusivity` — the object-level check on an object navigation item that refuses `filters` combined with `recordId` / `viewName`, and `runAction` combined with `recordId` — is now EXPORTED from `@objectstack/spec/ui`, one function per refinement in the same posture as the `check*` exports. A hand-written mirror of the object nav item chains this very function in its own `superRefine` instead of restating the rule from prose; a restatement is what drifts: objectui's hand-written mirror re-implements neither rule — its `superRefine` checks only `id` / `label`, and the file names no `filters` rule beyond the field's declaration (measured at the pinned `.objectui-sha` and at objectui `origin/main`).
6+
7+
**What moves for consumers: one new export.** No schema's accept set moves. `NavigationItemSchema` chains the check exactly where it did — its `type: 'object'` branch — and the exported `ObjectNavItemSchema` still does not chain it: which schema mounts the check is a separate question from whether a mirror can, and it is not decided here. The two deliberate asymmetries are unchanged and now pinned: `recordId` + `viewName` stays a tolerated legacy combination, and `runAction` is refused with `recordId` only (it still composes with `filters` / `viewName`).
8+
9+
**Also corrected, in the same file:** the `filters` docblock stated a complete precedence order (`recordId``filters``viewName`) a few lines above saying the combination is unrepresentable, and the mirror copied that first half. The docblock now states only what the guard refuses, says in as many words that no precedence order is stated and why, and names the one legacy combination the guard tolerates. The `.describe()` strings — what reaches the generated references — are unchanged.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os i18n extract --source-hashes` no longer writes a provenance companion with no bundle module beside it, and names the sections it commits from the payloads those modules hold instead of from two literals.
6+
7+
The command narrows the provenance table to "the sections this run commits" before writing `<locale>.source-hashes.generated.ts`. The half that decided WHICH modules were emitted already read the emitted set; the half that named them pushed the string `'objects'` or `'metadataForms'`.
8+
9+
- **A zero-record orphan is no longer written.** With no module emitted for a locale — a stack whose only surface is apps, under the default `--objects-only` with `--no-metadata-forms` — the committed-section list is empty, `narrowToCommittedSections` returns `{}`, and `{}` is truthy at the emit gate. The run therefore wrote one file holding an empty table, describing nothing, with no bundle module beside it for it to be about. Because `--check` compares the companion by bytes like any other emitted file, that orphan once committed is a file the gate demands forever: deleting it made `--check` report `missing` and exit 1. Such a run now writes nothing, and reports `Generated 0 file(s)`.
10+
- **The section list is derived.** `translationModuleSections(bundle, kind)` sits beside `translationModulePayload` and is switched on the same `kind`, so what a module holds and which sections it commits are one decision rather than two. Under `kind: 'stack'` the module holds every group the stack authors and the caller now names all of them; a group added later needs no edit, and a further aggregate kind fails to compile at that one site rather than silently committing its own name as a section.
11+
12+
**No provenance record changes in this repository, and none is restored.** The generated tables only ever carry the two sections `collectFilledFromHashes` walks (`objects` and `metadataForms`), so `'objects'` was the right name for both stack sub-tree modes — the old list was correct by coincidence, not by construction. In particular an `apps.*` record is not restored by this change: no such record is built, so none was being filtered out.
13+
14+
**Already committed an empty companion?** Nothing needs doing and nothing is deleted. `--check` compares only the files a run writes and reports `missing` / `stale` over that set, so a leftover empty companion is in neither category — it is tolerated where it sits, and is inert to the next extract, which reads it back as an empty record set exactly as it would read its absence. Delete it at your convenience.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
Correct the remaining out-of-package comments that still described
6+
`SqlDriver#formatOutput`'s two timestamp passes as gated on `if (this.isSqlite)`.
7+
8+
Since ADR-0053 D-F1 (#13973) both passes — the `AUDIT_TIMESTAMP_COLUMNS` pass and the
9+
`normalizeSqliteDatetimeOutput` pass over `datetimeFields` — run on every dialect, so the
10+
record read door presents the builtin audit columns and every declared `Field.datetime`
11+
as canonical ISO-8601-`Z` text on Postgres and MySQL as well as SQLite. Measured on the
12+
tree rather than recalled: in `packages/drivers/driver-sql/src/sql-driver.ts` the
13+
`if (this.isSqlite)` arm inside `formatOutput` opens at line 16965 and closes at 17026,
14+
covering only the JSON codec and the numeric-scalar repair, while the audit-column loop
15+
(17046) and the `normalizeSqliteDatetimeOutput` loop (17061) both sit at the method's top
16+
level, below that closing brace.
17+
18+
Two of the corrected comments were load-bearing rather than merely stale. The
19+
`service-storage` one drew a conclusion for a live read door from the false premise, and
20+
it also claimed that folding at the driver's read boundary "would reverse the deliberate
21+
`withPostgresCalendarDayAsText` decision" — which is what #13973 ruled and did. The two
22+
`packages/cli` ones attached the wrong reason to a true fact: the holder probe reads
23+
through the raw-SQL seam, so `formatOutput` never runs on that path at all, and the
24+
dialect divergence there survives the ruling for that reason and not because of a gate.
25+
26+
Comments only — no runtime behaviour, no exported symbol and no public type changes.
27+
`@objectstack/cli` is the one package named here because its per-file build carries the
28+
amended text verbatim into `dist/commands/migrate/duplicates.js` and
29+
`dist/commands/migrate/duplicates.d.ts`, so its published output changes.
30+
`@objectstack/metadata-protocol` is deliberately NOT named: its edits are all in test
31+
files, which are not published. `@objectstack/service-storage` and `@objectstack/metadata`
32+
are deliberately NOT named either: their source edits are JSDoc blocks on the internal
33+
`usableCreatedAt` and `canonicalTimestampText`, and both bundles strip them — measured
34+
absent from `dist/`, with each package's identifier found in the same `dist/` (and the
35+
exported `StrandedOrphanInventoryEngine` docblock present in `dist/index.d.ts`) as the
36+
firing control that the probe works.
37+
38+
Three carve-outs are preserved rather than flattened: `withPostgresCalendarDayAsText` is
39+
untouched by that ruling (D-F2 — the client library still materialises `timestamptz` /
40+
`DATETIME(3)` as a `Date`); the Invalid `Date` residue still stands (D-F3 — the one `Date`
41+
shape with no canonical text leaves the read door unchanged, so no sentence claims the
42+
read door never hands out a `Date`); and the ruled-B consumer arms stay, with only the
43+
prose explaining why they exist corrected.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/core": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
The `Server is ready` line now reports the degraded boot it is standing on, instead of printing a green `` over it.
7+
8+
`✓ Server is ready` and the kernel's `System started with degraded capabilities. Missing core services: …` were two statements about one boot, produced by two packages — the banner in `@objectstack/cli`, the conclusion in `@objectstack/core` — with **no data path between them**. So the ready signal did not depend on the thing that broke, and therefore could not report it. Measured twice within a day, from unrelated causes: an objectui CI boot where the auth plugin failed and not one `sys_*` table existed, and this repo's own weekly registry canary on the published `npx create-objectstack@latest` on-ramp, where the tick printed directly **above** four boot warnings. In the second case the ready line carried no weight in the job's verdict at all — it was present, green, wrong, and believed by nobody.
9+
10+
- **The data path.** `ObjectKernel.validateSystemRequirements()` now publishes the list it had already computed — the same array behind its own warning — on the kernel's service registry, which is the seam boot facts already cross to reach the banner (`serve` reads `auth` and `seed-summary` off it the same way). No member and no type is added to `@objectstack/core`'s public surface, and nothing re-derives which services count as `core`: that judgement stays in `ServiceRequirementDef` alone.
11+
- **The line.** On a degraded boot the banner prints `⚠ Server is ready — DEGRADED: missing core services: <names>`, naming exactly what the kernel found missing. On a healthy boot the ready block is byte-for-byte unchanged, so an ordinary boot's output does not move.
12+
- **Readiness is NOT made strict.** Nothing about what boots, binds, or exits changes. A machine deliberately running without auth still starts, still prints ready, and still exits 0 — the line just says what state it is ready in.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
docs(spec): the comparand door's array parenthetical states what the drivers do today
6+
7+
`filter-comparand-type.ts`'s list of the cases its door deliberately does not rule described the array cell as "`driver-sql` refuses it with its own message; the document stores give it array-equality semantics". Measured on this tree, that second clause is no longer a true statement about `@objectstack/driver-memory`: its reference matcher compared an array comparand by REFERENCE (matching nothing), while its live query path deep-equalled it (matching the row) — one package, two answers, neither of them a stable "array-equality semantics" a reader could build on. With that driver's cell now refused, the sentence names the two dispositions that exist: `driver-sql` and `driver-memory` refuse it, each with its own message, and `driver-mongodb` hands it to MongoDB and inherits that engine's array semantics.
8+
9+
The paragraph's point is unchanged and deliberately kept: the door does not rule this position, the matrix did not measure it, and it is left to the layers that already answer it. Only the description of what those layers do is corrected — a stale factual clause is how the next reader re-derives "the spec promises array-equality" from a passage that explicitly promises nothing. The neighbouring pass-through pin's comment carried the same stale characterisation and is corrected the same way; its assertion (that `parseFilterAST` leaves `{ tags: ['a','b'] }` untouched) is unchanged and still pins the door not judging.

.changeset/stack-refusal-envelopes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ Not narrowed: `defineStack` accepts and refuses exactly the inputs it did before
2525

2626
**Why `minor`, not the `patch` the #14552 precedent took.** Six new `STACK_*` spellings ship in `packages/spec/dist/index.js` and `dist/index.mjs`; nothing reaches `.d.ts`, but once shipped a consumer's `catch (e) { switch (e.code) … }` depends on them and they cannot be renamed without breaking it. That is a purely additive widening of a published package's public surface, and the maintainer ruling of 2026-09-04 (decision batch #35, on #15294) requires at least `minor` for it: the commit type may raise a bump but never lower it below what the act requires, so a `fix(` that widens the surface is `minor`. The `patch` precedent is pre-rule; this level is required by that ruling, not chosen by taste.
2727

28-
`@objectstack/runtime` carries one classification row per new code in the dispatcher error-code vocabulary (`door: 'none'`, `verdict: 'boot-refusal'` — the measured verdict), which `pnpm check:dispatcher-error-vocabulary` enforces in both directions.
28+
`@objectstack/runtime` carried one classification row per new code in the dispatcher error-code vocabulary (`door: 'none'`, `verdict: 'boot-refusal'` — the measured verdict) when this changeset was written, and carries none now: the same #16449 (PR #16652) landing that registered the seven codes ratcheted all seven rows back out of `packages/runtime/src/dispatcher-error-vocabulary.ts`, because a registered code drops out of that gate's scan and the reachability each row recorded now lives on its ledger row. `pnpm check:dispatcher-error-vocabulary` still reconciles sites and rows in both directions, and under the same ruling it now refuses any verdict but `foreign-vocabulary` / `runtime-pinned` for a stamp site under `packages/spec/src/**` — so these seven cannot take a `boot-refusal` row again.

content/docs/data-modeling/field-type-decision-tree.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ flowchart TD
3333
TEXT -->|Email address| T5[email]
3434
TEXT -->|Website URL| T6[url]
3535
TEXT -->|Phone number| T7[phone]
36-
TEXT -->|Secret value| T8[password]
36+
TEXT -->|Sensitive secret value| S5[secret]
3737
3838
%% Number branch
3939
NUM -->|Any number| N1[number]
@@ -88,7 +88,6 @@ flowchart TD
8888
style T5 fill:#059669,color:#fff
8989
style T6 fill:#059669,color:#fff
9090
style T7 fill:#059669,color:#fff
91-
style T8 fill:#059669,color:#fff
9291
style N1 fill:#d97706,color:#fff
9392
style N3 fill:#d97706,color:#fff
9493
style N4 fill:#d97706,color:#fff

0 commit comments

Comments
 (0)