Skip to content

Commit 59c5031

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17356-reachability-root-enumeration
2 parents dc98ee2 + fed4a15 commit 59c5031

65 files changed

Lines changed: 4768 additions & 1469 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
docs(rest): the `'platform'` virtual-id docblock names the live `/environments/` URL family (#15858)
6+
7+
`RestServer`'s `environmentId === 'platform'` docblock described the reserved virtual id as being addressed *"through the regular project URL shape (`/projects/platform/...`)"* — the spelling ADR-0006 v4's second addendum (D2, executed 2026-08-28) retired with **no alias and no grace period**. It now reads *"through the regular environment URL shape (`/environments/platform/...`)"*.
8+
9+
**The prefix is corrected rather than the paragraph retired, because the shape is live.** The fork this card opened — *"if the shape is live the sentence needs its prefix corrected, and if it is not, the paragraph may want retiring"* — was decided by a cross-repo reading: the host enables environment scoping precisely so `/api/v1/environments/platform/...` resolves to the control-plane protocol, its kernel resolver returns no per-environment kernel for that id, and a live test drives `routePath: '/environments/platform/meta'`. Framework-side, `resolveProtocol` still short-circuits `environmentId === 'platform'` to the control-plane protocol. Every behavioural claim in the paragraph is true today; only the URL spelling and the phrase "the regular project URL shape" were not.
10+
11+
What reaches a consumer of this package: the docblock ships inside `dist/index.d.ts` and `dist/index.d.cts` (and the bundles), so `projects/platform` no longer appears anywhere in the published artifact. **No behaviour moves** — comment-only, and the file is line-count neutral at 13,877 lines before and after.
12+
13+
⚠️ Two things deliberately left alone, both measured rather than overlooked:
14+
15+
- The sibling site that calls `/projects/:environmentId` **"the retired spelling"** is *correct* — it documents the repair that landed under #16538. Harmonising the two would make the right one wrong.
16+
- The same paragraph's *"It is NOT a row in the projects **table**"* is about a table, not a URL. That is a different question — it turns on what the control-plane row is called today — and it is not guessed into this edit.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@objectstack/metadata': patch
3+
---
4+
5+
`@objectstack/metadata` no longer declares `@objectstack/platform-objects`.
6+
7+
The dependency was the retired `adr-0030-notification-event` migration runner's,
8+
and that runner was its only consumer. Nothing under `packages/metadata/src`
9+
carries a `@objectstack/platform-objects` specifier any more, so the declaration
10+
described an edge the package no longer has. The two test-tooling entries that
11+
existed only to serve it go with it: the `@objectstack/platform-objects/system`
12+
alias in `vitest.config.ts` (whose comment still cited the retired migration's
13+
receipt cases as its reason) and the matching `paths` mapping in `tsconfig.json`.
14+
15+
## What an installing consumer should check
16+
17+
⚠️ This is a **published** package dropping a declared dependency, so it changes
18+
what an install tree contains, not just what this repo builds. If you import
19+
`@objectstack/platform-objects` **without declaring it**, and it resolved for you
20+
only because `@objectstack/metadata` hoisted it, that resolution is gone — the
21+
fix is one line, and it is the supported spelling either way:
22+
23+
```
24+
pnpm add @objectstack/platform-objects # or npm/yarn equivalent
25+
```
26+
27+
`@objectstack/platform-objects` is published on its own and is unchanged by this;
28+
nothing is renamed, removed or re-exported.
29+
30+
⛔ Nothing `@objectstack/metadata` itself ships is affected. Measured rather than
31+
asserted: its built `dist/` (30 files, 10 declaration files) carries **zero**
32+
occurrences of `platform-objects`, against a positive control in which all nine
33+
of its other declared dependencies appear in four to twelve dist files each. No
34+
runtime import and no type reference reaches it, so no consumer can arrive at it
35+
through anything this package publishes.
36+
37+
Grade `patch`, measured rather than defaulted: no export moves, no accept-set
38+
widens, no runtime behaviour changes. Not `skip-changeset` either — `package.json`
39+
is shipped by `npm pack`, and a consumer's install tree is what changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
A datastore created from empty now attests **two** creation-attested migration ids, not
6+
three.
7+
8+
`attestFreshDatastore` (`@objectstack/platform-objects/system`) writes one `sys_migration`
9+
row per id in `CREATION_ATTESTED_MIGRATION_IDS` (`@objectstack/spec/system`) at the moment
10+
a store is created from empty. That tuple lost `'adr-0030-notification-event'` when the
11+
ADR-0030 notification cut-over was retired, so a store born on this version is attested for
12+
`'adr-0104-file-references'` and `'adr-0104-value-shapes'` alone.
13+
14+
## What an operator sees
15+
16+
- A fresh deployment's `sys_migration` table holds **two** creation-attested rows where it
17+
held three. Nothing else about them moves: both carry the same
18+
`attested: 'datastore-created-empty'` marker in `details`, and both ADR-0104 gates are
19+
enabled from birth exactly as before.
20+
- **No row is written under `'adr-0030-notification-event'` any more, and nothing reads
21+
one.** A deployment that already holds such a row keeps it, untouched —
22+
`NOTIFICATION_EVENT_MIGRATION_ID` (`@objectstack/spec/system`) survives as that row's
23+
name so the table stays readable by an operator. The id gates nothing, and never did.
24+
- Nothing this package exports is renamed, removed or re-signed. `attestFreshDatastore`
25+
takes the same arguments and answers the same shape; a caller passing its own
26+
`migrationIds` is unaffected, because only the default moved.
27+
28+
There is nothing to adopt and no command to run. Pre-ADR-0030 `sys_notification` rows are
29+
not carried by the platform on this line, so a store created from empty has nothing the
30+
retired id could have attested.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os build` and `os validate` now report a **permission-set name collision** — the compile-time half of the #17516 refusal, raised behind the SAME predicate and the SAME sentence as the runtime door so the two cannot drift (#18024).
6+
7+
When two packages in one artifact declare a permission set under the same name, `bootstrapDeclaredPermissions` refuses to write into the row the first one owns. That refusal is correct under ADR-0086 D4 and is **unchanged here** — the whole declared set (its object, field, tab and system permissions) is dropped at boot, and nothing else reports it. #17516 gave that drop a runtime door; until now no door said anything at compile time, so the first an author heard of it was a boot warning on a deployed environment.
8+
9+
Measured on the pre-change tree (`origin/main` 8fe5cb8e5), by grep over `packages/cli/src`, `packages/spec/src` and `packages/metadata/src`:
10+
11+
```
12+
permission-set collision diagnostic, compile time = 0 files
13+
control: `collision|duplicate` in packages/cli/src = 20 files (so the zero is a reading,
14+
not a dead grep)
15+
```
16+
17+
Both commands now compute it, and the findings ride the `warnings` key both payloads already declare — no new top-level key, and no new published export.
18+
19+
- **Reports; it never refuses.** `severity: 'warning'` is declared at the producer and the failure direction is CLOSED: the set is not installed, so nothing is over-granted. Exiting non-zero would narrow what `os build` accepts, which is the option #14553's ruling weighed for `navigationContributions` and did not take.
20+
- **One derivation, so the two doors cannot drift.** The owner comparison is `permissionSetNameIsForeign` and the sentence is `permissionSetNameCollisionDiagnostic` + `formatPermissionSetNameCollisionDiagnostic`, both consumed from `@objectstack/plugin-security`'s package entry — where #17516 published them for exactly this consumer. No second predicate, no retyped sentence: two doors phrasing one refusal differently is the defect, not the fix.
21+
- **Only the composed case is judged.** A name owned by a package some *other* artifact installed is invisible without a database and stays unreported — the same bound the navigation-contribution check keeps for a contribution aimed at an app no package here ships.
22+
- **A package re-declaring its own set name is not a collision.** That is an idempotent re-seed at runtime, which is why the check asks the shipped ownership predicate rather than counting duplicate names. Ablated on disk: removing that one call leaves the suite at 1 failed / 9 passed, and restoring it returns 10 / 10.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`@objectstack/spec/data` publishes the case-insensitive-contains **text-comparand door** — `isRefusedTextComparand(target)` and `textComparandRefusalReason(field, operator, target)` — so every face reads one implementation of a refusal the package already declared as data (#18113, objectui#9048 ruling D).
6+
7+
`FILTER_TEXT_CASES` has carried two REJECTION rows for that operator since #5701 — an empty comparand and a non-string one, both `code: 'INVALID_FILTER'`, both `mustMention: ['$icontains']` — but only as cases a backend is *checked against*. Every face that honoured them wrote its own copy of the discrimination and its own wording, which is how the same authored filter came to be refused in one dialect and lowered onto the wire in another. The rule now lives with the producer of the rule.
8+
9+
- **`isRefusedTextComparand(target)`** answers `true` for exactly those two shapes. It answers `true` for `undefined` as well: a vocabulary with an "absent" the `$` dialect does not have (a stored view rule whose operator takes no comparand) must test for absence **before** this door — that carve-out is the caller's, not a third row.
10+
- **`textComparandRefusalReason(field, operator, target)`** returns the CONTRACT half of the message: **no leading capital, no trailing period, no envelope**, so each face seats it in its own sentence — a matcher that has a row to exclude logs it, a producer that has none throws it. ⛔ No new error code: `INVALID_FILTER` is declared and already in the ADR-0112 ledger.
11+
- **`operator` is the spelling that ARRIVED** (`$icontains` from a `$`-dialect filter, `icontains` from the infix/view vocabulary), never a canonical substitute — telling an author about a key their dialect cannot contain is the misdirection this door exists to end.
12+
- ⚠️ **Consequence for the infix dialect**: `mustMention` is spelled `$icontains` because the published rows' filters are, so for an arriving `icontains` the reason names what arrived and does **not** carry the `$`-dialect token. The face serving that vocabulary names the `$` twin in its own tail. Pinned in both directions in `filter-text-comparand.test.ts`.
13+
- **The message bytes are the contract, not prose.** They are the bytes two shipped faces already emit byte for byte; `mustMention` is what makes a reword a different failure to honour the same row, and a transcription pin catches the reword `mustMention` cannot. ⛔ Change them only by changing the rows they answer.
14+
15+
Additive: no existing export changes, no behaviour moves. `describeComparand` — the guard that keeps a BigInt or a cyclic comparand from making `JSON.stringify` throw *inside* the refusal — travels with the reason as a module-internal helper and is deliberately not published; exporting it is a published-surface decision for the PR that needs it.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`translation-target-unknown` no longer calls a locale key for a CONTRIBUTED navigation item an orphan — the remedy it printed deleted a translation the runtime honours (#18203)
6+
7+
`validateTranslationReferences` built the `apps.<app>.navigation.*` universe from the app's authored `navigation` array alone. An item injected by another package through `manifest.navigationContributions` (ADR-0029 D7, ADR-0130) is never in that array, so every locale key for it was reported as naming an item *"which app X does not declare"*, at `error` since 17.4.0, with the remedy *"Match the key to the navigation item's `id`, or drop it."*
8+
9+
⚠️ **That remedy is wrong in the worst direction a false positive can point: following it deletes a working translation.** Measured on `objectstack-ai/hotcrm` `be11c07` (pin 17.4.0), where a service module contributes five items into `crm_enterprise`:
10+
11+
| | measured |
12+
| :-- | :-- |
13+
| `os build` | **15** findings — 5 contributed items × 3 non-default locales |
14+
| `GET /api/v1/meta/app?id=crm_enterprise` | returns all 5 items, `zh-CN` labels **resolved** from the app's own pack |
15+
16+
The universe now folds in every contribution aimed at the app, walked by the same `walkNav` a declared subtree gets, so what the rule judges is the population the runtime serves rather than the array the author typed.
17+
18+
**Both carriers are read**, because a stack in hand has two shapes and `os build` runs the rule table over both:
19+
20+
- `packages[].manifest.navigationContributions` — the ADR-0130 D4 artifact entry. This is the shape the per-package leg needs (`compile.ts` step 3b-ii): the app's owning package declares no contribution of its own, and the union run above it de-duplicates, so a fix reading only the union would have left that leg reporting the finding alone.
21+
- `manifest.navigationContributions` — the stack's own `StackSchema.manifest`, where a single-`defineStack` project's contributions live. `os validate` judges only the union stack, so reading the artifact form alone would have left the fast inner-loop command still reporting what the build no longer does.
22+
23+
**The runtime's fold is deliberately not imported, and the union is faithful anyway.** `@objectstack/lint` depends on `@objectstack/spec` and never on a runtime; `applyNavContributions` is a `SchemaRegistry` method in `@objectstack/objectql`. A second implementation would normally be exactly the drift this class of defect is made of — except that the fold pushes the contributed items in *every* branch: into a `group` that resolves, at the app top level when the `group` id names nothing (a `nav_contribution_group_missing` diagnostic, never a refusal), and at the top level when `group` is omitted. It chooses **where** an item lands and never **whether**, so the set of addressable ids is invariant under it. All three placements are pinned side by side so that invariant cannot quietly stop holding.
24+
25+
**The control, which is the point of the change.** Widening a universe trades a false positive for a blind spot unless the genuine orphan still reports. A key that nothing contributes is still an `error` carrying `translation-target-unknown`, its path and its message; a contribution aimed at app B does not make its ids addressable under app A; and the contributed ids join the population the hint enumerates, so the remedy an author is handed lists what they may actually key to.
26+
27+
**What this still cannot see, stated rather than implied.** Contributions registered imperatively by plugin code (`engine.registerAppNavContribution` from a plugin's `init`) are not metadata, and no static rule can read them — that is the population `pnpm check:app-nav-i18n` has to boot a composition to judge. A locale key for one of those is still reported here.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`ComponentPropsMap` declares `object-map`, `object-gantt` and `object-tree` — the three object-bound SDUI blocks #7751 enumerated past — with each row's key set derived from the objectui renderer's own read points (#18305).
6+
7+
**Clause-②: yes (widening)** — three new declared rows on a published surface, so the accept set a consumer writes against grows. Nothing previously admitted is refused, and nothing is retired. Contract-review tier.
8+
9+
Until now the `object-*` family carried six rows, `object-chart` carried a written note saying its key set is not derivable with this section's confidence, and these three carried neither: they were not ruled out, they were never measured. The cost was the one #7751 exists to remove — the `@objectstack/lint` props gate had no schema to dispatch on, so every authored key inside `properties` on one of these nodes parsed clean, stored, shipped and was ignored by the renderer with a success receipt. It also left objectui's own `@object-ui/types` mirror standing in as the authority for `object-map.data` and `object-gantt.data`, and left `object-tree`'s record-source read undeclared on every published face (objectui#8348, PR objectui#9234). Executing the ruling 「8348 以协议为准」 (decision batch #83, 2026-09-08) and batch #136 item 3 (Q1-C).
10+
11+
Key sets measured from `plugin-map/src/ObjectMap.tsx`, `plugin-gantt/src/ObjectGantt.tsx` and `plugin-tree/src/ObjectTree.tsx` at the `.objectui-sha` pin `53ded82b`, with per-key read-point citations in each schema's header:
12+
13+
- **`object-map`** — `objectName`, `data`, `staticData`, `filter`, `sort`, `map`, `mapStyle`, `navigation`, `enableClustering`.
14+
- **`object-gantt`** — the same record-source and query keys, plus `gantt`, `navigation`, `label`, `skipWeekends`, `holidays`, `persistLayout`, `viewName`, `markers`, `criticalPath`, `showBaselines`, `readOnly`, `mobileReadOnly`.
15+
- **`object-tree`** — `objectName`, `data`, `staticData`, `filter`, `tree`, `navigation`. No `sort`: this renderer's fetch carries `$filter`, `$top` and `$expand` and no `$orderby`, so a `sort` door here would publish a key with no read site.
16+
17+
Three things the derivation decided rather than assumed, each pinned:
18+
19+
- **`data` is the `ViewData` object arm on all three**, because rung 1 of the shared record-source ladder returns the authored value verbatim as a `ViewData`. For map and gantt that agrees with objectui's mirror — verified from the read points first and read back as a check, never as the source. For **`object-tree` it does not**: the mirror declares no `data`, no `staticData`, no `filter` and no `navigation` at all, while the renderer reads all four (`data` on two sites). The row follows the read points, which is what 「以协议为准」 resolving for this block means.
20+
- **The flat top-level config spellings stay unauthorable.** `ObjectView` / `ListView` build these nodes by spreading `options.map` / `options.gantt` / `options.tree`'s CONTENTS at the top level; that is an internal transport form, not a second authoring surface (maintainer ruling objectui#5018, 2026-08-17, inherited by objectui#6469). Writing one now gets a wrong-layer prescription naming the config block instead of a bare unknown-key refusal — the channel `object-calendar` already uses for its own flat field spellings.
21+
- **`filter` and `sort` are the family's one orthography from birth** — `ViewFilterRule[]` and `SortItem[]`, not the `z.unknown()` the original six carried before #15449 and objectui#8221 pulled them back.
22+
23+
Nothing about the parse of a page changes: `PageComponentSchema.type` already accepted all three through its open string arm, and it still does. What changes is that an authored props bag on one of them is now judged instead of skipped.

0 commit comments

Comments
 (0)