Skip to content

Commit 42abf1d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19339-protocol-primitive-does-not-read-it
2 parents adbbc66 + 408ca2e commit 42abf1d

42 files changed

Lines changed: 4205 additions & 76 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the four `z.unknown()` navigation doors in `ComponentPropsMap` list all seven `NavigationModeSchema` modes (#18459)
6+
7+
Clause-②: no
8+
9+
`object-grid`, `object-map`, `object-gantt` and `object-tree` declare `navigation`
10+
as `z.unknown()`, so the `.describe()` on each is the WHOLE published account of
11+
what a mode may be — nothing else in the protocol narrows those four doors, and
12+
the generated reference renders their type as `any` beside that sentence. Three
13+
of them listed six of the seven `NavigationModeSchema` values (no `new_window`)
14+
and `object-grid`, the precedent the other three copied, listed five (no
15+
`popover` either). An author reading the shipped reference was told a value the
16+
platform honours does not exist.
17+
18+
**Re-measured at the current `.objectui-sha` pin `87af769e9a3e`, not at the pin
19+
the finding was taken at.** All four blocks hand `schema.navigation` straight
20+
into the shared `useNavigationOverlay` hook; that hook types its own mode union
21+
AS this package's `NavigationModeSchema` (its own docblock: *"the seven modes
22+
this hook switches on are exactly the seven the exported union publishes"*,
23+
held by a parity test on the objectui side); its click router carries a
24+
`new_window` branch that delegates to `onNavigate` and otherwise falls through
25+
to a `window.open`, and `object-gantt` additionally implements that action
26+
itself. `popover` is an overlay mode in the same router and every one of the
27+
four passes it an anchor. So all seven modes reach all four doors.
28+
29+
**Why `object-grid` is in the same change.** It is the row the other three were
30+
copied from and it understates by two rather than one; correcting three while
31+
leaving the source of the pattern intact would leave the family in the state
32+
that produced the defect. All four now name the schema as well as the values,
33+
so the next member added to `NavigationModeSchema` has a named edge into these
34+
rows instead of four independently drifting lists.
35+
36+
**Why `Clause-②: no`.** The doors stay `z.unknown()` — a `navigation` value is
37+
accepted before and after this change, whatever its `mode` reads. Nothing is
38+
added to, removed from or narrowed on any authorable surface: the diff is four
39+
description strings and the reference page regenerated from them, and
40+
`check:authorable-surface`, `check:api-surface` and `check:export-origins` all
41+
pass with no artifact to regenerate. What moves is what an author is TOLD, which
42+
is why this ships as a `patch` rather than as no changeset at all: the sentence
43+
is published bytes — `src/ui/component.zod.ts` ships verbatim under this
44+
package's `files[]` entry `src/**/*.zod.ts`, and the compiled string ships in
45+
`dist/ui/index.js` and `dist/ui/index.mjs`.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING for authored metadata** — `undoable: true` on a registered `action` is now legal only on a shape some runtime actually fulfils, and refused at parse time everywhere else.
6+
7+
Clause-②: yes
8+
9+
The accept set narrows. `undoable` was a plain optional boolean that no refinement read, so it parsed clean on every action shape while only two of them ever produced an Undo — the declared-but-inert case the spec refuses at author time (ADR-0078).
10+
11+
**The two fulfilled shapes, and which runtime fulfils each**
12+
13+
| shape | who takes the snapshot |
14+
| --- | --- |
15+
| `operation: 'update'` (with a `patch`) | the framework runtime — the prior value of every field in the merged write bag, `patch` UNDER the collected `params` |
16+
| `type: 'api'` | the pinned console — it builds the undo envelope from `undoable` alone |
17+
18+
Both stay accepted, byte-identically. Naming the console in the contract is deliberate: the spec is the contract for every runtime including the console, and a closed table of fulfillable combinations is what the declared-is-delivered rule asks for.
19+
20+
**What is refused**
21+
22+
`undoable: true` on `type: 'script'` (the default route) or `type: 'url'`, and on the dormant `type: 'flow'` / `'modal'` / `'form'`, in each case without `operation: 'update'`. Nothing reads the flag on those shapes, so it promised an Undo that never appeared.
23+
24+
```
25+
✗ undoable: `undoable: true` has no runtime that can fulfil it on this action. An Undo is
26+
captured on exactly two shapes: `operation: 'update'`, where the framework runtime snapshots
27+
the prior value of every field the write bag touches, and `type: 'api'`, which the console
28+
snapshots. …
29+
```
30+
31+
**⛔ What is deliberately NOT refused, because it was measured wrong.** Requiring `operation: 'update'` — the obvious repair — would refuse the published `ReassignLeadAction` skill example (`type: 'api'` + `undoable: true`, no `operation`) at import time, since `defineAction` IS `ActionSchema.parse`, and every console api action with undo along with it. The console's two readers gate the undo envelope on `action.undoable` alone with zero reads of `action.operation`, and those same two files are the entire recorded evidence for this package's own liveness verdict `action/undoable: live`. `undoable` absent or `false` is untouched on every type, and the rule lives on `ActionSchema`'s refine chain alone — an inline action is not a registered action.
32+
33+
### Migration — FROM → TO
34+
35+
| You wrote | Write instead |
36+
| --- | --- |
37+
| `{ type: 'script', body, undoable: true }` | `{ operation: 'update', patch: { … }, undoable: true }` if the action is a single-record field write, or drop `undoable` and keep the handler |
38+
| `{ type: 'url' \| 'flow' \| 'modal' \| 'form', undoable: true }` | the same action without `undoable` — those routes never had a capture, so behaviour is unchanged |
39+
40+
⛔ Not mechanically convertible, so this ships as an ADR-0087 D3 structured TODO rather than a D2 conversion: which of the two fulfilled shapes an author meant is an intent no artifact records — a `script` action with an inline handler and an api action calling an endpoint are different dispatches, not two spellings of one — and dropping the flag automatically would remove an Undo the author asked for.
41+
42+
<!-- adr-0087: registered ui-action-undoable-unfulfillable-refused -->
43+
44+
**Published surface.** No export is added, removed or renamed; `ActionType` still carries all six types. The `undoable` `.describe()` and the comment above it are corrected in the same change: both claimed that an action with no `operation` has nothing anchoring the capture, which is false against the pinned console.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": patch
4+
---
5+
6+
45 declared-but-unoffered scalar metadata keys are authorable in the metadata form. Each was **declared** by an object-rooted metadata schema, graded `live` by the liveness ledger, and offered by **no** form in `METADATA_FORM_REGISTRY` — so the generic metadata form rendered no row for any of them and an author's only door was the Source tab: free-text JSON, where a mis-spelled sibling key is written, stored, and refused by the runtime later.
7+
8+
**The population was re-derived, not inherited.** The reconciliation gate's own helper block (`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`, lines 104-469 verbatim) was run over the live registry with a lit control (`name`, offered by 17 of 17 forms) and a dark control (a fabricated key, 0 forms and 0 schemas) asserted in the same probe. Readings on the tree this change starts from: **142** top-level zod-only keys across the 17 forms once the ADR-0010 provenance overlay is skipped, **94** of them on the 11 object-rooted types (`view` is union-rooted and contributes the other 48), **87** of those graded `live`, and **49** of those resolving to a scalar schema node. After the change the same probe reads 4, which are the four rows deliberately not landed.
9+
10+
**Four keys are deliberately still unoffered**, each because a control for it would be an authoring trap rather than an offer:
11+
12+
- `object.displayNameField` — `[DEPRECATED → nameField]`. Its canonical replacement `nameField` lands here; offering the alias beside it would teach an author the retired spelling.
13+
- `app._unpublished` — the schema's own text says `Never authored`: a machine-managed publish gate written by the AI materialization path and cleared by publish-drafts.
14+
- `field.system` — the auto-injected/system-column marker the platform stamps (`applySystemFields`, the search companion). It is read widely on the write path — the record validator skips required and multi-value checks for a flagged column — so a control for it lets an author assert a false provenance that silently disables validation for that field.
15+
- `field.format` — **one `z.string()` key carrying three vocabularies**, so no help text can be written for it until someone rules which one it has. The engine reads it as an **autonumber pattern**: `resolveAutonumberFormat` (`packages/spec/src/data/autonumber-format.ts:196-202`) falls back from `autonumberFormat` to `format`, and `packages/objectql/src/engine.ts:5043-5051` calls it for every `autonumber` field — as does the SQL driver. objectui reads it as a **date display style**, `short` / `relative`, pinned at the `.objectui-sha` this repo builds against by `packages/fields/src/__tests__/datetimeCell.formatVocabulary-8853.test.tsx` and `packages/plugin-detail/src/__tests__/DetailSection.dueLikeReachesTheCell-9729.test.tsx`. The published `describe` names a third — `email`, `phone` — that **nothing measured honours**: an author who follows it on an autonumber field gets the literal string `email` rendered as their number.
16+
17+
**The control follows the scalar type and the copy states what the runtime enforces**, including what ABSENCE resolves to, which is the half an author cannot read off an enum: `object.sharingModel` says a custom object that omits it resolves to `private`; `field.step` says the write path does not reject a value off the step grid; `action.undoable` says an action with no `operation` has no write set to capture. Nineteen rows carry a `visibleWhen` MEANINGFULNESS gate mirrored from the same key's row in the object designer's quick-add grid — the schema accepts each key whatever the sibling value is, but only some field types, page kinds or action operations ever read it.
18+
19+
Three enums (`object.managedBy`, `action.execution`, `action.openIn`) deliberately carry **no** inline `options` list: `FormSelectOptionSchema.value` is a system identifier (`^[a-z][a-z0-9_.]*$`), so members such as `system-data`, `engine-owned` or `perRecord` cannot be spelled as option values at all. Those rows derive their enum from the served JSON Schema, which carries every member verbatim, and the meanings ride the help text.
20+
21+
⛔ **No schema accept set moves and no export changes.** `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks — hence the regenerated `platform-objects` metadata-form bundles, whose 90 new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills, because those three catalogs are ratcheted against undecided echoes.
22+
23+
⛔ **The gate that would notice a missing row is NOT landed here.** The top-level `zodOnly` direction of the reconciliation gate stays unwired: turning it on today would turn the remaining absences into red lines with no offers behind them, which is the shape the census round explicitly refused. This change lands offers; the assertion is a separate card.

‎.github/workflows/lint.yml‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,12 +1355,35 @@ jobs:
13551355
# the throttle would simply stop throttling — which is the shape
13561356
# `check:self-test-wired` requires this step for.
13571357
#
1358-
# Its last battery is the wiring: the five write transports each call both
1358+
# Its last battery is the wiring: the seven write transports each call both
13591359
# halves, and the roster IS the set of files under scripts/pm that issue a
1360-
# write verb, so a SIXTH write path cannot land unpaced.
1360+
# write verb, so an EIGHTH write path cannot land unpaced. Two batteries
1361+
# run REAL child processes: the lease that keeps one write in flight
1362+
# fleet-wide, and the batch gap one process announces and two others obey,
1363+
# are claims about separate processes and are measured on separate
1364+
# processes, by their timestamps.
13611365
- name: Write-pace throttle self-test
13621366
run: pnpm check:pm-write-pace
13631367

1368+
# The fleet identity and the two doors — same family, same split. The
1369+
# minter's LIVE path is a POST to the platform signed with a private key,
1370+
# so the offline half is all CI can hold: a throwaway RSA pair, a fake
1371+
# platform through every branch, and the assertion that matters most — a
1372+
# known token and a known key fed through every output path and grepped
1373+
# out of ALL of them. The card-creation door and the shell wrapper both
1374+
# pin their wiring into the throttle above; the wrapper's self-test reads a
1375+
# fixture cache in place of the platform and proves what git sees (the
1376+
# bot as committer, no token in any remote or config file, a credential
1377+
# helper reached from the environment alone).
1378+
- name: Fleet token minter self-test
1379+
run: pnpm check:pm-fleet-token
1380+
1381+
- name: Issue-create door self-test
1382+
run: pnpm check:pm-issue-create
1383+
1384+
- name: with-fleet wrapper self-test
1385+
run: pnpm check:pm-with-fleet
1386+
13641387
- name: Stamped-post helper self-test
13651388
run: pnpm check:pm-post-stamped
13661389

‎AGENTS.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,13 @@ and posts a `Release:` line. Findings are filed unassigned; assign at the moment
413413
maintainer's own work under the shared account carries no claim comment and is indistinguishable
414414
from a dispatch — an accepted blind spot; per-seat identities are not introduced.
415415

416+
**Every GitHub write leaves through `scripts/pm/`, as `objectstack-fleet[bot]`, behind the shared write
417+
gate.** `fleet-token.mjs` mints the App token from `OS_FLEET_APP_ID` / `OS_FLEET_INSTALLATION_ID` /
418+
`OS_FLEET_PRIVATE_KEY`; `write-pace.mjs` serialises and spaces every write verb fleet-wide; `issue-create`,
419+
`label-write`, `post-stamped` and `close-cards` own their writes; `with-fleet.sh -- <command…>` runs anything
420+
else behind the gate (`--read` for reads). ⛔ Never a bare `curl` / `fetch` / `gh api -X POST|PATCH|PUT|DELETE`
421+
write; never a token in a remote URL, a `git config` value on disk or a log line. Each header is the authority.
422+
416423
**State on your PR that you did not set belongs to another actor — ask, never "correct"
417424
it.** Under one shared identity every other participant's write arrives unsigned: the PM
418425
flipping your draft to ready and arming auto-merge, a bot re-labelling, the platform

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ const result = ApiMethod.parse(data);
461461
| **successMessage** | `string \| Record<string, string>` | optional | Success message to show after execution |
462462
| **errorMessage** | `string \| Record<string, string>` | optional | Error message to show when the action fails (overrides the raw error). |
463463
| **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution |
464-
| **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. `operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set, so nothing anchors the capture there. |
464+
| **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. `operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set here, but `type: 'api'` is fulfilled by the console, which builds the undo envelope from `undoable` alone. Those two shapes — `operation: 'update'` and `type: 'api'` — are the whole fulfillable set; on a registered action `undoable` beside any other shape is refused, because nothing would anchor the capture. |
465465
| **resultDialog** | `{ title?: string \| Record<string, string>; description?: string \| Record<string, string>; acknowledge?: string \| Record<string, string>; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). |
466466
| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. |
467467
| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |

‎content/docs/references/kernel/metadata-plugin.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ const result = MetadataBulkResultSchema.parse(data);
330330
| **successMessage** | `string \| Record<string, string>` | optional | Success message to show after execution |
331331
| **errorMessage** | `string \| Record<string, string>` | optional | Error message to show when the action fails (overrides the raw error). |
332332
| **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution |
333-
| **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. `operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set, so nothing anchors the capture there. |
333+
| **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. `operation: 'update'` is the one declared operation and the declared form of that action: what the undo captures is the prior value of EVERY field the action writes — the merged write bag, `patch` UNDER the collected `params`, not `patch` alone. An action with no `operation` declares no write set here, but `type: 'api'` is fulfilled by the console, which builds the undo envelope from `undoable` alone. Those two shapes — `operation: 'update'` and `type: 'api'` — are the whole fulfillable set; on a registered action `undoable` beside any other shape is refused, because nothing would anchor the capture. |
334334
| **resultDialog** | `{ title?: string \| Record<string, string>; description?: string \| Record<string, string>; acknowledge?: string \| Record<string, string>; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). |
335335
| **visible** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is offered when it evaluates TRUE. Omit = always visible. |
336336
| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |

0 commit comments

Comments
 (0)