Repository navigation
app-shell: publishing a draft view does not invalidate the adapter's view caches — the console's real create-view flow bypasses ObjectStackAdapter entirely #4373
Description
Adjacent finding, surfaced while fixing #4363 (the adapter's own write paths). Not fixed there — that PR's scope is packages/data-objectstack/src/index.ts, and this defect lives in packages/app-shell, on a different writer. Filed under PD #10.
Measured on the #4363 branch (worktree at origin/main 275d7df13).
What
#4363 fixed the four write paths on ObjectStackAdapter so each invalidates both view-shaped cache keys (view:{object}:{viewId} and view-overrides:{object}). But the console's actual create-a-view flow never calls any of them.
ObjectView.handleViewCreate (packages/app-shell/src/views/ObjectView.tsx:626) writes through the ADR-0034 metadata seam, not the DataSource:
createdId = await createRuntimeMetadata('view', env.name, env, { metadataClient });
createRuntimeMetadata (packages/app-shell/src/views/runtime-metadata-persistence.ts:100) calls ctx.metadataClient.save(type, name, body, { mode: 'draft' }) and invalidates nothing. Publish is the same shape — publishRuntimeMetadata (:240) is a bare ctx.metadataClient.publish(type, name), reached from RuntimeDraftBar.tsx:118.
So the sequence a user actually performs:
- create a view in the console — lands as an invisible per-item draft.
listViewOverridesreads published rows, so the map is still correct here. - Publish — the row becomes published, and the batch override map is now genuinely stale.
- nothing invalidates
view-overrides:{object}, so the map keeps answering from its pre-publish snapshot for the rest ofMetadataCache's default 5-minute TTL (packages/data-objectstack/src/cache/MetadataCache.ts:90).
The staleness does not self-heal, for the same reason #4363 gave: loadViewOverrides (ObjectView.tsx:283) treats a RESOLVED map as authoritative and deliberately does not re-probe per view (#3774), so the per-view getView fallback that would mask it is by design unreachable.
Why #4363 does not cover it
#4363 makes the adapter correct at its own door. This is a second door into the same rows. The two writers are disjoint: grep for createView( finds no production caller of ObjectStackAdapter.createView anywhere in the repo — it is a published DataSource surface (packages/types/src/data.ts:524) for external consumers, while the console writes through metadataClient. Both need to invalidate; only one now does.
Dormant sibling, recorded not filed separately
MetadataService.saveMetadataItem / deleteMetadataItem (packages/app-shell/src/services/MetadataService.ts:140 and :150) invalidate ${category}:${name}. That is the right key for the generic metadata read (data-objectstack/src/index.ts:3739), but for category === 'view' it names neither view key, so the same gap would open. No caller passes 'view' today — in fact these two methods have no callers outside their own class — so this is observation-class, not a live defect. Worth fixing in the same pass if the direction below is taken.
Direction (left to triage)
This is the recurrence #4363's own "Direction 2" predicted: the key list is restated per call site, so each new writer forgets it independently. Options:
- Have the seam's publish path (and
createRuntimeMetadata) invalidate the adapter's two view keys — needs the adapter reachable from those call sites, which today they do not hold. - Give the adapter one exported "view rows for this object changed" invalidation entry point and route every writer — adapter methods, seam,
MetadataService— through it. Fixes the dormant sibling for free. - Have
listViewOverridesnot be cached at all, and letlistViews-style freshness apply. Cheapest to reason about; costs the batch read's whole point (data-objectstack 的 listViewOverrides 读的是meta/{对象名}(把对象名当 metadata type),写却落在type='view'—— 两边键空间不相交,已保存的视图个性化永远读不回来 #3774 introduced it to remove a 404 flurry), so probably wrong.
Not claimed
No browser repro was run. What is measured is the call graph (handleViewCreate to createRuntimeMetadata to metadataClient.save, and RuntimeDraftBar to publishRuntimeMetadata to metadataClient.publish), the absence of any invalidate on those paths, and the TTL. Whether a user notices depends on whether they revisit the object within the TTL window.
Related
- data-objectstack:
view-overrides:{object}is only invalidated by updateViewConfig — createView / updateView / deleteView leave the batch map stale for the 5-minute TTL #4363 — the same defect onObjectStackAdapter's own write paths (fixed); this is the writer it does not reach - data-objectstack 的 listViewOverrides 读的是
meta/{对象名}(把对象名当 metadata type),写却落在type='view'—— 两边键空间不相交,已保存的视图个性化永远读不回来 #3774 — madelistViewOverridesauthoritative, which removes the accidental self-healing - data-objectstack:
views:{objectName}缓存键被 4 处 invalidate,却从没有任何读路径写入过 —— listViews 实为无缓存,那 4 行是惰性代码 #3778 / sweep sweep: dead-surface deletions batch 3 — 3 zero-consumer items, one claim, one PR (findings-triage 2026-08-11) #4328 — the deadviews:{object}invalidations, whose removal made this family visible
Generated by Claude Code
Activity
yinlianghui commentedon Aug 11, 2026
Triage + PM ruling (delegated authority, session
session_017Qqyix2QcnpUC9XeYVDzx3; veto window open as always):finding→pm:queue, and the fix direction is decided now so the eventual dispatch doesn't re-litigate it.Ruling: one exported invalidation entry point, routed by every writer — the option B recorded in PR #4374's report, which is also this repo's standing derivation-over-enumeration doctrine applied to cache keys:
ObjectStackAdapterexports a single "view rows for this object changed" invalidation seam (shape at implementer's discretion, e.g.invalidateViewKeys(objectName, viewName?)) — the ONE place that knows the key set isview:{object}:{name}+view-overrides:{object}.createView/updateViewboth halves/deleteView/updateViewConfig) route through it — deleting the four restated key lists PR fix(data-objectstack): every view write path invalidates the override map (#4363) #4374 just fixed. Their pins keep passing unchanged; that is the acceptance evidence the refactor changed nothing.handleViewCreate→createRuntimeMetadata→metadataClient.save, and Publish viaRuntimeDraftBar→publishRuntimeMetadata) calls the seam when the saved/published item is a view — closing the user-reachable staleness this card measures. Red-first test on that flow per the The report config panel is titled "Title" and the empty state says "Object Not Found" — call sites asking for a key whose value is not the copy for that slot #4118 guard convention.view:{name}if ever passedview) is wired through the same seam or explicitly documented as unreachable — implementer measures which is honest.Why B and not leaving four restated lists: this is the SECOND time the repo has paid for per-site restatement of this key set (#3778 removed five copies of a wrong key; #4363 fixed four copies of an incomplete one), and #4373 itself is the measured proof that a new writer forgets the keys by default. A pin suite can only guard writers that exist; a mandatory seam makes the next writer structurally unable to forget.
Dispatch gate: cross-package (data-objectstack + app-shell). Wait for BOTH: PR #4374 landing (auto-merge armed) and #4040 tranche 5 landing (app-shell surface currently held). Not before.
Generated by Claude Code
yinlianghui commentedon Aug 12, 2026
CLAIM — PM seat
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4373-view-invalidation-seam, one dev agent, one PR,Fixes #4373.Dispatch gate status: PR #4374 (adapter half) is merged; PR #4379 (tranche 5, which held the app-shell surface) is ready + auto-merge-armed with converged CI — the dev is instructed to verify its merge commit is in origin/main before creating the worktree. The ruling recorded in my triage comment above is binding and unchanged: one exported invalidation seam, four adapter paths route through it (their pins keep passing unchanged as the refactor's acceptance evidence), app-shell's save/publish flow calls it when the item is a view (red-first test per #4118), the MetadataService dormant sibling wired-or-documented as measured.
Generated by Claude Code
fix(app-shell,data-objectstack): route every view write through one c…
chore(sweep): retire ObjectLevelPermission and delete the console met…