Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apps/web/alchemy.run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ import {
optimisticDefinitionAddFlag,
optimisticDefinitionDeleteFlag,
optimisticEditsFlag,
panoBaseFeedFlag,
panoDraftSaveFlag,
panoStampWaveFlag,
profileCanvasFlag,
Expand Down Expand Up @@ -91,10 +90,6 @@ export default Alchemy.Stack(
// The mecmua write-path dark-ship flag, default-off (#2497, epic #2467) — the
// single seam mecmua.publish + mecmua.saveDraft gate behind until a human release.
yield* mecmuaWriteFlag(flagship.appId);
// The base-feed / viewer-overlay split dark-ship flag, default-off (#2322,
// epic #2316 leg B) — the single seam the GET-able base feed + PostOverlay read
// gate behind until a human release.
yield* panoBaseFeedFlag(flagship.appId);
// The earned-authorship loop (çaylak→yazar) dark-ship flag, default-off
// (#1204, epic #1202) — the single seam the authorship-loop epic gates behind.
yield* authorshipLoopFlag(flagship.appId);
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/pano/PanoPostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ export function PanoPostCard({
rank?: number;
onHide?: (id: string) => void;
/**
* Base + overlay composition (#2323, leg B, dark behind `pano-base-feed`). When set,
* the viewer scalars (`myVote`/`isSaved`) render through the identity guard
* Base + overlay composition (#2323, leg B). Set on the viewer-invariant base feed
* rows: the viewer scalars (`myVote`/`isSaved`) render through the identity guard
* (`panoFeedOverlay`): neutral while the session is still resolving (base painted,
* overlay pending), then the viewer's own scalars once the session lands under a
* matching identity — so a stale/foreign overlay (e.g. a snapshot's prior-identity
* scalars) never paints. Off (the default / flag-off) ⇒ scalars read straight off the
* post, byte-identical to today.
* scalars) never paints. Unset (the default) ⇒ scalars read straight off the post,
* the authed path (`?sort=saved` / detail) whose read already stamps the viewer.
*/
compose?: boolean;
/**
Expand Down
13 changes: 0 additions & 13 deletions apps/web/src/flags/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,6 @@ export const PHOENIX_PANO_STAMP_WAVE = "phoenix-pano-stamp-wave";
*/
export const MECMUA_WRITE = "mecmua-write";

/**
* Base-feed / viewer-overlay split dark-ship flag (#2322, epic #2316 leg B). The
* SINGLE seam every leg-B surface gates behind — the server split (the GET-able
* viewer-invariant base feed + the authed `PostOverlay` read), the client
* composition (#2323), and the edge cache (#2324) reuse this one key. Default-off
* so the whole base/overlay path ships dark until a human flips it at release (ADR
* 0083): with it off the `GET /fate/pano/feed` route 404s, the `PostOverlay` source
* resolves inert (null scalars), and the existing per-viewer `posts` feed is the
* unchanged source of truth. Its own `pano-` key (the surface is pano-only).
*/
export const PANO_BASE_FEED = "pano-base-feed";

/**
* mecmua public-read dark-ship flag (#2498, epic #2467). The SINGLE seam the
* anonymous read surface gates behind — the `GET /fate/mecmua/post/:slug` route's
Expand Down Expand Up @@ -322,7 +310,6 @@ export const DECLARED_FLAGS: readonly FlagDeclaration[] = [
{key: PHOENIX_SOZLUK_STAMP_WAVE, defaultValue: false},
{key: PHOENIX_PANO_STAMP_WAVE, defaultValue: false},
{key: MECMUA_WRITE, defaultValue: false},
{key: PANO_BASE_FEED, defaultValue: false},
{key: MECMUA_PUBLIC_READ, defaultValue: false},
{key: MECMUA_FEED, defaultValue: false},
{key: PHOENIX_AUTHORSHIP_LOOP, defaultValue: false},
Expand Down
20 changes: 2 additions & 18 deletions apps/web/src/pages/PanoFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {useSetPanoSubnavContent} from "../components/pano/PanoSubnavLayout";
import {Screen} from "../fate/Screen";
import {FEED_SNAPSHOT_ENABLED} from "../fate/snapshot";
import {LoadMoreButton} from "../fate/wire";
import {MEMBER_MUTE, PANO_BASE_FEED, PHOENIX_NAV_IA} from "../flags/keys";
import {MEMBER_MUTE, PHOENIX_NAV_IA} from "../flags/keys";
import {useFlag} from "../flags/useFlag";
import {markFeedPaintOnce} from "../lib/feedPerf";
import {
Expand Down Expand Up @@ -138,13 +138,6 @@ function FeedContent({
pending: boolean;
chrome: Chrome;
}) {
// Base + overlay composition (#2323, leg B), dark behind `pano-base-feed`. On ⇒ the
// rows compose the viewer scalars through the identity guard (`PanoPostCard compose`):
// base content paints de-gated from the session, `myVote`/`isSaved` stay neutral until
// the overlay lands under the confirmed identity. Off (default) ⇒ scalars read straight
// off the post, byte-identical to today. Sort feed only — `?sort=saved` is inherently
// per-viewer and stays on the authed path (`SavedContent`), out of the base/overlay split.
const {value: composeOverlay} = useFlag(PANO_BASE_FEED, false);
// Member-mute (#3117), dark behind `member-mute`. Read once here and threaded to every row
// so a card can hide a muted member's post + offer the "sustur" action without each card
// re-evaluating the flag. Off (default) ⇒ no mute surface, byte-identical to today.
Expand All @@ -169,7 +162,6 @@ function FeedContent({
host={host}
pending={pending}
chrome={chrome}
compose={composeOverlay}
muteEnabled={muteEnabled}
/>
);
Expand All @@ -184,14 +176,12 @@ function FeedRows({
host,
pending,
chrome,
compose,
muteEnabled,
}: {
connection: PostConnection;
host?: string;
pending: boolean;
chrome: Chrome;
compose: boolean;
muteEnabled: boolean;
}) {
const [items, loadNext] = useLiveListView(PostConnectionView, connection);
Expand Down Expand Up @@ -222,13 +212,7 @@ function FeedRows({
}
>
{items.map(({node}, i) => (
<PanoPostCard
key={node.id}
post={node}
rank={i + 1}
compose={compose}
muteEnabled={muteEnabled}
/>
<PanoPostCard key={node.id} post={node} rank={i + 1} compose muteEnabled={muteEnabled} />
))}
</div>
{loadNext ? (
Expand Down
53 changes: 16 additions & 37 deletions apps/web/tests/integration/pano-base-feed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@
* request and a signed-in one — the whole point of a viewer-invariant, cacheable
* base. An anon GET succeeds with no session (and the response sets no cookie), so
* it does no session validation.
* 2. With the leg-B flag OFF (the default), the GET route 404s — the surface ships
* dark. The existing per-viewer `posts` feed on `POST /fate` still stamps the
* signed-in viewer's `myVote`/`isSaved` (the split changed nothing there).
* 2. The base feed and the per-viewer `posts` feed on `POST /fate` are separate
* surfaces: the `posts` feed still stamps the signed-in viewer's `myVote`/`isSaved`
* (the split changed nothing there).
*
* The base feed is dark behind `pano-base-feed` (default-off). Integration deploys run
* `ENVIRONMENT=development` (`_integration.ts` → `ensureIntegrationEnv`), so the dev-only
* override wrapper (`FlagsDevOverrideLive`, #622) is installed — this test flips the flag
* on for a single request by sending the `phoenix_flag_overrides` cookie. The default-off
* gate is the shipped state; the cookie only unlocks the flag-ON path for this test.
* The base feed serves unconditionally — its leg-B dark-ship flag graduated to on@100%
* and was retired (ADR 0136), so the split is now the source of truth.
*
* Shared-stage NS isolation (ADR 0104): every seeded title/host carries the `${NS}-`
* prefix and the base feed is HOST-scoped to this file's `${NS}.example.com`, so the read
Expand All @@ -31,13 +28,6 @@ const h = sharedStack();
const NS = nsToken(import.meta.url);
const FEED_HOST = `${NS}.example.com`;

// The dev-override cookie that forces `pano-base-feed` on for a single request (#622 —
// `phoenix_flag_overrides`, a URL-encoded JSON `{key: boolean}` map). Only takes effect
// because integration deploys run with `ENVIRONMENT=development`.
const BASE_FEED_ON_COOKIE = `phoenix_flag_overrides=${encodeURIComponent(
JSON.stringify({"pano-base-feed": true}),
)}`;

interface BaseNode {
__typename: string;
id: string;
Expand Down Expand Up @@ -89,13 +79,8 @@ beforeAll(async () => {

describe("pano base feed — GET surface + viewer-invariant base (#2322)", () => {
it("serves the base feed over GET with sort/host in the URL, no viewer-scoped field", async () => {
const res = await getBaseFeed(`sort=new&host=${FEED_HOST}&first=50`, BASE_FEED_ON_COOKIE);
// Name the flag in the failure so an ineffective override fails HERE with a clear
// cause, not as a confusing shape mismatch downstream.
expect(
res.status,
`GET base feed expected 200 with the flag ON, got ${res.status} — pano-base-feed override did not take effect`,
).toBe(200);
const res = await getBaseFeed(`sort=new&host=${FEED_HOST}&first=50`);
expect(res.status).toBe(200);
const conn = (await res.json()) as Connection<BaseNode>;
const titles = conn.items.map((e) => e.node.title).sort();
expect(titles).toEqual([`${NS}-alpha`, `${NS}-bravo`, `${NS}-charlie`]);
Expand All @@ -107,7 +92,7 @@ describe("pano base feed — GET surface + viewer-invariant base (#2322)", () =>
});

it("paginates via the URL cursor (first + nextCursor)", async () => {
const first = await getBaseFeed(`sort=new&host=${FEED_HOST}&first=2`, BASE_FEED_ON_COOKIE);
const first = await getBaseFeed(`sort=new&host=${FEED_HOST}&first=2`);
expect(first.status).toBe(200);
const page1 = (await first.json()) as Connection<BaseNode>;
expect(page1.items).toHaveLength(2);
Expand All @@ -117,7 +102,6 @@ describe("pano base feed — GET surface + viewer-invariant base (#2322)", () =>

const second = await getBaseFeed(
`sort=new&host=${FEED_HOST}&first=2&after=${encodeURIComponent(cursor!)}`,
BASE_FEED_ON_COOKIE,
);
expect(second.status).toBe(200);
const page2 = (await second.json()) as Connection<BaseNode>;
Expand All @@ -129,8 +113,8 @@ describe("pano base feed — GET surface + viewer-invariant base (#2322)", () =>

it("is byte-identical for an anonymous request and a signed-in one, and sets no cookie", async () => {
const query = `sort=new&host=${FEED_HOST}&first=50`;
const anon = await getBaseFeed(query, BASE_FEED_ON_COOKIE);
const authed = await getBaseFeed(query, `${viewer.cookie}; ${BASE_FEED_ON_COOKIE}`);
const anon = await getBaseFeed(query);
const authed = await getBaseFeed(query, viewer.cookie);
expect(anon.status).toBe(200);
expect(authed.status).toBe(200);
// No session validation: the base sets no cookie for either caller.
Expand All @@ -141,16 +125,11 @@ describe("pano base feed — GET surface + viewer-invariant base (#2322)", () =>
});
});

describe("pano base feed — dark behind the leg-B flag (#2322)", () => {
it("404s the GET route when the flag is off (default dark state)", async () => {
const res = await getBaseFeed(`sort=new&host=${FEED_HOST}`);
expect(res.status).toBe(404);
});

it("leaves the existing POST /fate posts feed unchanged (still stamps myVote when off)", async () => {
// Vote a seeded post as the viewer, then read the existing `posts` feed WITHOUT any
// flag override: the per-viewer stamp is untouched by the split — myVote rides as
// before. (The author never self-votes; the viewer votes the author's post.)
describe("pano base feed — the per-viewer posts feed is a separate surface (#2322)", () => {
it("leaves the existing POST /fate posts feed stamping myVote", async () => {
// Vote a seeded post as the viewer, then read the per-viewer `posts` feed on
// `POST /fate`: the split left that stamp untouched — myVote rides as before. (The
// author never self-votes; the viewer votes the author's post.)
const target = seeded[0]!;
const voted = await h.fate(
{kind: "mutation", name: "post.vote", input: {id: target}, select: ["id"]},
Expand Down Expand Up @@ -180,7 +159,7 @@ describe("pano base feed — dark behind the leg-B flag (#2322)", () => {

describe("pano base feed — edge-cache headers (#2324, ADR 0170)", () => {
it("stamps Cache-Control + Cache-Tag: pano-feed on the served base feed", async () => {
const res = await getBaseFeed(`sort=new&host=${FEED_HOST}&first=50`, BASE_FEED_ON_COOKIE);
const res = await getBaseFeed(`sort=new&host=${FEED_HOST}&first=50`);
expect(res.status).toBe(200);
// The TTL backstop + the purge tag the fanned-mutation seam targets (AC#1).
expect(res.headers.get("cache-control")).toContain("s-maxage=");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* The dark-ship default-=-safe-state invariant for mecmua public read (#2498, epic
* #2467). Inspected off the exported `MECMUA_PUBLIC_READ_FLAG` record (the same object
* the factory spreads into `FlagshipFlag`), so no alchemy resource is constructed —
* mirrors `pano-base-feed.invariant.test.ts` (#2322).
* mirrors `funnel-readout.invariant.test.ts` (#1589).
*/
import {assert, describe, it} from "@effect/vitest";
import {MECMUA_PUBLIC_READ} from "../../../src/flags/keys.ts";
Expand Down
26 changes: 0 additions & 26 deletions apps/web/worker/features/flagship/pano-base-feed.invariant.test.ts

This file was deleted.

42 changes: 3 additions & 39 deletions apps/web/worker/features/flagship/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
MECMUA_PUBLIC_READ,
MECMUA_WRITE,
MEMBER_MUTE,
PANO_BASE_FEED,
PANO_DRAFT_SAVE,
PHOENIX_ADMIN_CONSOLE,
PHOENIX_AUTHORSHIP_LOOP,
Expand Down Expand Up @@ -97,7 +96,6 @@ export const demoTargetingFlag = (appId: Input<string>) =>
});

export {
PANO_BASE_FEED,
PANO_DRAFT_SAVE,
PHOENIX_AUTHORSHIP_LOOP,
PHOENIX_BILDIRIM,
Expand All @@ -108,40 +106,6 @@ export {
PHOENIX_OPTIMISTIC_EDITS,
};

/**
* The base-feed / viewer-overlay split dark-ship flag config (#2322, epic #2316 leg
* B). The SINGLE seam every leg-B surface gates behind (server split, client
* composition #2323, edge cache #2324). Default-OFF so the whole base/overlay path
* reaches production dark — with it off the `GET /fate/pano/feed` route 404s and the
* `PostOverlay` source resolves inert, so the per-viewer `posts` feed is unchanged;
* flipping it on is the human release act (ADR 0083).
*
* Exported as a plain object so the default-=-safe-state invariant is
* unit-inspectable WITHOUT constructing the alchemy resource (mirrors
* `PANO_DRAFT_SAVE_FLAG`, #746).
*
* Per-flag metadata (the IaC ownership record `feature-flags-schema-lifecycle.md`
* asks for):
* - owner: pano (the feed read path)
* - originating: #2322 (epic: reload-to-feed-paint floor, #2316 leg B)
* - removal trigger: once the base/overlay feed graduates to on at 100% and stable
* for one release, retire the flag and inline the split.
*/
export const PANO_BASE_FEED_FLAG = {
key: PANO_BASE_FEED,
description:
"base-feed / viewer-overlay split dark-ship (#2322, epic #2316 leg B). owner: pano. removal: retire once on at 100% and stable.",
defaultVariation: "off",
variations: {off: false, on: true},
} as const;

/**
* A plain boolean kill-switch, no targeting rules. `appId` is resolved at deploy
* (see `demoTargetingFlag` for why it's a factory, not a module constant).
*/
export const panoBaseFeedFlag = (appId: Input<string>) =>
Cloudflare.Flagship.Flag("pano_base_feed", {appId, ...PANO_BASE_FEED_FLAG});

/**
* The pano `taslak` (draft-save) dark-ship flag config (#746) — the feature-flag
* substrate's first real consumer (ADR 0091/0093). Default-OFF so it reaches
Expand Down Expand Up @@ -730,7 +694,7 @@ export const emailDeliveryNoticeFlag = (appId: Input<string>) =>
* the authoring/publish path (#2497) ships behind its own seam.
*
* Exported as a plain object so the default-=-safe-state invariant is unit-inspectable
* WITHOUT constructing the alchemy resource (mirrors `PANO_BASE_FEED_FLAG`).
* WITHOUT constructing the alchemy resource (mirrors `PANO_DRAFT_SAVE_FLAG`).
*
* Per-flag metadata (`feature-flags-schema-lifecycle.md`):
* - owner: mecmua (the long-form read surface)
Expand Down Expand Up @@ -794,7 +758,7 @@ export const navIaFlag = (appId: Input<string>) =>
* Flipping it on is the human release act (ADR 0083).
*
* Exported as a plain object so the default-=-safe-state invariant is unit-inspectable
* WITHOUT constructing the alchemy resource (mirrors `PANO_BASE_FEED_FLAG`).
* WITHOUT constructing the alchemy resource (mirrors `PANO_DRAFT_SAVE_FLAG`).
*
* Per-flag metadata (`feature-flags-schema-lifecycle.md`):
* - owner: sözlük (the definition read path)
Expand Down Expand Up @@ -828,7 +792,7 @@ export const sozlukStampWaveFlag = (appId: Input<string>) =>
* not the pano feed (the #2322 base/overlay split).
*
* Exported as a plain object so the default-=-safe-state invariant is unit-inspectable
* WITHOUT constructing the alchemy resource (mirrors `PANO_BASE_FEED_FLAG`).
* WITHOUT constructing the alchemy resource (mirrors `PANO_DRAFT_SAVE_FLAG`).
*
* Per-flag metadata (`feature-flags-schema-lifecycle.md`):
* - owner: pano (the thread/comment read path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* The dark-ship default-=-safe-state invariant for the sözlük parallel-stamp-wave
* read collapse (#2709, epic #2567). Inspected off the exported `SOZLUK_STAMP_WAVE_FLAG`
* record (the same object the factory spreads into `FlagshipFlag`), so no alchemy
* resource is constructed — mirrors `pano-base-feed.invariant.test.ts`.
* resource is constructed — mirrors `funnel-readout.invariant.test.ts`.
*
* Load-bearing: with the default OFF the definition reads run their stamp wave at
* `concurrency: 1` (serial, byte-for-byte today) — so the containment is real, the
Expand Down
Loading
Loading