The canonical structural vocabulary for phoenix. Use these terms exactly when you reason or write about the shape of the code — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point: an audit, a review, an ADR, and a PR description all mean the same thing by "deep module" or "seam."
This file is two layers:
- The general architecture vocabulary — module / interface / implementation / depth / seam / adapter / leverage / locality, the deletion test, and the other principles. Evergreen and project-agnostic; it ports the vocabulary the architecture-audit work is grounded in.
- The phoenix structural terms — the project's own named structures (the test tiers, the fate loader/resolver split, the LiveDO roles), each anchored to the ADR that decided it. These are what the general vocabulary names when applied to this codebase.
Module Anything with an interface and an implementation. Deliberately scale-agnostic — it applies equally to a function, a class, a package, or a tier-spanning slice. Avoid: unit, component, service.
Interface Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. Avoid: API, signature (too narrow — those refer only to the type-level surface).
Implementation What's inside a module — its body of code. Distinct from adapter: a thing can be a small adapter with a large implementation (a real D1-backed repository) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
Depth Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is deep when a large amount of behaviour sits behind a small interface. A module is shallow when the interface is nearly as complex as the implementation.
Seam (from Michael Feathers) A place where you can alter behaviour without editing in that place. The location at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it. Avoid: boundary (overloaded with DDD's bounded context).
Adapter A concrete thing that satisfies an interface at a seam. Describes role (what slot it fills), not substance (what's inside).
Leverage What callers get from depth: more capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
Locality What maintainers get from depth: change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
- Depth is a property of the interface, not the implementation. A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface.
- The deletion test. Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
- The interface is the test surface. Callers and tests cross the same seam. If you want to test past the interface, the module is probably the wrong shape.
- One adapter means a hypothetical seam. Two adapters means a real one. Don't introduce a seam unless something actually varies across it.
- A module has exactly one interface (the surface it presents to callers and tests).
- Depth is a property of a module, measured against its interface.
- A seam is where a module's interface lives.
- An adapter sits at a seam and satisfies the interface.
- Depth produces leverage for callers and locality for maintainers.
- Depth as ratio of implementation-lines to interface-lines (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- "Interface" as the TypeScript
interfacekeyword or a class's public methods: too narrow — interface here includes every fact a caller must know. - "Boundary": overloaded with DDD's bounded context. Say seam or interface.
The general vocabulary above, applied to this codebase, names a handful of recurring structures. Each is anchored to the ADR that decided it — read the ADR for the why; this section fixes the term so an audit, a review, and a test file all mean the same thing by it.
phoenix runs exactly two test tiers, split by which fidelity a claim needs — not
by folder. (ADR 0082 — Two test
tiers: unit (no DB) and integration (real D1 via alchemy Test.make), the source of
truth, extended by 0104. 0082
supersedes 0040, whose
four-tier T0–T3 taxonomy rested on a faked in-memory node:sqlite D1 stand-in
(makeSqliteTestDb) and the now-falsified premise that node:sqlite is the same engine
as D1. There is no in-memory SQL tier; the helper is deleted.)
unit— pure logic + in-process service contracts, no database. Pure functions / Effect logic and feature-service contracts that are wrong-or-right even if the DB behaves perfectly (normalization, clamping, envelope shaping, pagination math, auth gates, empty/cursor-miss branches, topic-key routing). No deployed worker and no SQL engine — theDrizzlestorage seam is substituted (arun/batchthat throws or a stubbed return proves the decision never touched the DB). Runs offline in the default node pool; files carry the*.unit.test.tsinfix (plus plain*.test.tsservice-contract tests). Examples: a keyset codec, the pasaportmeauth gate,Bookmark.unit.test.ts.integration— real behavior over real remote Cloudflare D1. Black-box over the deployed phoenix stack via the alchemyTest.makeidiom: a file runs against a real worker + D1 (+ DOs) and asserts over HTTP/SSE. This is the only tier that can prove a claim that could only be wrong if the real engine differed — D1's FTS5 build, tokenizer, and collation are notnode:sqlite's, so search/ranking fidelity,ON CONFLICT/soft-delete round-trips against real rows, and the DO + SSE + D1 composite all land here. Per ADR 0104 the tier runs in two modes: a run-scoped shared stage for the pure-logic-dominant files and per-file dedicated stages for the few that need isolation (tests/integration/_integration.ts). Examples:search.test.ts,fate-live-posts.test.ts.
integration names two different fidelities, one per app. The paragraph above is
apps/web's and stays the definition ADR 0082 fixed: real remote Cloudflare, real D1, real
credentials. apps/tuval deploys nothing and has no D1, so its integration project means
the other half of what the tier was always for — the fidelity a claim needs that a
substituted seam cannot give. There it is a real Pi AgentSession behind a real loopback
socket and a real WebSocket codec, on Pi's own faux provider, so the tier is slow but not
remote and needs no cloud credentials (apps/tuval/vitest.config.ts; epic
#7497). Two rules hold across both: the
split is by fidelity and never by folder, and a claim that only a real engine could falsify
belongs in integration whichever app it is in. What you may not do is read one app's
integration and expect the other's — say which app you mean.
One Database seam. A single Database tag holds the raw D1Database handle; both
the Drizzle service and the better-auth adapter derive from it, so they share one
underlying handle by construction — the one-handle invariant is type-enforced by the layer
graph, not upheld by hand. At unit that seam is substituted; the real handle exists only
at integration, where it is real remote D1.
Seam-graduation (organic framework evolution). A test seam is born app-local and graduates only when it has earned it:
- Gate A — rule-of-three → extract an app-local factory. At ≥ 3 in-app call sites,
extract a factory (fresh per call; never a shared mock-layer instance). Its home
stays app-local under
worker/, notpackages/. - Gate B — graduate to a package / upstream. Requires proven-in-app and a second
consumer or an upstream home. An empty
packages/is load-bearing signal that nothing has earned graduation.
In phoenix's fate data layer, sources LOAD; operations RESOLVE. The split is a seam, and the two sides have fixed interfaces. (ADR 0016 — fate is pure transport, Effect services are the domain; the loader contract is the fate-effect sources pattern.)
- Loader (a source).
Fate.source(View, {id}, handlers)declares the per-entity loader: at least one ofbyId/byIds, silent reads (a missing id returnsnull/ a short list, not a failure), and a failure channel pinned tonever(infrastructure failures die one layer down, inside the domain service).byIdsis the workhorse that kills N+1 under the interpreter'sRequestResolverbatching, and it must be membership-stable — its rows must be a function of the id set (every SQLIN-shaped loader qualifies); a cursor-limited or order-sensitivebyIdssilently diverges under a merged batch window. - Resolver (an operation / custom query). What runs the read-or-write, masks the
resolved fields down to what the view permits, and translates failures to wire codes at
the boundary. Connections are resolvers, not loader handlers: a source carries no
connectionhandler — keysetORDER BYlives in the domain service, surfaced by a custom resolver inqueries.ts/lists.ts.
The seam matters because the two sides cross different trust boundaries: the loader is pure-membership and batch-mergeable, the resolver is where authorization-masking and error-translation happen. fate itself never queries the database — handlers delegate to the domain services.
A branded ID schema is an entity id typed as a nominal brand over a plain string —
Schema.String.pipe(Schema.brand("Name")), output type Brand.Branded<string, "Name"> —
so two ids that are both string at runtime (a user id, a definition id) become distinct
types the checker won't let you pass for one another. The brand is compile-time only:
it narrows the output type without validating, so .make/decode return the input unchanged
and the wire + D1 bytes stay byte-identical — no runtime allocation, no runtime check. (Epic
#2700; idiom grounded in Effect-TS/effect
packages/effect/SCHEMA.md §Branding — the top-level Schema.brand form, not a hand-rolled phantom symbol.)
- The shared home + the mint. All branded ids live in one module,
apps/web/worker/lib/ids.ts, and are minted by itsbrandedId(name)helper (export const UserId = brandedId("UserId")). A write boundary brands a raw session string at the call site with.make— e.g.UserId.make(user.id)infeatures/sozluk/mutations.ts. - Shared cross-feature vs. feature-owned.
UserIdis the cross-feature id — the authenticated user threaded as every write's actor/author/voter/reactor argument. A feature-owned id belongs to one product surface: sözlük'sDefinitionId/TermSlug. Feature-owned ids co-locate inlib/ids.tsbesideUserId(one import for every child of #2700), but stay conceptually owned by their feature — aPostId(pano) is declared with its own surface, not reused across features. Distinct brands are what make an argument swap likevoteDefinition({definitionId, voterId})a typecheck error instead of a live bug.
Two unrelated module kinds in the same feature folder once both wore the word "view"; the vocabulary now keeps them apart.
- Store of record (
*_recordDrizzle table). The authoritative, mutated D1 table add/edit/remove write directly to under the D1-direct model (ADR 0009 — there is no projection layer).definition_record/comment_record(DrizzledefinitionRecord/commentRecord) are the canonical stores whose loss is data loss. The_recordsuffix is load-bearing: it reads as "store of record" and reserves "view" for the data view below. They were renamed off the projection-era*_viewname in #853 — that name lied about the module kind (a reader assumes a rebuildable read-projection) and collided one capital apart with the fate data view. - Data view (fate
FateDataView). A pure read-shape declaration — the fields a fate entity exposes, not a table.DefinitionView/CommentView(PascalCase, in each feature'sviews.ts) declare what the loader/resolver above project; they hold no rows and are never written. See the "data view" row inTERMS.md.
The test: if you can INSERT/UPDATE it, it's a store of record; if it only declares
fields to read, it's a data view (*View). Never name a write target a "view." The
*_record suffix is narrower than "any store of record": it marks a store that is
shared across packages (the canonical @kampus/db-schema declaration). A worker-private
mutated store — user_profile, content_report, the stats singletons — is a store of record
too, but lives in the worker schema without the suffix, so lacking *_record is not a
convention violation for those.
Cross-isolate live (SSE) fan-out is carried by one LiveDO Durable Object class that
plays two roles, selected by instance-name prefix. (ADRs
0023 — live views over SSE, the original
fan-out; 0025 — the connection/topic
split, now superseded; 0028 — the
Effect DO model the roles are authored on; 0037
— the current state: one void-aligned class, two roles, KV storage.)
- Connection role (
connection:<id>). Owns one client's held SSE stream, its subscription list, and its persistedgeneration(the stale-detection counter). It is the per-client endpoint of the fan-out. - Topic role (
topic:<key>). Owns the durable subscriber registry for one topic, the publish fan-out to that topic's connections, and the reap of dead connections. It is the per-topic hub.
A publish-only LiveEventBus forwards live.* events to the topic role, which delivers
to the connection role. The two roles share no runtime state — an instance is always
exactly one role. The history is worth knowing: ADR 0023 packaged both roles in one class;
0025 split them into separate ConnectionDO / TopicDO classes to make invalid cross-role
calls unrepresentable; 0037 reunified them into a single class (the split's two
motivations — a mutual-DO Layer cycle and a SQLite registry — both went away), where a
misroute now no-ops at runtime (role-guarded) rather than failing to compile.
A shell wraps a nav/page primitive and owns a whole zone, exposing it to the page
as flat element-props — one ReactNode prop per zone — so an element not assigned to a
declared zone is a type error, not a lint finding. The recipe is the composition idiom
a shell is built on. (ADR
0182 — SubnavShell / PageShell
composition API — flat element-props, which coins both terms; it builds on ADR
0176's nav element taxonomy. The SubnavShell /
PageShell source is defined by that ADR — the term is grounded in the ADR until the shells
land in apps/web/src/components/layout/.)
recipe(UI sense; the pipeline sense is below) — the composition-primitive idiom: one flat element-prop per zone, orphan-as-type-error. NOT a zones-object, NOT compound components (<Shell.Destinations>…) — a compound API leaves an orphan slot (an element rendered but placed nowhere) that only a lint pass catches; flat element-props make that unrepresentable, because an element with no declared zone prop has nowhere to compile in.shell— the layout-composition wrapper (SubnavShell/PageShell) that names a page's zone-plus-content shape once and hands each zone to the consumer as one prop.SubnavShellcomposes the per-productSubnavprimitive (wrapping it, not replacing it);PageShellcomposesSubnavShellplus the routed page content below it. A shell sits between a primitive and the page — distinct from a primitive, which owns no zone and exposes raw slots.
A lane is one unit of pipeline work driven by a machine document plus an append-only
events.jsonl, folded fresh on every read — no resident process, no snapshot (#5673). A lane
is addressed by a key, and the key decides where its ledger lives:
chore lane— a lane keyed by a name rather than an issue number, because a recurring chore has no issue to be keyed by; its state lives at.fabrika/chores/<name>/events.jsonl, against.fabrika/lanes/<n>/for an issue lane. Same fold, same six-event vocabulary (DONE/PASS/FAIL/BLOCKED/WIP/UNBLOCKED), different key. Source:packages/fabrika-cli/src/lane/key.ts(#5840).recipe(pipeline sense) — one deterministic fabrika verb a chore workflow state applies: a fixed sequence with named exit codes and no judgment in it. A recipe relays a verb's answer, it never derives the decision — ADR 0228. The operator that runs one is a thin executor of the machine it is handed; the known/novel split lives in the verb's exit codes, never in operator prose.
Two senses of recipe. This is the pipeline one. The other is the UI composition idiom
above ("The composition shell / recipe", ADR 0182) — one flat element-prop per zone. They share
nothing but the word; name which one you mean when the context does not fix it.
The canonical section order every packages/*/README.md follows — explanation (what it is /
why it exists, citing the forcing ADR) → how-to (runnable recipes) → reference (dry,
look-it-up, last or linked out) → a short testing tail — with two hard rules: no tutorial at
package scale (a walkthrough moves to its own linked surface), and scope/non-goals live in the
explanation half. A small package may satisfy it in three short sections; the order is canonical,
the length is not. The diataxis skill is
the single-mode classifier over any page. Pinned by
package-readme-shape.md.
"phoenix" carries three distinct meanings, each with a graduation name —
disambiguate which one a doc means, because they're about to fork harder (phoenix-ops
leans on sense 3, product milestones lean on sense 2):
- phoenix, the monorepo — this repository: the multi-app, multi-worker Cloudflare
workspace (
apps/*,packages/*,infra/*). Product/company side — the ground the other two stand on. - phoenix, the product codename — kamp.us vNext, the reborn community, under its build-time codename. Product/company side. When it ships it simply becomes kamp.us — the codename is retired, not renamed; the product comes home to its own name.
- phoenix, the framework — the batteries-included application framework the other two are quarried from: alchemy + Effect + fate, battle-tested in production, general-purpose, future-facing. Not a kamp.us artifact — a standalone one forged here, built for keeps. It graduates to anka — the Turkish phoenix (Anka kuşu / Zümrüdüanka) — as its permanent name.
Monorepo + product-codename are product/company side; the framework is the durable, reusable side.
The through-line worth preserving: the framework is the true phoenix — the part built to
outlive its origin — so it earns the Turkish phoenix name anka and rises to live
independently; the product simply comes home to kamp.us. Rebirth named in English
(phoenix), completed in Turkish (anka), landing on the repo's Turkish-for-brand /
English-for-technical rule (§3). anka is a framework name, not user-facing product
copy, so it lives here in sense (3) rather than as a §3 Turkish-surface brand-noun row.
The canonical section order every packages/*/README.md follows — explanation (What it is /
Why it exists) → how-to → reference tail → testing — scaled down to a three-section minimum
for small packages. No tutorial at package scale: walkthroughs live on their own linked
surface. The pattern doc is .patterns/package-readme-shape.md;
the diataxis skill is the single-mode classifier over any docs page, READMEs included.
A milestone is an initiative. An initiative has a Definition of Done. A catch-all with no DoD is a label, not a milestone. A fixes-bucket like "Sözlük/Pano fixes" is an oxymoron to this definition — it has no DoD, so it can't be an initiative, so it isn't a milestone; its disposition is to retire (convert to a plain label if the grouping is still wanted), not "close as done." (ADR 0072 — milestones encode strategic sequencing.)
A standing cross-cutting axis — a perpetual concern with no terminal DoD (token
efficiency, test/CI health, pipeline hardening) — is likewise a label, not a milestone,
by the same DoD test. Its label is axis:*, distinct from area:*: area:* = product
area, axis:* = cross-cutting concern (e.g. axis:token-efficiency, axis:test-ci-health,
axis:pipeline-hardening vs area:sozluk-pano-ui). Tag go-forward only — open issues get
the axis label; closed issues are not retro-tagged (the area:sozluk-pano-ui precedent).
An axis:* label does not anchor p1 — only a live fire or an active bounded
milestone does. A standing axis is p2-by-default; an individual item rises to p1 only
when it is a genuine fire (a broken gate, a false-verdict trust bug). This keeps a lane's
sustained importance out of the priority spine, so it can't rebuild the p1-inflation backdoor
the milestone-relative p1 rule closed (#1936 / #2078). Founder-override-open — the founder
may later designate an axis:* (e.g. axis:pipeline-hardening) a p1-anchor; until relayed,
this rule stands. (Extends the #2093 → #2095 milestone-governance convention.)
Three terms epic #5631 pinned while turning
fabrika's repo-specific literals into .fabrika.jsonc keys (ADR
0273). Shaped in
.patterns/fabrika-config-key-groups.md; named here so
a doc, a skill and a verb use one word for each.
The config surface is the set of keys .fabrika.jsonc may carry, each with its shipped
default — packages/fabrika-cli/src/config/registry.ts. It is not the file: the file is one repo's
instance of the surface, and a repo that ships no file still stands on the whole surface. fabrika status settings prints the surface, which is why it can answer for a repo that declared nothing.
A shipped default is the value a key resolves to when the file or that key is absent. Two
rules ride the term. It is never an empty set where empty would turn a gate off — an empty governed
root list reads as "nothing is governed", so the default reproduces today's behaviour and an
explicitly-declared empty is Malformed. And it is never what an unreadable file resolves to: a
read that failed proves nothing about what the repo declared, so every key is UNKNOWN and the caller
refuses. Absent and unreadable are opposite answers all the way down, and collapsing them is how a
gate reports "this repo declared nothing" about a repo it never read.
Delete authority is what a triage facet's owns pattern grants: every label the pattern matches
and the keep set does not is removed. The term carries the #4285
incident — a declared lane no facet owned was written once and never superseded. This epic makes
delete authority a property of loaded data rather than of source: it is composed in
packages/fabrika-cli/src/config/resolve-board.ts from triageFacets joined against
boardVocabulary, so a repo that renames a status cannot end up with a facet that deletes labels
nobody declared.
Tuval's three nouns, ruled by #7484 R1.1
("let's be boring and choose program and process for now") and first used in code by the registry
slice of epic #7496 under apps/tuval (ADR
0345). English technical terms, per §3.
- program — one registry row: a stable id, a private Demlik core machine, public typed port
schemas, host handlers, a capability request list, an optional renderer reference, and the
#7467 identity / capability / placement records as inert data. A program is exactly one row;
there is no second species and no view-only exemption. Source:
apps/tuval/src/registry/program.ts. - process — one running instance of a program: a stable id, a parent, ports, a lifecycle. Always say "OS process" for the operating-system kind; a bare "process" in Tuval prose is this one.
- window — a view onto a process, the Vim-buffer model: many windows may show one process at once, all sharing the process state, each window owning its own view state (scroll, selection).
"Widget" and "actor" are retired for Tuval's own surfaces — an older doc that says either means program (definition) or process (running instance). "Grain" (Orleans' virtual actor) is noted as a future-feeling alternative and is not adopted.
The layout tree's four nouns, first used in code by
apps/tuval/src/shell/layout/ (#7551).
- stack — a node holding an ordered list of child windows and stacks. Windows are the leaves; a stack is never empty, and only the root may hold a single child.
- orientation — how a stack lays its children out.
"horizontal"means the children sit side by side in a row;"vertical"means they stack top to bottom. Studio'slayout-treeinverts this at its render boundary (itsorentationFromDirectionmaps left/right to"vertical"); phoenix does not, and no orientation flip appears anywhere in the port. - size — a child's share of its stack's extent, in percent, never pixels: every tab mirrors one desk at a different width, so a pixel would mean something different in each. Min and max are render-time props of the panel component and live nowhere in the tree.
- zoom — the one window rendered alone,
zoomedon the tree. Setting or clearing it never writes a size, so unzoom restores the layout exactly.
The shell's three nouns, first used in code by
apps/tuval/src/shell/core/ (#7554). English technical terms,
per §3, and named in the epic's own vocabulary ruling
(#7499).
- workspace — one named desk: a layout tree and the window focus sits in. The shell holds many
and exactly one is active; the last one cannot be removed, because a shell with no desk has
nothing to show. Re-derived from the founder's Studio, where workspaces are a keyed map beside an
activeWorkspaceid (monorepo/packages/studio/studio.ts). - prefix — the one key that arms the shell for the sequence after it (
<c-b>by default, tmux's shape). With the prefix unarmed every key belongs to the focused window; there is no shell-wide mode, and "mode" is retired with the Runekeeper lineage. - attach — a page joining the running kernel over one socket, the tmux client sense. A restart is literal: kill Node, boot Node, re-attach the page, and the desk that comes back is the checkpointed one.
"Pane" is not adopted: the tmux word for what Vim calls a window stays window. "Widget" and "rune" are retired with the same lineage; "spell" is re-coined below as a Tuval command, not the Studio widget verb.
Tuval's command-framework nouns, ruled by the thirteen decisions on grilling
#7617 and built by epic
#7627. English technical terms, per §3. The why is
ADR 0348; the
shapes are .patterns/tuval-spells.md.
- spell — one addressable command in Tuval's spell registry: a path, a one-sentence description,
an Effect Schema for its parameters and one for its result, an Effect
execute, and an inert capability list. Source:apps/tuval/src/commands/spell.ts. - registry — ambiguous on its own in Tuval prose, so always qualify it. The spell registry
is the one table of every callable spell, keyed by path, built from the core spell list plus each
program row's
spellsand replaced whole on a config reload (apps/tuval/src/commands/registry.ts). The program registry is the separate kernel table of program rows (apps/tuval/src/registry/Registry.ts); it never reads a row's spells. - palette (the Tuval palette) — Tuval's desk-level command overlay at the top center of the app, fixed width and never anchored to a window, where a person types a spell and picks from ranked completions. Not apps/web's ⌘K command palette (ADR 0186) and not a reaction palette (ADR 0139); a bare "palette" in Tuval prose is this one.
- scope — where one spell call came from, as the kernel decides it: the workspace and client
always, plus the window and process when the caller was inside one. The page names a window and
nothing else; the kernel resolves the rest, so a page cannot address a process by putting its id
on the wire. Not Effect's
Scope, the resource-lifetime handle Tuval prose also uses (every process runs in its own Effect Scope forked from its parent's); a bare "scope" in Tuval spell prose is this record. Source:apps/tuval/src/commands/scope.ts. - the Tuval protocol — the one versioned page-to-kernel wire: four Effect Schema messages
(
SpellCall,SpellReply,Snapshot,Patch), one union per direction, JSON text only. Source:apps/tuval/src/protocol/messages.ts.
- partial — a transcript item still being written, marked
partialon the item itself. The backend re-upserts the sameItemIdas the text grows and leaves the marker off the last upsert, so absent means final and one field carries the whole distinction. It is not an item kind and not an event kind, and it names no backend: the window learns "still growing" once and every agent program streams the same way (#8142's ruling, epic #8160). Two kinds grow one — the assistant reply and the thinking row, since a turn's reasoning streams before its answer does (#8288) — and every predicate that reads the marker reads it throughin, so a third costs no arm. Source:apps/tuval/src/ai-agent/ports/transcript-item.ts.
Three of the six rows a Tuval chat window renders, minted by epic
#8142 phase 1. English technical terms, per §3,
and model-blind: none of them names a backend, a model or a session id. Source:
apps/tuval/src/ai-agent/ports/transcript-item.ts.
- thinking row — the agent's reasoning for one turn, as content. Not
ports/thinking.ts, which is the effort-level control; a bare "thinking" in Tuval transcript prose is this row, and the control is always "thinking level". - compaction marker — where a session compacted its context. Its own kind rather than a session row, because it is a boundary the window draws rather than a notice it prints, and it is the one place a reader needs to see why earlier turns are gone.
- session row — one backend notice, collapsed: a summary line always shown plus optional detail the window folds away. Every notice lands here — status, hooks, local command output, refusals, rate limits — and the row deliberately does not name which it was.
The naming convention is Turkish for product / brand, English for technical:
product and brand names stay Turkish and are never translated; everything technical is
English — URL routes/paths, code identifiers, D1 table/column names, file names. The
canonical example is that the route is /search?q=, not /ara.
User-facing copy in apps/web is Turkish and English, behind the i18n catalog
(ADR 0347). The reader picks a
locale; Turkish is the default, so a reader who picks nothing reads what the site
always read. Both locales are served from one typed catalog per locale under
apps/web/src/i18n/, and the brand nouns in the table below are not translated in
either one — the English interface still says sözlük, pano, mecmua, yazar, çaylak.
Tuval, Fabrika and Demlik are English-only: only the product name is Turkish, and none of
them coins a Turkish term.
Three technical terms come with that rule:
- locale — the reader's chosen language,
troren.tris the default. - catalog — the typed per-locale message record (
Record<Key, string>) underapps/web/src/i18n/, split one file per surface. There is no runtime i18n dependency; a key present in one locale and missing in the other is apnpm typecheckfailure. - catalog key — the identifier a surface passes to read one message out of the catalog. Keys are technical, so they are English, whatever locale they resolve into.
Two axes hide inside this one rule — keep them separate. Canonical glossary-term
language (the name a concept carries in TERMS.md) and UI copy language
(the strings rendered on a user's/mod's screen) are decided independently:
- A technical / analytics / infra concept keeps an English canonical term, even when
the concept appears on a Turkish-speaking user's or mod's screen. A conversion funnel
is recorded as
funnel / conversion funnel, never force-translated into a manufactured "dönüşüm hunisi" (an ugly, needless translation of a technical concept). This holds when the ADR 0092 glossary-freshness gate demands a term for a new internal/analytics surface: coin the English term, don't translate it — the concept stays English regardless of what language its page renders in. - User-facing UI copy is whatever locale the reader picked — it comes out of the
catalog, in
troren. The mod-facing funnel page renders localized copy while the concept underneath keeps its one English canonical name inTERMS.md. Adding English copy changes nothing on the glossary axis: a term is not re-decided because the page it appears on can now render in English.
The bildir row below already models this split: the brand lexeme surfaces in the
user-facing copy (bildir / bildirildi, in both locales — it is a brand noun) while its
technical surface (features/report, the Report service, content_report) stays
English. The funnel is the mirror case — a technical concept whose canonical term is
English (funnel / conversion funnel) whatever locale the surface it powers renders in.
Collapsing the two axes ("the concept shows on a Turkish screen, so its glossary term must
be Turkish") is the mistake this note exists to stop.
On a showcase / exhibit surface, the chrome is technical, only the sample content is
product copy. Any surface that demonstrates components — a design-system storyboard,
a component gallery, a pattern showcase — layers a component/primitive that renders some
example content. Split them: the names — component names, primitive names, section
and exhibit labels, and every code identifier or route path around them — are technical,
so they stay English; only the in-exhibit example/sample content the component
renders is user-facing product copy, so it reads in the product's voice. The sample
never bleeds up into the technical chrome that frames it: a Button exhibit is labeled
Button (English name) showing a Gönder sample (product copy), never a Düğme exhibit.
This is the general rule for every showcase surface — Turkish-naming the components was
the atölye-storyboard inversion the founder ruled a standing don't. Exhibit sample content
is not a user surface, so it is exempt from the catalog and stays as authored.
The Turkish product/brand nouns this repo uses:
| Noun | What it names |
|---|---|
| sözlük | the dictionary product (terms + definitions) |
| pano | the link/discussion board product (posts + comments) |
| kampus | the umbrella product / community |
| bildir | the report / notify surface — the brand lexeme surfaces in the user-facing copy (the ReportButton labels bildir / bildirildi / zaten bildirildi); the technical surface (features/report dir, Report service, content_report table) is English per this convention |
| künye | the per-user identity DO (karma, invite-only access, privileges) |
| depo | the internal asset store/CDN (was imge) |
| divan | the proving-ground reviewer surface — the gated /divan destination where yazar + moderatör review a çaylak's sandboxed work ("work goes before the divan") |
| mecmua | the serious long-form blogging / publishing product (a third surface beside sözlük + pano, epic #2429) — a yazar authors and publishes a long-form post (başlık + markdown body) that anyone may read; a çaylak cannot publish (authorship is earned). v1 is a surface on the existing apps/web worker, not its own app. Turkish for "magazine / journal / anthology" |
| sustur | mute — the one-directional, silent, notification-suppressing member-mute lever (epic #2571; v1 semantics fixed by ADR 0188). Muting a member both read-masks their content and suppresses the bildirim their interactions would generate to the muter; the muted member is never notified. Distinct from engelle (block) — mute is one-directional and lighter. Turkish for "silence / mute" |
| engelle | block — the heavier, mutual interaction-prevention lever (preventing replies/mentions/mutual visibility, symmetry TBD). Deferred from mute v1 and scoped to its own later decision/epic (ADR 0188); named here to keep it distinct from the lighter one-directional sustur (mute). Turkish for "block / obstruct" |
This is the brand-noun seed. The full domain-noun glossary (the entities and their precise definitions) lives in its own
.glossary/TERMS.md; this table fixes only the product/brand spellings and the Turkish-vs-English rule so they aren't duplicated inCLAUDE.md.
.decisions/— the why and the history behind every term here (an ADR is the source for each phoenix structural term)..patterns/— how the current code is shaped (the loader contract, the test seams, the DO wiring).CLAUDE.md— points here for the canonical vocabulary.