Skip to content

Commit 99ad620

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19365-automation-runs-hasmore
2 parents 1be868a + eec56c3 commit 99ad620

20 files changed

Lines changed: 1522 additions & 107 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/lint": patch
3+
"@objectstack/plugin-security": patch
4+
---
5+
6+
`PermissionEvaluator.checkObjectPermission` and `buildAccessMatrix` now ASK `@objectstack/spec`'s `objectPermissionGrants` instead of restating the super-user fold — one rule, one definition (#18785).
7+
8+
"Does this effective object permission grant this verb?" had three independent implementations: the spec helper published in 17.4, the enforcement door in `@objectstack/plugin-security`, and the access-matrix snapshot in `@objectstack/lint`. A differential over the full input space — every declared object-permission bit (`allowCreate` / `allowRead` / `allowEdit` / `allowDelete` / `allowTransfer` / `allowExport` / `viewAllRecords` / `modifyAllRecords`) in all three authorable states, 6561 entries by 6 verbs — found **zero** disagreements, so this is a structural convergence and **no behaviour changes**.
9+
10+
- **No API change, no bit changes meaning.** The read bypass is still `viewAllRecords || modifyAllRecords`, the write bypass is still `modifyAllRecords` alone, `allowCreate` still has no super-user bypass, and `export` is still `grant ∧ read`.
11+
- **The export door keeps its cross-set shape.** `checkObjectPermission('export', …)` still asks `(∃ set granting export) ∧ (∃ set granting read)` across the resolved set list — the same answer the `/me/permissions` most-permissive merge hands the client. Folding it per set would have narrowed the door.
12+
- **Both consumers are pinned to the fold independently of the helper**, so a change to one cell of `objectPermissionGrants` reddens them rather than propagating silently.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
**Clause-②: yes** — `os build` accepts a source layout it previously read nothing from, so what an author may write and have collected widens. ⛔ Nothing narrows: every tree that built green still builds green, with the same `docs[]` and the same warnings.
6+
7+
`os build` now derives **each package's docs directory from the packages the artifact registers**, not from a fixed depth under `src/` (maintainer ruling, decision batch #204 item 5, letter B).
8+
9+
Before this, the sweep asked one question per direct child of `src/`: does `src/CHILD/docs/` hold Markdown? So a project whose packages sit one level deeper — the shape this repo's own ADR-0130 D4 reference fixture `examples/app-multi-package` has, `src/packages/PKG/` — was invisible to it. A doc at `src/packages/orders/docs/ord_guide.md` was dropped **silently**: no `docs[]` entry, exit 0, and not even the `docs/uncollected-directory` warning, because the sweep never looked there. That is the #18170 defect verbatim, one level down, and after #18431 it was out of reach of both the diagnostic and the collection.
10+
11+
Both layouts are now one case rather than two:
12+
13+
```
14+
src/orders/docs/sales_guide.md -> packages[].manifest.docs (unchanged)
15+
src/packages/orders/docs/sales_guide.md -> packages[].manifest.docs (new)
16+
```
17+
18+
**How the directory is found.** A registered package carries no source path — `ArtifactPackageSchema` is a `strictObject` whose only key is the assembled body — so the only thing that can locate one on disk is its NAME, and the two spellings a docs directory is matched against are unchanged: the package's `id`, and the last dot-separated segment of that `id`. ⛔ Never `name` (a display string, free to be re-worded) and ⛔ never `namespace` (ADR-0130 D1 exists so N packages may share one).
19+
20+
**No second depth was pinned.** The walk descends only in SEARCH of a registered package and stops at the first directory that names one — so a package's own subtree stays its source, and a `docs/` inside it is not a second docs directory. With no `packages[]` there is nothing to search for, so there is no descent at all: a single-package stack is walked exactly one level, its `docs[]` and its warning text byte-for-byte what they were. That is the fence the ruling preserved from batch #147 item 4, held by construction rather than by a branch guarding it.
21+
22+
**One new refusal.** Depth-free resolution makes one package able to answer to two doc-bearing directories (`src/core/docs` and `src/packages/core/docs` in one tree). Both are reported and neither is collected — the same answer this collector already gives when one directory names two packages. ⛔ It is not merged and ⛔ not silently halved: docs attach by package index, so collecting both would drop one without a word.
23+
24+
A directory that matches **no** package and one that matches **more than one** keep their existing, distinct diagnostics, now at whatever depth they are found.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING for callers** — `parseFilterAST` now refuses a `{ $field }` column reference as a `$between` endpoint, exactly as the authoring schema has since 2026-08-11. A reference at either bound is refused with `INVALID_FILTER` / 400, and the refusal names the side — MIN or MAX, plus the index (#19377).
6+
7+
Clause-②: yes
8+
9+
## What changed, and why it is the implementation catching up rather than a new rule
10+
11+
`RANGE_ENDPOINT_DESCRIPTION` — the published endpoint contract shared by both of `$between`'s bounds — has stated verbatim since 2026-08-11 that "A { $field } reference is NOT an endpoint shape: no backend resolves one inside a list". The ruling that wrote it (ADR-0049 enforce-or-remove) removed `FieldReferenceSchema` from both endpoint unions, and it shipped at the authoring schema alone. The runtime door disagreed with it: `parseFilterAST({ f: { $between: [{ $field: 'a' }, 'M'] } })` returned the filter unchanged, same object reference, measured on `origin/main` before this change and re-measured after.
12+
13+
One published sentence therefore had two truth values, decided by which door a caller came through — and the door that passed it is the one an embedder reaches by handing a lowered filter straight to a driver. There, nothing resolves the reference: the in-memory matchers compare the raw reference OBJECT and the range silently matches nothing, while both SQL faces refuse the position. A filter that names a window and answers no rows, or 400s one layer down, is what a caller got instead of a refusal they could act on.
14+
15+
```
16+
FROM parseFilterAST({ close_date: { $between: [{ $field: 'contract.start' }, '2026-12-31'] } })
17+
-> the same object, unchanged, straight on to the driver
18+
19+
TO throws INVALID_FILTER / 400:
20+
'Operator "$between" on field "close_date" does not accept a { "$field": … }
21+
reference as an endpoint (at where.close_date.$between[0], the MIN bound). …'
22+
```
23+
24+
## Migration — FROM → TO
25+
26+
| You wrote | Write instead |
27+
| --- | --- |
28+
| `{ $between: [{ $field: 'contract.start' }, '2026-12-31'] }` | `{ $between: ['2026-01-01', '2026-12-31'] }` — the literal bound the range was meant to stop at |
29+
| a range that was genuinely meant to be column-to-column | `{ "$gte": { "$field": "a" }, "$lte": { "$field": "b" } }` — two scalar bounds, the position that compiles on every face |
30+
31+
**The one-line fix: write the literal bound, or — if the range really was column-to-column — drop `$between` and write the two bounds separately as `$gte` / `$lte`.** Nothing was evaluating the old filter, so treat the replacement as a new one and test it: at every backend the reference range either matched nothing or was refused.
32+
33+
## What does NOT change
34+
35+
- **A `{ $field }` reference as the WHOLE comparand of `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte`.** That is #5222's shipped column-to-column capability, it is the alternative this refusal prescribes, and it lowers exactly as before — pinned by a lit control in the same file.
36+
- **Every legal range.** Numbers, Dates, ISO days, UTC instants, clock times and non-temporal text all lower byte-identically, same object reference.
37+
- **The three older endpoint carve-outs.** Arity, `null` (2026-08-31) and blank (2026-09-17) are checked first, so a pair carrying one of those keeps the message and the prescription it already had — an author who wrote `null` is still sent to the null predicate, not to a scalar comparison.
38+
- **A plain object that is not a reference** keeps the comparand-TYPE door's own sentence, one step further on.
39+
- **`$in` / `$nin` members.** The same 2026-08-11 decision rules a reference out of those positions too and `SET_MEMBER_DESCRIPTION` publishes it, but that is a second split over a different published sentence; it is measured and filed separately, and this change deliberately does not move it.
40+
- **The published export surface.** No export is added, removed or renamed; the refusal rides the existing `$between` arm of the shared comparand-shape door, so the engine's delegating wrapper inherits it unchanged.
41+
42+
<!-- adr-0087: registered filter-between-field-reference-endpoint-refused -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
Keep the `userFilters` element tokens English in the translated metadata-form tooltips
6+
7+
`metadataForms.view.fields.userFilters.helpText` names the legal values of `UserFiltersSchema.element`, a strict `z.enum(['dropdown', 'tabs', 'toggle'])`, and it is the only place the `view` panel names them at all. All three translated catalogs rendered those tokens as ordinary words — 下拉 / 标签页 / 开关, ドロップダウン / タブ / トグル, desplegable / pestañas / interruptor — so an author working in a translated locale was shown a value the schema refuses.
8+
9+
The enum values are now verbatim English inside the translated sentence and the prose around them stays translated. The `page` Interface panel is a neighbour here, not a precedent: its tooltip keeps `None / Tabs / Dropdown` English too, but those are the `filter-mode` widget's UI mode names — capitalised, and `z.enum` is case-sensitive, so the enum refuses all three; `None` stands for the absence of the config rather than a value; and `toggle` is deliberately not offered there. What this change keeps verbatim is the enum's own tokens, which is the stricter requirement, because they are values an author types.
10+
11+
`user-filters-element-tokens.test.ts` pins the repaired leaf in the three locales. It derives the accepted set from `UserFiltersSchema` and asserts set equality against it, so a value added to the enum reddens instead of going unnamed; it requires each tooltip to name that set and nothing else; and it holds each translated sentence's prose at both ends, so the assertion cannot be satisfied by copying the English sentence back in.

0 commit comments

Comments
 (0)