Embedded OLE reference-count aliases, 2026-09-21

Addresses the remaining Pass-5 recommendation 5 duplicate-handler drain in fable-review.md. IPersistStorage, IOleCache and IViewObject had three copies each of the same AddRef and Release dispatch body. Six API rows now name two shared generated-handler aliases, removing four redundant bodies. API IDs, names, argument counts and vtable slots are unchanged.

This is not a merge into generic direct-object lifetime handling. The shared AddRef first resolves the embedded interface through ole_static_root, then increments the controlling object's count. Release still calls ole_static_release_api with the same root and eight-byte stdcall cleanup, preserving client-site, advisory-sink and cached-medium guest continuations. QueryInterface behavior is untouched.

The ROT test pins all six metadata aliases and absence of private duplicate bodies, then invokes each interface's real AddRef/Release vtable slots. It checks the controlling count, balanced AddRef/Release, final Release, and the existing thunk runner's ESP restoration and continuation termination checks. The static-handler and guest-callback suites provide adjacent teardown coverage. The latter additionally releases each embedded interface with both an owned DLL-private client site and advisory sink, checking that each guest Release callback runs exactly once and restores the caller's sole reference.

Validation on main: ROT 28/28 plus the new vtable/stack assertions; static handler 65/65; expanded guest callbacks 114/114. Each suite compiles current source. No browser run or full build-gate run was performed for this refactor. A negative test substitutes the generic direct-object Release in the compiled dispatch source in memory, without editing files. It fails the new IPersistStorage guest-site/sink teardown assertion, demonstrating that the test distinguishes the specialized cleanup from an incorrect generic alias.

The live exact-duplicate census falls from 142 groups / 548 members to 140 groups / 542 members. The existing ratchet passes without raising or re-recording its baseline. API append-only and generated-dispatch freshness checks pass. The overall recommendation remains partial: other duplicate families still require individual contract and ownership review.

Follow-up: IOleLink is not IViewObject2

The subsequent contract audit found an existing incorrect QueryInterface alias: Data1 0000011D returned the view interface, and the static-handler test used that ID while naming it IViewObject2. Microsoft's SDK oleidl.h identifies 0000011D-0000-0000-C000-000000000046 as IOleLink and 00000127-0000-0000-C000-000000000046 as IViewObject2. Their vtables are not interchangeable: IOleLink starts with SetUpdateOptions, while the view interface starts with Draw. The old comment incorrectly called 11D a cache-control interface.

Removed the false IOleLink success. This static embedded handler does not implement linking, so IOleLink now returns E_NOINTERFACE, clears output and does not AddRef; the actual IViewObject2 IID still returns the view pointer. The fixture now uses the correct complete IIDs. Added public-vtable queries through IOleObject and each embedded IPersistStorage/IOleCache/IViewObject face, with reference-count and stdcall checks. Validation: static-handler 66/66 and ROT 28/28 plus its added public-vtable assertions pass on main. Substituting HEAD's pre-fix OLE source in memory fails the public IOleLink rejection assertion (S_OK rather than E_NOINTERFACE). Fragment and diff checks pass; no new browser run was made.

This does not complete QueryInterface correctness. The common helper still matches most requests by Data1 only; full GUID validation and the separate IDataObject face's controlling-IUnknown identity remain audit items. The duplicate QueryInterface wrappers have deliberately not been consolidated before that contract review.

Follow-up: complete static-object GUID checks

The shared query helper now translates the IID once and validates the other three DWORDs before dispatching by Data1. Classic OLE/automation requests require the canonical COM suffix. IPersistStreamInit uses its distinct 7FD52380-4E07-101B-AE2D-08002B2EC713 identity, verified against Microsoft's SDK ocidl.h. Rejected queries clear output, do not AddRef, and cannot trigger lazy IDataObject allocation. Existing interface/class availability is unchanged.

The public-vtable regression flips each of the twelve suffix bytes for eight classic IDs through three embedded faces (288 rejections), plus 24 mutations of real IPersistStreamInit/IDispatch IDs on CommonDialog. Valid requests still succeed and their returned references release normally. Old source fails at the first forged IUnknown suffix byte. ROT 28/28 plus these assertions and static handler 66/66 pass. The storage suite passes 79/79 in the current worktree, including corrected CommonDialog GUID fixtures; its pre-existing, unrelated EOF-expectation edit is deliberately excluded from this commit, so that result is not a clean-commit storage-suite claim. The IID fixtures are committed independently of that edit. Initial excess parentheses were caught by the fragment check and corrected before runtime testing.

Remaining: the separate IDataObject face's controlling-IUnknown identity, complete class-GUID checks in the CommonDialog factory, and the duplicate QI dispatch bodies. Full-GUID matching alone does not close those contracts. Fragment and A/W checks pass. The shared-tree duplicate gate currently reports three new ACM audio-stub members outside this change; no baseline was raised. No full build or browser claim is made for this slice.