experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: sweep stale Phase tag comments in source files

+150 -192
+3 -3
app/_test/index.html
··· 7 7 </head> 8 8 <body> 9 9 <!-- 10 - Privileged test renderer (v1-removal Phase 2). 10 + Privileged test renderer. 11 11 12 12 This is a hidden resident BrowserWindow launched by 13 13 `backend/electron/test-fixture-glue.ts` only when `E2E_TEST=true`. It 14 - replaces the v1 `peek://app/background.html` iframe that Playwright 15 - tests previously used as `bgWindow` for pub/sub + datastore calls. 14 + is the test fixture that Playwright tests use as `bgWindow` for 15 + pub/sub + datastore calls. 16 16 17 17 Mechanics are identical to cmd/hud/page core renderers: 18 18 - Loaded with `tile-preload.cjs` + a `createTrustedBuiltinGrant('test')`
+1 -1
app/background.html
··· 8 8 </head> 9 9 <body> 10 10 <!-- 11 - Core background renderer (v1-removal Phase 2.5 #1). 11 + Core background renderer. 12 12 13 13 Hidden resident BrowserWindow created by 14 14 `backend/electron/core-glue.ts::initCore()`. Uses the same tile
+6 -6
app/cmd/panel.js
··· 18 18 const api = window.app; 19 19 20 20 // Initialize the tile-preload surface before any API calls. Without this, 21 - // `tokenValid` is false and api.settings/api.subscribe/api.commands all 22 - // silently no-op or reject — see docs/v1-removal-smoke-regressions.md §1. 21 + // `tokenValid` is false and api.settings / api.subscribe / api.commands 22 + // all silently no-op or reject. 23 23 if (api.initialize) { 24 24 await api.initialize(); 25 25 } ··· 35 35 let adaptiveDataCache = null; 36 36 37 37 /** 38 - * Load persisted adaptive data from extension settings. 39 - * Phase 4 API: each key fetched individually via api.settings.get(key). 38 + * Load persisted adaptive data from tile settings. Each key is fetched 39 + * individually via api.settings.get(key). 40 40 */ 41 41 const loadAdaptiveData = async () => { 42 42 if (adaptiveDataCache) { ··· 100 100 // Cached prefs for URL search behavior 101 101 let showUrlsInResults = defaults.prefs.showUrlsInResults; 102 102 103 - // Load prefs on startup. Phase 4: api.settings.get(key) — request the 104 - // 'prefs' row directly rather than the (now removed) no-arg whole-object form. 103 + // Load prefs on startup. api.settings.get(key) requires the per-row key; 104 + // the legacy no-arg whole-object form was removed. 105 105 api.settings.get('prefs').then(result => { 106 106 if (result.success && result.data) { 107 107 showUrlsInResults = result.data.showUrlsInResults ?? defaults.prefs.showUrlsInResults;
+1 -1
app/components/bundle.js
··· 133 133 component: true 134 134 }, 135 135 136 - // Phase 4 components 136 + // Form controls / overlays 137 137 'peek-select': { 138 138 path: './peek-select.js', 139 139 exports: ['PeekSelect'],
+1 -1
app/components/index.js
··· 95 95 export { PeekTabs, PeekTab, PeekTabPanel } from './peek-tabs.js'; 96 96 export { PeekDetails } from './peek-details.js'; 97 97 98 - // Components - Phase 4 98 + // Components - Form controls / overlays 99 99 export { PeekSelect } from './peek-select.js'; 100 100 export { PeekDropdown, PeekDropdownItem, PeekDropdownDivider } from './peek-dropdown.js'; 101 101 export { PeekSwitch } from './peek-switch.js';
+3 -3
app/settings/settings.js
··· 3212 3212 // Initialize 3213 3213 const init = async () => { 3214 3214 // Initialize the tile-preload surface before any API calls. Without this, 3215 - // `tokenValid` is false and api.datastore/api.theme/api.subscribe all silently 3216 - // no-op or reject — createDatastoreStore returns empty, theme CSS is never 3217 - // injected, and --theme-font-sans is absent (see docs/v1-removal-smoke-regressions.md §7). 3215 + // `tokenValid` is false and api.datastore / api.theme / api.subscribe all 3216 + // silently no-op or reject — createDatastoreStore returns empty, theme 3217 + // CSS is never injected, and --theme-font-sans is absent. 3218 3218 if (api.initialize) { 3219 3219 await api.initialize(); 3220 3220 }
-2
backend/electron/core-glue.ts
··· 39 39 * depends on the renderer being initialized (e.g. the 40 40 * `core:ready` subscriber that kicks off extension loading) can trust 41 41 * the renderer's subscribers are in place. 42 - * 43 - * See docs/v1-removal-plan.md — Phase 2.5 #1. 44 42 */ 45 43 46 44 import { BrowserWindow } from 'electron';
+2 -2
backend/electron/protocol.ts
··· 623 623 } 624 624 625 625 // peek://test/{path} resolves to app/_test/ — the privileged test 626 - // fixture renderer used by the Playwright suite (v1-removal Phase 2). 627 - // Only launched when E2E_TEST=true, via test-fixture-glue.ts. 626 + // fixture renderer used by the Playwright suite. Only launched when 627 + // E2E_TEST=true, via test-fixture-glue.ts. 628 628 if (host === 'test') { 629 629 const testPath = pathname || 'index.html'; 630 630 const absolutePath = path.resolve(rootDir, 'app', '_test', testPath);
+4 -8
backend/electron/session.ts
··· 1 1 /** 2 2 * Session Snapshot — Save/Restore window state 3 3 * 4 - * Phase 1: Save session snapshot on quit. 5 - * Phase 2: Restore session snapshot on startup. 6 - * Phase 3: Periodic autosave timer + manual save/restore commands. 7 - * Phase 4: Crash recovery dialog, snapshot validation, error handling. 8 - * 9 - * Captures all visible user windows and writes to feature_settings 10 - * using synchronous better-sqlite3 APIs (safe for before-quit handler). 11 - * On startup, reads the snapshot and recreates windows. 4 + * Captures all visible user windows and writes to feature_settings using 5 + * synchronous better-sqlite3 APIs (safe for before-quit handler). On startup, 6 + * reads the snapshot and recreates windows. Periodic autosave timer + manual 7 + * save/restore commands; crash recovery dialog with snapshot validation. 12 8 * 13 9 * NOTE: Pure logic shared copy lives in app/lib/session.js for frontend/Tauri use. 14 10 */
+5 -5
backend/electron/tile-api.d.ts
··· 10 10 * - docs/tile-api.md (reference documentation) 11 11 * 12 12 * This file does NOT auto-generate — update manually when tile-preload.ts or 13 - * tile-ipc.ts adds/removes surfaces. The surface is stable post-Phase 4. 13 + * tile-ipc.ts adds/removes surfaces. 14 14 * 15 15 * Usage in a tile: 16 16 * /// <reference path="../../backend/electron/tile-api.d.ts" /> ··· 650 650 adblocker: TileAdblocker; 651 651 darkMode: TileDarkMode; 652 652 653 - // ── Removed v1 shims (Phase 4) — these throw if called ──────────── 653 + // ── Removed v1 shims — these throw if called ────────────────────── 654 654 655 - /** @deprecated Removed in Phase 4. Use api.pubsub.publish. */ 655 + /** @deprecated Removed. Use api.pubsub.publish. */ 656 656 publish(...args: unknown[]): never; 657 - /** @deprecated Removed in Phase 4. Use api.pubsub.subscribe. */ 657 + /** @deprecated Removed. Use api.pubsub.subscribe. */ 658 658 subscribe(...args: unknown[]): never; 659 - /** @deprecated Removed in Phase 4. Use api.window.close(). */ 659 + /** @deprecated Removed. Use api.window.close(). */ 660 660 closeWindow(...args: unknown[]): never; 661 661 662 662 // ── Capability-gated surfaces ──────────────────────────────────────
+2 -2
backend/electron/tile-ipc.ts
··· 466 466 // ── Tile Ready ── 467 467 // 468 468 // Private lifecycle IPC (`tile:lifecycle:ready` / `tile:lifecycle:shutdown`) 469 - // moved to tile-lifecycle.ts in Phase 4. See 470 - // docs/pubsub-state-machine.md §Topics that are NOT pubsub. 469 + // lives in tile-lifecycle.ts. See docs/pubsub-state-machine.md 470 + // §Topics that are NOT pubsub. 471 471 472 472 // ── PubSub ── 473 473
-2
backend/electron/tile-launcher.ts
··· 547 547 * - Add the window to the `tileWindows` registry so the pubsub 548 548 * broadcaster in main.ts forwards messages to it. 549 549 * - Wire the same close/revoke cleanup the regular launcher uses. 550 - * 551 - * See docs/v1-removal-plan.md — Phase 1a/1b. 552 550 */ 553 551 export function registerTrustedBuiltinWindow( 554 552 tileId: string,
+3 -3
backend/electron/tile-lifecycle.test.ts
··· 1 1 /** 2 - * Unit tests for tile-lifecycle.ts — Phase 4. 2 + * Unit tests for tile-lifecycle.ts. 3 3 * 4 4 * Covers: 5 5 * - `tile:state-changed` System-topic emission on every transition. ··· 246 246 it('a subscriber registered at t=0 boot receives a cmd:register published immediately after', () => { 247 247 // This simulates the core subscriber (inside app/index.js → cmd 248 248 // background) landing synchronously during core init, BEFORE any 249 - // tile fires its first `cmd:register`. With the Phase 4 gate in 250 - // place, tiles can't transition REGISTERED→LOADING until bgWindow 249 + // tile fires its first `cmd:register`. With the bgWindow-ready gate 250 + // in place, tiles can't transition REGISTERED→LOADING until bgWindow 251 251 // signals ready — by that time these subscribers exist, so the 252 252 // first publishes are never dropped. 253 253 const received: unknown[] = [];
+5 -5
backend/electron/tile-lifecycle.ts
··· 12 12 * engine that applies those transitions against concrete electron 13 13 * primitives. 14 14 * 15 - * Phase 4: ownership of the private lifecycle IPC channels 16 - * (`tile:lifecycle:ready` / `tile:lifecycle:shutdown`) moves here 17 - * from tile-ipc.ts. Also introduces: 15 + * Owns the private lifecycle IPC channels 16 + * (`tile:lifecycle:ready` / `tile:lifecycle:shutdown`). Also provides: 18 17 * - `registerBgWindow()` + bgWindow-ready latch: the subscribe- 19 18 * before-publish invariant. No non-bgWindow tile may transition 20 19 * REGISTERED→LOADING until bgWindow signals `tile:lifecycle:ready`. ··· 123 122 // 124 123 // Implementation: a simple one-shot latch. Once bgWindow signals ready, 125 124 // the latch resolves and stays resolved for the rest of the process 126 - // lifetime. Crashing bgWindow mid-boot is a separate concern (Phase 7 127 - // timeout UX) — for Phase 4 a permanent latch is sufficient. 125 + // lifetime. Crashing bgWindow mid-boot is a separate concern (handled 126 + // by the load-on-dispatch timeout UX in tile-lazy); a permanent latch 127 + // is sufficient here. 128 128 129 129 interface BgWindowRegistration { 130 130 tileId: string;
+1 -1
backend/electron/tile-manifest.test.ts
··· 99 99 assert.strictEqual(detectManifestVersion({ manifestVersion: 1 }), 'v1'); 100 100 }); 101 101 102 - it('should detect v1 for legacy manifestVersion 2 (pre-v1-removal compat fields)', () => { 102 + it('should detect v1 for legacy manifestVersion 2 (pre-v3 compat fields)', () => { 103 103 assert.strictEqual(detectManifestVersion({ manifestVersion: 2 }), 'v1'); 104 104 }); 105 105 });
+7 -7
backend/electron/tile-manifest.ts
··· 428 428 // ─── Manifest Types ────────────────────────────────────────────────── 429 429 430 430 /** 431 - * Tile Manifest format (the v2 architecture, schema bumped to v3 with the 432 - * v1-removal milestone — `type`/`windowHints` compat fields dropped). 431 + * Tile Manifest format. Current schema version is 3; legacy `type` and 432 + * `windowHints` compat fields are no longer accepted by the validator. 433 433 */ 434 434 export interface TileManifestV2 { 435 435 /** Manifest version — must be 3 for tile manifests */ ··· 545 545 /** 546 546 * Detect whether a manifest JSON is v1 (legacy extension) or v2 (tile). 547 547 * 548 - * The v2 architecture's schema is currently version 3 (bumped with the 549 - * v1-removal milestone). v2 manifests are identified by having: 548 + * The v2 architecture's schema is currently version 3. v2 manifests are 549 + * identified by having: 550 550 * - `manifestVersion: 3`, OR 551 551 * - Both `tiles` array AND `capabilities` object present 552 552 * ··· 1164 1164 * that could be non-exhaustive is omitted — when the bypass is 1165 1165 * respected, the allowlist is never consulted. 1166 1166 * 1167 - * See `docs/v1-removal-plan.md` for the rationale: core renderers 1168 - * participate in the same tile mechanics as feature tiles, but do not 1169 - * require capability gating because they are trusted code. 1167 + * Core renderers (cmd, hud, page) participate in the same tile mechanics 1168 + * as feature tiles, but do not require capability gating because they 1169 + * are trusted bundled code. 1170 1170 */ 1171 1171 export function createTrustedBuiltinGrant(rendererId: string): CapabilityGrant { 1172 1172 const capabilities: TileCapabilities = {
+4 -9
backend/electron/tile-preload-no-request-registers.test.ts
··· 1 1 /** 2 - * Phase 5: replay machinery removal. 3 - * 4 2 * Asserts the compiled tile-preload.cjs carries no reference to the 5 3 * deleted `cmd:request-registers` replay topic, the `registeredPayloads` 6 - * cache, or the `ensureCmdRequestRegistersListener` installer. Phase 4 7 - * made the subscribe-before-publish invariant load-bearing (Core's 8 - * subscribers are live before any tile reaches `ready`), so the replay 9 - * topic has no job and must not linger as dead code. 10 - * 11 - * Mirrors the "tile-ipc.js does not register a tile:command:result 12 - * handler" style assertion from tile-ipc.test.ts (Phase 3). 4 + * cache, or the `ensureCmdRequestRegistersListener` installer. The 5 + * subscribe-before-publish invariant is load-bearing (Core's subscribers 6 + * are live before any tile reaches `ready`), so the replay topic has 7 + * no job and must not linger as dead code. 13 8 */ 14 9 15 10 import { describe, it } from 'node:test';
+47 -44
backend/electron/tile-preload-stubs.test.ts
··· 1 1 /** 2 2 * tile-preload-stubs.test.ts 3 3 * 4 - * Phase 4: Verify that removed v1-compat shim names on tile-preload expose 5 - * hard-fail stubs that throw the correct error messages. These tests import 6 - * the compiled preload and exercise only the stub detection logic — we don't 7 - * need a full Electron environment because the stubs are plain arrow functions 8 - * registered on the api object before contextBridge.exposeInMainWorld runs. 4 + * Verify that removed v1-compat shim names on tile-preload expose 5 + * hard-fail stubs that throw the correct error messages. These tests 6 + * import the compiled preload and exercise only the stub detection 7 + * logic — we don't need a full Electron environment because the stubs 8 + * are plain arrow functions registered on the api object before 9 + * contextBridge.exposeInMainWorld runs. 9 10 * 10 - * Strategy: rather than trying to invoke the preload (which needs ipcRenderer), 11 - * we parse the compiled JS text to assert that each removed name: 11 + * Strategy: rather than trying to invoke the preload (which needs 12 + * ipcRenderer), we parse the compiled JS text to assert that each 13 + * removed name: 12 14 * 1. Is assigned a function (not an alias to a compat implementation) 13 - * 2. Contains a throw with the '[tile-preload]' prefix and 'Phase 4' marker 15 + * 2. Contains a throw with the '[tile-preload]' prefix and a 16 + * 'removed' marker 14 17 * 15 - * This is a static analysis test — it is fast, offline, and does not require 16 - * Electron's IPC to be wired up. 18 + * This is a static analysis test — it is fast, offline, and does not 19 + * require Electron's IPC to be wired up. 17 20 */ 18 21 19 22 import { test, describe } from 'node:test'; ··· 36 39 37 40 function hasHardFailStub(name: string): boolean { 38 41 if (!src) return true; // skip when file not compiled yet 39 - // Each stub should throw with '[tile-preload]' and 'Phase 4' 42 + // Each stub should throw with '[tile-preload]' and a 'removed' marker. 40 43 // The stub pattern looks like: 41 - // api.xxx = (..._args) => { throw new Error('[tile-preload] ... Phase 4 ...') } 44 + // api.xxx = (..._args) => { throw new Error('[tile-preload] ... removed ...') } 42 45 // or: 43 - // xxx: (..._args) => { throw new Error('[tile-preload] ... Phase 4 ...') } 46 + // xxx: (..._args) => { throw new Error('[tile-preload] ... removed ...') } 44 47 const stubRegex = new RegExp( 45 - `['"]?${name.replace('.', '\\.')}['"]?\\s*[:=]\\s*[^;{]*=>\\s*\\{[^}]*\\[tile-preload\\][^}]*Phase 4[^}]*\\}`, 48 + `['"]?${name.replace('.', '\\.')}['"]?\\s*[:=]\\s*[^;{]*=>\\s*\\{[^}]*\\[tile-preload\\][^}]*removed[^}]*\\}`, 46 49 'm', 47 50 ); 48 - // Fallback: look for the error string containing the name 51 + // Fallback: look for the error string containing the name + 'removed' 49 52 const errorRegex = new RegExp( 50 - `\\[tile-preload\\][^'"]*${name.replace('.', '\\.')}[^'"]*removed in Phase 4`, 53 + `\\[tile-preload\\][^'"]*${name.replace('.', '\\.')}[^'"]*removed`, 51 54 'm', 52 55 ); 53 56 return stubRegex.test(src) || errorRegex.test(src); 54 57 } 55 58 56 - // NOTE: api.publish / api.subscribe were INTENDED to be hard-fail stubs in 57 - // Phase 4, but several shared libraries in app/ (app/index.js, app/hud/ 58 - // widgets/*, app/lib/tag-action-affordances.js, app/settings/settings.js) 59 - // still call them directly. They were restored as aliases to 60 - // api.pubsub.publish / api.pubsub.subscribe so those shared libs keep 61 - // working. Tracked in docs/tasks.md for a proper sweep later. 59 + // NOTE: api.publish / api.subscribe were INTENDED to be hard-fail stubs, 60 + // but several shared libraries in app/ (app/index.js, app/hud/widgets/*, 61 + // app/lib/tag-action-affordances.js, app/settings/settings.js) still call 62 + // them directly. They were restored as aliases to api.pubsub.publish / 63 + // api.pubsub.subscribe so those shared libs keep working. Tracked in 64 + // docs/tasks.md for a proper sweep later. 62 65 63 - describe('Phase 4 hard-fail stubs — api.closeWindow', () => { 64 - test('api.closeWindow stub contains Phase 4 error', () => { 66 + describe('hard-fail stubs — api.closeWindow', () => { 67 + test('api.closeWindow stub throws with removed marker', () => { 65 68 assert.ok( 66 69 hasHardFailStub('closeWindow'), 67 - 'api.closeWindow should have a hard-fail stub with Phase 4 error message', 70 + 'api.closeWindow should have a hard-fail stub with a removed marker in the error message', 68 71 ); 69 72 }); 70 73 }); 71 74 72 - describe('Phase 4 hard-fail stubs — api.modes', () => { 75 + describe('hard-fail stubs — api.modes', () => { 73 76 for (const method of ['getWindowMode', 'setMajorMode', 'listModes', 'getCommandContext', 'onModeChange']) { 74 - test(`api.modes.${method} stub contains Phase 4 error`, () => { 77 + test(`api.modes.${method} stub throws with removed marker`, () => { 75 78 assert.ok( 76 79 hasHardFailStub(method), 77 - `api.modes.${method} should have a hard-fail stub with Phase 4 error message`, 80 + `api.modes.${method} should have a hard-fail stub with a removed marker in the error message`, 78 81 ); 79 82 }); 80 83 } 81 84 }); 82 85 83 - describe('Phase 4 hard-fail stubs — api.files', () => { 86 + describe('hard-fail stubs — api.files', () => { 84 87 for (const method of ['save', 'open', 'readFromPath', 'writeToPath']) { 85 - test(`api.files.${method} stub contains Phase 4 error`, () => { 88 + test(`api.files.${method} stub throws with removed marker`, () => { 86 89 assert.ok( 87 90 hasHardFailStub(method), 88 - `api.files.${method} should have a hard-fail stub with Phase 4 error message`, 91 + `api.files.${method} should have a hard-fail stub with a removed marker in the error message`, 89 92 ); 90 93 }); 91 94 } 92 95 }); 93 96 94 - describe('Phase 4 hard-fail stubs — api.settings.getKey/setKey', () => { 95 - test('api.settings.getKey stub contains Phase 4 error', () => { 97 + describe('hard-fail stubs — api.settings.getKey/setKey', () => { 98 + test('api.settings.getKey stub throws with removed marker', () => { 96 99 assert.ok( 97 100 hasHardFailStub('getKey'), 98 - 'api.settings.getKey should have a hard-fail stub with Phase 4 error message', 101 + 'api.settings.getKey should have a hard-fail stub with a removed marker in the error message', 99 102 ); 100 103 }); 101 104 102 - test('api.settings.setKey stub contains Phase 4 error', () => { 105 + test('api.settings.setKey stub throws with removed marker', () => { 103 106 assert.ok( 104 107 hasHardFailStub('setKey'), 105 - 'api.settings.setKey should have a hard-fail stub with Phase 4 error message', 108 + 'api.settings.setKey should have a hard-fail stub with a removed marker in the error message', 106 109 ); 107 110 }); 108 111 }); 109 112 110 - // NOTE: Phase 4 initially replaced these datastore compat methods with 111 - // hard-fail stubs, but feature code was never migrated off them — the 112 - // stubs broke every tile UI. The methods were restored as thin routing 113 - // wrappers to the legacy `datastore-*` IPC channels. Tracked in 114 - // docs/tasks.md under "Phase 4 datastore restoration". This test block 115 - // is intentionally left out until we finish the strict migration and 116 - // can reintroduce the hard-fail stubs. 113 + // NOTE: the datastore compat methods were initially intended as hard-fail 114 + // stubs, but feature code was never migrated off them — the stubs broke 115 + // every tile UI. The methods were restored as thin routing wrappers to 116 + // the legacy `datastore-*` IPC channels. Tracked in docs/tasks.md under 117 + // "datastore restoration". This test block is intentionally left out 118 + // until we finish the strict migration and can reintroduce the hard-fail 119 + // stubs. 117 120 118 - describe('Phase 4 — strict surfaces still present', () => { 121 + describe('strict surfaces still present', () => { 119 122 test('api.datastore.get strict surface still present', () => { 120 123 if (!src) return; 121 124 assert.ok(
+55 -87
backend/electron/tile-preload.cts
··· 73 73 return validationPromise; 74 74 } 75 75 76 - // ─── Phase 4: v1-compat violation logger REMOVED ───────────────────── 77 - // The wrapCompat / wrapCompatObject helpers and the compatSeen set were 78 - // used during Phases 1-3 to log first-use of v1 shims. All shims have 79 - // now been replaced with hard-fail stubs or strict-only paths, so the 80 - // logger infrastructure is no longer needed. 76 + // V1 shims are gone; every legacy api.* surface is either a strict-only 77 + // path through capability-gated `tile:*` IPC or a hard-fail stub that 78 + // throws with a migration hint. No first-use violation logger is needed. 81 79 82 80 // ─── API Builder ───────────────────────────────────────────────────── 83 81 ··· 202 200 // api.publish(topic, data) // v1-style 203 201 // api.pubsub.publish(topic, data) // v2-style (kept for clarity) 204 202 // 205 - // Scope (SYSTEM / SELF / GLOBAL) was removed in Phase 6 (see 203 + // The legacy SYSTEM / SELF / GLOBAL scope param is gone (see 206 204 // docs/pubsub-state-machine.md). Privilege is enforced by the 207 205 // capability grant's topic allowlist in main. 208 206 ··· 253 251 }; 254 252 } 255 253 256 - // Phase 4 intent was to remove top-level api.publish / api.subscribe as 257 - // hard-fail stubs, but several shared libraries in app/ (app/index.js, 258 - // app/hud/widgets/*, app/lib/tag-action-affordances.js, app/settings/ 259 - // settings.js) still call them directly. Restored as thin aliases to 260 - // api.pubsub.publish / api.pubsub.subscribe so those shared libs work 261 - // under tile-preload without touching every call site. 254 + // Top-level api.publish / api.subscribe are kept as thin aliases for 255 + // api.pubsub.publish / api.pubsub.subscribe — several shared libraries 256 + // in app/ (app/index.js, app/hud/widgets/*, app/lib/ 257 + // tag-action-affordances.js, app/settings/settings.js) call them 258 + // directly and the alias avoids touching every call site. 262 259 api.publish = publishImpl; 263 260 api.subscribe = subscribeImpl; 264 261 ··· 537 534 // 538 535 // Strict-only: every call routes through `tile:window:*` and is 539 536 // gated on the tile's `window` capability shape (or trustedBuiltin). 540 - // Legacy `window-*` IPC handlers were removed in v1-removal Phase 541 - // 3.7h. Tiles that call api.window.* must declare a `window` 542 - // capability in their manifest. `showSelf`/`hideSelf` and `resize` 543 - // (caller's own window only) remain ungated since the token itself 544 - // is the authority. 537 + // Tiles that call api.window.* must declare a `window` capability 538 + // in their manifest. `showSelf`/`hideSelf` and `resize` (caller's 539 + // own window only) remain ungated since the token itself is the 540 + // authority. 545 541 546 542 api.window = { 547 543 /** ··· 891 887 // 892 888 // Strict surfaces (get/set/query/extractPageContent) route through 893 889 // `tile:datastore:*` which enforces per-table capability. The v1-compat 894 - // helper methods were removed in Phase 4 and replaced with hard-fail stubs. 890 + // helper methods were replaced with hard-fail stubs. 895 891 896 892 const datastoreStrict = { 897 893 /** ··· 947 943 }; 948 944 949 945 // Named datastore methods that features rely on. Each routes to the 950 - // corresponding legacy `datastore-*` IPC channel in ipc.ts. Phase 4 951 - // originally turned these into hard-fail stubs, but feature code was 952 - // never migrated off them (Phase 3 explicitly said "keep API name"), 953 - // so the stubs broke every tile UI. Restored as thin routing wrappers. 946 + // corresponding legacy `datastore-*` IPC channel in ipc.ts. These are 947 + // thin routing wrappers, not strict capability-gated paths. 954 948 // 955 949 // Follow-up (tracked in docs/tasks.md): build equivalent strict 956 950 // `tile:datastore:*` handlers for the methods that don't have them ··· 1241 1235 }, 1242 1236 1243 1237 /** 1244 - * Phase 4: getKey REMOVED. Use api.settings.get(key) instead. 1238 + * Removed: use api.settings.get(key) instead. 1245 1239 */ 1246 1240 getKey: (..._args: unknown[]) => { 1247 - throw new Error('[tile-preload] api.settings.getKey removed in Phase 4; use api.settings.get'); 1241 + throw new Error('[tile-preload] api.settings.getKey removed; use api.settings.get'); 1248 1242 }, 1249 1243 1250 1244 /** 1251 - * Phase 4: setKey REMOVED. Use api.settings.set(key, value) instead. 1245 + * Removed: use api.settings.set(key, value) instead. 1252 1246 */ 1253 1247 setKey: (..._args: unknown[]) => { 1254 - throw new Error('[tile-preload] api.settings.setKey removed in Phase 4; use api.settings.set'); 1248 + throw new Error('[tile-preload] api.settings.setKey removed; use api.settings.set'); 1255 1249 }, 1256 1250 1257 1251 /** 1258 - * Cross-tile foreign-read: fetch another tile's stored setting 1259 - * value (read-only). Dual-path routing: 1260 - * 1261 - * - STRICT: when the tile's manifest declared 1262 - * `settings.readForeign` or `settingsForeign`, route through 1263 - * `tile:settings:get-foreign`. The main-process handler checks 1264 - * the allowlist and scopes the DB query to the named foreign 1265 - * tile. 1266 - * 1267 - * - V1-COMPAT: otherwise fall back to the legacy un-gated 1268 - * `feature-settings-get-key` channel. Preserves behaviour for 1269 - * manifests that haven't declared the new allowlist yet. 1270 - * 1271 - * The decision is made per-call because `grantedCapabilities` is 1272 - * populated asynchronously by `initialize()`. 1273 - */ 1274 - /** 1275 - * Phase 4: getExtKey v1-compat fallback REMOVED. Strict path only. 1276 - * Tile manifest must declare settings.readForeign to use this. 1252 + * Cross-tile foreign-read: fetch another tile's stored setting value 1253 + * (read-only). Strict path only — the tile's manifest must declare 1254 + * `settings.readForeign` (or top-level `settingsForeign`). Routes 1255 + * through `tile:settings:get-foreign`; the main-process handler 1256 + * checks the allowlist and scopes the DB query to the named foreign 1257 + * tile. 1277 1258 * 1278 - * Returns a normalised `{success, data, value, error}` object so that 1279 - * callers using either the legacy `{success, data}` shape or the strict 1280 - * `{value, error}` shape both work correctly. 1259 + * Returns a normalised `{success, data, value, error}` object so 1260 + * that callers using either the legacy `{success, data}` shape or 1261 + * the strict `{value, error}` shape both work correctly. 1281 1262 */ 1282 1263 getExtKey: async (extId: unknown, key: unknown) => { 1283 1264 if (!extId || !key) { 1284 1265 return { success: false, data: undefined, value: undefined, error: 'extId and key are required' }; 1285 1266 } 1286 1267 if (!hasSettingsForeignCapability()) { 1287 - throw new Error('[tile-preload] api.settings.getExtKey requires settings.readForeign capability in manifest (Phase 4: v1-compat fallback removed)'); 1268 + throw new Error('[tile-preload] api.settings.getExtKey requires settings.readForeign capability in manifest'); 1288 1269 } 1289 1270 // Handler returns { success, data } (native shape) — normalise into the 1290 1271 // legacy { success, data, value } envelope so callers using either ··· 1325 1306 1326 1307 // ── Shortcuts ───────────────────────────────────────────────────── 1327 1308 // 1328 - // Dual-path implementation: 1329 - // 1330 - // - STRICT: when the tile's manifest declared a `shortcuts` capability 1331 - // (`true` or `{ keys: [...] }`), route through `tile:shortcuts:*`. 1332 - // The main-process handlers validate the token + capability + keys 1333 - // allowlist. This is the path consuming features get automatically 1334 - // once their manifest declares `capabilities.shortcuts`. 1335 - // 1336 - // - V1-COMPAT: when no shortcuts capability was declared (or the token 1337 - // hasn't been validated yet), fall back to the legacy 1338 - // `registershortcut` / `unregistershortcut` IPC channels. Those 1339 - // channels remain available for the Phase 3/4 migration per the 1340 - // tile-preload trimming plan. 1309 + // Strict-only: route through `tile:shortcuts:*`. The main-process 1310 + // handlers validate the token + capability + keys allowlist. Tiles 1311 + // that call api.shortcuts.* must declare a `shortcuts` capability 1312 + // (`true` or `{ keys: [...] }`) in their manifest; missing capability 1313 + // throws. 1341 1314 // 1342 - // The decision is made per-call (not at API-build time) because 1315 + // The capability check is per-call (not at API-build time) because 1343 1316 // `grantedCapabilities` is populated asynchronously by `initialize()`. 1344 1317 1345 1318 function rndm(): string { ··· 1398 1371 }); 1399 1372 } 1400 1373 1401 - // Phase 4: registerShortcutCompat / unregisterShortcutCompat REMOVED. 1402 - 1403 - // Phase 4: v1-compat shortcuts fallback REMOVED. Strict path only. 1404 - // Tiles must declare `shortcuts` in their manifest capabilities. 1374 + // Strict-only shortcuts surface. Tiles must declare `shortcuts` in 1375 + // their manifest capabilities. 1405 1376 api.shortcuts = { 1406 1377 register: (shortcut: unknown, cb: unknown, options: unknown) => { 1407 1378 if (!hasShortcutsCapability()) { 1408 - throw new Error('[tile-preload] api.shortcuts.register requires shortcuts capability in manifest (Phase 4: v1-compat fallback removed)'); 1379 + throw new Error('[tile-preload] api.shortcuts.register requires shortcuts capability in manifest'); 1409 1380 } 1410 1381 registerShortcutStrict(shortcut, cb, options); 1411 1382 }, 1412 1383 unregister: (shortcut: unknown, options: unknown) => { 1413 1384 if (!hasShortcutsCapability()) { 1414 - throw new Error('[tile-preload] api.shortcuts.unregister requires shortcuts capability in manifest (Phase 4: v1-compat fallback removed)'); 1385 + throw new Error('[tile-preload] api.shortcuts.unregister requires shortcuts capability in manifest'); 1415 1386 } 1416 1387 unregisterShortcutStrict(shortcut, options); 1417 1388 }, 1418 1389 }; 1419 1390 1420 - // ── Files (Phase 4: v1-compat REMOVED) ─────────────────────────── 1391 + // ── Files (removed v1-compat) ──────────────────────────────────── 1421 1392 // Use api.dialogs.save / api.dialogs.open for file dialogs, 1422 1393 // and api.filesystem.read / api.filesystem.write for path operations. 1423 1394 api.files = { 1424 - save: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.save removed in Phase 4; use api.dialogs.save'); }, 1425 - open: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.open removed in Phase 4; use api.dialogs.open'); }, 1426 - readFromPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.readFromPath removed in Phase 4; use api.filesystem.read'); }, 1427 - writeToPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.writeToPath removed in Phase 4; use api.filesystem.write'); }, 1395 + save: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.save removed; use api.dialogs.save'); }, 1396 + open: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.open removed; use api.dialogs.open'); }, 1397 + readFromPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.readFromPath removed; use api.filesystem.read'); }, 1398 + writeToPath: (..._args: unknown[]) => { throw new Error('[tile-preload] api.files.writeToPath removed; use api.filesystem.write'); }, 1428 1399 }; 1429 1400 1430 - // ── Modes (Phase 4: REMOVED — was unused per trimming plan §1 row 6) ── 1401 + // ── Modes (removed — was unused) ───────────────────────────────── 1431 1402 // api.modes was confirmed zero-usage. Hard-fail stubs catch any survivor. 1432 1403 api.modes = { 1433 - getWindowMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused); use api.context.*'); }, 1434 - setMajorMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); }, 1435 - listModes: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); }, 1436 - getCommandContext: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); }, 1437 - onModeChange: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed in Phase 4 (was unused)'); }, 1404 + getWindowMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused); use api.context.*'); }, 1405 + setMajorMode: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); }, 1406 + listModes: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); }, 1407 + getCommandContext: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); }, 1408 + onModeChange: (..._args: unknown[]) => { throw new Error('[tile-preload] api.modes removed (was unused)'); }, 1438 1409 }; 1439 1410 1440 1411 // ── Context ─────────────────────────────────────────────────────── ··· 1527 1498 // through `tile:dialogs:*` when the tile declared a `dialogs` 1528 1499 // capability (`true` or `{ types: [...] }`). When the capability is 1529 1500 // not declared the strict handler rejects with `dialogs capability 1530 - // not granted`. 1531 - // 1532 - // v1-compat continues through `api.files.save` / `api.files.open`, 1533 - // which directly hit the un-gated `file-save-dialog` / `file-open- 1534 - // dialog` channels. The two surfaces coexist until Phase 4 removal. 1501 + // not granted`. The v1 `api.files.save` / `api.files.open` surface 1502 + // is a hard-fail stub directing callers here. 1535 1503 1536 1504 api.dialogs = { 1537 1505 save: (content: unknown, options: unknown = {}) => { ··· 1806 1774 set: (mode: string) => ipcRenderer.invoke('tile:darkMode:set', { token: tileToken, mode }), 1807 1775 }; 1808 1776 1809 - // ── closeWindow (Phase 4: REMOVED — was unused per trimming plan §1 row 8) ── 1777 + // ── closeWindow (removed — was unused) ─────────────────────────── 1810 1778 // Use api.window.close() instead. 1811 1779 api.closeWindow = (..._args: unknown[]) => { 1812 - throw new Error('[tile-preload] api.closeWindow removed in Phase 4 (was unused); use api.window.close()'); 1780 + throw new Error('[tile-preload] api.closeWindow removed (was unused); use api.window.close()'); 1813 1781 }; 1814 1782 1815 1783 // ── Screen (always available; read-only) ──────────────────────────