Skip to content

Commit d71d6c0

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19580-retire-connector-connection-timeout
Conflict in packages/spec/src/migrations/registry.ts, step18.rationale (the hand-written tail, outside the generated markers): main's paragraph for the translation bundle split is kept whole after the shared closing line, and this branch's connector.connectionTimeoutMs paragraph is appended after it. The only byte changed in main's paragraph is its terminator (`.",` becomes `. "`) so the concatenation continues. conversionIds merged as a set without conflict: both ids present. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
2 parents 9e2843a + 6bfe011 commit d71d6c0

337 files changed

Lines changed: 25265 additions & 7188 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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
`composeStacks(…, { manifest: 'preserve' })` emits each definition **once**, in the body of the package that owns it: a multi-package release artifact no longer carries a flattened copy of its collections at the top level (#14512, ADR-0130 D4's 2026-09-22 addendum).
6+
7+
Measured on `examples/app-multi-package` (two packages, three definitions): `dist/objectstack.json` goes from 8,223 to 5,328 bytes, −35%, and the ratio does not improve with size — it was one extra copy of everything a package owns. The artifact's own `manifest`, `packages`, `plugins`, `devPlugins`, `devLogins`, `api`, `server`, `i18n`, `runtimeModule`, `onEnable` and `devHint` are untouched at the top level; the 35 package-owned collections are what moves.
8+
9+
**BREAKING** for anything that read a compiled multi-package artifact's top-level collections directly. Every reader the platform ships was converted first — the ruling's order was readers first, emitter last — and this lands only with #15004's acceptance probe green on both shapes, which boots a two-package collection zoo through all five load boundaries and fails if any subsystem sees an empty collection.
10+
11+
- **No authored metadata changes, and no artifact on disk is invalidated.** The authoring shape is identical: N `defineStack` packages plus a project config composing them with `manifest: 'preserve'`. ADR-0130 D4's read-both rule is untouched — `packages` present ⇒ iterate it, `packages` absent ⇒ `manifest` as a single-element list — so an artifact built before this change, which carries both halves, loads exactly as it did.
12+
- **A single-package artifact keeps today's shape byte for byte** (ADR-0130 D7): the emitter strips nothing below two package entries.
13+
- **The flattened half is dropped only where it is a COPY — three conditions, and a composition failing any one of them keeps today's additive shape**: (1) two or more package entries; (2) every input's collections attributed to a body — an input declaring no `manifest` has no package that could own its collections, and an input already carrying `packages` contributes those entries untouched; (3) the bodies reproduce the flattened collections item for item. Condition 3 is what `objectConflict: 'merge'` / `'override'` and a standalone action bound onto a SIBLING package's object fail: composition RECONCILES those into a top-level definition no body carries, so the flattened half is not a copy of anything and stays. Nothing here is refused — a composition that was legal before stays legal.
14+
- **Why the copy goes rather than being compressed**: one definition was serialized twice with nothing keeping the copies equal. Where they differ today the flattened one is the reconciled copy and the platform's reader prefers it deterministically, which is exactly why this change removes the second copy only where the first one is redundant. One measured consequence on the compiled path: a seed dataset declared once reached the runtime's shared seed registry twice, and now reaches it once.
15+
16+
Clause-②: yes (narrowing)
17+
18+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes moves: no spec key, export or config field is removed or renamed, and no stored metadata representation changes, so `objectstack migrate meta` has nothing to rewrite. What changed is the SHAPE A COMPILER EMITS, and every reader of it in this repository was converted by the reader half of the same ruling (#15005, #15006, #15007, #15229, #15232) before this landed. -->
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: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-settings': minor
4+
---
5+
6+
**BREAKING for per-app translation bundles** — the translation bundle type splits in two: `settings` is a PLATFORM group and a per-app bundle may no longer declare it (#15178)
7+
8+
Clause-②: yes
9+
10+
`TranslationDataSchema` served two different bundles at once — the per-app one an
11+
application authors (`stack.translations`, `defineTranslationBundle`) and the
12+
code-authored bundles the platform packages ship. It now names the **per-app**
13+
bundle entry and declares ten groups; the new `PlatformTranslationDataSchema` /
14+
`PlatformTranslationBundleSchema` (types `PlatformTranslationData` /
15+
`PlatformTranslationBundle`) carry the eleven-group platform face, `settings`
16+
included.
17+
18+
### Migration — FROM → TO
19+
20+
| You wrote | Write instead |
21+
| --- | --- |
22+
| `defineTranslationBundle({ 'zh-CN': { settings: { mail: { title: '邮件投递' } } } })` | delete the `settings` group — there is no per-app replacement key |
23+
| `defineStack({ translations: [{ 'zh-CN': { settings: … } }] })` | delete the `settings` group from the bundle entry |
24+
| `const b: TranslationBundle = { en: { settings: … } }` — a PLATFORM package's own bundle | `const b: PlatformTranslationBundle = { en: { settings: … } }` |
25+
| `const d: TranslationData = { settings: … }` — a PLATFORM package's own locale entry | `const d: PlatformTranslationData = { settings: … }` |
26+
27+
**The one-line fix for an application: delete the `settings` group.** Settings copy
28+
is not application-authorable at all — `settings` is keyed by
29+
`SettingsManifest.namespace` and only platform code declares a manifest, so the
30+
only namespaces a per-app entry could ever address were the platform's own.
31+
`settingsCommon` is **not** affected — the Settings UI shell strings (the source
32+
badges, under `settingsCommon.sourceLabels`) stay on the per-app face; only the
33+
per-namespace manifest copy under `settings` leaves.
34+
Run `os migrate meta --from 17` to list the mechanical edits for existing
35+
sources; apply them by hand.
36+
37+
### What the deletion changes, which is not nothing
38+
39+
⚠️ This is **not** a lossless delete, and the record says so rather than claiming
40+
the house phrase. Both bundles load into ONE served tree — `AppPlugin`'s
41+
`loadTranslations` and every platform plugin's `kernel:ready` contribution both
42+
call `II18nService.loadTranslations`, which deep-merges — and the
43+
`resolveSettings*` family and the console's settings labels read that merged
44+
tree. So an app-authored `settings` branch did resolve.
45+
46+
**It was a gap filler, not an override.** The app's bundles are loaded in
47+
`AppPlugin`'s own `start()` (kernel Phase 2); the platform's settings
48+
translations arrive from `SettingsServicePlugin`'s `kernel:ready` hook (Phase
49+
3); `deepMerge` gives the **later** source the leaf. So the platform won every
50+
key both bundles defined, and a per-app entry rendered **only where the platform
51+
bundle carried no string for that key and locale** — the platform ships `en`,
52+
`zh-CN`, `ja-JP` and `es-ES`.
53+
54+
**What to expect after upgrading.** Where the platform already carried the
55+
string, nothing changes on screen — that value was the one being served all
56+
along. Where your entry was filling a gap, that Settings screen now renders the
57+
**manifest's own literal, which is English** (the `?? fallback` every
58+
`resolveSettings*` helper ends in). Those are the screens to re-read. If a
59+
platform string is wrong or missing for your locale, correct it in the platform
60+
bundle (`@objectstack/service-settings`'s `settingsBuiltinTranslations`) — do
61+
not re-add the app-side copy, which the platform overwrites on every boot
62+
wherever it has its own value.
63+
64+
No deprecation window: the per-app door refuses the key by name from this major,
65+
and the rejection carries the prescription above.
66+
67+
### Unchanged
68+
69+
The registered `translation` metadata type (`TranslationItemSchema`) still
70+
declares `settings` — this ruling covers the file-authored bundle. `GET
71+
/api/v1/i18n/translations/:locale` still declares it on its response, because the
72+
served document is the merged tree; `GetTranslationsResponseSchema` is typed
73+
against the platform face for exactly that reason.
74+
75+
Ruling batch #132 item 2 letter ② (2026-09-13) — 「同意」. The card's original
76+
"removal" disposition is struck: `settings` is a live platform key.
77+
78+
<!-- adr-0087: registered translation-per-app-settings-removed -->
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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
`ObjectStackProtocolImplementation` and `SysMetadataRepository` no longer open their refusal messages with a bracketed tag restating the `code` the same throw declares — `error` carries the human sentence, `code` carries the machine token, and the token is no longer duplicated onto the prose axis.
7+
8+
Clause-②: yes
9+
10+
Every refusal `ObjectStackProtocolImplementation` and `SysMetadataRepository` raised opened with a lowercase `[tag]` that was the restatement of the `code` that very throw declared: `[no_draft]` in front of `NO_DRAFT`, `[item_locked]` in front of `ITEM_LOCKED`, and so on for 38 throw sites across the two producers. They were not invisible. `withoutDeclaredCodePrefix` strips a leading restatement only when the message opens with the declared code followed by a colon (`INVALID_REQUEST: …`); the bracketed lowercase spelling matches neither the casing nor the separator, so it was never stripped and reached the caller in `error.message`. The repo's own de-duplication mechanism existed and did not fire here.
11+
12+
The maintainer ruling of 2026-08-29 on the `/data` door shipping `FORBIDDEN:` in front of a localized refusal is ONE envelope semantics — `error` is HUMAN LANGUAGE, `code` is the MACHINE TOKEN — and a prefix is removed *because* the same fact already rides the `code` axis. All 38 met that condition by construction.
13+
14+
## FROM → TO
15+
16+
| before | now |
17+
| --- | --- |
18+
| `error: "[no_draft] No pending draft exists for view/task_list."` | `error: "No pending draft exists for view/task_list."` |
19+
| `error: "[item_locked] view/task_list is locked (_lock=…)."` | `error: "view/task_list is locked (_lock=…)."` |
20+
| `error: "[NOT_OVERRIDABLE] 'action' is not allowOrgOverride…"` | `error: "'action' is not allowOrgOverride…"` |
21+
22+
**`code` is unchanged on every one of them**, and it is where the token always also was — `NO_DRAFT`, `ITEM_LOCKED`, `NOT_OVERRIDABLE`, and the 14 others. A reader matching `error.message` for a bracketed tag reads `error.code` for that tag, upper-cased, instead; a reader already using `code` needs no change. The HTTP `status` is untouched.
23+
24+
- **Measured, not assumed, before it was removed**: 37 literal openers plus one written as `` `[${code}]` `` from the same variable the throw assigns to `err.code` three lines down — that one spelled by interpolation, so it was invisible to every grep for a literal tag and is absent from the card's own inventory.
25+
- **Nothing consumed the tag.** The only consumers found anywhere are strippers: `@object-ui/react`'s `extractWriteErrorMessage` and two `plugin-detail` call sites each remove a leading bracketed prefix before showing the sentence to a user, next to the `SCREAMING_SNAKE:` strip. They confirm the tag was arriving and they cannot break on its absence — the regex simply matches nothing.
26+
- **Two bracketed vocabularies are deliberately kept**: the `path [zod code]` locators inside a validation headline and the `[rule]` locators the author-time gate composes. Neither restates a declared `code` — they name WHICH finding, a fact the envelope carries nowhere else.
27+
- **The published docs that quoted the openers are corrected in the same change.** `ProtocolSchema`'s promotion `describe()` said the lookup 「answers 404 `[no_draft]`」 and now names `NO_DRAFT`, the axis that still carries it; `content/docs/references/api/protocol.mdx` is regenerated from it, never hand-edited. The error catalog's two documented `INVALID_REQUEST` payloads showed a `message` opening with the tag beside a `code` field already carrying the token, and now show what the platform emits.
28+
- ⛔ **Three carriers in `content/docs/releases/v17/` are deliberately left**: release pages record what shipped and a code change does not rewrite them.
29+
- **Pinned as an absence**, because nothing else would notice one coming back: a re-introduced tag reds exactly one per-door pin and a newly-written refusal reds none.
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.

0 commit comments

Comments
 (0)