Skip to content

Commit ad4203d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-12068-findone-doubles-remainder
2 parents 88784b7 + f28f00f commit ad4203d

182 files changed

Lines changed: 11011 additions & 1742 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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): the `os environments` family calls the entity an environment, not a project, in every string it prints (#12153)
6+
7+
Per ADR-0006 the v5.0 rename `project` → `environment` has no aliases, and AGENTS.md
8+
states "Project now only means the npm/monorepo sense". #10967 (PR #11227) renamed the
9+
**command** (`os projects …` → `os environments …`) across these same five files; the
10+
**entity noun** inside the strings oclif prints was left behind. A user ran
11+
`os environments switch <id>` and the tool answered `✓ Active project: …`.
12+
13+
25 user-visible string literals in `packages/cli/src/commands/environments/` are swapped
14+
to the post-rename noun. No behaviour, no flag or argument names, no exit codes, and no
15+
`--format json` / `--format yaml` payloads change — those are produced by
16+
`formatOutput(res, …)` straight from the control-plane response and are untouched.
17+
18+
| where | count | printed by |
19+
| --- | --- | --- |
20+
| `static override description` | 5 | `os environments --help` |
21+
| flag / arg `description` | 7 | each command's own `--help` |
22+
| success-path console output | 9 | `list` · `bind` · `create` · `show` · `switch` |
23+
| `examples` arg placeholder (`<project-id>` → `<environment-id>`) | 3 | `os environments bind --help` |
24+
| the `switch` id-not-found error | 1 | `os environments switch` on a bad id |
25+
26+
The five `static override description` strings now read as
27+
`content/docs/deployment/cli.mdx`'s command table has described them since the rename
28+
("List environments visible to the current session", "Provision a new environment", …),
29+
so the shipped `--help` and the shipped docs agree for the first time.
30+
31+
What is deliberately NOT renamed, because it is API surface in other packages rather than
32+
CLI wording, and each needs its own decision: `client.projects.*` (the `@objectstack/client`
33+
SDK method names), the `res.project` / `res.projects` response fields, the locals bound
34+
directly from them, and the docblock comments in these files.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
chore(cli): pin the multi-org runtime name `os doctor` prints, so a roster rename cannot drift it silently (#12464)
6+
7+
`doctor.ts`'s `TENANCY_POSTURE_FIX_HINTS` repeated `serve`'s `isolated` posture
8+
sentence verbatim, carrying its own bare `@objectstack/organizations` literal
9+
under no check at all. PR #12463 single-sourced and pinned every operator-facing
10+
occurrence in `serve.ts`; this copy was outside that card's file surface and was
11+
correctly left alone. The defect it left behind is the same class: a roster-key
12+
rename would leave `os doctor` naming a package that boot no longer resolves,
13+
**with every gate green** — the roster pin only ever sees the declaration, and
14+
nothing read this hint table's text.
15+
16+
The `isolated` hint now interpolates a module-level `ORGANIZATIONS_RUNTIME_PKG`
17+
in `doctor.ts`, and a new sibling test pins it on two legs: the **rendered**
18+
bullet compared whitespace-included against text built from that declaration
19+
(the #12463 shape — asserting what the operator sees, never that a constant
20+
appears in source), and a **roster** leg asserting that declaration IS a key of
21+
the spec-owned `PLATFORM_PLUGIN_WIRED_RUNTIMES`. The roster leg is the load-bearing
22+
one: without it the hint and its expectation move together under a rename and
23+
nothing goes red.
24+
25+
**This deliberately does not single-source the spelling, and the constant's
26+
docblock says so at the site.** The literal is still declared three times (the
27+
roster key, `Serve.ORGANIZATIONS_RUNTIME_PKG`, and now this const). The roster
28+
cannot supply the name — it is keyed BY package name, its row type carries no
29+
`package` field by design, and its own header records that it is "not a
30+
resolution registry" — and importing `serve`'s export into a diagnostic command
31+
would be a worse coupling than the duplication it removes. What changes is that
32+
this copy can no longer drift in silence. The duplication ends properly when a
33+
shared tenancy-hint table lands (tracked at #12492); the docblock carries that
34+
deletion condition.
35+
36+
**No behaviour change.** The declared value is byte-identical to the literal it
37+
replaces and the rendered bullet is unchanged.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`FormFieldSchema`'s constraint keys converge on the value shapes the object-field surface landed (#12174): `maxLength`/`minLength` tighten from bare `z.number()` to `z.number().int().min(1)` (the #11566/#11949 template — a character length is a positive integer, and `minLength: 0` is a vacuous always-true declaration refused loudly; "no minimum" is expressed by omitting the key), and `precision`/`scale` tighten to `z.number().int().min(0)` (the #8321 template — digit counts). `min`/`max` are unchanged: they are values, not counts, exactly as on the object-field surface.
6+
7+
The row keys are live, measured in objectui: the spec bridge (`form-view.ts` mapField, objectui#5898) and plugin-form (`sectionFields.ts`) copy all six onto the runtime field, the console FormPage merges `override.maxLength ?? def.maxLength` onto the rendered input (objectui#5595), the fields package builds validation rules from `minLength`/`maxLength`/`min`/`max`, and `precision`/`scale` drive the number widgets — so `maxLength: 0` on a form row reached the DOM as an input that accepts nothing, and `GET /forms/:slug` serves the rows verbatim to anonymous renderers. What newly gets rejected: `0` (length pair only), negative and non-integer values on the four count-shaped keys. Unlike the object-field twins there is no type-conditional applicability gate: a form row references its object field by name and usually omits `type`, so the referenced field's type is invisible at parse time — value shape is checkable here, key placement stays the object field's own schema's job. Already-legal declarations round-trip byte-identically, and absence stays absence — no default materializes.
8+
9+
<!-- adr-0087: registered ui-form-field-length-malformed-refused, ui-form-field-precision-scale-integer-refused -->
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/core": minor
4+
---
5+
6+
fix(spec,core): `HotReloadConfig.stateStrategy` refuses the two values it never implemented; `distributedConfig` retired (#12340, ADR-0049)
7+
8+
<!-- adr-0087: registered hot-reload-inert-state-strategies-retired -->
9+
10+
**BREAKING** accept-set narrowing + export removal, landing after the v17.0.0
11+
cut (the lockstep launch-window convention ships it as `minor`; the
12+
prescription is registered under protocol major 18 —
13+
`RETIRED_DEFS_BY_MAJOR[18]` + the D3 semantic entry
14+
`hot-reload-inert-state-strategies-retired` — where `os migrate meta` users
15+
will look).
16+
17+
This is ADR-0049 applied one level INSIDE the library the 2026-08-25 #11825
18+
ruling deliberately kept. That ruling retired the authorable lifecycle-config
19+
container and kept `HotReloadConfigSchema` as a host-driven library parameter
20+
type; this change measures the kept vocabulary's own remainder and finds the
21+
same defect in it. The keep itself stands — `HotReloadConfigSchema`,
22+
`PluginStateSnapshotSchema` and the health vocabularies still export, and
23+
`HotReloadManager` / `PluginHealthMonitor` are untouched.
24+
25+
The `'disk'` and `'distributed'` arms of `PluginStateManager.saveState` both
26+
wrote to the SAME in-memory `Map` as `'memory'` — the in-source comments said
27+
"memory fallback" — and announced the substitution at DEBUG level only. A host
28+
that asked for durable or cluster-replicated state got process-local memory
29+
and no error: state that does not survive the restart it was configured to
30+
survive. `distributedConfig` had ZERO readers anywhere, so an author could
31+
name a Redis endpoint, a TTL and a replication factor and nothing ever opened
32+
a connection.
33+
34+
FROM → TO:
35+
36+
- `stateStrategy: 'disk'` → `stateStrategy: 'memory'` — byte-identical runtime
37+
behaviour, because `'disk'` already stored to memory. It is the spelling
38+
that was false, not the behaviour.
39+
- `stateStrategy: 'distributed'` → `stateStrategy: 'memory'` — same, or
40+
`'none'` to disable state preservation outright.
41+
- `distributedConfig: { … }` → *(removed)* — delete the key. It left with the
42+
`'distributed'` value its own doc comment called it "required" for.
43+
- `DistributedStateConfigSchema` / `DistributedStateConfig` /
44+
`DistributedStateConfigParsed` → *(removed)* — the orphan value schema of
45+
that one key.
46+
47+
One-line fix: replace `'disk'` or `'distributed'` with `'memory'` and delete
48+
any `distributedConfig` — you were already getting in-memory state. There is
49+
no in-tree replacement for durable or distributed plugin state; persist it in
50+
the host, which owns the process lifetime these strategies pretended to
51+
outlive. Real disk or distributed persistence returns only via the ENFORCE
52+
route of ADR-0049 — the implementation first, the declaration with it.
53+
54+
The retirement kit:
55+
56+
- **enum-value narrowing** (`['memory','disk','distributed','none']` →
57+
`['memory','none']`): invisible to all four ratchets by construction (the
58+
def still emits), so the prescription hangs on the enum's own `error` map
59+
dispatched by `issue.input` — the `crypto.hash` / `managedBy: 'system'`
60+
precedent. A value that was never legal still gets zod's own enum message,
61+
so a typo is not told it "was removed".
62+
- **whole-def deletion** (route 3 — `HotReloadConfig` is not an authorable
63+
surface: no metadata-type binding, stack collection or manifest embed ever
64+
carried it, and nothing in the tree parses `HotReloadConfigSchema` outside
65+
its own unit test, so there is no authored document to rewrite and nobody
66+
who could receive a parse-time tombstone): `kernel/DistributedStateConfig`
67+
in `RETIRED_DEFS_BY_MAJOR[18]` plus the D3 semantic entry. Ratchets moved as
68+
a def removal must — `api-surface` −3, `authorable-surface` −8,
69+
`json-schema.manifest` −1.
70+
- **runtime doors** in `@objectstack/core`, because route 3 leaves no
71+
parse-time prescription: `HotReloadManager.registerPlugin` now refuses an
72+
unhonoured `stateStrategy` and a leftover `distributedConfig` with an
73+
ADR-0112 envelope (`code: VALIDATION_ERROR`, `status: 400`) carrying the
74+
prescription. Refused BEFORE the `enabled` check, so a disabled config
75+
cannot smuggle the false declaration through. TypeScript hosts never reach
76+
it — `HotReloadConfigParsed['stateStrategy']` is now `'memory' | 'none'`, a
77+
compile error at the call site.
78+
- **pin move, declared**: `DistributedStateConfigSchema` was NAMED in the
79+
#11825 survivor list, so this reverses one line of that ruling on new
80+
evidence — #11825 measured the container's six groups, never this key's own
81+
readers. The pin in `kernel/plugin-lifecycle-advanced-retirement.test.ts`
82+
moves in the same commit with the reasoning recorded beside it, and asserts
83+
the surrounding keep is intact.
84+
- zero in-tree consumers passed `'disk'` or `'distributed'` (measured at
85+
cdbd9204b6 with a firing positive control; every live caller passes
86+
`'memory'` or `'none'`), so no in-repo source changes ride along.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/cli": patch
3+
"@objectstack/lint": patch
4+
---
5+
6+
fix(cli,lint): stop `os lint` demanding translation keys the liveness ledger warns authors for writing (#11624)
7+
8+
`os lint` computes i18n coverage and runs the authoring-rule registry in a
9+
single pass over the same stack, and for the `flows` translation group the two
10+
halves pointed opposite ways:
11+
12+
| the author does | which rule fires | what it says |
13+
|---|---|---|
14+
| omits `flows.*` from the bundle | `i18n/missing-flow` | the key is missing a translation for locale X |
15+
| adds it (`os i18n extract` scaffolds it) | `liveness-planned-property` | the `flows` group is `planned` — nothing reads it |
16+
17+
Measured on one stack, one run: omitting produced **4** `i18n/missing-flow`
18+
findings and 0 liveness findings; authoring produced 0 demands and **2**
19+
`liveness-planned-property` findings ("sets `flows` but this translation
20+
property is planned"). There is no per-rule suppression in `os lint`, only
21+
`--skip-i18n`, which silences the entire `i18n/missing-*` family — so the
22+
author's only escape cost them every other coverage signal. Under
23+
`--i18n-strict` the demand side is an **error**, so a project could be forced
24+
to author keys it is then warned for.
25+
26+
⛔ The warning is not the bug and is unchanged: no shipped screen-flow runner
27+
reads the group, so a translated wizard string is stored and never shown — the
28+
failure mode `validationMessages` was removed in 17.0.0 for. The premature half
29+
is the demand.
30+
31+
**The fix.** `collectExpectedEntries` — the single definition of what is
32+
translatable at all, shared by the coverage gate and the `os i18n extract`
33+
skeleton — now leaves out any translation group the liveness ledger warns
34+
authors for authoring. It reads that set from `@objectstack/lint`'s new
35+
`authorWarnedProperties(type)`, which returns the very warn-map
36+
`lintLivenessProperties` iterates, so the demand side and the warn side cannot
37+
drift into disagreeing about the same keys again.
38+
39+
Two properties fall out of reading the ledger rather than switching on `flows`
40+
by name: the bucket **turns itself back on** the day an objectui screen-flow
41+
runner lands and the row flips to `live` (no flag, no follow-up edit), and any
42+
future group that acquires an `authorWarn` is covered on the day it is marked
43+
rather than re-opening this collision one group at a time. Today `flows` is the
44+
only such group — pinned as an equality so a second one goes red instead of
45+
shipping.
46+
47+
No other bucket changes: `objects`, `apps`, `pages`, `dashboards`,
48+
`globalActions` and `metadataForms` are all `live` and are reported exactly as
49+
before. `@objectstack/spec` is untouched — the `flows` row keeps `planned` +
50+
`authorWarn: true`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/plugin-webhooks': patch
3+
---
4+
5+
Fixed the `sys_webhook` object's `ja-JP` display name (`label`) to ウェブフック, matching the already-Japanese `pluralLabel` and the bundle's own help prose (`object_name.help`, `triggers.help`), which both use the same term. Previously `label` was a stale `Webhook` fill left over from a prior source revision, so the object rendered its own name two different ways within the same locale in the Setup/admin UI. `description` (which uses "Webhook" inside a longer Japanese sentence) and the `Webhook ID` field label are unchanged — this only renames the object itself. `zh-CN` is deliberately untouched: it uses `Webhook` for both slots on purpose, as there is no established Chinese rendering of the term in this codebase.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
"@objectstack/platform-objects": minor
3+
"@objectstack/service-automation": minor
4+
"@objectstack/objectql": minor
5+
"@objectstack/core": minor
6+
---
7+
8+
feat(platform-objects): packaged disable works without the automation service, and the activation ledger has one implementation (#12359, #12350)
9+
10+
Two halves of ADR-0126's "ledger convergence", bundled by maintainer ruling
11+
(2026-08-26, verbatim and untranslated: 「同意」).
12+
13+
## The registration follows the declaration (#12359)
14+
15+
`sys_metadata_activation` is declared in `@objectstack/platform-objects`, but
16+
the only thing that REGISTERED it was the automation service's manifest —
17+
because flows were the ledger's first and, until packaged actions landed, only
18+
consumer. Packaged actions are a second consumer with a different owner: their
19+
consult and write path live on the ObjectQL engine, present in every
20+
composition that can execute an action.
21+
22+
So a deployment with actions and no automation service had no ledger table, and
23+
the activation door answered **503 SERVICE_UNAVAILABLE** on every flip —
24+
correctly (ADR-0126 §6 wall 3: a flip that cannot be made durable must not be
25+
reported as one) and permanently. Measured on a real boot; it is now this
26+
change's positive test, measured on the same boot:
27+
28+
```
29+
POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {"enabled":false}
30+
before -> 503 SERVICE_UNAVAILABLE after -> 200, and dispatch refuses 409 ACTION_DISABLED
31+
```
32+
33+
`PlatformObjectsPlugin` registers it now, so every composition carrying
34+
platform-objects has the ledger and each future ADR-0126 §8 consumer (`tool`,
35+
`skill`, `position`) inherits it. **MOVE, not add** — the automation service no
36+
longer names the object. That was not a style choice: a second code package
37+
claiming one object throws `Object "…" is already owned by package "…"`
38+
(ADR-0029 D3/D7), measured, so adding a registrant would have been a boot
39+
failure rather than a duplicate.
40+
41+
**Upgrade is a no-op for existing data, and that is measured rather than
42+
asserted.** A manifest is also a ROUTING decision — `resolveDatasourceBinding`
43+
step 4 routes an object by its owning package's `defaultDatasource` — so the
44+
registrar carries the table's datasource with it:
45+
46+
```
47+
owner com.objectstack.service-automation (defaultDatasource:'cloud') -> 'cloud'
48+
owner com.objectstack.platform-objects (none) -> undefined (global default driver)
49+
```
50+
51+
The ledger table already exists in live databases, so on any deployment
52+
carrying a `cloud` datasource that difference would leave the rows in one
53+
database and read another — every disabled artifact silently re-arming. The
54+
ledger therefore rides its own manifest from the same plugin, carrying the
55+
automation manifest's `scope` / `namespace` / `defaultDatasource` triple
56+
verbatim. The three siblings (`sys_migration`, `sys_migration_journal`,
57+
`sys_secret`) deliberately do not get it and keep riding the project database.
58+
59+
## One implementation of the §4 row contract (#12350)
60+
61+
ADR-0126 §4 declares one activation ledger; it had two independent
62+
implementations of that one row contract — `ObjectStoreFlowActivationStore`
63+
(service-automation) and `ObjectStoreActionActivationStore` (objectql). They
64+
agreed because the second was written from the first, and nothing structurally
65+
held them together; §8 pre-charts `tool`, `skill` and `position`, and a third
66+
and fourth copy is where the org-row skip and the `0`-is-false read get lost
67+
quietly, in the direction (an artifact re-arming) nothing else measures.
68+
69+
Neither consumer could import the other, so the contract now lives once in
70+
`@objectstack/core` — the package both already depend on — as
71+
`ObjectStoreMetadataActivationStore(engine, metadataType)`, exported alongside
72+
`InMemoryMetadataActivationStore`, `MetadataActivationRow`,
73+
`MetadataActivationStore`, `MetadataActivationStoreEngine` and
74+
`METADATA_ACTIVATION_TABLE`. Each consumer keeps its own name, its own
75+
one-argument constructor and its own docs, and fixes the discriminator.
76+
77+
**No behaviour change and no API break.** `ObjectStoreFlowActivationStore` /
78+
`InMemoryFlowActivationStore` / `FlowActivationStoreEngine` and
79+
`ObjectStoreActionActivationStore` / `InMemoryActionActivationStore` /
80+
`ActionActivationRow` / `ActionActivationStore` / `ActionActivationStoreEngine`
81+
/ `ACTION_ACTIVATION_TABLE` are exported from the same modules with the same
82+
shapes. Row semantics are byte-equivalent: install-level rows only
83+
(`organization_id` never written), org-carrying rows skipped on read and
84+
ignored when deciding insert-vs-update, a driver `0` read as false,
85+
read-then-write rather than a blind upsert, and no `delete` in the engine slice
86+
because re-enabling rewrites the row.
87+
88+
Both existing pin suites stay green **unchanged**, which is what makes them the
89+
proof the consolidation lost nothing — verified by ablation: removing the
90+
org-row skip from the one shared implementation turns both of them red on their
91+
own org-skip assertion, so both really reach it.

0 commit comments

Comments
 (0)