Skip to content

Ledger convergence: registration home + one store implementation (ADR-0126 §4/§8, maintainer-ruled) - #12419

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-12159-ledger-convergence
Aug 26, 2026
Merged

Ledger convergence: registration home + one store implementation (ADR-0126 §4/§8, maintainer-ruled)#12419
os-support-ai merged 4 commits into
mainfrom
claude/issue-12159-ledger-convergence

Conversation

@claude

@claudeclaudeBot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes#12159
Closes#12350
Closes#12359

The maintainer-ruled "ledger convergence" bundle (2026-08-26, live PM chat, verbatim and untranslated: 「同意」 — full record on #12359 comment 5419050253). Three parts were chartered; two are implemented here and the third is a measured stop-and-report, per the card's own instruction not to choose for the maintainer when the choice is customer-visible either way. Each part is one commit, verifiable independently in the diff.

Verified at 3ad9030e7 — every gate result below was re-run on that commit.


Part 1 — registration follows the declaration (#12359) · 9b0e0657 + 1f309525 + 3ad9030e7

sys_metadata_activation is declared in @objectstack/platform-objects, but the only thing that REGISTERED it was the automation service's manifest — because flows were the ledger's first and, until packaged actions landed, only consumer. Packaged actions are a second consumer with a different owner: their consult and write path live on the ObjectQL engine, present in every composition that can execute an action.

So a deployment with actions and no automation service had no ledger table, and the activation door refused every flip — correctly (ADR-0126 §6 wall 3: a flip that cannot be made durable must not be reported as one) and permanently. PlatformObjectsPlugin registers it now.

MOVE, not add. That was not a style choice, and the question #12359's triage left open ("whether that double registration is benign was not measured") is now measured: it is not benign, it throws.

ql.registerApp({ id: 'com.objectstack.service-automation', …, objects: [LEDGER] });
ql.registerApp({ id: 'com.objectstack.platform-objects', …, objects: [LEDGER] });
-> Error: Object "sys_metadata_activation" is already owned by package
"com.objectstack.service-automation". Package "com.objectstack.platform-objects"
cannot claim ownership. Use 'extend' to add fields.

ADR-0029 D3's single-owner invariant. Adding a registrant would have been a boot failure for every kernel objectstack serve produces (it auto-injects platform-objects unconditionally), not a duplicate — so MOVE was the only available shape, whichever way the ownership question was answered.

The smooth-upgrade wall, discharged by measurement rather than assertion

The card required proving the move is a no-op for existing data. It is not unconditionally, and finding out why changed the implementation.

A manifest is not only a list of objects — it is also a ROUTING decision.ObjectQL.resolveDatasourceBinding step 4 routes an object by its owning package'sdefaultDatasource, so the registrar carries the table's datasource with it. Measured on a real engine holding a default driver plus one named cloud, registering the same object definition under each manifest in turn:

owning packagedefaultDatasourceresolveEffectiveDatasource('sys_metadata_activation')
com.objectstack.service-automation'cloud''cloud'
com.objectstack.platform-objects(none)undefined — rides the global default driver, a different database

The ledger table already exists in live databases. On any deployment carrying a cloud datasource, the naive fold would have left its rows in one database and started reading the other: every artifact an administrator had switched off would silently re-arm — precisely the ADR-0126 §6 wall 3 failure, arriving through a door nobody was watching.

So the ledger rides its own manifest from the same plugin, carrying the automation manifest's scope / namespace / defaultDatasource triple verbatim. Single owner, one registration, routing byte-identical everywhere.

  • ⛔ The three siblings (sys_migration, sys_migration_journal, sys_secret) deliberately do not get the triple — they ride the project database today and moving them is a larger question.
  • ⛔ It is not spelled as datasource: 'cloud' on the object: that is binding step 1, which answers even when no such driver is registered, so getDriver would throw Datasource 'cloud' … is not registered on every deployment without a control plane. Step 4 is conditional on the driver existing, which is exactly the behaviour this object has today.

⚠️For the maintainer: which datasource this ledger should live on is a real question, and this PR refuses to answer it by accident rather than on purpose. The ruling was about registration; the routing consequence was not part of what was presented. Moving it later means moving the rows too.

Both ends measured on real boots

packages/qa/dogfood/test/packaged-activation-ledger-reach.dogfood.test.ts — 11 tests, each describe carrying its own anti-vacuity control on whether the automation service is actually composed.

Actions and NO automation — the exact boot #12359 measured:

POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {"enabled": false}
before -> 503 SERVICE_UNAVAILABLE after -> 200

and then, on the same boot: the row is install-level (metadata_type: 'action', organization_id NULL, active false through the 0/1 spelling); dispatch refuses the action 409 ACTION_DISABLED ahead of the param contract and the record load; re-enabling updates the row rather than deleting it (§6 wall 3 — the ledger records the administrator's choice) and dispatch re-arms.

WITH automation — flows and actions both keep working: exactly one owner for the ledger object; the datasource binding unchanged; the action projection hydrating (flip lands, dispatch refuses) and the flow projection hydrating from the same table (POST /automation/showcase_task_completed/toggle → 200 with a durable row); and neither consumer's rows visible to the other.


Part 2 — one implementation of the §4 row contract (#12350) · 6064eaec

ADR-0126 §4 declares one activation ledger; it had two independent implementations of that one row contract — ObjectStoreFlowActivationStore (service-automation, #12296) and ObjectStoreActionActivationStore (objectql, #12348). They agreed because the second was written from the first, and nothing structurally held them together. §8 pre-charts tool, skill and position, and a third and fourth copy is where the org-row skip and the 0-is-false read get lost quietly, in the direction — an artifact re-arming — that nothing else measures.

Neither consumer can import the other (service-automation does not depend on @objectstack/objectql; the engine must not depend on a service), so the contract now lives once in @objectstack/core — the package both already depend on, and the same reasoning that put recordNotFoundError there for the ADR-0076 D2 closure. ⛔ Not @objectstack/platform-objects, which declares the object: objectql does not depend on it and that edge would invert the tiering. Where the code lives is a module-import question; where the object's registration lives is a composition question with its own answer, above.

ObjectStoreMetadataActivationStore(engine, metadataType). Each consumer keeps its own name, its own one-argument constructor and its own docs, and fixes the discriminator — a binding, not an implementation. A caller that had to pass the discriminator could pass the wrong one; each leg has exactly one correct value.

No API break.ObjectStoreFlowActivationStore / InMemoryFlowActivationStore / FlowActivationStoreEngine and ObjectStoreActionActivationStore / InMemoryActionActivationStore / ActionActivationRow / ActionActivationStore / ActionActivationStoreEngine / ACTION_ACTIVATION_TABLE are exported from the same modules with the same shapes.

The pin suites are the proof, and the proof was tested

Both pin suites stay green byte-unchanged — neither file is in this diff. That claim is only worth something if they actually reach the consolidated code, so it was ablated: the org-row skip was removed from the one shared implementation, @objectstack/core was rebuilt, the marker was confirmed present in dist/ by scripts/ablation-dist-preflight.mjs, and both suites went red on their own org-skip assertion:

flow : 1 failed | 28 passed × SKIPS rows carrying an organization_id — a per-org row is not an install-level answer
action: 1 failed | 17 passed × SKIPS a row carrying an organization_id — the per-org dimension is reserved (§5)

Restore leg re-run and re-verified absent from dist/ (12 built files). (A first ablation attempt is not quoted: its marker string contained /, which broke the replacement's confirmation leg. The run above is the clean one.)

The new suite beside the implementation pins what neither consumer suite can: that the discriminator is a parameter rather than a constant, that two bindings over one table see none of each other's rows in either direction, and that a type nobody has written yet (tool) behaves identically.


Part 3 — permission-set convergence: stopped and reported

The card requires a decision note on the issue before this part's code, and instructs stopping rather than choosing if the choice is customer-visible either way. The full note with all measurements is on #12159. In short, measured on a real boot:

There is also a family the card does not mention: sys_position.active (16 rows, 0 packaged) and sys_capability.active (10 rows, 2 packaged) are the same ADR-0049 switch judged by the same isRowActive predicate, so whatever answer fits here has to be argued for all three.

Parts 1 and 2 land independently of however this is decided; no rework needed either way.


Verification

Repo-wide pnpm lint (eslint . --no-inline-config) — exit 0, whole tree, no narrowing.

Full suites of every touched package, all at 3ad9030e7:

packageresult
@objectstack/coreTest Files 39 passed (39) · Tests 968 passed (968)
@objectstack/platform-objectsTest Files 31 passed (31) · Tests 497 passed (497)
@objectstack/service-automationTest Files 91 passed (91) · Tests 1082 passed (1082)
@objectstack/objectqlTest Files 235 passed (235) · Tests 4174 passed (4174)
dogfood (this card's file)Test Files 1 passed (1) · Tests 11 passed (11)
dogfood (boot-shape exposure subset)Test Files 6 passed (6) · Tests 45 passed (45)
@objectstack/runtime (activation dispatch + posture)Test Files 2 passed (2) · Tests 36 passed (36)

typecheck green for objectql, platform-objects, dogfood, runtime (core and service-automation declare no typecheck script).

Gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after the diff grew, and re-run on the final head. All exit 0; each gate's own verdict line:

  • check-engine-double-contract: OK — 416 pinned, 133 in the DEBT ledger, 2 exempt. (+1 grow-only RETAINED coverage row for the new store suite; the DEBT baseline is untouched)
  • ✓ where-matcher conformance holds: 303 matcher(s) discovered, 303 answer the combinator battery correctly or refuse it loudly (190 refuse). 0 silently-wrong and 0 unjudged … none new.
  • OK: 18 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. — this one failed first and changed the design: the "automation no longer registers the ledger" pin was reading across the package boundary, so it moved into service-automation and became a behavioural pin over a real init() instead of a grep.
  • check-nul-bytes: OK (scanned 6869 text file(s) … no raw ASCII control bytes).
  • ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) … none new. baseline key set verified against fe3d74f: no files added.
  • ✓ slot-lookup ratchet holds: 107 unswept site(s) … none new.
  • check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured … none above its recorded number. surplus: none
  • check-test-source-alias OK — 72 packages with tests scanned · check-type-source-resolution OK — 93 tsc program(s) · check-i18n-bundles: OK (9 package(s) — all bundles in sync) · check-i18n-stale-fill: OK … 0 stale-fill leaf/leaves · ✓ durability-degradation log levels: 29 … all loud · ✓ check:plugin-teardown-shape … ⛔ SHRINK-ONLY, baseline fully burned down · ✓ kernel hook pin pairing · ✓ check:published-files · check-page-declaration-shape: OK · ✓ check-empty-changeset/--adr-0087/--changeset-no-major self-tests · OK: all 104 declared cross-package glob(s) · OK check:comment-mask-adoption · spec-liveness family (check:liveness, check:empty-state, check:strictness-ledger, check:variant-docs) all ✓.
  • check:engine-split-ratio initially refused on the shallow clone (correctly, rather than answering wrongly); re-run after git fetch --shallow-since=2026-05-21 → exit 0, ratio: 97.6%.

No packages/spec surface. Nothing under content/docs/releases/. Changeset: .changeset/ledger-convergence-registration-and-one-store.md (minor × 4).


Generated by Claude Code

…its declaration (#12359)
`sys_metadata_activation` is declared in this package, beside `SysMigration` /
`SysMigrationJournal` / `SysSecret`, but the only thing that REGISTERED it was
the automation service's manifest — because flows were the ledger's first and,
until packaged actions landed, only consumer.
Actions are a second consumer with a different owner: the consult and the write
path live on the ObjectQL engine, which is present in every composition that
can execute an action, automation service or not. So a deployment with actions
and no automation service had no ledger table, and the activation door answered
503 SERVICE_UNAVAILABLE on every flip — correctly (a flip that cannot be made
durable must not be reported as one, ADR-0126 section 6 wall 3) and permanently.
Registration now follows the DECLARATION. Maintainer ruling, 2026-08-26,
verbatim and untranslated: 「同意」. Every composition carrying platform-objects
has the ledger, so packaged disable works without the automation service and
each future ADR-0126 section 8 consumer (`tool`, `skill`, `position`) inherits
it.
MOVE, not add — single owner, one registration. The automation service no
longer names the object in its own manifest: two manifests owning one object is
the governed contributor case (ADR-0029 D7), and the ruling closed it by naming
one owner rather than by declaring a double registration benign. A grep-level
pin in `plugin.test.ts` is what a later edit re-adding it has to argue with,
since the two registrations live in different packages and neither one's unit
tests can see the other.
`runObjectRegistered` no longer vouches for this table, and the comment at the
flow store's attach site now says so — the `probe()` beside it is the gate that
actually decides, and always was. The attach policy itself is untouched: this
moves a registration, not a policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh
…on-ledger row contract (#12350)
ADR-0126 section 4 declares ONE activation ledger for the whole disable+clone
family. It had two independent implementations of that one row contract:
ObjectStoreFlowActivationStore @objectstack/service-automation #12296
ObjectStoreActionActivationStore @objectstack/objectql #12348
They agreed on every load-bearing detail because the second was written from
the first, and nothing structurally held them together. Section 8 pre-charts
`tool`, `skill` and `position` as later consumers, and a third and fourth copy
is where the row semantics start drifting — the org-row skip and the
`0`-is-false read are exactly the kind of detail a copy loses quietly, in the
direction (an artifact silently re-arming) nothing else measures.
Neither consumer could import the other: `service-automation` does not depend
on `@objectstack/objectql` (devDependency only), and the engine must not depend
on a service. `@objectstack/core` is the package BOTH already depend on, so it
is the one home that needs no new edge — the same reasoning that put
`recordNotFoundError` there for the ADR-0076 D2 closure. NOT
`@objectstack/platform-objects`, which declares the OBJECT: `objectql` does not
depend on it and that edge would invert the tiering. Where the code lives is a
module-import question; where the object's registration lives is a composition
question with its own answer.
`ObjectStoreMetadataActivationStore(engine, metadataType)` carries the row
semantics once. Each consumer keeps its own name, its own one-argument
constructor and its own docs, and fixes the discriminator — a binding, not an
implementation. A caller that had to pass the discriminator could pass the
wrong one; each leg has exactly one correct value.
Byte-equivalent semantics: install-level rows only (`organization_id` never
written), org-carrying rows skipped on read AND ignored when deciding
insert-vs-update, a driver `0` read as false, read-then-write rather than a
blind upsert, no `delete` in the engine slice because re-enabling rewrites the
row.
Both existing pin suites stay green UNCHANGED — they pin the contract from each
binding's side, so their staying green is the proof the consolidation lost
nothing. The new suite beside the implementation pins what neither of them can:
that the discriminator is a PARAMETER rather than a constant, that two bindings
over one table see none of each other's rows in either direction, and that a
type nobody has written yet behaves identically.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh
…he registrar move (#12359)
Measured while discharging this card's smooth-upgrade wall, and it is not what
the ruling considered: a manifest is not only a list of objects, it is also a
ROUTING decision. `ObjectQL.resolveDatasourceBinding` step 4 routes an object by
its OWNING PACKAGE's `defaultDatasource`, so the registrar carries the table's
datasource with it.
Measured on a real engine holding a default driver plus one named `cloud`,
registering the same object definition under each manifest in turn:
owner com.objectstack.service-automation (defaultDatasource: 'cloud')
-> resolveEffectiveDatasource('sys_metadata_activation') === 'cloud'
owner com.objectstack.platform-objects (no defaultDatasource)
-> resolveEffectiveDatasource('sys_metadata_activation') === undefined
(rides the global default driver — a DIFFERENT database)
The ledger table already exists in live databases. On any deployment carrying a
`cloud` datasource, folding the object into the platform-objects manifest would
leave its rows in one database and start reading the other: every artifact an
administrator had switched off would silently re-arm — the ADR-0126 section 6
wall 3 failure arriving through a door nobody was watching.
So the ledger is registered under its OWN manifest from the same plugin,
carrying the automation manifest's scope / namespace / defaultDatasource triple
verbatim. Single owner, one registration, routing byte-identical everywhere. The
three siblings deliberately do NOT get the triple — they ride the project
database today and moving them is a larger question. It is also NOT spelled as
`datasource: 'cloud'` on the object: that is binding step 1, which answers even
when no such driver exists, so `getDriver` would throw on every deployment
without a control plane.
Which datasource this ledger SHOULD live on is a real question and this refuses
to answer it by accident, not on purpose.
Also measured, because the triage that raised #12359 left it as the open
question: a double registration is NOT benign. `registerObject` throws `Object
"sys_metadata_activation" is already owned by package "…"` the moment a second
code package claims the name (ADR-0029 D3/D7), so adding a registrant would have
been a boot failure rather than a duplicate — MOVE was the only shape available.
Both ends of the move are now measured on real boots rather than reasoned about
(`packaged-activation-ledger-reach.dogfood.test.ts`), each with its own
anti-vacuity control on whether the automation service is actually composed:
- actions and NO automation, the exact boot #12359 measured: the flip that
answered 503 SERVICE_UNAVAILABLE answers 200, the row is install-level
(`organization_id` NULL, `metadata_type: 'action'`), dispatch refuses the
disabled action 409 ACTION_DISABLED ahead of the param contract and the
record load, and re-enabling updates the row rather than deleting it;
- WITH automation: exactly one owner, the datasource binding unchanged, both
projections hydrating from the same table, and neither consumer's rows
visible to the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh
…ere the file lives
The "the automation service no longer registers the ledger" pin started life in
`platform-objects/src/plugin.test.ts`, reading the automation plugin's source
across the package boundary. `check:cross-package-test-inputs` is right to
refuse that: a test whose real inputs are wider than its package is invisible to
both the affected-subset filter and the turbo cache, so it can go red on `main`
while every PR reports green (#7802).
Moved, and improved on the way. It is now a BEHAVIOURAL pin in the package that
owns the file — a real `init()` against a recording manifest service, asserting
the registered object list by EQUALITY — rather than a grep over module text. A
grep passes against an object added through a different spelling, and this is
the kind of invariant a well-meaning refactor re-breaks without ever typing the
old symbol name.
What it guards is not a duplicate but a BOOT FAILURE: `registerObject` throws
`Object "sys_metadata_activation" is already owned by package "…"` (ADR-0029
D3), and `objectstack serve` auto-injects platform-objects unconditionally, so
every served kernel would fail to start.
Also records the new store suite's engine double in the RETAINED coverage
ledger (`--write`, grow-only, +1 row) — new pinned coverage the gate asks for by
name, not a debt baseline movement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/objectql, @objectstack/platform-objects, @objectstack/service-automation, touching 33 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/core/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 2e24115710640fba45938baf24a31fd9cc80694f.

2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/index.ts) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2e24115710640fba45938baf24a31fd9cc80694fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from a9b88b4c1f9c90b74c34cc3fd5c536db0244e050 — the merge of head 3ad9030e7078c8bf7325e0e66e8241f2837d07e8 into base 2e24115710640fba45938baf24a31fd9cc80694f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a9b88b4c1f9c90b74c34cc3fd5c536db0244e050 && git checkout a9b88b4c1f9c90b74c34cc3fd5c536db0244e050
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2e24115710640fba45938baf24a31fd9cc80694f 3ad9030e7078c8bf7325e0e66e8241f2837d07e8 && git checkout -B drift-repro 2e24115710640fba45938baf24a31fd9cc80694f && git merge --no-ff 3ad9030e7078c8bf7325e0e66e8241f2837d07e8
node scripts/docs-audit/affected-docs.mjs --json 2e24115710640fba45938baf24a31fd9cc80694f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2e24115710640fba45938baf24a31fd9cc80694f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

Clause-② contract review — PASS (PM session session_01KWRU3s15AJz7PGW7a7wdCh, reviewed at head 3ad9030e7).

Wire surface: zero change. No route added, removed or reshaped; no error code added (the ERROR_CODE_LEDGER census is untouched); no packages/spec surface. The two activation doors (POST /automation/:name/toggle, POST /actions/_activation/:object/:action) answer byte-identically before and after — verified by the runtime activation suites (36/36) and the new dogfood boots on both composition shapes.

What did change is composition-level, and each shift is measured, not asserted:

  • Registration ownership of sys_metadata_activation moved to PlatformObjectsPlugin as one manifest carrying the previous owner's scope/namespace/defaultDatasource verbatim — datasource routing proven byte-identical (the naive fold's silent re-arm hazard is documented in the PR body and closed by this shape). Double registration measured NOT benign (ADR-0029 D3 throws), so MOVE was the only lawful shape.
  • The §4 row contract now has one implementation (@objectstack/core); both consumer APIs unchanged, both pre-existing pin suites byte-unchanged and ablation-proven to reach the shared code.

Part 3 is concluded by maintainer ruling (option A, decline-and-record — #12159 comment 5419767057); no code, so nothing to review. The ledger's datasource home stays as-is by design, recorded as informational.

Proceeding to ready + queue per the program's standing authorization.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 02:32
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit af56546Aug 26, 2026
35 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-12159-ledger-convergence branch August 26, 2026 02:51
baozhoutao pushed a commit that referenced this pull request Aug 26, 2026
…ge (#12457)
ADR-0126 SS7.4's Setup page shipped complete in the console (objectui
app-shell, component ref automation:packaged) but no framework
NavigationContribution ever named the ref - the page was reachable only
by a hand-typed URL and Setup's sidebar carried no entry (epic #12150
L5/L6-UI closed with the framework half missing).
- SETUP_NAV_CONTRIBUTIONS: nav_packaged_automation (type component,
componentRef automation:packaged) in group_apps beside Packages
(ADR-0084: package administration is Operate). Deliberately no
requiresService (action switches work without the automation service,
#12419) and no requiredPermissions (matches nav_packages; write doors
enforce manage_metadata / the SS5 operator gate server-side)
- labels in all four locales + recorded source hashes
- setup-packaged-automation-nav.test.ts pins the framework half of the
cross-repo contract (objectui's navContribution test pins its half)
- FOLLOW-UPS SS8a D16 row updated with the filed issue ref
- changeset: @objectstack/platform-objects patch
Validated: platform-objects vitest suites green (204 tests incl. i18n
parity/tombstone/source-hash), check:app-nav-i18n green (54 merged setup
nav ids, 4 locales).
Fixes#12457. Refs #12438, Epic #12150, ADR-0126 SS7.4
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKUXt6sKgAeCEtjiuwNuhq
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment