Skip to content

Commit a0e8590

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15403-generator-title-rule
2 parents 2da3e23 + 0283cb9 commit a0e8590

198 files changed

Lines changed: 9065 additions & 1387 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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-automation": minor
4+
"@objectstack/lint": minor
5+
"@objectstack/metadata-protocol": minor
6+
"@objectstack/metadata-core": patch
7+
---
8+
9+
feat(automation)!: an edge-branched `decision` is exclusive — the first out-edge whose condition holds, in declaration order, wins; `mode: 'inclusive'` takes every one (#15429)
10+
11+
<!-- adr-0087: registered flow-decision-mode-inclusive-explicit -->
12+
13+
Clause-②: yes
14+
15+
**BREAKING** — the run-time semantics of a shipped node type change. A `decision` node that
16+
declares no `config.conditions` and branches on its out-edges used to take EVERY out-edge whose
17+
condition held, one after another, while its schema, the docs and the engine's own comment all
18+
called it an exclusive gateway; hotcrm#1555 rendered a refusal screen AND ran the conversion in
19+
one execution. Maintainer ruling on #15429 (2026-09-23, 「跟主流对齐」): the gateway follows
20+
BPMN's exclusive gateway, Salesforce Flow's Decision and n8n's Switch default, and taking every
21+
true branch is a declaration the author writes down.
22+
23+
| | before | after |
24+
|:--|:--|:--|
25+
| two conditioned out-edges, both hold | both successors run, sequentially, nothing reported | the FIRST declared one runs; the second records a `skipped` step |
26+
| `config: { mode: 'inclusive' }` | accepted, never read | every out-edge whose condition holds runs, sequentially |
27+
| none holds | the `isDefault` edge runs | unchanged |
28+
| `mode` beside a non-empty `conditions` list, or outside `'exclusive' \| 'inclusive'` | refused by a direct parse only | refused at `registerFlow` and by `os validate`, with the schema's own sentence |
29+
30+
## Migration: FROM → TO
31+
32+
`os migrate meta --from 17` lists the mechanical edits for existing sources and applies them
33+
to the migrated stack: the ADR-0087 D2 conversion `flow-decision-mode-inclusive-explicit`
34+
writes `mode: 'inclusive'` onto every decision that has no `conditions` list and two or more
35+
conditioned out-edges, inside ADR-0031 regions included, so a migrated flow runs exactly as it
36+
did.
37+
38+
```ts
39+
// FROM — every true out-edge ran
40+
{ id: 'verdict', type: 'decision', label: 'Verdict?' }
41+
// TO — what the conversion writes; delete the key where the conditions partition
42+
{ id: 'verdict', type: 'decision', label: 'Verdict?', config: { mode: 'inclusive' } }
43+
```
44+
45+
Then review each written key (the paired D3 entry `flow-decision-edge-branching-first-match`
46+
carries the acceptance criteria): delete it where the conditions partition (`== 'a'` beside
47+
`!= 'a'`, `>` beside `<=`, a guard beside `isDefault: true`), keep it where the flow relies on
48+
more than one branch running for one record, and where the overlap was accidental narrow the
49+
conditions into a partition and delete the key. `os validate` reports
50+
`flow-decision-inclusive-overlap` on every decision that keeps the key with two or more
51+
conditioned out-edges, so the review list is the lint output.
52+
53+
## BREAKING for flows stored in `sys_metadata` — maintainer ruling letter C on #15429
54+
55+
A `decision` node stored in `sys_metadata` (a flow built or edited in the Studio designer) with
56+
**no `config.conditions`, no `mode`, and two or more out-edges carrying a `condition`** evaluates
57+
**first-match** after this upgrade: where it took every out-edge whose condition held, it now takes
58+
only the first one that holds, in the order the flow declares its edges. Nothing rewrites that row
59+
— no stored-row migration, no cutoff, no read-path completion — because nothing about a stored row
60+
says it was saved before the flip. The one-line fix, for a node that meant every branch:
61+
62+
```ts
63+
{ id: 'route', type: 'decision', label: 'Route', config: { mode: 'inclusive' } }
64+
```
65+
66+
`os migrate meta --stored` (and `POST /api/v1/meta/_migrate-stored`) lists every such node under
67+
`decisionModeReview` — flow row, node id, label and path — on a preview and an `--apply` run
68+
alike, and writes nothing for it: the list moves no row outcome, no count and no exit code, so an
69+
operator can review the candidates before and after the upgrade. A node leaves the list once it
70+
declares `mode`, either member. Every such node in the measured corpus below is a partition, where
71+
the new meaning runs exactly what the old one did.
72+
73+
Authored sources and built artifacts keep the old behaviour instead, where the source's age is a
74+
fact: `os migrate meta --from 17` writes `mode: 'inclusive'` (above), while the authoring funnel,
75+
the automation engine's flow rehydration seam and the artifact-ingestion door all refuse the
76+
conversion by id — a default flip replayed there would turn a decision written today against this
77+
contract, where an omitted `mode` means exclusive, into an inclusive gateway.
78+
79+
## Reach, measured at landing
80+
81+
- Release state: the npm registry's `latest` `@objectstack/spec` is `17.4.0` (`npm view`,
82+
2026-09-27), whose `json-schema/automation/DecisionConfig.json` declares `conditions` only —
83+
`mode` has not shipped; `.changeset/19867-decision-config-mode.md` and
84+
`.changeset/20168-decision-mode-beside-conditions-refused.md` are still unconsumed in this
85+
tree. So `mode` reaches its first release together with the traversal that reads it and the
86+
conversion that writes it; no published accept set narrows, and the registration and
87+
`os validate` refusals narrow nothing that shipped.
88+
- Corpus census (this repository at the branch base and `objectstack-ai/hotcrm` at `2f7b2326`,
89+
read-only): 30 decision nodes across 48 flows; 17 have two or more conditioned out-edges and
90+
no `mode` (the conversion's positives — every one a hand-written partition, including
91+
hotcrm's `lead_conversion.decision_duplicate`, the #1555 node), 13 have one conditioned
92+
out-edge (left alone), and no node of any other type carries a conditioned out-edge, so the
93+
exclusive traversal is scoped to `decision` with nothing else to migrate.
94+
- What the published surface gains: the D2 conversion and its D3 entry in the protocol-18
95+
chain (`spec-changes.json`, the upgrade guide), `DecisionConfigSchema.mode`'s describe and
96+
docblock now state the run-time semantics, and `@objectstack/lint` gains
97+
`flow-decision-mode-invalid` (gating) and `flow-decision-inclusive-overlap` (advisory).
98+
99+
The traversal change is scoped to `decision` nodes: conditioned out-edges of any other node
100+
type keep the every-true-edge traversal they had (none was measured to exist).
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/connector-mcp': patch
4+
'@objectstack/connector-openapi': patch
5+
'@objectstack/connector-rest': patch
6+
'@objectstack/connector-slack': patch
7+
'@objectstack/service-automation': patch
8+
---
9+
10+
feat(spec)!: retire the connector resilience family — `health` (health probe + circuit breaker), `status` and the nested `webhooks`, sixteen keys nothing read (#20273)
11+
12+
**BREAKING** — `connector.health` (the `healthCheck` probe, eight keys, and the
13+
`circuitBreaker`, six keys), `connector.status` and the connector-nested
14+
`webhooks` are removed from `ConnectorSchema` and `DeclarativeConnectorEntrySchema`
15+
— so from `defineConnector`, `stack.connectors[]`, the `PUT /api/v1/meta/connector/:name`
16+
door and `AutomationEngine.registerConnector`. ADR-0049 enforce-or-remove, one
17+
batch for the family, by the maintainer's criterion: does the mainstream platform
18+
offer this capability? Author-configured health probes and circuit breakers are
19+
not connector metadata in the mainstream (breakers live in API-gateway
20+
infrastructure), and an authored status and a nested webhook list duplicate what
21+
is already delivered here by other keys.
22+
23+
Measured before removal, each against a lit control: zero reads of any of the
24+
sixteen keys outside `packages/spec`. No loop ever polled a connector endpoint,
25+
counted consecutive failures or tripped a breaker, and none of the four
26+
`fallbackStrategy` behaviours existed. Nothing read an authored `status`: the
27+
runtime's dispatchability answer is the COMPUTED `state` (`ready` / `degraded`)
28+
on `GET /api/v1/automation/connectors`, which no authored value sets. A webhook
29+
nested in a connector was never registered as a `webhook` item, so it was never
30+
materialized into `sys_webhook` and never delivered.
31+
32+
### FROM → TO
33+
34+
| removed | what to write instead |
35+
| --- | --- |
36+
| `connector.health` (`healthCheck.*`, `circuitBreaker.*`, including `monitoringWindowMs` and the pre-rename `monitoringWindow`) | delete the block. Put health probes and circuit breaking in the connector provider or an upstream gateway. |
37+
| `connector.status` | delete the key. `enabled: false` on a declarative entry is what withdraws a materialized instance or marks a catalog-only descriptor; whether a registered connector can be dispatched is the computed `state`. |
38+
| `connector.webhooks` | delete the array. A webhook that is actually delivered is declared in the stack's top-level `webhooks:` collection — moving one there STARTS deliveries this connector never made, so decide per webhook. `events` and `signatureAlgorithm` have no counterpart there. |
39+
| `ConnectorHealth`, `HealthCheckConfig`, `CircuitBreakerConfig`, `ConnectorStatus`, `WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm` (schemas, types, `…Parsed` types) | no replacement — nothing parsed or constructed them. |
40+
41+
**The one-line fix: delete `health:`, `status:` and `webhooks:` from every connector.**
42+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
43+
44+
⚠️ Runtime behaviour is deliberately **unchanged**: none of the sixteen keys ever
45+
changed what a connector did. What changes is the answer an author gets — each
46+
key is refused at parse with a prescription, and in `tsc` (its input type is
47+
`never`), instead of being saved with no effect.
48+
49+
### The retirement kit
50+
51+
- **Tombstones.** `health`, `status` and `webhooks` are `retiredKey()` tombstones
52+
on the private `ConnectorBaseSchema` both published carriers wrap (the schema
53+
is not `.strict()`, so a bare deletion would be a silent strip, ADR-0104).
54+
`RETIRED_KEYS_BY_MAJOR[18]`: `integration/Connector:{health,status,webhooks}`
55+
and `integration/DeclarativeConnectorEntry:{health,status,webhooks}`.
56+
- **Retired-default residue.** `status` was `.default('inactive')`, so every 17.x
57+
parse emitted `status: 'inactive'` into every connector; that exact value joins
58+
`connectionTimeoutMs: 30000` in the residue stage (accepted and stripped, so a
59+
def a 17.x toolchain built still registers). Every other value is refused.
60+
- **Seven defs leave whole** (`RETIRED_DEFS_BY_MAJOR[18]`): the four
61+
`integration/` schemas and three enums listed above.
62+
- **D2 conversion `connector-resilience-keys-removed`** (step 18, retired from
63+
the load path): strips the three keys from `connectors[]` and from stored
64+
`sys_metadata` connector rows (the rehydration seam replays it), one notice per
65+
key, as a lossless delete. Nested webhooks are stripped, never moved.
66+
- **The chain.** In the same step, `connector-health-and-trigger-durations-unit-in-key`
67+
renamed `health.circuitBreaker.monitoringWindow` to `monitoringWindowMs`. That
68+
breaker half is absorbed by this removal: the renamed key is itself removed, so
69+
an author holding either spelling ends with no `health` block. The
70+
conversion's `triggers[].interval` → `intervalSeconds` rename is unaffected.
71+
- **D3 entry `connector-resilience-keys-retired`** carries the family's
72+
judgement: which probe, breaker or nested webhook the author actually relied
73+
on, and where it goes now.
74+
- **Writers deleted.** The four shipped connector packages wrote
75+
`status: 'active'` and the automation service's degraded husk wrote
76+
`status: 'error'`; nothing read either back, and both writes are gone.
77+
- **No deprecation window**, per the project's startup-stage posture.
78+
79+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
80+
is published, so this is breaking for consumers no telemetry was consulted for.
81+
82+
Clause-②: no (narrowing)
83+
84+
<!-- adr-0087: registered connector-resilience-keys-removed, connector-resilience-keys-retired -->
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
An analytics cube's `public` now takes effect, and it defaults to visible: `CubeSchema.public` defaults to `true` (it was `false`), and the analytics service hides a cube that declares `public: false` from discovery and refuses every query against it (#20282).
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: registered analytics-cube-public-default-visible-enforced -->
11+
12+
**BREAKING**: this narrows what the analytics API answers. A query or SQL dry run against a cube declared `public: false` (`POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql`) was answered before this change and is now refused with `404 CUBE_NOT_FOUND`, and `GET /api/v1/analytics/meta` no longer lists that cube. The same happens to every cube in an artifact built by `os compile` before this release, which carries a materialized `public: false` from the old default. The remedy: delete `public: false` from any cube that is meant to be queried (cubes are visible by default), and recompile pre-release artifacts. It ships as `minor` under the launch-window convention; the widening half is the default moving to visible.
13+
14+
Until this change nothing read `public`. `GET /api/v1/analytics/meta` listed a `public: false` cube and every query door answered it, so the flag withheld nothing. Its declared default, `false`, could not simply be switched on: enforcing it as declared would have hidden every cube that omits the key. The default is now the Cube.dev default (visible), and an explicit `false` is enforced:
15+
16+
- `GET /api/v1/analytics/meta` omits a cube declared `public: false`, and `?cube=` naming one answers `[]`, the same as a name no cube has.
17+
- `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` refuse it with `404 CUBE_NOT_FOUND` — the same refusal, byte for byte, that an unknown cube name gets, so a caller cannot use it to learn that a hidden cube exists. The one shared message names both possibilities, so it still tells an author how to expose a hidden cube. The refusal comes before any SQL is built, and it is never an empty result.
18+
19+
`public` is visibility on the analytics API, not row security. An object's records stay governed by its permissions and row-level security on every door, whether or not a cube over it is hidden. What `public: false` does is exactly the two points above: the cube is left out of `/analytics/meta`, and queries and SQL generation against it are refused. The cube's definition stays readable on the metadata door, like any other authored schema.
20+
21+
What to expect after upgrading:
22+
23+
- **A cube that omits `public`** stays visible and queryable. It was visible before too, because nothing read the key. A client that parses cube metadata through the published JSON Schema now materializes `public: true` where it materialized `false`.
24+
- **A cube that writes `public: false`** is now hidden and refused. If you wrote it only because it was the old default, delete the line (cubes are visible by default). A dashboard or report that queries such a cube starts answering `404 CUBE_NOT_FOUND` until you do.
25+
- **A compiled artifact built before this release** carries a materialized `public: false` on every cube, because `os compile` writes the parsed stack with its defaults applied. Recompile it with this release before serving cubes from it.
26+
- **Cubes the platform mints itself** stay visible: the cube inferred for an ad-hoc query on an object (the KPI path), a compiled dataset's cube (`POST /api/v1/analytics/dataset/query`), and `CubeRegistry.inferFromObject`. Each wrote a literal `false`, the old default, and now writes `true`.
27+
28+
The showcase example's `showcase_delivery` cube, which is the app's demonstration of `/api/v1/analytics/*`, drops its `public: false`.

0 commit comments

Comments
 (0)