Commit Graph
9 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 500c2f2179 name the six over-long foreign keys, so a push can honestly be a no-op
Drizzle generates foreign-key names past Postgres's 63-character identifier limit.
Postgres stores the truncated form, drizzle keeps comparing against its own longer
one, and every `db:push` on every host drops and re-adds constraints that are
already identical. Eight statements, for ever.

Documented in src/databases/CLAUDE.md on 2026-08-19 and deferred as "noise rather
than a fault". What ended that was last night's deploy plan saying "expect no
changes, stop and report if it wants to alter something" — which this made
impossible to satisfy on any host, so the deploy agent had to overrule its own
instructions and say why. An annoyance had become an instruction nobody could
follow.

Two of the six are in this repo; the other four are in the atelier and
bitcoin-wallet plugin repos and are committed there. Verified the way the doc
prescribes: push twice against a scratch database, and the second run went from ten
statements to two.

The two that remain are `pk_music_now_playing`, and that one is tested rather than
assumed. Removing its explicit name produces the identical churn under drizzle's
generated name, so it is the named-composite mis-diff and not the truncation one,
and a primary key cannot escape to an index form. A push now prints exactly those
two lines — which is a far more useful baseline than ten, because anything else
really is your change.

Naming a foreign key means removing the inline `.references()` or drizzle creates
both, and `walletChainCache` had to move to the three-argument `pgTable` form
because there was nowhere else to put the declaration. Both noted in the doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:16:19 +00:00
pastilhas 46d621c91b chat notifications reach the phone, on the same rule as the bell
the browser bell and phone push are now two subscribers to one signal. both
ask shouldNotify(); neither owns the rule.

deliberately a second subscriber rather than a branch inside notify-delivery.
the two channels share only the question "does this account want to hear about
this" — the browser needs an open tab and sends the chat's real title over a
loopback socket on the owner's own tailnet, the phone needs no tab and must
send NO title, because that hop goes through apple. one function doing both
means an `if (isPush)` at every step, and the first thing to go wrong is a
title leaking because the branch was missed in one place.

two codes, officer/core:chat.turn-ended and chat.input-needed. the route is
written out with its scheme rather than derived from the target: the hub has
no chat screen — chat is Butler, a sibling app — so these deep-link OUT over
officer-chat://, and two of the suite's twenty-one schemes already break the
officer-<app> pattern (officer-videos, officer-readaloud). deriving that works
until it silently does not.

`thread` interpolates now, so `chat:{id}` groups per conversation. a static
thread forces a choice between "three pings from one chat collapse" and "two
busy chats stay apart", and the wrong answer is that every chat is the same
chat.

dropped officer/core:agent: chat.turn-ended supersedes it and its route
pointed at officer://chat/{id}, a hub screen that does not exist.

the banner cannot name which conversation. a chat's title is content and
content does not travel; it says what happened, and tapping opens the chat,
which is where the name lives.
2026-08-19 22:00:17 +00:00
pastilhasandClaude Opus 5 595dd082a7 delete Task Logs
A complete read path over a table nothing could write to. task-logger.ts exported
createTaskLog, appendToLog and finalizeLog, and none of the three was called
anywhere in the tree — so `task_logs` could never gain a row, and the screen was
permanently empty for everyone.

The read half was fully wired: mounted router, capability claim, dock icon, two
routes and a page-title rule. That is why it looked alive.

Gone, in the order it was reached:

  Screens/Dashboard/TaskLogs/       the screen
  App.tsx                           /task-logs and /task-logs/:id
  Dashboard/index.tsx               the export
  Layout/Dock.tsx                   the 'Logs' icon, and ScrollText with it
  state/usePageTitle.ts             the title rule
  api/task-logs/task-logs.ts        the router, and its mount in hono.ts
  api/task-logger.ts                101 lines of orphaned writer
  officer_db/src/operations/        the directory
  officer_db/src/schema.ts          the export line
  officer_db/src/types.ts           TaskLogSelect / TaskLogInsert

capabilities/registry.ts loses '/task-logs' from the `tasks` capability's `api`
AND `routes`. The api half is not optional: assertCapabilityTotality check 2
refuses to boot on a capability claiming a prefix nothing mounts, so unmounting
the router while leaving the claim would have stopped the server starting.

db:push now creates 21 tables, down from 43 at the start of the evening.

