Datum Index
Every datum on this site, by id. Each id links to the line where the claim lives.
A datum is one claim that can be pointed at: a principle, decision, rule, requirement, concept, learning or outcome. Its id is global and never reused, so D11 always means the same claim. This page is built from the pages themselves; the Decisions Log records when each decision was made.
266 datums: 3 principles, 76 decisions, 68 rules, 115 requirements, 4 outcomes.
| ID | Kind | Claim | Page |
|---|---|---|---|
| D1 | principle | Sidekick is built for one person, who decides: every field, link and piece of metadata is optional and adapts to that person's workflow. | Individual First |
| D2 | principle | Draw the MVP line by what an item costs to add later: irreversible foundations ship now, everything else waits, and a foundation needs a planned near-term use. | Cost to Add Later |
| D3 | principle | Every capability has one interface that Sidekick's own clients use exactly as bots, jobs, agents and third parties do; there is no private path. | One Interface |
| D4 | rule | All handlers to be wrapped in withApiGuard. | Best Practices |
| D5 | rule | Always use row-level security via withRLS. | Best Practices |
| D6 | rule | Never mutate outside the API layer. | Best Practices |
| D7 | rule | Follow dependency flow. | Best Practices |
| D8 | rule | Use the repository client. | Best Practices |
| D9 | rule | Keep APIs idempotent. | Best Practices |
| D10 | rule | Never hard-delete syncable entities. A syncable table is one a client may sync offline; non-syncable tables (such as Synergy's outbox and idempotency keys) hard-delete on purpose. | Best Practices |
| D11 | rule | All select queries filter soft-deleted rows. The one exception is a table whose tombstones must stay visible (Synergy's, see Tombstone visibility): its repository is the only way in, and every query it runs states whether it wants live rows. | Best Practices |
| D12 | rule | Every content table participating in embedding must include embedding_status field, never fail silently, and must support retries. | Best Practices |
| D13 | rule | Feature manifests remain the canonical feature contract. | Best Practices |
| D14 | rule | Background embedding generation must never block user writes. | Best Practices |
| D15 | rule | Server Components are preferred for data-fetching. | Best Practices |
| D16 | rule | Client Components should only exist where interactivity is required | Best Practices |
| D17 | rule | Drizzle must never execute in browser/client components. | Best Practices |
| D18 | rule | Install dependencies at package-level when they are specific to that package’s runtime only (e.g. drizzle-org for packages/core). | Best Practices |
| D19 | rule | Install the dependencies at root when given dependency is shared across multiple package. | Best Practices |
| D20 | rule | Always add dependencies in package.json of each package even though such dependency is available through pnpm hoisting. Implicitly relying on pnpm hoisting works in monorepo context but it breaks when the package runs in isolated context or different monorepo environment. Therefore, being explicit about each dependency in package.json is safeguard we want to enforce. | Best Practices |
| D21 | rule | No barrel exports, and similarly, do not use main prop of package.json to expose barrel file. Use exports prop to explicitly expose individual artifacts. | Best Practices |
| D22 | rule | Include ”side-effects": false in package.json from day one. This will let bundlers perform aggressive tree-shaking and highlight errors and bad practices that comes out of side effects. | Best Practices |
| D23 | rule | Do not hardcode user-visible strings. Until lesson I1, define and use them from packages/copy; from I1, from the owning package's catalog. Excludes non-user strings such as instrumentation message sent to server, css class names, and attribute values. | Best Practices |
| D24 | rule | Prefer JSON configuration. | Best Practices |
| D25 | rule | Validate every external input at the boundary (request bodies, query parameters, external API responses, database rows) with Zod; TypeScript types are erased at runtime. | Best Practices |
| D26 | rule | Add export const dynamic = 'force-dynamic' to the layout of every route group that touches Supabase cookies. | Best Practices |
| D27 | rule | List every environment variable in the env array of the build task in turbo.json, not in the Vercel dashboard. | Best Practices |
| D28 | rule | Style with CSS modules only. No Mantine style props, no Tailwind, no utility classes; Mantine's behavioural props are fine. | Best Practices |
| D29 | rule | Test at four layers and prove a behaviour at the cheapest layer that can observe it; integration tests through the guarded API are the main seam. Mock only at boundaries. | Best Practices |
| D30 | rule | Packages that use React list react (and react-dom) as peer dependencies; apps/app owns the installed version. | Best Practices |
| D31 | rule | In proxy.ts (Edge runtime) use createProxyClient, never createServerClient, and use createProxyClient nowhere else. | Best Practices |
| D32 | rule | Build REST route handlers that return JSON. No GraphQL and no /api/v1 prefix until the deferred decisions change. | Best Practices |
| D33 | rule | Every metadata field a user or bot fills in is optional; Sidekick's own bookkeeping (ids, counters, timestamps) stays out of the user's way. Individual First | Best Practices |
| D34 | rule | Ask of every item what it costs to add later. A new endpoint or a stylesheet waits; a data migration across every record, or a change to every caller of a shared contract, ships now. A foundation needs a planned, near-term use. Cost to Add Later | Best Practices |
| D35 | decision | Runtime queries use the non-superuser app_runtime role, so the database itself enforces row-level security on every request. | App Runtime for Drizzle |
| D36 | decision | All user-visible strings live in @sidekick/copy; source files never hardcode them. | Centralized Copy |
| D37 | decision | Environment variables load through dotenv-cli, not Node's --env-file flag. | dotenv CLI |
| D38 | decision | Edge-runtime code uses createProxyClient, and nothing else does. | Edge Runtime |
| D39 | decision | Every environment variable is listed in the build task's env array in turbo.json. | Env Vars in Turbo JSON |
| D40 | decision | A curated set of ESLint rules, each aimed at a class of bug, runs on top of the recommended presets; formatting stays with Prettier. | Best Practice Rules |
| D41 | decision | The ESLint plugin package compiles with tsup, because Node must load compiled CommonJS at lint time. | tsup |
| D42 | decision | ESLint uses the flat config format in eslint.config.js. | Flat Config |
| D43 | decision | The layout of every route group that touches Supabase cookies exports dynamic = 'force-dynamic'. | Force Dynamic |
| D44 | decision | The MVP API is REST; evaluating GraphQL and Relay is deferred. | GraphQL and Relay |
| D45 | decision | tsconfig.base.json sets moduleResolution to bundler. | Module Resolution: Bundler |
| D46 | decision | API routes carry no version prefix for now; versioning is deferred. | API Versioning |
| D47 | decision | Every component in the App and in packages is styled with a CSS module; global CSS is allowed only in a named list of files, and inline styles and utility classes such as Tailwind are banned. | CSS Modules |
| D48 | decision | Every capability has one interface, and Sidekick's own clients use it the way bots, jobs and third parties do. | Public API Only |
| D49 | decision | External input is validated at runtime with Zod at every boundary, because TypeScript types are erased. | Runtime Validations |
| D50 | decision | A single tsconfig.base.json at the repo root is extended by every package except the temporary public site, which extends astro/tsconfigs/strict. | Shared tsconfig |
| D51 | decision | TypeScript is a root devDependency, so every package uses the same version. | Installation at Workspace Level |
| D52 | decision | Corepack pins the package manager version, and nobody installs pnpm globally. | Corepack |
| D53 | decision | A Postgres trigger on auth.users creates each user's profile row in the same transaction as signup. | Profile Creation Postgres Trigger |
| D54 | decision | Vercel hosts the App, with apps/app as its root directory and Skip deployment on, so a commit that touches only content/ leaves the App unbuilt. | Hosting and Domains |
| D55 | decision | Turborepo runs tasks across packages, for parallelism and caching that pnpm alone does not provide. | Turborepo |
| D56 | decision | Prettier runs once from the repo root, not once per package. | Prettier at Repo Level |
| D57 | decision | A platform layer, packages/platform/*, sits between features and the foundation, one package per shared service; Synergy is the first. | Dependency Flow |
| D58 | decision | Packages that use React list it as a peer dependency, and each app that renders React (apps/app, and apps/www for its content components) installs it from the pnpm catalog. | Dependency Flow |
| D59 | decision | Vitest is the single unit and integration test framework for the whole monorepo. | Testing Layers |
| D60 | decision | Tests run at four layers, and a behaviour is proven at the cheapest layer that can observe it. | Testing Layers |
| D61 | decision | A link can point at a passage of its target, identified by an anchor stored in the target's content with a quote as the safety net. | Anchors in Content |
| D62 | decision | Synergy versions only content Sidekick holds, so a provider captures a copy of external content when the user saves it. | Capture External Content |
| D63 | decision | The entity index keeps a content_seq counter that goes up on every write to an entity's content. | Content Sequence |
| D64 | decision | Lexicon highlighting splits data from mechanism: an editor spots a term's first use and suggests a defined-by link. | Lexicon Highlighting |
| D65 | decision | Synergy's store keeps list links in the one relationships table and settles three other smaller choices. | Synergy Store Choices |
| D66 | decision | Synergy's soft-deleted rows stay visible to Synergy's own queries, and restoring one is allowed. | Tombstone Visibility |
| D67 | decision | A link lives while its source mentions it in any way, and it is soft-deleted when the source drops its last mention. | Mentions and Link Lifetime |
| D68 | decision | Stay on ESLint and do not migrate to Oxlint yet. | Stay on ESLint, Revisit Oxlint |
| D69 | outcome | Connect. Any entity in any module can link to any other, and every entity knows who references it. | Overview of Synergy |
| D70 | outcome | Keep true. A reference remembers what its target looked like when it was made, and says so when the target changes, is deprecated, is superseded or is gone. Sources telling the same truth are linked, never copied, so they can't quietly drift apart. | Overview of Synergy |
| D71 | outcome | Render. The same link shows as a line of text, a card or the whole target in place, for people. Bots and agents get the same data as structured JSON through the same API. | Overview of Synergy |
| D72 | outcome | Find. Titles, types, statuses and tags are searchable across every module. | Overview of Synergy |
| D73 | requirement | Every entity in every module is linkable. | Synergy - Feature Specifications |
| D74 | requirement | Every field a user fills in is optional. A one-sentence note never needs more metadata than content. | Synergy - Feature Specifications |
| D75 | requirement | An entity without a title appears by a short name taken from its content. | Synergy - Feature Specifications |
| D76 | requirement | Mark a save as a new version when the change may invalidate what others built on it. | Synergy - Feature Specifications |
| D77 | requirement | Ordinary saves (typos, formatting, additions) count as revisions only. | Synergy - Feature Specifications |
| D78 | requirement | Add an optional one-line change note to a new version. | Synergy - Feature Specifications |
| D79 | requirement | Deprecate an entity. It stays readable. | Synergy - Feature Specifications |
| D80 | requirement | Supersede an entity with one or more newer ones. Referencers are pointed at a replacement. | Synergy - Feature Specifications |
| D81 | requirement | Superseding first confirms how many entities reference the old one. | Synergy - Feature Specifications |
| D82 | requirement | Only content edits (title, body, blurb) bump the version or revision. Status, tags and metadata never do. | Synergy - Feature Specifications |
| D83 | requirement | Link one entity to another. The connection is stored, never copied. | Synergy - Feature Specifications |
| D84 | requirement | Give a link an optional label that says what the target answers ("Why?"). | Synergy - Feature Specifications |
| D85 | requirement | Give a link an optional type from the starter vocabulary. | Synergy - Feature Specifications |
| D86 | requirement | An untyped link counts as a reference. | Synergy - Feature Specifications |
| D87 | requirement | Associations (related-to, inspired-by, next, contradicts, violates) never go stale and never block anything. | Synergy - Feature Specifications |
| D88 | requirement | Backlinks show each type by its inverse name ("explains", "source of"). | Synergy - Feature Specifications |
| D89 | requirement | Link the same pair more than once, in the same direction, with different labels. | Synergy - Feature Specifications |
| D90 | requirement | Make a list link: several ordered targets under one label ("Why? guilt, new ideas, perfectionism"). | Synergy - Feature Specifications |
| D91 | requirement | Reorder the members of a list link. | Synergy - Feature Specifications |
| D92 | requirement | Tag a relationship. | Synergy - Feature Specifications |
| D93 | requirement | Delete a link at any time. | Synergy - Feature Specifications |
| D94 | requirement | The link picker greys out entries that would close a reference loop ("would create a loop") and offers to mention the existing link instead. | Synergy - Feature Specifications |
| D95 | requirement | (bot, agent) A request that would close a reference loop is refused with the chain named. | Synergy - Feature Specifications |
| D96 | requirement | Mention one link from both of its entities, as often as needed. It stays one relationship. | Synergy - Feature Specifications |
| D97 | requirement | (target's author) Mentions of a link its source removed show "No longer linked" until dismissed or relinked. | Synergy - Feature Specifications |
| D98 | requirement | Undoing an unlink restores the same link. Linking the pair again later creates a new one, and old mentions can be relinked to it. | Synergy - Feature Specifications |
| D99 | requirement | Linking into a module the account doesn't have is refused. | Synergy - Feature Specifications |
| D100 | requirement | Define a term in the Lexicon (Taxila): an entity of type term with a short definition or acronym expansion, aliases and abbreviations. | Synergy - Feature Specifications |
| D101 | requirement | Link any word to a term with a defined-by association. It renders as a minimal link: hovering shows the definition in the browser's native tooltip (an abbreviation element for acronyms), and clicking opens the term in Taxila. | Synergy - Feature Specifications |
| D102 | requirement | Tag any entity with flat or key:value tags. | Synergy - Feature Specifications |
| D103 | requirement | Tags are lowercased automatically. Anything else is rejected with a hint. | Synergy - Feature Specifications |
| D104 | requirement | Autocomplete suggests the keys already in use and the keys Sidekick ships. | Synergy - Feature Specifications |
| D105 | requirement | Rename a tag once; it changes everywhere. | Synergy - Feature Specifications |
| D106 | requirement | Merge one tag into another. The old name then suggests and applies the merged tag. | Synergy - Feature Specifications |
| D107 | requirement | Deprecate a tag or a relation type. Existing uses stay; new ones are refused. | Synergy - Feature Specifications |
| D108 | requirement | (agent) Suggest tags. They wait for approval unless the user turns on auto-accept. | Synergy - Feature Specifications |
| D109 | requirement | A link whose target was edited since it was made shows a standard-colour dot. | Synergy - Feature Specifications |
| D110 | requirement | A link whose target has a new version shows a red dot, and the change note where there's room. | Synergy - Feature Specifications |
| D111 | requirement | A link shows its target's single status: active, deprecated, superseded or deleted. | Synergy - Feature Specifications |
| D112 | requirement | A deleted target keeps its name as plain, unclickable text. | Synergy - Feature Specifications |
| D113 | requirement | A link into a module the account no longer has shows name, type and status, and no content. | Synergy - Feature Specifications |
| D114 | requirement | (source's author) Mark a changed link "still holds" to clear its dot. | Synergy - Feature Specifications |
| D115 | requirement | (source's author) Point a link at one of the entries that superseded its target. | Synergy - Feature Specifications |
| D116 | requirement | (source's author) Unlink a stale link. | Synergy - Feature Specifications |
| D117 | requirement | Deleting an entity that others reference is refused with the referencer count. Deprecate or supersede it instead. | Synergy - Feature Specifications |
| D118 | requirement | Deleting a source removes its outgoing links, which can free its targets. | Synergy - Feature Specifications |
| D119 | requirement | Restore brings back exactly what a delete removed and never touches the version. | Synergy - Feature Specifications |
| D120 | requirement | Permanent erasure overrides the delete block and leaves referencers a "permanently removed" placeholder. | Synergy - Feature Specifications |
| D121 | requirement | Links into a disabled module stay as they are until it is re-enabled or the link is removed. | Synergy - Feature Specifications |
| D122 | requirement | Select a word, sentence or paragraph and link it. | Synergy - Feature Specifications |
| D123 | requirement | Point a link at a passage of its target: a paragraph, sentence or word. | Synergy - Feature Specifications |
| D124 | requirement | The editor marks passages that others reference. | Synergy - Feature Specifications |
| D125 | requirement | Deleting a referenced passage offers keep, save as a new version, or supersede. | Synergy - Feature Specifications |
| D126 | requirement | Editing a referenced passage asks whether it is a new version. | Synergy - Feature Specifications |
| D127 | requirement | A link whose passage was removed in a new version shows the red dot and "passage removed" until its author re-anchors it, widens it to the whole entity, or unlinks. | Synergy - Feature Specifications |
| D128 | requirement | Removing a link from an entry's content counts as a revision of that entry. Synergy removing a link (an association into a deleted passage) never does. | Synergy - Feature Specifications |
| D129 | requirement | Choose per direct mention: minimal, inline, card or live preview. The two ends of a link may show it differently. | Synergy - Feature Specifications |
| D130 | requirement | Inline reads {title} ({type}) - [{status}], followed by any dot. | Synergy - Feature Specifications |
| D131 | requirement | A minimal mention or an alias mention shows only its label (the link's label, the target's title when there is none, or the author's words), with a red dot when the target needs attention, and a browser-default tooltip reading {type} - {status}. | Synergy - Feature Specifications |
| D132 | requirement | A card shows the title, an excerpt and module fields. | Synergy - Feature Specifications |
| D133 | requirement | A live preview shows the whole current target in place. | Synergy - Feature Specifications |
| D134 | requirement | Links inside a live preview render inline. Previews never nest. | Synergy - Feature Specifications |
| D135 | requirement | (developer) Register a renderer for an entity type. The component calls it wherever that type is linked. | Synergy - Feature Specifications |
| D136 | requirement | Renderers are async and batched: one call per provider per page. | Synergy - Feature Specifications |
| D137 | requirement | Loading, error and empty states look the same for every module. | Synergy - Feature Specifications |
| D138 | requirement | Links are real links in focus order. The dots have text equivalents and differ in shape as well as colour. | Synergy - Feature Specifications |
| D139 | requirement | Long lists of links or backlinks load more as the reader scrolls. | Synergy - Feature Specifications |
| D140 | requirement | (third-party app) Show a Sidekick link as a card or live preview through the public API. | Synergy - Feature Specifications |
| D141 | requirement | (developer) Add a new linkable type (a Twitter thread, a news article) by registering its resolver and renderer, without changing Synergy. | Synergy - Feature Specifications |
| D142 | requirement | Search every module by title, type, provider, status and tag. | Synergy - Feature Specifications |
| D143 | requirement | Search tolerates typos and partial words: "dep wor" finds "deep work". | Synergy - Feature Specifications |
| D144 | requirement | See everything that references an entry, with labels and types. | Synergy - Feature Specifications |
| D145 | requirement | Backlinks and search results page. | Synergy - Feature Specifications |
| D146 | requirement | (bot, job, agent) Use the same guarded API that Sidekick's own screens use. | Synergy - Feature Specifications |
| D147 | requirement | (bot, agent) Get each resolved link as structured data: relationship, target fields, state and marks. | Synergy - Feature Specifications |
| D148 | requirement | (bot, agent) Retry a create safely with an idempotency key. | Synergy - Feature Specifications |
| D149 | requirement | (bot, agent) Get stable error codes: delete_blocked with the referencer count, passage_referenced, cycle, locked, and stale-content conflicts. | Synergy - Feature Specifications |
| D150 | requirement | Every write records who made it (a person, the system, an agent or an API key) and why. | Synergy - Feature Specifications |
| D151 | requirement | A save from a stale copy is refused, whether it comes from a second tab or, later, a second person. | Synergy - Feature Specifications |
| D152 | requirement | A playground page lists mock notes and tasks. | Synergy - Feature Specifications |
| D153 | requirement | Create, edit, version, deprecate, supersede and delete mock entities. | Synergy - Feature Specifications |
| D154 | requirement | Add relationships of every type, with labels, list links and tags. | Synergy - Feature Specifications |
| D155 | requirement | Each mock entity shows its fields, tags, outgoing links and backlinks. | Synergy - Feature Specifications |
| D156 | requirement | The same link shows in all four display types side by side. | Synergy - Feature Specifications |
| D157 | requirement | Switch the mock modules' entitlement off and on to watch links lock and come back. | Synergy - Feature Specifications |
| D158 | requirement | The playground and mock modules sit behind the sandbox-notes and sandbox-tasks entitlements, which real users never hold. | Synergy - Feature Specifications |
| D159 | requirement | Cite a link from a field (such as related-to) or a links panel. It counts as a mention of the source, so the link lives until the source drops its last mention of any kind. | Synergy - Feature Specifications |
| D160 | requirement | An entity's title shows a dot when any of its outgoing references needs attention, with the links as the reason. | Synergy - Feature Specifications |
| D161 | requirement | Mark that an entity breaks a rule, principle or decision with a violates link. The rule's backlinks read "violated by". | Synergy - Feature Specifications |
| D162 | requirement | Record an exception to a rule with an exception-to link. It goes stale when the rule changes, and the rule can't be deleted while it has one. | Synergy - Feature Specifications |
| D163 | requirement | Resolve a violation by unlinking it. | Synergy - Feature Specifications |
| D164 | requirement | Waive a violation by changing its type to exception-to. It stays the same link, with a fresh snapshot of the rule. | Synergy - Feature Specifications |
| D165 | requirement | Filter an entity's links or backlinks by type: "what violates this rule?" | Synergy - Feature Specifications |
| D166 | requirement | List links of one type across every module: every open violation in one list. | Synergy - Feature Specifications |
| D167 | rule | Every field the vision needs, from day one. This covers the entity index (owner, provider, type, title, status, version, revision, timestamps, tombstone), the version log (change notes), relationships (single and list, label, type, version snapshot, to_anchor with its quote, cited and mentioned), tags (with key, aliases and deprecation), and the relation-type vocabulary (with its inverse name and reference/association kind). Columns a near-term phase needs, like the anchors Taxila uses in Phase 4, exist now. Adding a nullable column later is easy. Backfilling meaning into rows that were written without it is not. | Foundations |
| D168 | rule | Provenance on every write. Each write records who made it: a person, the system, an agent or an API key, and why. AI tag approval, "removed by system" revisions and any future audit all depend on it. | Foundations |
| D169 | rule | Client-generated UUIDs, updated_at and soft delete on every table. This is what lets the PWA in Phase 8 sync offline without redesigning the tables. | Foundations |
| D170 | rule | Rules live in the database, not only in code. Foreign keys, unique constraints, a check constraint for the tag format, and one transaction for each rule that spans rows. "Block delete while referenced" and "reject cycles" run under a per-user lock in the same transaction as the write. Bots and agents call the API concurrently. A rule checked in app code alone breaks under a race. | Foundations |
| D171 | rule | Optimistic concurrency. An update carries the content_seq it started from, and a stale write gets 409. content_seq moves on every content write, versions and anchors included, so a version save or an anchor added by another link can't be silently overwritten (Content sequence). Two tabs, or a person and an agent, editing the same entity must not silently overwrite each other. | Foundations |
| D172 | rule | RLS on every table, comparing the uuid column directly (col = nullif(current_setting(...), '')::uuid, never col::text = ...). The cast form converts every row's uuid to text, so Postgres can't use the uuid index and scans the whole table. The right form converts the setting once and lets the index find the user's rows directly. Every Synergy query filters by owner. Phase 3 also fixes the existing policies in sidekick migrations 0002 and 0007, which use the cast form today . | Foundations |
| D173 | rule | Locked, redacted and deleted states are decided on the server, before any renderer runs. No content leaves a module the reader can't see. | Foundations |
| D174 | rule | Indexes for every hot path. Backlinks (to_id), outgoing links (from_id), list order (parent_id, position), by tag, by owner and status, and a trigram index on titles. Popular entities will have thousands of backlinks. | Foundations |
| D175 | rule | Staleness is computed when it's read, never fanned out on write. It comes from a join of the relationship's snapshot with the index row. Flagging every referencer when a popular entity changes turns one edit into thousands of writes. Post-MVP digests ("you reference stale or deprecated entries") fit this: the digest job is the reader and runs one indexed query per user. Instant alerts fan out asynchronously from the outbox, never inside the save. | Foundations |
| D176 | rule | Batched resolution. A document with 50 links makes one resolver call per provider (resolveMany), not 50. The resolver and renderer contracts take lists from the start. | Foundations |
| D177 | rule | Cache keys that carry the content sequence. Rendered card and preview data is cached under (entity, content_seq, display type), and HTTP responses carry content_seq as an ETag. Any content write means a new key, so invalidation is never a purge problem. Only render data is cached; states are read fresh. The cache sits behind an interface: in-process or Postgres now, a shared cache (Redis) later without changing callers. | Foundations |
| D178 | rule | Cursor pagination on every list. This covers backlinks, search, tags and the version log. Offset pagination collapses at the sizes Synergy is meant for. | Foundations |
| D179 | rule | An outbox from day one. Every change writes an event row in the same transaction, with the transaction id so readers can follow commit order: entity, relationship, tag and passage events, listed in the system design. Nothing consumes the outbox in Phase 3. Notifications, Factory automations, cache warming and search reindexing all start from it later, and an event log can't be reconstructed for writes that never recorded one. | Foundations |
| D180 | rule | One public contract. Typed request and response schemas, stable error codes (delete_blocked with the referencer count, cycle, locked) and idempotency keys on create, so a retried bot request doesn't link twice. Everything goes through withApiGuard, which also logs. Bulk endpoints can wait: adding them later changes nothing that exists. | Foundations |
| D181 | rule | Lists that page from the start. Any list the component shows (backlinks, list-relationship members, search results) loads incrementally by cursor and can scroll to load more, with virtualization for long lists. A component built for "load everything" gets rewritten at the first popular entity. | Foundations |
| D182 | rule | Async resolvers with every state. The frame owns loading, error, empty, locked, redacted and deleted. The resolver is async from day one and returns serialisable render data; the renderer is a pure function from that data to React. The component always awaits the resolver. A synchronous contract would force a signature change on every caller the first time one provider needs a fetch. | Foundations |
| D183 | rule | Batched fetching in the component. Links on one page are collected, grouped by provider (Taxila, War Room, Alter Ego; note and task in Phase 3), and resolved in one request per provider (pairs with item 10). A plan page with 20 datums, 8 tasks and 2 chat sessions makes 3 calls, not 30. Fetching per link can't be fixed without changing how every caller mounts the component. | Foundations |
| D184 | rule | Display type and states as props and data attributes. The four display types and the states are explicit props, exposed to styling as attributes and tokens. That's what keeps icons, pills and hover cards purely additive later. | Foundations |
| D185 | rule | Accessible semantics. Real links, focus order, and a text equivalent for the edited and changed dots, which also differ in shape so colour is never the only cue. Retrofitting accessibility means touching every renderer. | Foundations |
| D186 | rule | A host-agnostic component. The component takes its data and a transport (resolveLinks(requests)) as inputs and assumes no app-wide context, so Notion, Slack or an embed page can show a Sneak Peek Link later. A component that reaches into Sidekick's app shell has to be rewritten the day a third party wants it. | Foundations |
| D187 | rule | An open type registry. Linkable types, resolvers and renderers are registered by packages, never listed in Synergy. Adding a Twitter thread or a news article as a linkable type is a new package, not a Synergy change. | Foundations |
| D188 | requirement | a link can carry a label and a type | Scope and Phases |
| D189 | requirement | a version bump with a change note shows the red dot | Scope and Phases |
| D190 | requirement | deprecate and supersede work | Scope and Phases |
| D191 | requirement | deleting a referenced target is blocked | Scope and Phases |
| D192 | requirement | reference cycles are rejected | Scope and Phases |
| D193 | requirement | key:value tags work | Scope and Phases |
| D194 | requirement | search finds entities by title and metadata | Scope and Phases |
| D195 | requirement | disabling a module makes its links locked | Scope and Phases |
| D196 | requirement | another user's entities never leak | Scope and Phases |
| D197 | rule | Rule for every module: each entity type that can appear as a Sneak Peek Link (datum, narrative, plan, chat session, and so on) ships its own renderer, registered for any consumer that needs to show that type. | Scope and Phases |
| D198 | decision | Synergy is a graph pattern on plain PostgreSQL, not a graph database, and its own repository is the only access path and the one place to change if scale ever needs a graph engine. | Synergy: System Design |
| D199 | decision | Synergy ships three entry points, /server, /react and /contracts, so a browser bundle can never pull in database code. | Synergy: System Design |
| D200 | decision | Each linkable type is registered by its provider in two halves, a server half that resolves and a pure React half that renders, and the registry hands Synergy the maps as data, so Synergy never imports a provider. | Synergy: System Design |
| D201 | decision | Synergy stores and returns keys only (types, statuses, states, error codes, message references); every word comes from a catalog, the provider's for its words and Synergy's for its own, and only user content is stored as written. | Synergy: System Design |
| D202 | decision | Every Synergy write takes the per-user advisory lock before it reads anything, so the delete block and the cycle check can't race and there is one lock order. | Synergy: System Design |
| D203 | decision | A save reports the current sets of block, anchor and mentioned relationship ids, and Synergy works out what changed, so the call heals itself after a missed save and is safe to retry. | Synergy: System Design |
| D204 | decision | Every link create snapshots the target's version and revision, and a reference create runs the cycle check. | Synergy: System Design |
| D205 | decision | A link into a passage returns an anchor receipt, so an editor that has the target open adds the mark itself; the 409 merge on the next save is the guarantee. | Synergy: System Design |
| D206 | decision | superseded-by is a reference stored from the old entity to each of any number of replacements; the first one marks the source superseded after a confirmation, and removing the last sets it back to active. | Synergy: System Design |
| D207 | decision | Restore and undo re-run the delete block and the cycle check under the lock, and are refused with restore_conflict naming each edge. | Synergy: System Design |
| D208 | decision | The render cache holds render data only; status, states and marks are read fresh on every request, including for links inside a live preview. | Synergy: System Design |
| D209 | decision | Another user's ids answer not_found, the same as ids that never existed, so a response never reveals whether an id exists. | Synergy: System Design |
| D210 | decision | A page learns that its entity changed elsewhere by asking GET /entities/:id with If-None-Match, which answers 304 while nothing changed; pushing changes waits for post-MVP. | Synergy: System Design |
| D211 | decision | Every provider builds the save call's sets with the shared extractRefs, and the quote re-matcher asks the user when a match is weak instead of guessing. | Synergy: System Design |
| D212 | decision | violates is an association and exception-to a reference; a violation has no status, so unlinking resolves it and retyping it to exception-to waives it. | Violations and Exceptions |
| D213 | decision | mehtapratik/sidekick is the one repo. It takes Arkive's full history (rewritten with git filter-repo) through a single merge commit, the only merge commit on main, and mehtapratik/arkive is archived, not deleted, so its PR links keep working. | One Repo |
| D214 | decision | The vault lives in content/docs/, the writings in content/writings/, and the public site in apps/www/. The App's apps/web is renamed apps/app, so each app folder is named for the address it serves, and every app takes the @sidekick/ scope (@sidekick/www, @sidekick/app). apps/ and packages/ stay as the parent folders, and the root AGENTS.md points into content/docs/. | One Repo |
| D215 | decision | content/ is a workspace package, @sidekick/content, that the site depends on. Vercel's Skip deployment setting then leaves the App unbuilt for commits that touch only content, and Turbo rebuilds the site when content changes. A content label marks PRs whose files are all under content/. | One Repo |
| D216 | rule | content/ stays out of Prettier. Obsidian writes the vault, and half its files fail Sidekick's Prettier config. The site's files follow the root config, except the files the site ships as given. | One Repo |
| D217 | decision | The root LICENSE stays MIT and covers the code. content/LICENSE holds CC BY-NC 4.0 and covers the writings and docs, and the README's Licence section says which is which. Fonts keep their own licences, next to the font files. | One Repo |
| D218 | decision | ossidekick.com is the project's domain. The public site serves its apex, www.ossidekick.com redirects to the apex, and the App gets app.ossidekick.com, a DNS-only CNAME to Vercel. Both move there on the same day, the cutover, after the redesign ships. Until then the site stays on arkive.blog and the App on sidekick-six-bay.vercel.app. | One Repo |
| D219 | decision | The public site shows no link to the App until Open House. The App serves one person until then, so a public link would only lead to a sign-in nobody else can use. Supabase sign-up is off for the same reason. | One Repo |
| D220 | decision | The public site runs on a Cloudflare Worker with static assets, sidekick-www, built by Workers Builds from the repo root, with a public preview for every branch. Its settings live in apps/www/wrangler.jsonc, so they are reviewed in a PR. A later ossidekick.com/app route could be a script on the same Worker. | One Repo |
| D221 | rule | No published URL changes. Moving the repo, rehosting the site, redesigning it and moving the domain must each leave every path in sitemap.xml resolving, and a diff of the sitemap paths before and after is the test. | One Repo |
| D222 | decision | The cutover has no downtime. ossidekick.com is added beside arkive.blog on the Worker first; only then does arkive.blog become a 301 to the same path and query on ossidekick.com, tagged from=arkive.blog. Visitors who arrive through the redirect see a one-line notice that the site moved. arkive.blog redirects for one year and then lapses. | One Repo |
| D223 | rule | The site uses no media queries for layout, with one exception: a single width breakpoint, @media (width < 58rem), which only swaps chrome (the header nav and theme control move into the Menu, the docs tree gives way to a Contents view, the / hint hides). Media queries are otherwise allowed only for prefers-color-scheme, prefers-reduced-motion and hover. Everything else adapts intrinsically, and a component that adapts to its own column uses a container query. | One Repo |
| D224 | decision | The home page's "Where things stand" shows one tile per phase, read from each 04-phases/NN-name/00-plan.md: order and label from NN, title and status from frontmatter. completed is done, deferred is deferred, the first other phase is up next. Sub-plans get no tile and appear under their phase in the "All phases" list. | One Repo |
| D225 | decision | /rss.xml carries Writings only, newest first by created, with the summary as each item's description. There is no docs feed until someone asks for one. | One Repo |
| D226 | rule | Project vocabulary lives in content/docs/07-glossary/, one page per term. When a skill asks for CONTEXT.md, the agent writes or updates the glossary page instead, and no CONTEXT.md is created. | One Repo |
| D227 | decision | An entry that needs UI Markdown cannot express uses a static React component, written as a sidekick:<name> fenced block with YAML props in plain .md and rendered to HTML at build time. There is no MDX until an entry needs an interactive island; adding it later costs what it costs now. Components are plain React styled with CSS modules and the variables in tokens.css, which moves into @sidekick/ui as the one token source. | One Repo |
| D228 | requirement | git log --follow on any doc or writing shows its Arkive commits. | Synergy - Feature Specifications |
| D229 | requirement | One PR can change code and the docs that describe it. | Synergy - Feature Specifications |
| D230 | requirement | pnpm install from a fresh clone installs everything, with no clone script, no symlink to set up and one lockfile. | Synergy - Feature Specifications |
| D231 | requirement | CI runs the site's build with its privacy and datum gates, and its type check, on every PR. | Synergy - Feature Specifications |
| D232 | requirement | A commit that touches only content/ builds the site and skips the App. | Synergy - Feature Specifications |
| D233 | requirement | Every branch that touches the site gets a public preview URL. | Synergy - Feature Specifications |
| D234 | requirement | [Reader] Every URL that worked on arkive.blog still works, on arkive.blog after Lesson 0 and through a 301 to ossidekick.com after the cutover, with its path, query and fragment kept. | Synergy - Feature Specifications |
| D235 | requirement | [Reader] A visitor who arrives through the arkive.blog redirect sees one line saying the site moved, once. | Synergy - Feature Specifications |
| D236 | requirement | [Reader] The home page shows how many phases are complete and which is up next, from the phase plans. | Synergy - Feature Specifications |
| D237 | requirement | [Reader] The feed brings each new Writing once, and an edit to an old one does not bring it back. | Synergy - Feature Specifications |
| D238 | requirement | An entry can show a component by naming it in a fenced block, and a wrong name or wrong props fail the build. | Synergy - Feature Specifications |
| D239 | requirement | [Reader] On a phone the header holds the wordmark, Search and Menu, and the docs tree opens from a Contents button. | Synergy - Feature Specifications |
| D240 | decision | Sidekick ships in English only but stays i18n-ready: adding a language means adding catalogs and an entry in the locale config, with no other code change. | Internationalization |
| D241 | decision | The App translates and formats with next-intl; a public face built to last uses use-intl, the library next-intl is built on, so both read the same ICU catalogs through the same t() and formatter API. | Internationalization |
| D242 | decision | The App takes the locale from the device (Accept-Language, en until a second language ships) in one place, i18n/request.ts, and keeps locales out of URLs; the site sets lang and dir from config and adds locale routes, English unprefixed, only when translated content exists. | Internationalization |
| D243 | rule | Every user-visible string lives in a JSON catalog next to the package that uses it, under one namespace named for that package; @sidekick/i18n alone owns the common namespace, and a string moves there once a second package needs it. | Internationalization |
| D244 | rule | A message is a whole sentence in ICU syntax, with plurals and selects inside it; code never builds a sentence by joining messages or changing their case. | Internationalization |
| D245 | rule | Numbers, currencies, dates, relative times and lists are formatted only through the locale's formatter and the shared named formats; an amount is always stored with its ISO 4217 currency code, and a date is shown in the person's time zone. | Internationalization |
| D246 | rule | Layout is direction-agnostic: CSS uses logical properties and values (padding-inline-end, margin-inline-start, text-align: start), <html> gets lang and dir from the locale, and code that maps keys or icons to a direction reads dir. | Internationalization |
| D247 | rule | Arabic (ar) is a mock locale for the test pages only: marked mock in the locale config, never offered to people, and required only of the catalogs the test pages read. | Internationalization |
| D248 | rule | Lint keeps the catalogs and direction honest: eslint-plugin-i18next's no-literal-string on user-facing code, stylelint-use-logical on CSS, sidekick/catalog-namespace on catalogs, and a catalog check that fails on the same English text in two catalogs or a missing catalog for a supported locale. | Internationalization |
| D249 | rule | @sidekick/ui holds no words and imports no i18n library; every word in a primitive is a required string prop named <purpose>Label (closeLabel, loadingLabel), which the caller fills from its own catalog. | Internationalization |
| D250 | decision | Every word a provider hands to Synergy is a message reference, { key, values? }, with a fully qualified key the provider names: in its registration for the words Synergy draws itself, and anywhere in its render data for the rest; Synergy never builds, parses or stores a provider's key. | Synergy: System Design |
| D251 | decision | The Sneak Peek Link takes plain data only, the resolved link and a words dictionary; the host fetches the link and translates its references, so no function and no i18n provider crosses into the component. | Synergy: System Design |
| D252 | rule | Every zod issue's message is a catalog key, set once with zod's customError, and the API returns validation failures in Sidekick's own shape, { path, message: { key, values } }, never zod's issue objects or sentences. | Runtime Validations |
| D253 | decision | The App shows dates in the device's time zone, which the browser writes to a tz cookie that i18n/request.ts passes to next-intl as timeZone, with UTC only before the cookie exists; a stored locale or time zone is optional, added only when something with no browser needs one. | Internationalization |
| D254 | decision | @sidekick/ui names the design tokens and holds no values in its components; Sidekick's values ship as an opt-in theme file that each app imports, and a contract check fails when a theme leaves a token unset. | Design Tokens |
| D255 | decision | @sidekick/ui builds its primitives on native HTML with no UI library, and Mantine leaves the App in lesson M1; the interface is Sidekick's own, the smallest one today's screens need. | Native UI |
| D256 | decision | apps/www is temporary: it is rewritten or retired once the MVP ships, so it favours speed over long-term maintainability and is held only to the rules that protect data, published URLs and shared code; the public face that replaces it follows every core principle and system design rule. | Public Site: System Design |
| D257 | rule | Any public face of Sidekick honours the content's contracts: published URLs never change, publish: true gates every page, Writings and their feed by created, "As of" from frontmatter, retired entries off every listing but still open, datum anchors and /datums.json, milestones from the phase plans, and the sidekick:<name> blocks written into content. | Public Site: System Design |
| D258 | decision | apps/www is its own boundaries element type, site, that may import foundation packages except core and that nothing imports, so the public site can never reach core's database client, the registry or a feature. | Dependency Flow |
| D259 | rule | content/ holds content only: its package.json has only name, private, version and license, no code, exports or scripts, and a check script fails CI and the pre-commit hook on anything else. | Dependency Flow |
| D260 | decision | Content components are listed in one map in apps/www, each name with its component and a zod schema for its props, and the remark plugin fails the build with the entry's path on an unknown name or props that don't parse. | Public Site: System Design |
| D261 | decision | Test helpers that more than one package needs live in @sidekick/test-utils, a root singleton that test files alone may import and that imports no workspace package. | Testing Layers |
| D262 | decision | Each package that owns tables records its applied migrations in its own table, drizzle.__drizzle_migrations_<package>, and Turbo runs the packages' migrations in dependency order. | Migration |
| D263 | decision | A restore changes only deleted_at and the provenance columns (updated_at, actor_kind, actor_id, reason); synergy_block_update_on_deleted refuses a restore that changes anything else. | Tombstone Visibility |
| D264 | decision | Every table chooses one of three delete models (hard delete, soft delete with tombstones hidden, soft delete with tombstones visible), names it in its schema file, and is listed below with its model. | Delete Models |
| D265 | rule | Every write in Synergy's repository runs inside withUserLock(...), which takes the user lock before the write function runs; the lint rule sidekick/require-user-lock fails any insert, update or delete in src/repository/ outside it. | Synergy: System Design |
| D266 | decision | A column that holds one of a fixed set of keys (a status, an actor kind) is text with a CHECK constraint, not a Postgres enum. | Migration |