Skip to content

Commit 67e369e

Browse files
committed
Merge origin/main into claude/issue-15178-translation-bundle-split
Resolves the one real conflict: both sides appended a `semantic` entry to step18's `<os-generated semantic:18>` region at the same anchor. Resolved as a union of both entries — `translation-per-app-settings-platform-only` (this branch) and `ui-action-undoable-unfulfillable-refused` (main) — which is the order the generator derives from the entry ids. Verified with `check:migration-registry` (exit 0): the resolved region is byte-identical to what `gen:migration-registry` emits from `src/migrations/entries/`. The two `content/docs/references/*.mdx` artifacts were deferred by the os-regen merge driver and are regenerated in the follow-up commit. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
2 parents 34b6a25 + fb7b746 commit 67e369e

120 files changed

Lines changed: 8776 additions & 939 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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`src/migrations/entries/README.md` — the ADR-0087 entry authoring rules now record that an entry's prose is scanned as source **twice**, and that the rule is never to spell a shape a live textual ratchet matches (#15130).
6+
7+
This file ships inside the package (`files[]` carries `README.md`, which matches at every depth — measured with `npm pack --dry-run`: 277 files, this one among them), so the rules an entry author reads are these.
8+
9+
- **The mechanism is the counter-intuitive part.** Every string an entry declares — `surface`, `replacement`, `reason`, `acceptanceCriteria` — is concatenated verbatim into the generated `src/migrations/registry.ts`, which is ordinary `.ts`. A repo-wide textual scan therefore reads the same sentence once in the entry file and once in the registry. The tree's one code/prose separator masks **comments** and leaves **string literals** intact by design, so a quoted example is code to every scan built on it, and prose in this package can turn **another package's** test red.
10+
- **The rule is the broad one, and the parenthesis is its instance.** A rule worded as "quote a retired call site without its parentheses" would make counter-examples of entries that spell a parenthesised call and are green — they go unmatched only because no live ratchet enumerates *those* methods, which is a fact about today's ratchets rather than a licence. What an author controls is not spelling a shape some ratchet matches; the guidance is to name the surface rather than spell a call of it.
11+
12+
No schema, no export and no authorable key moves; the three existing rules in that section are unchanged and no entry was edited.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/plugin-sharing': patch
4+
---
5+
6+
`plugin-sharing` recognises the engine's organization refusal through objectql's own published recognizer instead of a locally re-spelled literal, and the `PROVENANCE_WAIVERS` row that excused that local spelling is retired with it (#16160).
7+
8+
Clause-②: no
9+
10+
The waiver carried its own expiry in its `reason`: *removed together with the stamp site when objectql publishes a recognizer*. It does, so both halves land here — `check:error-code-provenance` reconciles a waiver in three directions at once (the `registeredUnder` key still lists the code, the waived package still does not, and the scan still finds a site for the pair), so removing either half alone reddens the gate on the other.
11+
12+
- **`ENGINE_ORGANIZATION_REFUSAL_CODE` is gone.** It was a `constdef` stamp site in `plugin-sharing/src/sharing-rule-service.ts` for a code this package only ever RECOGNISES — `@objectstack/objectql` is the emitter and already carries the row. The per-grant catch now asks `isSystemWriteOrganizationRequiredError(err)`, and the `warn` that reports an absorbed refusal names `SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE`. Both are imported from `@objectstack/objectql`, which exports them for exactly this: a consumer performs the `code` compare without authoring the string, so it acquires no stamp site of its own and cannot drift from what the engine throws.
13+
- **Nothing about the absorbed set moves.** The catch stays as narrow as it was — one engine refusal absorbed, everything else rethrown unchanged — and `plugin-sharing` still emits this code nowhere: the surviving mention is a structured log field on the refusal it just absorbed, not a refusal envelope of its own.
14+
- **No error-code membership moves.** `ERROR_CODE_LEDGER` and `StandardErrorCode` are untouched; `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` stays registered under `@objectstack/objectql` exactly as before. The only ledger change is one `PROVENANCE_WAIVERS` element, 10 waivers → 9, and the gate's site census 339 → 338 with `listed` unchanged at 322.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
A package body now has a declaration at every stage it really passes through: `ArtifactStagePackageBodySchema` and `RecordStagePackageBodySchema` join `AssembledPackageBodySchema`, and the installed-package read rows are declared against the record stage instead of two `z.unknown()` holes (#17518).
6+
7+
ADR-0130 D4 says an artifact is inert JSON — "a plugin written inside `packages[i].manifest` could never be constructed by a loader, so a reader that resolved it there would register garbage where it used to skip in silence". Of `AssembledPackageBodySchema`'s 55 members exactly two declare that they accept a callable: `functions`, whose entry union opens with `z.function()`, and `hooks`, whose `handler` carries a `z.custom()` branch. One unrepresentable member costs every embedder its whole JSON Schema, which is why `api/ListInstalledPackagesResponse` and `api/GetInstalledPackageResponse` could only carry the body with both keys written `z.unknown().optional()` — accepted without being checked, as that file's own docblock said.
8+
9+
- **⛔ The assembled body is untouched, and that is the point.** Those callables are LIVE on the stage it declares itself for: `composeStacks(stacks, { manifest: 'preserve' })` builds exactly such a body and the load path registers it, and `stack.zod.ts` states the invariant that binds the two. Narrowing in place would refuse a published composition function's own output. The two JSON stages are declared BESIDE it instead.
10+
- **Artifact stage** — what `objectstack build` writes: `functions` entries are the lowered spellings (a bare handler ref, or `FlowFunctionLoweredDeclarationSchema`), `hooks[].handler` is a string. **Record stage** — what `SchemaRegistry.installPackage` stores: the artifact stage with `functions[].handler` OPTIONAL, in both the map-record and the array form. That single difference is the whole distance between the two: `build` mints a ref for every callable, while `toRecordManifest`'s structural projection DROPS the callable and mints nothing in its place, so a record states what each function is named and what it declared with `handler` absent where the callable was. Measured: both bodies convert under `z.toJSONSchema` over the whole body, where the assembled body still does not.
11+
- **`FlowFunctionLoweredDeclarationSchema` is exported** from `@objectstack/spec/automation`, with its `FlowFunctionLoweredDeclaration` / `…Parsed` aliases. It was a module-local `const`, and `export * from './flow-function.zod'` only re-exports what is already exported — so `unemitted-schemas.baseline.json`'s reason for `Automation.FlowFunctionDeclarationSchema`, which says the lowered record "is the serialisable half … and it publishes normally", pointed at a schema no consumer could reach. It publishes now: `automation/FlowFunctionLoweredDeclaration` is in the schema manifest.
12+
- **`effect` is READ, not minted.** `FlowFunctionDeclarationSchema.effect` is `FlowFunctionEffectSchema.default('pure')` — a default, not a requirement — and the array member's is `.optional()` with no default. Both JSON stages inherit each form's optionality by deriving from it rather than restating it.
13+
- **⚠️ What narrows, stated plainly**: on the two installed-package responses, `functions` and `hooks` move from `unknown` (accepts anything) to their declared JSON shapes. No row the doors really serve is withdrawn — measured through the real `SchemaRegistry.installPackage` on the shape `examples/app-showcase` ships, on the array form, and on the already-lowered body an artifact boot installs. Every other key, `objects` included, is checked exactly as before, and both stages still refuse an authoring glob and an unknown key.
14+
- One correction in the same edit: `package-api.zod.ts` said those two members were also why `ArtifactPackageSchema` and `ObjectStackDefinitionSchema` publish no JSON Schema. They are not — `src/stack.zod.ts` is not one of the subpath namespaces `build-schemas.ts` walks, so neither is ever reached by the emit loop.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
`GET /packages` reports every function a package declares. A bare callable `functions` entry is normalised to the declared form at the assembly boundary, so the registry record no longer drops it (#17518).
6+
7+
`SchemaRegistry.installPackage` stores `toRecordManifest(manifest)`, a structural JSON projection whose rule is "a live object reached the record" and deliberately ⛔ not a key denylist. That rule treated the two authored `functions` spellings unequally through no fault of its own: a DECLARED entry (`{ handler, effect: 'writes' }`) is a plain object, so it survived with its callable dropped, while a BARE callable entry IS the callable, so the whole key vanished. `examples/app-showcase` ships one of each, so a package declaring two functions was reported as declaring one — a machine-readable read door under-reporting by construction.
8+
9+
- **The repair is at the assembly boundary, ⛔ not in the projection.** `installPackage` makes the two spellings structurally equal before projecting, so the structural rule is untouched and no key name is special-cased. The projection then leaves `{ effect }` for both.
10+
- **⛔ No ref is minted.** `objectstack build` mints refs with `uniqueName(base, taken)` and dedupes by function identity, so a ref minted in the registry is not guaranteed to be the one `build` mints — a record could assert a handler that resolves in no sibling module. An absent `handler` is the honest statement "declared here, not serialisable", which is exactly what `@objectstack/spec`'s new `RecordStagePackageBodySchema` declares.
11+
- **⛔ No entry is dropped**, either: under-reporting by design was the other arm, and it also throws away the `effect` declaration, the one half that survived.
12+
- The caller's manifest is never mutated — `ObjectQL.registerApp` and the hook binder read the live callables off that object — and a copy is made only when an entry really needed rewriting. The ARRAY form is untouched: its entries are objects carrying their own `name`, so the projection already kept them.
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.

0 commit comments

Comments
 (0)