officer_db/src/operations was one of the two lopsided directories the
schema/queries merge exposed. integrations/ is the remaining one, and it is
legitimate — it spans server and user-data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:56:07 +00:00
pastilhasandClaude Opus 5 288b4bf683 each feature declares its own exports; the barrel is one line per feature
src/index.ts is 47 lines and reads as a list: `export * from './agent-panels';`
and 22 more, under `db` and `schema`. It was 297 lines of hand-written named
exports.

What a feature exports now lives in <feature>/index.ts, beside the schema and
queries it describes. Adding a query function is one file in one directory rather
than that file plus a list three levels up that nothing enforces — a function
missing from that list was invisible to all 107 consumers while existing and
compiling perfectly.

The old file had drifted in the ways a hand-maintained list does: twelve features
were listed twice because values and types were separate statements repeating the
path, soulseek three times, two features used an inline `type` specifier instead,
and `db` and `schema` — the package's most fundamental exports — sat at line 270
with notify, types and app-store appended after them.

The surface is byte-for-byte the same set. Checked rather than asserted: 247
exported names before, 247 after, no missing and no extra. The per-feature index
files carry the same named lists the barrel did, so `export *` widens nothing.

`operations` is deliberately not in the list, and the root file says why: it has a
schema and no queries, its task_logs is reached as `schema.taskLogs` from
src/servers past this package's boundary, and its other two tables are read by
nothing at all.

Verified: every file in the package parses, every relative import resolves to a
real file, and all 107 consumers still parse. Not typechecked — empty
node_modules, frozen installs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:45:31 +00:00
pastilhasandClaude Opus 5 68f2c55ecf one directory per feature: schema.ts and queries.ts together
src/databases/officer_db/src/<feature>/{schema.ts,queries.ts}, replacing the
parallel schema/ and queries/ trees. 24 feature directories, 46 files moved with
git mv so history follows.

The parallel trees had drifted, which is what the restructure is really fixing:

  four features were named differently on each side — app-store/sidecar-installs,
  email/email-accounts, server/server-config

  operations had a schema and NO query file: its task_logs is reached directly
  from src/servers/api/task-logger.ts, bypassing this package's own boundary

  integrations had queries and NO schema, because it spans two features'
  tables — server_integrations and user_integrations

Both lopsided cases survive as directories holding one file, which states the
problem instead of hiding it across two trees.

Nothing outside the package changed how it imports. `officerdb`, `officerdb/types`
and `officerdb/db` resolve exactly as before; index.ts absorbed the path changes.
Added `"./*": "./src/*"` so the new layout is reachable — `officerdb/soulseek/schema`
— which one script needed, because soulseek is a plugin and therefore commented
out of the aggregator.

schema/index.ts became src/schema.ts, keeping the core/plugin split from earlier
tonight. drizzle.config.ts and the package's "./schema" export follow it.

Verified rather than assumed: all 52 files in the package parse, every relative
import resolves against the new layout (checked by walking each specifier to a
real file, since parsing does not check paths), and everything in the tree
importing officerdb still parses. Not typechecked — empty node_modules, frozen
installs.

One rewrite bug worth recording: the rule mapping a query module's sibling import
also matched the './schema' this pass had just written, turning it into
'../schema/queries' in 22 files. Caught by the resolver check, not by parsing —
both spellings parse fine.

Also corrects every path reference the move invalidated: src/databases/CLAUDE.md's
layout diagram, the root CLAUDE.md data section, three docs, and seven sidecar
comments naming queries/<x>.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:34:04 +00:00
pastilhasandClaude Opus 5 a9b6494f56 db: stop push re-creating every composite key on every run
`bun db:push` planned 32 statements against a database that already matched the
schema, and stopped on a "do you want to truncate screens?" prompt that answering
could not resolve — the same question came back next run. Root cause found and
fixed rather than worked around.

drizzle-kit mis-diffs named composite unique CONSTRAINTS. It reads one back,
compares it against a schema declaring the identical name, columns and order,
decides they differ, and emits DROP + ADD. Fifteen of those, forever. Reproduced
on a database drizzle had itself created seconds earlier, so it is not drift.
Single-column .unique() is diffed correctly; only unique('name').on(a, b) is
affected. Unique indexes go through a different code path and are stable, so all
fifteen are now uniqueIndex.

A unique index enforces exactly what the constraint did — verified, a duplicate
insert still fails on uq_screens_user_name — and onConflictDoUpdate accepts it as
an arbiter. It cannot be a foreign-key target, but nothing here targets a
composite key; checked before converting.

