|
| 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