diff --git a/.glossary/TERMS.md b/.glossary/TERMS.md index 967a3e73d..3d42899a0 100644 --- a/.glossary/TERMS.md +++ b/.glossary/TERMS.md @@ -32,7 +32,7 @@ This file names the *what*. | bildirim | The notification **feature** (`features/bildirim`, epic #1666): the recipient-keyed store + read model behind the `/bildirimler` center page and the topbar **unread badge** — the `notification` D1 table, the `Notification` domain service, and the `bildirim.*` fate views/mutations (`list` / `unreadCount` / `markRead` / `markAllRead`). Ships dark behind the single default-off `phoenix-bildirim` flag every emitter sibling reuses. Turkish for "notification". | the **report (bildir)** feature (content-reporting for moderation — `bildir`, a different lexeme and module); an emitter (siblings #1695–#1699 produce notifications; this is the spine) | | divan | The proving-ground reviewer surface (`features/divan`): the gated destination where the established community (yazar + moderatör) reviews a çaylak's sandboxed work and promotes (mod-direct) or vouches (kefil) them toward yazar — work "goes before the divan". Reads the `sandboxBacklogWhere` backlog over a yazar-OR-mod gate. Its topbar entry (`divanTo` NavLink, `Topbar.tsx`) is a **destination** under the ratified nav element taxonomy (destination / primary action / utility / signal — wayfinder map #2583, fork #2586). Turkish for "council/court". | a widening of inline çaylak visibility — it is a gated **destination** that reads the sandbox backlog, not a change to who sees sandboxed items inline; not a **primary action** (it goes nowhere near the `+ gönderi` CTA class) | | divan section nav (boxed-pill tabs) | The intra-`/divan` section switcher (`.kp-divan__nav-tab`, `DivanPage.tsx` / `Divan.css`): the çaylaklar (roster) ↔ raporlar (mod queue) tab row shown when `raporlarVisible`. Each tab is a **boxed pill** — `1px` border + `r-sm` + a resting `surface` fill, switching to an `accent-faint` fill + `accent` border on `aria-current`. Under the ratified nav taxonomy (map #2583, fork #2586) these are intra-destination **destination** switchers, and the map flags the **resting boxed fill** as a containment-law violation candidate (the nav-surface inventory #2585): a section tab may paint accent on the *active* state (transient state paint is legal) but should not carry a boxed resting chrome. | the divan topbar entry (that is the destination-level `divanTo` link, one level up); a containment-legal transient accent (the *violation* is the resting boxed fill, not the active-state accent) | -| funnel / conversion funnel | The conversion-funnel readout (`features/funnel`, #1589): the mod-gated, dark-shipped surface that measures the human **çaylak→yazar** journey. Its tracer read is the **tier population** — a humans-only (`user.type = 'human'`) `GROUP BY tier` count of how many accounts sit at each rung of the authorship ladder (`FunnelSummary`, the `funnel.summary` fate view). Served behind the default-off `phoenix-funnel-readout` flag, read-gated by the `ViewFunnel` capability (founder/mod). The Phase-2 rate/time metrics extend this same service. | a public metrics page (founder/mod only, flag-gated); a per-product stat (that is **stats**, the landing counts); an agent/bot metric (humans-only by construction) | +| funnel / conversion funnel | The conversion-funnel readout (`features/funnel`, #1589): the mod-gated, dark-shipped surface that measures the human **çaylak→yazar** journey. Its tracer read is the **tier population** — a humans-only (`user.type = 'human'`) `GROUP BY tier` count of how many accounts sit at each rung of the authorship ladder (`FunnelSummary`, the `funnel.summary` fate view). Read-gated by the `ViewFunnel` capability (founder/mod). The Phase-2 rate/time metrics extend this same service. | a public metrics page (founder/mod only); a per-product stat (that is **stats**, the landing counts); an agent/bot metric (humans-only by construction) | | depo | kampus's **internal asset store / CDN** (designed — ADR 0144, supersedes 0044's imge; **not built**). A decoupled infra primitive, never user-facing: its own `infra/depo` alchemy stack, R2-backed, public-read at `depo.kamp.us` (capability-URL — GitHub-embeddable ⇒ never read-sensitive), writes through a doorman worker (pasaport `apiKey`, content-addressed write-once), client is a standalone `depo` CLI. Turkish for "depot/warehouse". First consumer: agent Playwright screenshots → GitHub PR descriptions (via the review-ui skill). Renamed from **imge** (the imgur-style scope is rejected). | the imgur-style public image/video product (that scope is **rejected**, ADR 0144) | | pano | HN-style link & discussion aggregator with threaded comments. Turkish for "board". | "board", "pinboard" | | pasaport | Identity/auth/profiles/karma domain; wraps better-auth. Turkish for "passport". | "auth" (broader) | diff --git a/apps/web/alchemy.run.ts b/apps/web/alchemy.run.ts index 65f576cf3..30de3ee56 100644 --- a/apps/web/alchemy.run.ts +++ b/apps/web/alchemy.run.ts @@ -48,7 +48,6 @@ import { emailDeliveryAdminFlag, emailDeliveryNoticeFlag, Flagship, - funnelReadoutFlag, karmaGatesFlag, mecmuaFeedFlag, mecmuaPublicReadFlag, @@ -92,9 +91,6 @@ export default Alchemy.Stack( // 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); - // The conversion-funnel readout dark-ship flag, default-off (#1589) — the - // founder/mod tier-count surface gates behind this key until a human release. - yield* funnelReadoutFlag(flagship.appId); // The optimistic in-place content-edit dark-ship flag, default-off (#1675, // epic #1637) — post/comment/definition edits pass an optimistic payload only // behind this key until a human release. diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index d06ce9780..7dcfd4e69 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -502,8 +502,8 @@ export function App() { )} } /> } /> - {/* The founder/mod conversion readout (#1589) — the page self-gates on - the funnel-readout flag (off ⇒ 404), so the route is dark by default. */} + {/* The founder/mod conversion readout (#1589) — access is server-authoritative + (the gated funnel.summary read denies a non-mod), so the route stays mod-only. */} } /> {/* The notification center (#1694) — the page self-gates on the phoenix-bildirim flag (off ⇒ 404), so the route is dark by default. */} diff --git a/apps/web/src/components/funnel/FunnelSummary.tsx b/apps/web/src/components/funnel/FunnelSummary.tsx index efc67807e..9a7f81def 100644 --- a/apps/web/src/components/funnel/FunnelSummary.tsx +++ b/apps/web/src/components/funnel/FunnelSummary.tsx @@ -3,8 +3,8 @@ * the promotion rate (#1593), the first-contribution rate (#1591), and the vouch rate * (#1592) — over the two tier counts (çaylak, yazar) the founder/mod front page * renders. Reads the gated - * `funnel.summary` DESTINATION (founder/mod, behind `phoenix-funnel-readout`); a - * non-mod read denies the invisible `UNAUTHORIZED`, caught by the page's ``. + * `funnel.summary` DESTINATION (founder/mod only); a non-mod read denies the + * invisible `UNAUTHORIZED`, caught by the page's ``. * * a11y (#1202 baseline): each headline rate is a labelled figure (`
` + * `
`), and the counts are a real description list (`
`) so each number diff --git a/apps/web/src/components/funnel/funnelGating.test.ts b/apps/web/src/components/funnel/funnelGating.test.ts deleted file mode 100644 index 733c69f5d..000000000 --- a/apps/web/src/components/funnel/funnelGating.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * The funnel-readout surface's gating contract (#1589) — the pure render decision - * asserted without a DOM (the `divanGating.test.ts` idiom; `apps/web/src` has no - * jsdom). The AC the surface lives or dies on: flag-off ⇒ the route renders the 404 - * (effectively absent), so nothing leaks with the flag off. - */ -import {describe, expect, it} from "vitest"; -import {shouldRenderFunnelPage} from "./funnelGating"; - -describe("shouldRenderFunnelPage — the flag-gated route", () => { - it("renders the page when the readout flag is on", () => { - expect(shouldRenderFunnelPage(true)).toBe(true); - }); - - it("renders the 404 (route absent) when the flag is off", () => { - // loading / fetch-error / undeclared all resolve to `false` upstream, so the - // off case covers every non-on flag state. - expect(shouldRenderFunnelPage(false)).toBe(false); - }); -}); diff --git a/apps/web/src/components/funnel/funnelGating.ts b/apps/web/src/components/funnel/funnelGating.ts deleted file mode 100644 index 09d3ca56e..000000000 --- a/apps/web/src/components/funnel/funnelGating.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * The funnel-readout surface's render decision, factored DOM-free so it is - * unit-testable without a DOM/React runtime — the pure-extraction idiom of - * `divanGating` / `flagGateChild` (`apps/web/src` has no jsdom). The funnel readout - * (#1589) is the founder/mod conversion-metrics surface, shipped dark behind the - * `phoenix-funnel-readout` flag. - * - * Access itself is SERVER-authoritative — the gated `funnel.summary` read denies a - * non-mod the invisible `UNAUTHORIZED` (`requireFunnelAccess`). This gate only - * decides the flag-off dark-ship: with the flag off the route renders the 404, so - * the surface is effectively absent until a human release. - */ - -/** - * Show the `/funnel` page content iff the readout flag is on. Off (and every flag - * failure mode — loading/error/undeclared all resolve to `false` upstream) renders - * the 404, so with the flag off the route is effectively absent. - */ -export function shouldRenderFunnelPage(flagOn: boolean): boolean { - return flagOn; -} diff --git a/apps/web/src/flags/keys.ts b/apps/web/src/flags/keys.ts index a59179049..4202da9b0 100644 --- a/apps/web/src/flags/keys.ts +++ b/apps/web/src/flags/keys.ts @@ -85,15 +85,6 @@ export const PHOENIX_AUTHORSHIP_LOOP = "phoenix-authorship-loop"; */ export const PHOENIX_BILDIRIM = "phoenix-bildirim"; -/** - * Conversion-funnel readout dark-ship flag (#1589). The founder/mod aggregate - * tier-count surface (`/funnel` + the `funnel.summary` read) gates behind this key; - * default-off so the readout reaches production dark until a human flips it at - * release (ADR 0083). Its OWN key, not the `phoenix-authorship-loop` seam — the - * funnel is a separate mod-only destination with its own lifecycle. - */ -export const PHOENIX_FUNNEL_READOUT = "phoenix-funnel-readout"; - /** * Optimistic in-place content-edit dark-ship flag (#1675, epic #1637). Gates the * three Class-A content edits (`post.edit`, `comment.edit`, `definition.edit`) @@ -167,8 +158,7 @@ export const PHOENIX_KARMA_GATES = "phoenix-karma-gates"; * until a human flips it at release (ADR 0083): with it off the mutations/read fail * the invisible `Denied` (like a non-admin call) and the client controls render * nothing, so no session is ever refused by an unreleased feature. Its OWN key, not - * a shared authz seam — ban is a distinct admin capability with its own lifecycle - * (the `phoenix-funnel-readout` mod-surface precedent). + * a shared authz seam — ban is a distinct admin capability with its own lifecycle. */ export const PHOENIX_USER_BAN = "phoenix-user-ban"; @@ -304,7 +294,6 @@ export const DECLARED_FLAGS: readonly FlagDeclaration[] = [ {key: MECMUA_FEED, defaultValue: false}, {key: PHOENIX_AUTHORSHIP_LOOP, defaultValue: false}, {key: PHOENIX_BILDIRIM, defaultValue: false}, - {key: PHOENIX_FUNNEL_READOUT, defaultValue: false}, {key: PHOENIX_OPTIMISTIC_EDITS, defaultValue: false}, {key: PHOENIX_OPTIMISTIC_DEFINITION_ADD, defaultValue: false}, {key: PHOENIX_OPTIMISTIC_DEFINITION_DELETE, defaultValue: false}, diff --git a/apps/web/src/pages/FunnelPage.tsx b/apps/web/src/pages/FunnelPage.tsx index 4d9ba9fba..95f09c4b5 100644 --- a/apps/web/src/pages/FunnelPage.tsx +++ b/apps/web/src/pages/FunnelPage.tsx @@ -1,9 +1,6 @@ /** * `FunnelPage` — the `/funnel` founder/mod conversion readout (#1589): the tracer - * bullet for the çaylak→yazar metrics, today just the current tier population. The - * whole surface ships dark behind the `phoenix-funnel-readout` flag: with the flag - * off the route renders the 404 (effectively absent); loading shows a neutral - * placeholder so the 404 never flashes before the flag resolves. + * bullet for the çaylak→yazar metrics, today just the current tier population. * * Access is SERVER-authoritative — the gated `funnel.summary` read denies a non-mod * the invisible `UNAUTHORIZED` (`requireFunnelAccess`). The `` catches that @@ -11,29 +8,10 @@ * may enter, mirroring `DivanPage`. */ import {FunnelSummary} from "../components/funnel/FunnelSummary"; -import {shouldRenderFunnelPage} from "../components/funnel/funnelGating"; import {Screen} from "../fate/Screen"; -import {PHOENIX_FUNNEL_READOUT} from "../flags/keys"; -import {useFlag} from "../flags/useFlag"; -import {NotFoundPage} from "./NotFoundPage"; import "../components/funnel/Funnel.css"; export function FunnelPage() { - const {value: flagOn, loading: flagLoading} = useFlag(PHOENIX_FUNNEL_READOUT, false); - - // Don't decide 404-vs-page until the flag resolves, or the 404 flashes first. - if (flagLoading) { - return ( -
-
-

yükleniyor…

-
-
- ); - } - - if (!shouldRenderFunnelPage(flagOn)) return ; - return (
diff --git a/apps/web/worker/features/flagship/bildirim.invariant.test.ts b/apps/web/worker/features/flagship/bildirim.invariant.test.ts index f6b941ff1..90bf3590c 100644 --- a/apps/web/worker/features/flagship/bildirim.invariant.test.ts +++ b/apps/web/worker/features/flagship/bildirim.invariant.test.ts @@ -2,7 +2,7 @@ * The dark-ship default-=-safe-state invariant for the bildirim (notification * system) flag (#1694, epic #1666). Inspected off the exported `BILDIRIM_FLAG` * record (the same object the factory spreads into `FlagshipFlag`), so no alchemy - * resource is constructed — mirrors `funnel-readout.invariant.test.ts`. + * resource is constructed — mirrors `authorship-loop.invariant.test.ts`. */ import {assert, describe, it} from "@effect/vitest"; import {PHOENIX_BILDIRIM} from "../../../src/flags/keys.ts"; diff --git a/apps/web/worker/features/flagship/funnel-readout.invariant.test.ts b/apps/web/worker/features/flagship/funnel-readout.invariant.test.ts deleted file mode 100644 index b78860781..000000000 --- a/apps/web/worker/features/flagship/funnel-readout.invariant.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * The dark-ship default-=-safe-state invariant for the conversion-funnel readout - * (#1589). Inspected off the exported `FUNNEL_READOUT_FLAG` record (the same object - * the factory spreads into `FlagshipFlag`), so no alchemy resource is constructed — - * mirrors `authorship-loop.invariant.test.ts` (#1204). - */ -import {assert, describe, it} from "@effect/vitest"; -import {PHOENIX_FUNNEL_READOUT} from "../../../src/flags/keys.ts"; -import {FUNNEL_READOUT_FLAG, funnelReadoutFlag} from "./resources.ts"; - -describe("funnel readout — the IaC default is the safe (off) state", () => { - it("the flag config ships defaultVariation off and variations.off === false", () => { - assert.strictEqual(FUNNEL_READOUT_FLAG.defaultVariation, "off"); - assert.strictEqual(FUNNEL_READOUT_FLAG.variations.off, false); - assert.strictEqual(FUNNEL_READOUT_FLAG.variations.on, true); - assert.strictEqual(FUNNEL_READOUT_FLAG.key, "phoenix-funnel-readout"); - }); - - it("the flag key is the shared constant (gate and declaration never diverge)", () => { - assert.strictEqual(FUNNEL_READOUT_FLAG.key, PHOENIX_FUNNEL_READOUT); - }); - - it("the factory is a function of appId (deploy-resolved, not a module constant)", () => { - assert.strictEqual(typeof funnelReadoutFlag, "function"); - }); -}); diff --git a/apps/web/worker/features/flagship/optimistic-edits.invariant.test.ts b/apps/web/worker/features/flagship/optimistic-edits.invariant.test.ts index de77e739d..cb7b68f73 100644 --- a/apps/web/worker/features/flagship/optimistic-edits.invariant.test.ts +++ b/apps/web/worker/features/flagship/optimistic-edits.invariant.test.ts @@ -3,7 +3,7 @@ * content-edit flag (#1675, epic #1637). Inspected off the exported * `OPTIMISTIC_EDITS_FLAG` record (the same object the factory spreads into * `FlagshipFlag`), so no alchemy resource is constructed — mirrors - * `funnel-readout.invariant.test.ts` (#1589). + * `authorship-loop.invariant.test.ts` (#1204). */ import {assert, describe, it} from "@effect/vitest"; import {PHOENIX_OPTIMISTIC_EDITS} from "../../../src/flags/keys.ts"; diff --git a/apps/web/worker/features/flagship/resources.ts b/apps/web/worker/features/flagship/resources.ts index 9bb9bdb5e..cb825123d 100644 --- a/apps/web/worker/features/flagship/resources.ts +++ b/apps/web/worker/features/flagship/resources.ts @@ -22,7 +22,6 @@ import { PHOENIX_EDGE_SHELL_BOOT, PHOENIX_EMAIL_DELIVERY_ADMIN, PHOENIX_EMAIL_DELIVERY_NOTICE, - PHOENIX_FUNNEL_READOUT, PHOENIX_KARMA_GATES, PHOENIX_NAV_IA, PHOENIX_OPTIMISTIC_DEFINITION_ADD, @@ -98,7 +97,6 @@ export { PANO_DRAFT_SAVE, PHOENIX_AUTHORSHIP_LOOP, PHOENIX_BILDIRIM, - PHOENIX_FUNNEL_READOUT, PHOENIX_KARMA_GATES, PHOENIX_OPTIMISTIC_DEFINITION_ADD, PHOENIX_OPTIMISTIC_EDITS, @@ -274,39 +272,6 @@ export const authorshipLoopFlag = (appId: Input) => rules: AUTHORSHIP_LOOP_RULES, }); -/** - * The conversion-funnel readout dark-ship flag config (#1589) — the founder/mod - * tier-count surface gates behind this key. Default-OFF so the readout reaches - * production dark; flipping it on is the human release act (ADR 0083). Its own key - * (not `phoenix-authorship-loop`) so the funnel destination has an independent - * lifecycle. - * - * 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: funnel (the conversion-readout founder/mod surface) - * - originating: #1589 (the çaylak→yazar conversion readout) - * - removal trigger: once the funnel readout graduates to on at 100% and stable - * for one release, retire the flag and inline the surface. - */ -export const FUNNEL_READOUT_FLAG = { - key: PHOENIX_FUNNEL_READOUT, - description: - "conversion-funnel readout dark-ship (#1589). owner: funnel. 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 funnelReadoutFlag = (appId: Input) => - Cloudflare.Flagship.Flag("phoenix_funnel_readout", {appId, ...FUNNEL_READOUT_FLAG}); - /** * The optimistic in-place content-edit dark-ship flag config (#1675, epic #1637). * The three Class-A content edits (`post.edit`/`comment.edit`/`definition.edit`) @@ -532,7 +497,7 @@ export const karmaGatesFlag = (appId: Input) => * 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 `FUNNEL_READOUT_FLAG`). + * unit-inspectable WITHOUT constructing the alchemy resource (mirrors `PANO_DRAFT_SAVE_FLAG`). * * Per-flag metadata (`feature-flags-schema-lifecycle.md`): * - owner: pasaport (the identity + session-boundary surface) diff --git a/apps/web/worker/features/funnel/fate-module.ts b/apps/web/worker/features/funnel/fate-module.ts index 7742eee61..f316aab89 100644 --- a/apps/web/worker/features/funnel/fate-module.ts +++ b/apps/web/worker/features/funnel/fate-module.ts @@ -4,8 +4,8 @@ import {queries} from "./queries.ts"; import {funnelSummaryDataView} from "./views.ts"; const roots: FateRootsRecord = { - // The conversion-funnel readout (#1589) — founder/mod-gated, behind the - // `phoenix-funnel-readout` flag; the `funnel.summary` resolver owns the gate. + // The conversion-funnel readout (#1589) — founder/mod-gated; the + // `funnel.summary` resolver owns the capability gate. "funnel.summary": funnelSummaryDataView, }; diff --git a/apps/web/worker/features/funnel/queries.ts b/apps/web/worker/features/funnel/queries.ts index 91a94da21..2a13e1f7b 100644 --- a/apps/web/worker/features/funnel/queries.ts +++ b/apps/web/worker/features/funnel/queries.ts @@ -2,14 +2,9 @@ * The funnel root query resolver (#1589) — `funnel.summary`, the founder/mod * conversion readout's single gated read. * - * Two gates, both enforced HERE (the `Funnel` service read is unconditional): - * - * 1. The `phoenix-funnel-readout` dark-ship flag (default-off, ADR 0083). Off ⇒ - * the read fails the invisible {@link Denied}, exactly like a non-mod read — so - * with the flag off (default / Flagship outage) nothing leaks, even on a direct - * call. Read with the safe `false` default (`stats`/`divan` idiom). - * 2. The {@link requireFunnelAccess} capability gate — platform-moderation only. - * `yield* ViewFunnel` makes the read unreachable without the discharged grant. + * The {@link requireFunnelAccess} capability gate — platform-moderation only — + * guards the read: `yield* ViewFunnel` makes it unreachable without the discharged + * grant, so a non-mod read fails the invisible {@link Denied}. * * A synthetic singleton like `stats.landingStats`: the wire type is the NAME string * (`"FunnelSummary"`), not the view class, so the entity stays off the source- @@ -20,21 +15,12 @@ import {Fate} from "@kampus/fate-effect"; import {Effect} from "effect"; import * as Schema from "effect/Schema"; -import {PHOENIX_FUNNEL_READOUT} from "../../../src/flags/keys.ts"; -import {Flags} from "../flagship/Flags.ts"; -import {provideRequestFlags} from "../flagship/FlagsContext.ts"; import {Denied} from "../kunye/errors.ts"; import {Funnel, promotionRate} from "./Funnel.ts"; import {requireFunnelAccess, ViewFunnel} from "./gate.ts"; const FUNNEL_SUMMARY_ID = "summary"; -/** Is the funnel readout on for this request? Safe-default `false` (dark). */ -const readoutOn = Effect.gen(function* () { - const flags = yield* Flags; - return yield* flags.getBoolean(PHOENIX_FUNNEL_READOUT, false).pipe(provideRequestFlags); -}); - // The post-gate summary read — `ViewFunnel`-gated in R (`requireFunnelAccess` // provides the grant). `yield* ViewFunnel` requires the proof; the counts are // unreachable without a discharged grant. @@ -62,9 +48,6 @@ export const queries = { "funnel.summary": Fate.query( {type: "FunnelSummary", error: Schema.Union([Denied])}, Effect.fn("funnel.summary")(function* () { - if (!(yield* readoutOn)) { - return yield* Effect.fail(new Denied({message: "Dönüşüm metrikleri şu an kapalı."})); - } return yield* requireFunnelAccess(summaryGated()); }), ),