Skip to content

fix(cloud-connection): the install-local listing marks a package the rehydrate refused as not loaded (#21822) - #21833

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21822-listing-not-loaded-marker
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21822-listing-not-loaded-marker

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21822

Clause-②: no

What changed

After a restart whose kernel:ready rehydrate refused a protocol-incompatible package (ADR-0087 D1), GET /api/v1/marketplace/install-local listed it like any loaded package, and each GET logged a warn that it could not read the package's seed rows. Now the rehydrate records each entry it refuses, and the listing serves that entry with a notLoaded marker carrying the refusal's code and the declared range. The listing reads no seed rows for it, so the per-request warn is gone. This is the direction triage ruled (5988934231): a marker, not omission.

packages/cloud-connection/src/marketplace-install-local-plugin.ts only. ⛔ No change to the refusal itself (its line, level and wording), to DELETE, to the install route or to handleReseed. No packages/spec path.

The wire shape (for objectstack-ai/objectui#11645 to render)

A refused entry, as an operator (manage_metadata) reads it:

{
  "packageId": "com.example.crm",
  "versionId": "…",
  "manifestId": "com.example.crm",
  "version": "…",
  "installedAt": "…",
  "notLoaded": { "code": "OS_PROTOCOL_INCOMPATIBLE", "requiredRange": "^16" },
  "installedBy": "…"
}
  • notLoaded is CLOSED: exactly code and requiredRange, the handshake diagnostic's own member names (the same requiredRange the install door's 422 carries in error.details). It is not a spread of the diagnostic.
  • notLoaded stands in place of withSampleData. The listing does not read the package's rows, so it makes no claim about them: the key is omitted, not false. This follows the same rule as installedBy for a narrowed caller.
  • A narrowed caller (authenticated, no manage_metadata) gets the same item without installedBy. The marker is served to every authenticated caller.
  • A loaded entry is byte-identical to before: withSampleData, no notLoaded key. total counts marked entries.
  • After DELETE the entry is gone from the ledger and the listing. Once a compatible version is installed over it, the entry is listed as loaded, with no marker.

Where "not loaded" comes from (H3, measured on the code)

I compared two sources:

  • A, the rehydrate's own record of what it refused.
  • B, re-running checkProtocolCompat on each ledger entry per request.

I chose A. It records what the rehydrate did, so it agrees with it by construction. B is a second judgement, and it disagrees with what happened in reachable states:

  • rehydrate returns before the handshake loop when there is no manifest service ("no manifest service — rehydrate skipped"). B would mark entries whose refusal never happened.
  • The listing re-reads the ledger directory on every request. The rehydrate's own comment names a ledger shared with "a runtime of another protocol", so an entry written there after this boot was never judged here, and B would report a refusal this runtime never made.

The record is keyed by manifest id and holds the refused entry's installedAt, which says which entry was refused. The install door is the only writer of a new entry, it always stamps a fresh installedAt, and it runs the same handshake first, so it can never write an incompatible entry. So a compatible re-install stops the marker without any write to the record from the install door or from DELETE. rehydrate clears the record when it starts.

Pins (the ruling's, verbatim, plus the order's addition)

  • "after a restart whose rehydrate refused an entry, GET /install-local lists it with the marker and the code."
  • "A loadable entry is unchanged, and DELETE on the marked entry still works."
  • No seed-row read and no withSampleData warn for the marked entry.

Unit (src/marketplace-install-local-listing-not-loaded.test.ts, 7 cases): a fresh plugin over a ledger with one refused and one loadable entry. The engine double answers only for registered objects, the way the real one answers Object '…' not found. The cases cover:

  • the marker, on the operator's item and the narrowed caller's;
  • the closed member set;
  • zero reads of the marked package's object and zero seed-row warnings, with a lit control that the loadable entry's object was read;
  • the loadable item, byte-equal to the one a ledger without the refused entry serves;
  • DELETE;
  • a compatible re-install clears the marker.

Door (packages/qa/dogfood/test/install-local-listing-not-loaded.dogfood.test.ts, 7 cases): real showcase boots over one databaseFile and one ledger.

  • Boot 1 installs the CRM package (28 seed rows) and a small loadable package.
  • Between the boots, the CRM ledger entry is rewritten to declare ^(major-1), an install made for an older runtime.
  • Boot 2's rehydrate refuses it; the capture holds the error line.
  • The GET serves the marker. The loadable item is byte-identical to its boot-1 item. DELETE answers 200, and the listing then holds the loadable package alone.
  • The no-warning assertion has a lit control: an unreadable ledger file planted before boot 2 makes the same GET log its own warn through the same logger, in the same capture window.

Reverse verification (one-off, not kept)

The plugin file was restored to the merge base (c4d57131, blob 50a71c2f) with a trap that restores HEAD. Each leg was proven on disk: the refusedAtRehydrate count went 6 → 0 → 6, the hash equalled the BASE blob, then the HEAD blob 411ad7a9, and git diff HEAD came back empty. Both suites import the plugin from src: the unit test by a relative path, and the dogfood isolated project aliases @objectstack/cloud-connection to ../../cloud-connection/src/index.ts. So no dist leg applies.

  • Unit, pre-fix: 3 failed | 4 passed (7). The marker cases fail, and the read/warn case shows both facts: readsOfMarkedEntry: ["qa_old_account"] and the warning com.example.qaold21822: the installed-apps listing could not read this package's seed rows (qa_old_account: Object 'qa_old_account' not found), so it answers withSampleData: false for it. With the fix: 7 passed.
  • Door, pre-fix: 2 failed | 5 passed (7). notLoaded is undefined, and the GET logs WARN [MarketplaceInstallLocal] com.example.crm: the installed-apps listing could not read this package's seed rows (crm_account: Object 'crm_account' not found; crm_contact: …; crm_opportunity: …; crm_lead: …; crm_activity: …). This is the card's own reading, reproduced. With the fix: 7 passed.

The preservation pins (the loadable entry, DELETE, the compatible re-install, the preconditions and the capture control) pass on both sides, as preservation pins should.

Tests and gates

All readings are from claude/issue-21822-listing-not-loaded-marker after one merge of origin/main (07bf21ff). Final head: 36817931.

  • Build: the @objectstack/cloud-connection and @objectstack/dogfood dependency closures, rebuilt after the merge: turbo run build, 63/63 tasks.
  • pnpm --filter @objectstack/cloud-connection test: 40 files / 492 tests passed (at 6bb9f960). The one later commit, 36817931, changes only the new test's engine double, and that file was re-run at 36817931: 7/7.
  • pnpm --filter @objectstack/cloud-connection typecheck: exit 0. tsc --listFiles includes the new test file.
  • pnpm --filter @objectstack/dogfood typecheck: exit 0. tsc --listFiles includes the new dogfood file.
  • Dogfood, narrowed: vitest run --project isolated over install-local-listing-not-loaded, install-local-listing-sample-data, install-local-purge-sample-data and install-local-no-active-organization: 4 files / 27 tests passed. This narrowing is proven, not assumed: git grep -l -E 'MarketplaceInstallLocalPlugin|marketplace/install-local' -- packages/qa/dogfood/test at HEAD names exactly these four files. The rest of the suite belongs to CI's Dogfood Regression Gate.
  • Gate battery: the 67 commands dispatch-gates --commands derives for this change set (the dispatch's 49 plus 18 more), all exit 0 at 36817931. dispatch-gates --ran: "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN".
    • check:dual-build-cjs-loads first answered PREREQUISITE NOT MET: eight packages outside this closure had no dist. They were built and the gate re-ran green: "106 published require entry point(s) across 66 package(s) load".
    • check:where-matcher caught the new engine double reading a combinator as a field name. The double now refuses what it does not implement: 461/461 conforming.
  • pnpm lint (eslint . --no-inline-config, the whole repository): exit 0 at 36817931.
  • Stale-tree note: after the one merge, origin/main gained 088428fb (fix(runtime): PATCH reaches a declared AI route through the /ai/* wildcards, and an undeclared method answers 405 #21823: the runtime dispatcher and scripts/check-route-envelope.mjs). So check:route-envelope ran on its pre-fix(runtime): PATCH reaches a declared AI route through the /ai/* wildcards, and an undeclared method answers 405 #21823 copy. CI runs on the merge ref.

Acceptance notes


Generated by Claude Code

claude added 7 commits October 5, 2026 06:25
…ehydrate refused as not loaded

The kernel:ready rehydrate records each ledger entry it refuses under the
ADR-0087 D1 handshake, by manifest id and installedAt. GET /install-local
lists that entry with notLoaded { code, requiredRange } in place of
withSampleData, and reads no seed rows for it.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-authored-by: Claude <noreply@anthropic.com>
… real boot across a restart

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 10 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via MarketplaceInstallLocalPlugin (symbol, a top-level class))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via OS_PROTOCOL_INCOMPATIBLE (literal, a string literal in a comment in MarketplaceInstallLocalPlugin))
  • content/docs/releases/v15.mdx (via OS_PROTOCOL_INCOMPATIBLE (literal, a string literal in a comment in MarketplaceInstallLocalPlugin))
  • content/docs/releases/v17/17-5.mdx (via OS_PROTOCOL_INCOMPATIBLE (literal, a string literal in a comment in MarketplaceInstallLocalPlugin))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 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 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 15ef59c9dc4fa3cc5c4ad1225043cda837a52392 — the merge of head 3681793122c73909ef9bdb707e78b44fd16f3ede into base 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8, 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 15ef59c9dc4fa3cc5c4ad1225043cda837a52392 && git checkout 15ef59c9dc4fa3cc5c4ad1225043cda837a52392
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8 3681793122c73909ef9bdb707e78b44fd16f3ede && git checkout -B drift-repro 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8 && git merge --no-ff 3681793122c73909ef9bdb707e78b44fd16f3ede

node scripts/docs-audit/affected-docs.mjs --json 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8

⚠️ 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 6fb71152ca5cc7c71723f15eaf33bb3f8f7138e8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 5, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 48297ad Oct 5, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21822-listing-not-loaded-marker branch October 5, 2026 08:34
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
… load as not loaded (objectui#11645) (objectstack-ai#11759)

Fixes objectstack-ai#11645

Clause-②: yes

Installed Apps now reads an install-local entry the runtime refused to
load at startup as **Not loaded**, names the reason in plain words, and
keeps Uninstall on the row. The package's Details page no longer offers
re-seed or purge for such an entry. A loaded entry renders as before,
byte for byte (measured below).

This is the console half of objectstack-ai/objectstack#21822. The server
half is objectstack-ai/objectstack#21833 (`48297ad980`), carried by
`@objectstack/*` 17.7.0, which objectui resolves since `c0862c1`.

Written by the os-dev agent dispatched on claim comment `6029532484`
(seat `domain:ui#3`, `mode:subagent`), session
`https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`.

## The server's wire shape, member by member

Read at objectstack `48297ad980`: `handleList` and the `NotLoadedMarker`
interface in
`packages/cloud-connection/src/marketplace-install-local-plugin.ts`, and
`ProtocolIncompatibleDiagnostic` in
`packages/metadata-core/src/protocol-handshake.ts`.

| Listing item member (server) | Present when | Console type
(`LocalInstallEntry` in `marketplaceApi.ts`) | What the console does
with it |
|---|---|---|---|
| `notLoaded.code` | the startup rehydrate refused this entry |
`notLoaded?: LocalInstallNotLoaded`, member `code: string` |
`OS_PROTOCOL_INCOMPATIBLE` selects
`marketplace.notLoaded.protocolIncompatible`. Any other code selects
`marketplace.notLoaded.otherReason`, which names the code. |
| `notLoaded.requiredRange` | same | member `requiredRange: string` |
interpolated into `marketplace.notLoaded.protocolIncompatible` |
| `withSampleData` | loaded entries only (on a refused entry it is
omitted, not `false`) | `withSampleData?: boolean`, unchanged | Details
draws re-seed and purge only when `notLoaded` is absent |
| `installedBy` | operator (`manage_metadata`) callers only |
`installedBy?: string or null`, was required | unchanged rendering: the
"by" line is drawn only when present |

- `notLoaded` is closed on the server: exactly `code` and
`requiredRange`. `LocalInstallNotLoaded` declares exactly those two
members.
- `code` is typed `string`, not the server's
`'OS_PROTOCOL_INCOMPATIBLE'` literal. The server's own acceptance notes
name other not-loaded states that are unmarked today. If a later server
marks one, the console still reads the row as not loaded and names the
code, instead of hiding the row the operator needs to uninstall. This is
not a lenient alias: there is one key, `notLoaded`, read one way.
- `installedBy` is now optional. The server omits it for a narrowed
caller, and the narrowed-caller pin below needs a fixture without it.
This is a bounded correction on the same interface: its only reader
already guards with `&&`, so no reader changes.

### Public surface, measured on the BUILT declarations

- **`LocalInstallEntry` and `LocalInstallNotLoaded` are not public.**
After `pnpm --filter @object-ui/app-shell build`, a walk of the
relative-import closure of `packages/app-shell/dist/index.d.ts` reaches
168 declaration files. None of them contains `LocalInstallEntry`,
`LocalInstallNotLoaded`, `notLoaded` or `marketplaceApi`. Positive
controls: `MarketplacePackagePage` is reached (2 files), and the emitted
`dist/console/marketplace/marketplaceApi.d.ts` does carry `notLoaded`.
The exports map has only `.` and `./styles.css`, so no deep import
reaches the file.
- **The five language-pack keys are public.** After `pnpm --filter
@object-ui/i18n build`, `dist/locales/en.js`, `dist/locales/en.d.ts` and
`dist/locales/zh.js` each carry them. So `Clause-②: yes` and the `minor`
changeset stand.

## New language-pack keys, in all ten packs

| Key | en value | Placeholders | Fed by |
|---|---|---|---|
| `marketplace.notLoaded.badge` | Not loaded | none | presence of
`notLoaded` |
| `marketplace.notLoaded.protocolIncompatible` | This runtime did not
load this package: it targets protocol {{requiredRange}}, which this
runtime does not support. | `requiredRange` | `notLoaded.requiredRange`,
when `notLoaded.code` is `OS_PROTOCOL_INCOMPATIBLE` |
| `marketplace.notLoaded.otherReason` | This runtime did not load this
package ({{code}}). | `code` | `notLoaded.code`, for any other code |
| `marketplace.uninstall.confirmNotLoaded` | Uninstall {{manifestId}}
v{{version}} from this runtime? (blank line) The cached manifest will be
removed. This runtime did not load the package, so none of it is
running. | `manifestId`, `version` | the row's `manifestId` and
`version` |
| `marketplace.uninstall.successNotLoaded` | Removed {{manifestId}}. It
was not loaded, so no restart is needed. | `manifestId` | the row's
`manifestId` |

- "targets protocol" is the server's own wording: the handshake message
reads "package 'ID' targets protocol RANGE ... but this runtime is
protocol VERSION".
- No console phrase existed for the range. `git grep` for
`OS_PROTOCOL_INCOMPATIBLE`, `requiredRange`, `protocol` and `incompatib`
over `packages/i18n/src/locales/en.ts` and `packages/app-shell/src`
found nothing relevant. Control: `versionBadge` is found in all ten
packs.
- The nine other packs are translated, not copied. The
`untranslated-identity` pin refuses an English copy in zh, ja, ko, ru
and ar, and it is green.
- **One decision for the reviewer: the two `uninstall.*NotLoaded`
keys.** The card asks for the row to keep Uninstall. The existing texts
for that action say the package "will remain loaded in the running
kernel until the next restart" (confirm) and "Restart the runtime to
fully unload it from the running kernel" (result). Both contradict the
row's own "Not loaded". If you read these as beyond the card, dropping
them removes two keys and one ternary in each place.

## What changed

- `InstalledListWidget.tsx`: a `destructive` "Not loaded" badge beside
the version badge, and one reason line under the meta line. Uninstall
stays and is enabled. The confirm and the result text are chosen by
`notLoaded`. A row without `notLoaded` adds no node.
- `MarketplacePackagePage.tsx`: the local menu's re-seed and purge items
are not drawn for a `notLoaded` entry. Uninstall stays, and so does the
primary Reinstall: that is the compatible re-install the server
documents as reachable. Measured before this change: a refused entry's
menu offered "Add sample data", enabled (the missing `withSampleData`
read as no sample data), which posts a re-seed into objects the runtime
never registered. It also showed purge, disabled.
- `marketplaceApi.ts`: types only.
- Locale packs: five keys in each of the ten packs.
- `.changeset/11645-installed-not-loaded.md`: `minor` for
`@object-ui/app-shell` and `@object-ui/i18n`.

## Tests

- New `InstalledListWidget.notLoaded-11645.test.tsx`, 9 cases.
`marketplaceApi` is not mocked. One stubbed `fetch` answers with the
listing body the server landed, over a ledger that DELETE changes. The
real `I18nProvider` renders in en and zh. Expected text is the pack's
value, read from the pack and interpolated; no sentence is copied into
the test. It pins:
- the operator's refused entry: badge, reason naming `^16`, Uninstall
enabled (en, zh);
- the narrowed caller's entry, with no `installedBy`: badge and reason
(en, zh);
  - a loaded entry: no badge and no reason, Uninstall enabled (en, zh);
- Uninstall on the refused row: the not-loaded confirm, DELETE for its
manifest id, the not-loaded result, and the re-read listing without the
row;
  - a loaded row keeps the loaded texts;
- an unknown code still reads not loaded, names the code, and keeps
Uninstall.
- New `MarketplacePackagePage.notLoadedMenu-11645.test.tsx`, 6 cases,
run in the catalog view (marketplace on) and in the offline local view
(objectui#11627). They pin: the refused entry's menu holds Uninstall
alone; Uninstall still issues DELETE; a loaded entry still gets re-seed
and purge.
- `pnpm exec vitest run packages/app-shell/` at `79a84a7`: `Test Files
1054 passed | 1 skipped (1055)`, `Tests 10317 passed | 9 skipped
(10326)`. The one later commit, `053d441`, edits one of the new test
files only. Both new files re-ran at `053d441`: 15/15.
- `pnpm exec vitest run packages/i18n/` at `79a84a7` (i18n has not
changed since): `Test Files 81 passed (81)`, `Tests 1310 passed | 13
skipped (1323)`.
- `pnpm --filter @object-ui/i18n type-check` and `pnpm --filter
@object-ui/app-shell type-check` at `053d441`: exit 0. The script name
echoes as `type-check`. `tsc -p tsconfig.test.json --listFiles` lists
both new test files (2 of 5063 lines). Control: the existing
`marketplaceDates.displayLocale-10331.test.tsx` is listed too.

## Reverse verification (one-off, not kept)

- The fix was committed (`053d441`). Then the `c0862c1` blobs of
`InstalledListWidget.tsx` and `MarketplacePackagePage.tsx` were checked
out, under a trap that restores `HEAD`.
- Proof that the change landed on disk: `git hash-object` equalled the
base blobs `d2eaeb1d` and `e06fd029`, and the `notLoaded` count went
from 10 to 0 (widget) and from 2 to 0 (page).
- Proof of the restore: the hashes equal the head blobs `0e1e48ae` and
`94d89725`, `git diff HEAD` is empty on both paths, the index is clean,
and the count is back to 10.
- Pre-fix: **8 failed, 9 passed (17)**.
- The 8 that fail are every not-loaded pin: refused entry (en, zh),
narrowed caller (en, zh), the Uninstall flow, the unknown code, and the
Details menu in both views.
- The 9 that pass on both sides are the preservation pins: loaded row
(en, zh), the loaded row's Uninstall texts, Details DELETE in both
views, and Details' loaded menu in both views. The other two are the
markup probe below.
  - With the fix: 17 passed.
- **A loaded row renders as before, byte for byte.** A one-off probe
rendered Installed Apps over two loaded entries (one with `installedBy`,
one without, one with a catalog id distinct from its manifest id) and
hashed the container's `innerHTML`. Base and head are identical: en 6659
bytes, sha256 `fdfbc6d0…`; zh 6461 bytes, sha256 `250c012d…`. A `cmp` of
the two dumps also reports them identical.

## Gates, at `053d441`

Each line gives the exit code and the gate's own verdict.

- `pnpm check:control-bytes`: 0, "check-control-bytes: OK (scanned 7750
tracked text file(s); skipped 85 binary)"
- `pnpm check:test-path-roots`: 0, "check-test-path-roots: OK"
- `pnpm check:changeset-claims`: 0, "No pending changeset names a file
this change touches."
- `pnpm check:pending-changeset-literals`: 0, "No test source names a
pending changeset."
- `pnpm check:i18n-keys`: 0, "Every in-scope call-site key resolves
against the en pack (3291 keys)…"
- `pnpm check:i18n-drift`: 0, "0 en value(s) changed (5 key(s) added, 0
removed …)"
- `pnpm check:i18n-dead-keys`: 0 (report-only). None of the five new
keys is in its candidate list.
- `pnpm check:i18n-designer-parity`: 0, "Every en row has a zh row, and
every shared row carries the same placeholders."
- `pnpm check:new-line-citations`: 0, "0 new citation(s)"
- `pnpm check:vi-mock-specifiers`, `check:vi-mock-inherit`,
`check:vi-mock-override-shape`: 0, OK
- `pnpm check:phantom-deps`: 0. `pnpm check:self-import`: 0
- `pnpm changeset:check`: 0, "No changeset declares a `major` bump."
- `node scripts/check-changeset-presence.mjs`: 0, "15 source file(s) of
2 released package(s) changed, and this change declares 1 changeset(s)"
- `node scripts/check-governed-queue-guard.mjs --test` over the 16
paths: "NOT GOVERNED"
- eslint, narrowed to the 15 touched `.ts`/`.tsx` files: exit 0, 0
errors, 49 warnings, 0 of them on a line this diff adds (mapped against
the `git diff -U0` hunks).
- Population: `isPathIgnored` is false for the touched files, and the
`--format json` output has 15 results.
- Invariance: the resolved config has no `parserOptions.project` or
`projectService`, so linting is not type-aware and this diff cannot
change the verdict on an untouched file.
  - The repo-wide `pnpm lint` is left to CI.
- NOT MEASURED: `check:sdui-registration-pins`. It exited 2 with
PREREQUISITE NOT MET, because it needs a console build
(`apps/console/dist`). This diff does not touch the
`ComponentRegistry.register` call. Left to CI.
- NOT MEASURED: `check:eager-locale-catalogues`, which also needs a
console build. The diff adds keys inside existing packs and no static
import. Left to CI.

## Acceptance notes

These were observed here and not changed. They are outside this card's
surface, and no issues were filed for them.

- The Details header still shows the green "Installed · vX" badge for a
not-loaded entry. The card's Details step covers actions only, so the
not-loaded marker is drawn on Installed Apps alone.
- Details' own "Uninstall from this runtime" still uses
`marketplace.uninstall.confirm` and `successInDetail`, which say the app
stays loaded until a restart. Details' Uninstall is not an action that
needs a loaded package, so it is outside the card's Details step.
- The catalog page (`MarketplacePage`) badges a local install "Installed
vX" whether or not it is loaded.
- Docs: no page in `content/docs` or the app-shell README describes the
Installed Apps rows, so no doc page changes.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants