You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 048467a
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec)!: retirement kits whose conversion retired at or below protocol 16.x leave the tree; the strict unknown-key refusal names the protocol upgrade guide
8
+
9
+
Clause-②: no (narrowing)
10
+
Metadata-protocol: narrowed
11
+
12
+
**BREAKING** for a source or a stored `sys_metadata` row still carrying a key whose ≤16.x kit aged out.
13
+
14
+
-**The rule (ADR-0087, amended 2026-10-26, #25767 ruling D2 = A as scoped by ruling 6434038284).** A retirement kit — the `retiredKey()` tombstone and its D2 conversion with fixtures — lives through the protocol major after the one it retired in and is removed when the next major is cut. At 18 that removes the kits whose conversion retired at or below 16.x; 19 repeats the move one major on. The kit ages out; the migration chain's step stays.
15
+
-**Before.** Such a key met a `retiredKey()` tombstone: the parse refused it with the key's own prescription in the error, `os migrate meta --from 16` listed and applied the mechanical edit, and a stored row in that shape was rewritten at read by the ≤16.x conversions.
16
+
-**Now.** Where the tombstone sat in a strict container, the key is gone from the shape, so a `strictObject` refuses it as an unknown key. The refusal carries one more sentence, after its fix channels and ahead of its history sentence (`RETIRED_KEY_UPGRADE_GUIDE_SENTENCE`, exported with `PROTOCOL_UPGRADE_GUIDE_URL` from `@objectstack/spec`): it says that keys retired before the previous major are no longer prescribed in the error and names the protocol upgrade guide, `https://objectstack.ai/docs/protocol-upgrade`, where the prescription lives; the CHANGELOG of the release that retired the key carries it too. A tombstone that stays (its container strips unknown keys) ends its error with a sentence that says the edit left `os migrate meta` with the 16.x conversions, to be applied by hand or on a release that still carries the conversion, and links the guide.
17
+
-**The chain stays.**`MIGRATION_SUPPORT_FLOOR` stays 16; step 17 and its entry files stay, and `os migrate meta --from 16` still replays 16 → 18, listing every surviving conversion and semantic to-do. Step 17's graduated list drops the ids of the 45 conversions that aged out, so their edits are no longer applied for you.
18
+
-**Stored rows.** A `sys_metadata` row still carrying such a spelling is no longer rewritten at read; canonicalize it once with `os migrate meta --stored --apply` on 18.2 or earlier, before moving to 18.3. `app-hidden-to-unpublished` is among the removed conversions, so a pre-split `hidden: true` app row is no longer rewritten to `_unpublished` by any seam, and the artifact door's refusal of it by id is removed as moot (`@objectstack/metadata-core`).
19
+
-**What left.** 50 D2 conversions with `retiredAfter` at or below 16.x (5 at 15.0.0, 45 at 16.1.0) and their fixtures; 55 tombstones already `[RETIRED]` at `@objectstack/spec@17.0.0` whose container is strict; 61 `[RETIRED]` lines of the authorable surface and their liveness-ledger rows. 58 same-age tombstones whose container strips unknown keys stay, because deleting one would turn a refusal into a silent strip; the four inline-credential refusals are not retirements and are untouched.
20
+
-**The deletion gate.**`check:authorable-surface` proof 1 ages a tombstone one major (was two). A deleted `[RETIRED]` line is dated by an exact `RETIRED_KEYS_BY_MAJOR` row or, new, by the previous major's release tag: a line already `[RETIRED]` on the surface at `@objectstack/spec@<current major − 1>.0.0` was retired before that cut. The tag is immutable, so neither a row a PR writes nor one it deletes moves the clock; where the tag cannot be read the gate dates by the registry alone and says so. No gate was added.
21
+
-**Who moves.** No `examples/**`, `ui/` or `hotcrm` source in this repository writes a removed key (the `ui/` hits for the removed names are the console's own same-named props and type mirrors, not readers of the retired spec keys, per the liveness ledger's measurements). A consumer source still carrying one meets the strict refusal and the guide link at `os validate`.
22
+
23
+
<!-- adr-0087: not-required (no-migration-prescription) the diff registers no retirement: it removes the ≤16.x kits that aged out under the 2026-10-26 ruling (ADR-0087 D2 = A), and nothing here prescribes a rewrite — each removed key's prescription lives in the protocol upgrade guide and in the CHANGELOG of the release that retired it -->
fix(components): the `filter-builder` renderer calls `onChange` with the `FilterGroup` that `FilterBuilderSchema.onChange` declares
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`FilterBuilderSchema.onChange` declares `(filter: FilterGroup) => void`, but the `filter-builder` renderer typed its own `onChange` as `(event: any) => void` and called it with a form-style `{ target: { name, value } }` event. A handler written from the declaration read `.logic` and `.conditions` off that event and got `undefined`, and the `any` kept the compiler from reporting the mismatch. The renderer now takes the schema's own `onChange` type and passes the group it holds, so the declaration is the contract on both sides. No in-repo host bound the event shape. A host that read `event.target.value` reads the argument itself instead.
11
+
12
+
The builder's own rows also admit the opt-in `exists` / `notExists` operators, which a `FilterGroup` cannot carry. The renderer no longer forwards a host's `extraOperators` to the builder, since `FilterBuilderSchema` declares no such key, so the node never offers them. The `FilterGroup` zod mirror refuses them in an authored `value`. If a stored value that skipped that validation still carries one, the renderer does not call `onChange` and logs a console error naming the operator, instead of handing on a group the declared type does not describe.
fix(spec)!: every filter door refuses a `{ $now, addDays }` anchor that carries any other key, as `NowAnchorSchema` already declares, instead of running it on the `addDays` reading alone
6
+
7
+
`NowAnchorSchema` is closed: `{ $now: true, addDays: -30, subDays: 7 }` fails to parse on its own. But the comparand-type walk every filter door runs (`normalizeFilterComparandTypes`, through `FilterConditionSchema` and `parseFilterAST`) judged an anchor by its `addDays` alone, so the extra key was kept, every executor ignored it, and the query answered the `addDays` reading with no word. The walk now refuses an anchor with any key other than `$now` and `addDays`, with the existing `INVALID_FILTER` / 400 envelope, and the refusal names the key: `it carries an undeclared key "subDays" — the anchor takes $now and addDays only (there is no subDays: a negative addDays subtracts)`.
8
+
9
+
Clause-②: no (narrowing) — every filter door refuses a `$now` anchor carrying a key beyond `$now` / `addDays`, which it ran before. Nothing is widened.
10
+
11
+
Metadata-protocol: narrowed (a filter saved through `FilterConditionSchema` may no longer carry a key beside `$now` and `addDays` on a now-anchored comparand)
12
+
13
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: NowAnchorSchema declared exactly $now and addDays since it shipped, and this aligns the walk with that declaration. An extra key never had a meaning at any executor, so there is no rewrite that keeps what the author meant; the author decides, and the refusal names the key. -->
14
+
15
+
**BREAKING** accept-set narrowing at the filter comparand-type walk (`@objectstack/spec`), served at every filter door.
16
+
17
+
-**Before.**`engine.find()` / `count()` / `update()` in object or array form, `aggregate()`'s `where`, `aggregations[i].filter`, `having`, and a filter saved through `FilterConditionSchema` all kept `{ $now: true, addDays: N, subDays: M }` or `{ …, note: 'x' }`. The query ran on `addDays` alone.
18
+
-**Now.** Each of them answers `INVALID_FILTER` / 400 (a save fails its parse), naming the undeclared key.
19
+
-**Unchanged.** A well-formed anchor, `{ $now: true, addDays: <integer | { $field }> }`, and its resolved form (`$now` holding the pinned ISO instant) run as before at every door. The refusals for a missing or malformed `addDays` keep their wording.
20
+
-**Who sees it.** An author who wrote an extra key on an anchor. That key was ignored before, so the rows answered were the `addDays` rows.
21
+
22
+
**The remedy.** Drop the extra key. To move the instant back, give `addDays` a negative value; to read the offset off a column, use `addDays: { $field: 'numeric_column' }`.
fix(service-automation): switching a packaged flow off now records which administrator did it
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: none
10
+
11
+
The flow toggle (`POST /api/v1/automation/:name/toggle`) writes a row in the activation ledger (`sys_metadata_activation`). Until now, that row and its audit entry named nobody: `created_by` / `updated_by` on the ledger row and `user_id` / `actor` on the `sys_audit_log` row were null, even when a signed-in administrator made the request. The action and security catalog doors were fixed in the previous release; the flow door reaches the ledger through `IAutomationService.toggleFlow`, which could not carry the caller until its `caller` parameter landed.
12
+
13
+
-`@objectstack/runtime`: the flow toggle door passes `toggleFlow(name, enabled, { userId })`, the signed-in user's id and nothing else.
14
+
-`@objectstack/service-automation`: `AutomationEngine.toggleFlow(name, enabled, caller?)` implements the contract's `caller`. The ledger write runs as that user, elevated and with no positions or permissions, so the engine and the audit plugin record the user's id on both rows. Only `userId` is read from `caller`. With no caller, or a caller without an id, the write stays unattributed, as before. `FlowActivationStore.setActive(row, options?)` takes the same optional `{ context }` as the shared activation store in `@objectstack/core`; a store that ignores it keeps working.
fix(service-settings): a secret overwrite's audit row records the previous value's keyed digest as `old_hash` (#26534)
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
Ruling-ref: #26534 triage `6436617436`; the server seat 2 ruling, arm (a)
10
+
11
+
A secret-valued setting's overwrite used to record `old_hash` null on `sys_setting_audit` / `sys_platform_setting_audit`, though the column declares null only when the key was previously unset. It now records the same keyed digest (`hmac-sha256:…`) that `new_hash` records, taken over the previous value, so an overwrite's `old_hash` equals the previous row's `new_hash` and a rewrite of the same secret reads `old_hash == new_hash`. It stays null when the key was unset or reset, when no keyed digest is available, or when the previous value can no longer be read, and it is never an unkeyed hash of the secret. Only a secret overwrite pays for it: one `sys_secret` read and one decrypt of the previous value, before the rotated-away secret is reaped.
fix(metadata-core): an exclusive protocol comparator (`'>18'`, `'<18'`) names the major it admits in the `OS_PROTOCOL_INCOMPATIBLE` refusal
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The refusal for a package whose `engines.protocol` excludes this runtime names a target major (`targetMajor` in the diagnostic and in `error.details`) and, for a package behind the runtime, `objectstack migrate meta --from N`. It used to find the target by peeling every leading operator off the range as if it were `^`, so on a protocol-18 runtime both `'>18'` (19 and up) and `'<18'` (17 and below) answered `migrate meta --from 18`, a migration from the runtime's own major.
11
+
12
+
Now the target is read from the majors the range admits, with the same grammar that decides whether it admits the runtime:
13
+
14
+
-**`'>18'` on an 18 runtime** targets 19. The package is ahead, so the answer is the upgrade remedy from the previous release: "Upgrade the runtime to protocol 19", and no `migrateCommand`. On a 17 runtime it now names protocol 19 too, where it used to name 18, which `'>18'` does not admit.
15
+
-**`'<18'` and `'<18.0.0'`** target 17: `migrate meta --from 17`.
16
+
-**A compound range** targets its floor in either order: `'<18 >=15'` targets 15 (it named 18), and `'>15 <18'` targets 16 (it named 15).
17
+
-**A `||` union** still targets its lowest alternative, now read the same way: `'<17 || >19'` targets 16.
18
+
-**A range that admits no major** (`'>=19 <15'`, `'<0'`) has `targetMajor: null` and the bare `objectstack migrate meta`.
19
+
20
+
`^`, `~`, `>=`, `<=`, wildcards, exact versions, hyphen ranges and partial bounds (`'>18.1'`, `'<18.5'`) answer as before. No range changes between admitted and refused: only the refusal's target and prescription change.
21
+
22
+
Two CLI readers of `targetMajor` follow: `objectstack migrate meta` now rewrites a `'<18'` range to `'^18'` when it migrates a stack to protocol 18 (it used to leave it for the load to refuse), and the protocol-version advisory now speaks for such a range.
fix(plugin-security): the seven built-in positions are served in Chinese, Japanese and Spanish
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
Setup → Positions lists the platform's built-in positions (Platform Admin, Organization Owner, Organization Admin, Organization Member, Organization External Member, Everyone, Guest) beside the app's own. Under zh-CN the app's positions read in Chinese while these seven stayed English, because this plugin's translation bundle carried no `positions` group. The bundle now carries `positions.<name>.label` and `.description` for each built-in position in zh-CN, ja-JP and es-ES, the locales its permission-set translations cover, so `GET /api/v1/meta/position` serves them in the requested locale. English is unchanged, and nothing else of a position changes.
0 commit comments