Separately, user_integrations_server_integration_id_server_integrations_id_fk is
65 characters and Postgres truncates identifiers at 63, so drizzle compared its
generated name against the stored, truncated one and re-created the FK every run.
Declared explicitly as fk_user_integrations_server_integration.

Measured on a scratch database, pushing twice each time:
  before        32 statements, interactive prompt
  after uniqueIndex   4
  after FK fix        2

The two that remain are a composite primaryKey with the same bug and no index
form to escape to — music_now_playing re-creates pk_music_now_playing every push.
Silent, no prompt even with rows, data unaffected, and naming it explicitly does
not help. Documented as expected.

The conversion itself was tested against populated tables, since that is what the
real database will do: no prompt, and all rows survived.

Docs rewritten in src/databases/CLAUDE.md — the rules committed an hour ago
described the broken behaviour and would have been wrong the moment this landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:35:26 +00:00
pastilhasandClaude Opus 5 6af80a20b0 db: add the user role column, and write down how push actually behaves
Adds `role` to users as a text column with a TS enum, matching how `status` is
done — no pgEnum, so adding a role stays a one-line schema change rather than an
ALTER TYPE while the set is still settling. USER_ROLES and UserRole are exported
from officerdb so the API and UI enumerate them from the column definition rather
than a second hand-written list. Defaults to 'Member', the least privileged, so a
row created by a path that does not think about authorisation cannot mint an
admin. NOT YET PUSHED — the column is in the schema, not in the database.

The larger half of this commit is a rule for anyone running `bun db:push`,
because the first time you run it, it looks like your change broke something.

It plans 16 statements against a database that already matches the schema, and
plans them again on the next run. Two separate causes, both diagnosed here:

- it drops and re-adds every NAMED COMPOSITE unique constraint — all 14, from
  uq_screens_user_name to uq_wallet_labels_wallet_kind_ref. Single-column
  .unique() diffs correctly and is untouched; only unique('name').on(a, b) is
  affected. Names, columns and order in the database are identical to what the
  schema declares. drizzle-kit 0.31.9 / drizzle-orm 0.45.1.
- it drops and re-adds one foreign key because
  user_integrations_server_integration_id_server_integrations_id_fk is 65
  characters and Postgres truncates identifiers at 63, so drizzle compares its
  generated name against the stored, truncated one and always differs.

The rules, ranked by how much damage getting them wrong does: never answer "Yes,
truncate the table" — it destroys rows and does not help, since the constraint is
re-added next push either way; never delete a constraint from the schema to
silence the prompt, because upserts depend on it existing; do not reach for
--force until someone has established on a scratch database which branch it takes.
Run with --verbose first and read what it is actually planning.

Pointers added to platform/CLAUDE.md and the workspace root CLAUDE.md, since
those are what a session reads before it ever opens the database directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:22:50 +00:00
pastilhasandClaude Opus 5 db9d17d6fe docs: the convention docs were describing a different codebase
Second pass. These three are the ones a new contributor reads first, and all three were
teaching things that are not true here.

src/databases/CLAUDE.md claimed "Three PostgreSQL databases", listed two, and there is
exactly one. Its type examples were Screenshot / Experiment / Company / GanOauth — none of
which have ever existed in this repo; it had been carried over from another project
wholesale. Rewritten against the real schema, queries and types, and it now carries the two
things that actually bite: push-not-migrations, and the rule that the schema is the source
of truth for what the database may CONTAIN, not just its shape — with the sql.raw trap in
check() written down, since getting it wrong breaks push for the whole schema.

src/apps/CLAUDE.md had the same problem in its examples (useExperimentsList, ExperimentCard,
a state/ directory layout that does not exist), listed a `useWebsockets` hook that is not
there while omitting useChatWebSocket, usePanelChannel and useJobs, and closed with links to
three app docs that have never existed. Examples now use real hooks, and it points at the
navigation audit — a frontend doc that did not mention the one rule the platform CLAUDE.md
calls authoritative was a real gap.

CONVENTIONS.md said, in bold, that useMemo and useCallback are "strictly prohibited" because
"React 19's compiler handles memoization automatically". Wrong twice: the React Compiler is
an opt-in build plugin that is NOT installed here, so React 19 memoizes nothing on its own —
and roughly 40 files use each hook regardless, including code added this week. Replaced with
guidance that matches both reality and the actual tradeoff, and says plainly what it used to
claim. A rule that is false and universally ignored makes every other rule in the file look
optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:42:47 +00:00
pastilhas 9ab0940ca4 first 2026-02-16 19:34:35 +00:00