fix(spec,rest,runtime)!: the ADR-0045 publish gate gets its own machine-managed key — app.hidden goes back to meaning navigation (#4829) - #6942
Conversation
…ne-managed key (#4829) `filterAppForUser` read `app.hidden` as an access gate. `hidden` does not mean that — its contract, written in `app.zod.ts` the day the key was born alongside the built-in Account app, is navigation presentation: hidden apps stay fully routable and permission-checked, they just do not appear in the App Switcher. So the platform's own `account` app, authored `hidden: true` on purpose, was erased from `GET /meta/app` for every user without builder access — password, avatar, sessions and inbox unreachable behind "App not available", while any admin saw a healthy system. ADR-0045 §3 never introduced `hidden`; it borrowed it, citing an "ADR-0019 launcher contract" that does not exist (ADR-0019 contains no `hidden`). One boolean carried two contracts that disagree on whether a normal user may reach the app. Per the maintainer's 2026-08-04 ruling (direction A1) and the 2026-08-07 window re-ruling (lands in v17): - `AppSchema` declares `_unpublished`, the machine-managed publish gate. The `_` prefix is this repo's existing marker for the channel tooling stamps onto artifacts (ADR-0010's `_lock` envelope). Declared rather than omitted because the write path validates against this schema, so an undeclared key would make the platform's own flip unwritable. The strict door answers `unpublished` / `published` / `draft` with "publish state is not authorable". - `hidden` returns to navigation semantics only; its docblock carries the incident. - The REST gate judges `_unpublished`; pins now assert both directions, plus an end-to-end wire pin of the account-app repro (`meta-app-publish-gate.test.ts`). - `publish-drafts` clears `_unpublished` and copies `hidden` through untouched. - ADR-0045 amended, its dangling ADR-0019 reference corrected, and both implementation sites anchored in `scripts/adr-anchors.json` — neither carried an anchor before, which is why §3 could be changed without anyone knowing a decision was being changed. - ADR-0087 conversion `app-hidden-to-unpublished` carries stored rows across. `retiredFromLoadPath` is load-bearing here: it confines the rewrite to stored rows, so an authored `hidden: true` — the Account app included — is never converted into an app no normal user may reach. Fixes #4829 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEKYRu2XjMPRA2AR4ueKUu
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
…#4829) `_unpublished` is machine-managed but DECLARED — the write path validates against AppSchema, so an undeclared key would make the platform's own visibility flip unwritable. What keeps it out of an author's hands is the `_` prefix plus the strict-door prescriptions, so both halves are pinned rather than asserted in a comment: the schema accepts the gate, the author-shaped spellings (`unpublished` / `published` / `draft`) get "publish state is not authorable" instead of a rename suggestion, and `hidden` still parses because the key was never the wrong one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEKYRu2XjMPRA2AR4ueKUu
…ublished-key-a1 # Conflicts: # packages/objectql/src/sys-metadata-repository.test.ts
📓 Docs Drift CheckThis PR changes 3 package(s): 120 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
Queue steward — queue-head entry is gate-blocked, not flaky. ⛔ Not requeued (round 71). This PR is currently the only entry in the objectstack merge queue ( Signature (full job log, not a tail — SKILL notes 7):
Four-branch verdict: no ledger row matches, and this is not a CI flake at all — it is a deterministic governance gate whose only green path runs through a human. So: ⛔ no requeue. Requeueing would reproduce the identical failure every time, and each rebuild costs every entry behind it a rerun. The steward is not requesting a ledger row either — nothing here is requeue-able by machine. One reading the lane may want before the next enqueue (SKILL notes 10). The same gate was already Yield check: this PR's recent comments carry no lane PM disposition of the gate failure (latest are Not in this seat's authority: approving, merging, ready/draft flips, and first-time enqueue all sit outside it — this is a record plus a pointer to the green path, nothing more. Unblocking needs @hotlong's own APPROVED review; the check re-runs itself on the Generated by Claude Code |
|
Queue steward audit — this PR landed with its governance gate red.
This also corrects the queue steward's earlier public reading of this PR: round 71 called it a governance deadlock and round 72 reported it as resolved by manual approval. There was never an approval. ⛔ No action is requested of this lane — whether ADR-0045 is ratified retroactively or reverted and re-landed through the approval path is the maintainer's call (#7022, item 2). This seat does not merge, revert, or change configuration. Generated by Claude Code |
Fixes #4829
The ruling chain (quoted verbatim, untranslated)
1. Direction — maintainer 2026-08-04 (comment 5173161521):
2. Window — maintainer 2026-08-07 (comment 5213737106):
3. Lane — maintainer-approved relabel to
domain:spec2026-08-08 (comment 5225112069):The cross-package touches on
packages/restandpackages/runtimeare authorized by ruling (1) itself.The defect
filterAppForUsertreatedapp.hiddenas an access gate:hiddendoes not mean that. Its contract — written inapp.zod.tsthe day the key was born, in the same commit as the built-in Account app — is navigation presentation: "Hidden apps stay fully routable and permission-checked", keep it out of the App Switcher, surface it from the avatar menu. So the platform's ownaccountapp, authoredhidden: truefor exactly that reason, was erased fromGET /api/v1/meta/appfor every user withoutstudio.access/setup.access: avatar → 个人资料 landed on "App not available — it may still be publishing", and password, avatar, linked accounts, sessions and inbox were all unreachable. Any admin saw a healthy system, which is why it survived a release candidate.ADR-0045 §3 never introduced
hidden; it borrowed it, citing an "ADR-0019 launcher contract (hidden,active)" as an existing read side. That contract does not exist — re-verified at this commit: ADR-0019 (0019-app-as-consumer-unit.md) contains zero occurrences of the wordhidden. The reference was dangling from the day it was written, which is why nothing caught the collision: one boolean, two contracts, disagreeing on the only question that matters — may a normal user reach this app?What this PR does
packages/spec/src/ui/app.zod.ts_unpublished— the ADR-0045 §3 publish gate. Restoreshidden's docblock to the pure-navigation birth contract with the incident attached. Adds strict-doorguidanceforunpublished/published/draftsaying publish state is not authorable.packages/rest/src/rest-server.ts_unpublished.hiddenis never read here.packages/rest/src/rest.test.tspackages/rest/src/meta-app-publish-gate.test.ts(new)packages/spec/src/ui/app.test.tspackages/runtime/src/domains/packages.tspublish-draftsclears_unpublished;hiddencopied through untouched.docs/adr/0045-…mdscripts/adr-anchors.jsonpackages/spec/src/conversions/registry.ts+migrations/registry.tsapp-hidden-to-unpublished+ its chain step.packages/spec/liveness/app.json_unpublishedclassifiedlivewith evidence;hidden's verdict re-verified and re-dated.authorable-surface/ui.json,spec-changes.json,docs/protocol-upgrade-guide.md,content/docs/references/ui/app.mdx— all via their generators, none hand-edited.Naming: why
_unpublishedand notunpublishedThe ruling left the name to implementation and asked for the trade-off. Measured against the repo rather than chosen by taste:
check:authorable-surfacederives the surface from the Zod graph, so declared and authorable are the same thing there. The one marker the repo actually has for "the machine writes this, an author does not" is the_prefix: ADR-0010's_lock/_lockSource/_provenance/_packageIdenvelope, documented as "internal — set by loader", and the prefixlintAuthoredRecordKeysskips as "the packaging/provenance channel that tooling stamps onto artifacts".unpublishedwould advertise itself. It would land inspec-changes.json, the upgrade guide and the generated reference as a new key authors may write — teaching exactly the mis-authoring the ruling's axis ② wants structurally prevented. Nobody reaches for_unpublishedby accident.AppSchema, not omitted. Measured: the write path validates against this very schema (saveMetaItem→ 422 with structured issues;Registry.validate('app', …)→AppSchema.parseatobjectql/src/registry.ts:1627). An undeclared key would make the platform's own flip and the ADR-0087 conversion output unwritable. So the honest statement of what is enforced: the strict door stops the author-shaped spellings with a prescription; the_prefix is what says "not yours" about this one.Migration reach —
retiredFromLoadPathis load-bearing herehiddenis not retired; it keeps its (narrowed) authoring contract. So a conversion that ran on the load path would rewritedefineApp({ hidden: true })— andACCOUNT_APPitself — into unpublished apps, reproducing #4829 through the machinery meant to repair it.retiredFromLoadPath: trueconfines the rewrite to the stored population:applyConversionsToStoredItem(which pinsincludeRetired) andos migrate meta. That is asserted directly, in the negative, inconversions.test.ts.Stored
hidden: trueis unambiguous under the old regime — that value was the gate, so nobody stored it to mean "keep me out of the switcher"; code-declared apps never entersys_metadata, andui/app.form.ts(the Studio app form) has nohiddencontrol, so no authoring path could have produced a second meaning.hiddenitself gets guidance prose, not a tombstone — it remains authorable with narrower meaning.Deliberately NOT done
unhiddenApps/unhideErrorkeep their spelling in thepublish-draftsresponse. They are a wire contract read by the objectui Publish button; renaming them from a repo that cannot verify or update that consumer would be a silent break of exactly the kind this PR is about. The rename rides the objectui follow-up, together.Premise re-verification (anchors were 5 days old; all re-measured at
183b4c4)origin/mainrest-server.ts:1837-1843:2648-2656rest.test.ts:2890-2922:2933-2967runtime/domains/packages.ts:195-234:195-300app.zod.ts:1073-1086:1244-12580045-*.md:96-102, 171-173:91-95, 169-173grep -c hidden 0019-app-as-consumer-unit.md→ 0Reverse verification (direction predicted before each run)
hiddenaccountapp" — the user-facing bug, reproducedpublish-draftsflip back tohiddensaveMetaItemcalls;hidden-untouched pin failsretiredFromLoadPathfrom the conversionThe PM's mechanism assumption — tested, and it holds
Confirmed: there is none. A whole-repo sweep for app-level
hidden: truewriters finds only test fixtures and thepublish-draftsflip (which writesfalse). Everyhidden: trueauthor in this repo isplatform-objects'ACCOUNT_APP, which is a navigation declaration and correctly stayshidden: true. The materialization write point is out of repo (cloud) and is a follow-up card.titanwind PLAT-DEF-040 needs no in-repo action — it is a downstream startup plugin pushing an env-level
{hidden:false}overlay in that repo; it is deletable once this ships, which is stated in the changeset.Follow-up contracts (MANDATORY ANSWER — sibling repos, out of scope here)
service-ai-studio, blueprint tools)_unpublished: trueon the built app where it stampshidden: truetoday. It must stop writinghidden— writing it now only affects App Switcher placement and no longer gates anything. Nothing else inapply_blueprint's envelope changes.app._unpublished === true(wasapp.hidden === true). The Publish button keeps callingPOST /packages/:id/publish-drafts— unchanged, the flip now clears_unpublishedserver-side — and a direct per-app publish isPUT /meta/app/{name}with body{"_unpublished": false}(was{"hidden": false}). The App Switcher / launcher keeps filtering onhidden, which now means only what it says, and must not filter on_unpublished(the server already withholds those apps). Thepublish-draftsresponse fieldsunhiddenApps/unhideErrorare unchanged in this PR; rename them in lockstep with objectui or not at all.{hidden:false}overlay is deletable once this ships; its side effect ("账户" appearing in the App Switcher) disappears with it.Who writes / clears the key, in one line: written by the cloud AI materialization path, cleared by
POST /packages/:id/publish-drafts(framework, this PR), read byfilterAppForUser(framework, this PR). No author, no UI, ever writes it.⛔ Merge discipline
This diff touches
docs/adr/**. Under AGENTS.md Prime Directive #14 an ADR PR is confirmed and merged by the maintainer, by hand — no AI seat merges it, queues it, or arms auto-merge, and "reviewed + green" does not override that. Opened as a draft and left there. The red "ADR maintainer approval" check is that gate (#6785) working as designed — its own log says "Drafting this PR was fine and stays fine — only the MERGE is reserved"; it goes green when @hotlong approves.🤖 Generated with Claude Code
https://claude.ai/code/session_01CEKYRu2XjMPRA2AR4ueKUu