diff --git a/CLAUDE.md b/CLAUDE.md index 3f7f33f1..a9f89cb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,16 +26,65 @@ Both toolchains live at the root: `package.json` is the Bun workspace, | `cargo build --workspace` · `cargo test --workspace` | the Rust half | | `bun run deploy:sandbox` | deploy a stage | -## Two rules that are not style preferences +## Three rules that are not style preferences -**Nothing closed may enter this repo.** Not source, not a dependency, not a -directory that "looked convenient". Before adding a top-level directory, know -which component it is and that the component is open. This has already been -caught once, in a commit that was never pushed. +### 1. THIS REPO IS PUBLIC. Write nothing that only makes sense to us. -**Versions are pinned once, centrally.** A Cargo member writes -`tokio.workspace = true` and never a version; a TS package uses the root -`catalog`. Two packages in one tree must not disagree about a dependency. +**Read this before writing a single comment, docstring or commit message.** It +has been violated twice, both times by someone who knew the repo was public, +both times about ten occurrences deep before anyone noticed. Being careful is +demonstrably not enough, so the rules below are mechanical. + +**Never, anywhere in this repo:** + +| ✗ | why | +|---|---| +| **Any relative path that escapes this tree** into an internal repo, or the filename of an internal document | A filename plus a title tells a reader exactly what to ask for | +| A quotation from an internal document, even one phrase | Restate the requirement in this repo's own words | +| The name of a component with no public surface | It discloses the shape of the system, which is the part deliberately kept | +| Anything of the above in a **commit message** | History is permanent here and is deliberately never rewritten — a message cannot be fixed by a later commit | +| Anything of the above in **published output** — an OpenAPI `description`, an error message, CLI text, a README | A docstring that becomes an API description reaches people who never open the source. Check where a string *goes*, not what file it is in | + +**The one sanctioned exception**, and the only way to cite internal reasoning: + +```ts +// A size tier sets vCPU, RAM and the output geometry. ref(d-0021) +``` + +`ref(d-NNNN)` · `todo(d-NNNN)` · `fixme(d-NNNN)`, in **source comments only**. +Note `d-NNNN` and never `d/NNNN` — a slash reads as a path. + +**The test that makes it decidable — apply it to every sentence:** + +> Delete the marker. Does the comment still say something true and useful about +> *this* code? + +If yes, it belongs. If the sentence collapses without the reference, it was +describing our topology rather than this component, and the fix is to state the +**requirement** instead of who set it. In practice a category noun does it — +*the caller*, *the host agent*, *an orchestrator*, *the control plane* — and the +result is a better sentence, because it says what is needed rather than who +happens to satisfy it. Every single case fixed so far got shorter and clearer. + +**A name a user types is not a leak.** `nessh` appears throughout this repo on +purpose: the product *is* `ssh nestri.io`, so hiding it would mean hiding what +we sell. The test narrows to: does this name appear because a **user** +encounters it, or because a **component** does? + +If you are unsure whether a name is internal, do not guess and do not grep for +permission — write the category noun. It is never wrong. + +### 2. Nothing closed may enter this repo. + +Not source, not a dependency, not a directory that "looked convenient". Before +adding a top-level directory, know which component it is and that the component +is open. This has already been caught once, in a commit that was never pushed. + +### 3. Versions are pinned once, centrally. + +A Cargo member writes `tokio.workspace = true` and never a version; a TS package +uses the root `catalog`. Two packages in one tree must not disagree about a +dependency. ## Where the detail is @@ -90,3 +139,8 @@ allowed kind: a name in prose, never a dependency in code. Conventional commits. Explain *why* in the body — the diff already shows what. Comments earn their place by saying something the code cannot; a comment restating the line below it is noise. + +**A commit message here is public and permanent.** No internal component names, +no decision numbers, no `ref(d-…)` markers — those are for source comments, +where a later commit can fix a mistake. Describe the change in this repo's own +terms: what changed, and why it is better. See rule 1. diff --git a/apps/api/app/routes/machine.ts b/apps/api/app/routes/machine.ts index 2621d133..60ee408a 100644 --- a/apps/api/app/routes/machine.ts +++ b/apps/api/app/routes/machine.ts @@ -76,11 +76,11 @@ export namespace MachineApi { ); } - // `machine.teamId` is notNull since 0048, so a team has to be - // resolved rather than defaulted to null. The order is: what the - // caller asked for, then the team they are acting inside, then - // their personal team — which `ensurePersonal` makes if this is a - // user who predates 0048 and has none. + // A team has to be resolved rather than defaulted to null, because + // `machine.teamId` is notNull. The order is: what the caller asked + // for, then the team they are acting inside, then their personal + // team — which `ensurePersonal` makes if this is an older user who + // has none. ref(d-0048) const owningTeam = teamId ?? (actor.type === 'member' @@ -120,7 +120,7 @@ export namespace MachineApi { tags: ['Machine'], summary: 'Move a box to another team', description: - 'Move a machine you own to a team you belong to. Hardware always belongs to exactly one team since 0048, so there is no way to unscope — name your personal team instead. This is not ownership transfer: the owner does not change.', + 'Move a machine you own to a team you belong to. Hardware always belongs to exactly one team, so there is no way to unscope — name your personal team instead. This is not ownership transfer: the owner does not change.', responses: { 200: { content: { 'application/json': { schema: Result(Machine.Info) } }, diff --git a/apps/api/test/heartbeat.test.ts b/apps/api/test/heartbeat.test.ts index fe7eb3e6..a3335428 100644 --- a/apps/api/test/heartbeat.test.ts +++ b/apps/api/test/heartbeat.test.ts @@ -53,10 +53,10 @@ describe('POST /machine/heartbeat', () => { expect(res.status).toBe(200); const body = (await res.json()) as any; - // These two field names are what `neslet`'s plane.rs reads out of the - // reply. A rename on either side produces a host that beats, parses - // nothing, and reports success — so the names are the contract and this - // is the test that holds them. + // These two field names are what a host agent reads out of the reply. A + // rename on either side produces a host that beats, parses nothing, and + // reports success — so the names are the contract and this is the test + // that holds them. expect(typeof body.data.lastSeen).toBe('string'); expect(body.data.intervalSeconds).toBe(Machine.HEARTBEAT_SECONDS); expect(new Date(body.data.lastSeen).getTime()).not.toBeNaN(); diff --git a/apps/api/test/routes.test.ts b/apps/api/test/routes.test.ts index fa191bbd..faa43831 100644 --- a/apps/api/test/routes.test.ts +++ b/apps/api/test/routes.test.ts @@ -386,10 +386,10 @@ describe('Box access', () => { }); test('teamId is required on the body, and null is no longer a value', async () => { - // Null used to mean "make it mine alone". Since 0048 made - // `machine.teamId` notNull there is no such state — hardware belongs to - // exactly one team and the personal team is the one to name — so null is - // now a validation error rather than a meaning. + // Null used to mean "make it mine alone". Now that `machine.teamId` is + // notNull there is no such state — hardware belongs to exactly one team + // and the personal team is the one to name — so null is a validation + // error rather than a meaning. const missing = await app.request('/machine/mch_whatever', { method: 'PATCH', headers: { ...adminHeaders(), 'content-type': 'application/json' }, diff --git a/apps/auth/src/index.ts b/apps/auth/src/index.ts index 0eff3d16..11336900 100644 --- a/apps/auth/src/index.ts +++ b/apps/auth/src/index.ts @@ -84,10 +84,10 @@ export default { }); // Every user needs a personal team, because `machine.teamId` is - // notNull since 0048 and registering a host has nowhere to put - // it otherwise. `packages/core/CLAUDE.md` documented this call - // as part of the login flow and it was never actually made, so - // no user in the database has one. + // notNull and registering a host has nowhere to put it + // otherwise. `packages/core/CLAUDE.md` documented this call as + // part of the login flow and it was never actually made, so no + // user in the database has one. ref(d-0048) // // Run on every login rather than only on creation: that is what // backfills the accounts made before this existed, and diff --git a/apps/nesdoctor/src/steam.rs b/apps/nesdoctor/src/steam.rs index 7b9153a1..2d329540 100644 --- a/apps/nesdoctor/src/steam.rs +++ b/apps/nesdoctor/src/steam.rs @@ -532,7 +532,8 @@ mod tests { #[test] fn the_window_is_the_shortest_wrapping_run_holding_half() { // Straddling midnight is the case a non-wrapping scan gets wrong, and - // it is exactly the evening peak 0017 is about. + // it is exactly the evening peak the demand question is about. + // ref(d-0017) let (h, n) = hist(&[(23, 6), (0, 6), (12, 1), (13, 1)]); assert_eq!(peak_window(&h, n), Some((23, 0))); } diff --git a/packages/core/migrations/0007_box_session_team_notnull.sql b/packages/core/migrations/0007_box_session_team_notnull.sql index b1e6f661..d53fa871 100644 --- a/packages/core/migrations/0007_box_session_team_notnull.sql +++ b/packages/core/migrations/0007_box_session_team_notnull.sql @@ -1,5 +1,5 @@ --- 0048: a box is a row, a session is the billing unit, and hardware belongs to --- exactly one team. +-- A box is a row, a session is the billing unit, and hardware belongs to +-- exactly one team. ref(d-0048) -- -- Three of the five changes here touch live rows, and the generated form of -- this migration would have failed on all three: @@ -105,7 +105,7 @@ WHERE m."team_id" IS NULL;--> statement-breakpoint -- -- This is the only destructive statement in the migration and it is a -- considered loss: `game_download` is a progress report a host writes about --- itself, and `neslet` re-derives it from what is on disk. A row that survives +-- itself, and the host re-derives it from what is on disk. A row that survives -- here is one whose host we can actually name; one that does not was -- unattributable, which is exactly the bug the foreign key exists to prevent. DELETE FROM "game_download" d diff --git a/packages/core/src/actor.ts b/packages/core/src/actor.ts index f1651ea6..5cba9e5d 100644 --- a/packages/core/src/actor.ts +++ b/packages/core/src/actor.ts @@ -51,9 +51,9 @@ const Machine = z.object({ properties: z.object({ machineID: z.string(), ownerUserID: z.string(), - // Not optional: `machine.teamId` is notNull since 0048, so a host that - // authenticated always has a team and the branch that used to handle - // its absence was handling a state that can no longer exist. + // Not optional: `machine.teamId` is notNull, so a host that authenticated + // always has a team, and the branch that used to handle its absence was + // handling a state that can no longer exist. teamID: z.string() }) }); diff --git a/packages/core/src/box/box.sql.ts b/packages/core/src/box/box.sql.ts index 73232f9a..04bee80f 100644 --- a/packages/core/src/box/box.sql.ts +++ b/packages/core/src/box/box.sql.ts @@ -7,41 +7,38 @@ import { UserTable } from '../user/user.sql.js'; /** * The named size a box was asked for. * - * A tier is the unit of sale and it includes output geometry - * ([0021](../../../../.nestri/decisions/0021-vm-size-tiers.md)), so this column - * decides vCPU, RAM *and* the resolution the guest is told to render at. It is - * a *request*: `(tier, gpu_model)` is what admission actually acts on, and - * admission does not exist yet. + * A size tier is the unit of sale and it includes output geometry, so this + * column decides vCPU, RAM *and* the resolution the guest is told to render + * at. It is a *request*: what gets admitted is the pair of tier and GPU model, + * and admission does not exist yet. ref(d-0021) */ export const BoxTier = pgEnum('box_tier', ['xs', 'sm', 'md', 'lg', 'xl']); /** - * What the box is doing, in `neslet`'s own vocabulary. + * What the box is doing, in the words the host agent reports. * - * Deliberately the same three states `neslet` reports over its control socket - * and no more. `starting` and `stopping` are the obvious additions and both are - * omitted, because nothing would ever write them: the agent's transitions are - * synchronous from its side, so a state nobody sets is a state that lies. A box - * that failed is `stopped` with `stopClean` false, which is also how `neslet` - * models it — "it is not running" and "it faulted forty seconds ago" are - * different facts, and the difference lives in the reason, not in the state. + * Deliberately the three states an agent actually reports and no more. + * `starting` and `stopping` are the obvious additions and both are omitted, + * because nothing would ever write them: those transitions are synchronous + * from the agent's side, so a state nobody sets is a state that lies. A box + * that failed is `stopped` with `stopClean` false — "it is not running" and + * "it faulted forty seconds ago" are different facts, and the difference lives + * in the reason, not in the state. */ export const BoxState = pgEnum('box_state', ['created', 'running', 'stopped']); /** * A VM someone owns. * - * Nothing represented a box until now - * ([0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md)): - * `machine` is the *host*, and a guest had no id a URL could carry, no owner, - * no place, and no state anything could poll. Every screen the desktop app - * still needs is a view over this table. + * Nothing represented a box until now: `machine` is the *host*, and a guest + * had no id a URL could carry, no owner, no place, and no state anything could + * poll. Every screen the desktop app still needs is a view over this table. * * **A box is owned by a person and placed on a team's hardware, and those are * two different relationships.** Hence both `userId` and `machineId`: the * person is who it belongs to and who gets billed through its sessions, the * machine is where it currently runs. Moving a box to another host changes the - * second and not the first. + * second and not the first. ref(d-0048) */ export const BoxTable = pgTable( 'box', @@ -57,13 +54,13 @@ export const BoxTable = pgTable( machineId: ulid('machine_id') .notNull() .references(() => MachineTable.id, { onDelete: 'restrict' }), - // The DNS label is the id, per 0019 — `.nestri.link`. This is the - // display string a person edits, and it is deliberately not unique: - // two boxes called "living room" are the owner's problem, not an error. + // The id is the DNS label; this is the display string a person edits, and + // it is deliberately not unique: two boxes called "living room" are the + // owner's problem, not an error. ref(d-0019) label: text('label').notNull(), tier: BoxTier('tier').notNull().default('sm'), state: BoxState('state').notNull().default('created'), - /** Why it stopped, verbatim from `neslet`. Null while it has never run. */ + /** Why it stopped, verbatim from the agent. Null while it has never run. */ stopReason: text('stop_reason'), /** Whether that stop was a clean exit. Null while it has never run. */ stopClean: boolean('stop_clean') diff --git a/packages/core/src/box/index.ts b/packages/core/src/box/index.ts index 19471d4f..fd7926fc 100644 --- a/packages/core/src/box/index.ts +++ b/packages/core/src/box/index.ts @@ -9,11 +9,10 @@ import { BoxState, BoxTable, BoxTier } from './box.sql.js'; /** * A VM someone owns. * - * The box is the thing with a name and a URL ([0010](../../../../.nestri/decisions/0010-the-name-is-the-interface.md), - * [0019](../../../../.nestri/decisions/0019-box-naming.md)); a - * {@link ../session/index.ts | session} is one run of it, and the session is - * what costs money. Keeping them apart is what lets a box be a durable thing a - * person owns rather than a synonym for "currently playing". + * The box is the thing with a name and a URL; a session is one run of it, and + * the session is what costs money. Keeping them apart is what lets a box be a + * durable thing a person owns rather than a synonym for "currently playing". + * ref(d-0010), ref(d-0019) */ export namespace Box { export const Info = z @@ -39,11 +38,11 @@ export namespace Box { example: Examples.Box.tier }), state: z.enum(BoxState.enumValues).meta({ - description: 'What the box is doing, in neslet’s vocabulary', + description: 'What the box is doing, as last reported by its host', example: Examples.Box.state }), stopReason: z.string().nullable().optional().meta({ - description: 'Why it stopped, verbatim from neslet. Null if it never ran', + description: 'Why it stopped, as reported by its host. Null if it never ran', example: Examples.Box.stopReason }), stopClean: z.boolean().nullable().optional().meta({ @@ -114,7 +113,7 @@ export namespace Box { }); /** - * Record what `neslet` says a box is doing. + * Record what a box's host says it is doing. * * The stop reason is cleared on any state that is not `stopped`, so a box * that ran, faulted, and was started again does not keep explaining a diff --git a/packages/core/src/db/fixtures.ts b/packages/core/src/db/fixtures.ts index 41cdabca..9903cf59 100644 --- a/packages/core/src/db/fixtures.ts +++ b/packages/core/src/db/fixtures.ts @@ -6,16 +6,15 @@ import { User } from '../user/index.js'; import { LinkedAccount } from '../user/linked-account.js'; /** - * Fixtures for the ownership chain, because since - * [0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md) - * it is a chain rather than a set of loose rows. + * Fixtures for the ownership chain, which is now a chain rather than a set of + * loose rows. * - * A box now needs a user, a team, and a machine to exist before it can, and a + * A box needs a user, a team, and a machine to exist before it can, and a * session needs a game and a linked account on top of that. Every test that * touches either was otherwise going to build the same four rows by hand, and - * the version built by hand is the version that quietly uses a `hst_…` string - * where a real machine id belongs — which is exactly what the new foreign key - * exists to catch. + * the version built by hand is the version that quietly uses a made-up host + * string where a real machine id belongs — which is exactly what the foreign + * key exists to catch. ref(d-0048) * * Test-only. Nothing here is imported by shipping code. */ diff --git a/packages/core/src/game/download.sql.ts b/packages/core/src/game/download.sql.ts index a2d98e15..1922c74f 100644 --- a/packages/core/src/game/download.sql.ts +++ b/packages/core/src/game/download.sql.ts @@ -17,9 +17,9 @@ export const GameDownloadTable = pgTable( { ...id, ...timestamps, - // A foreign key since 0048. It was a bare `text` column — the one place a - // host was referred to by a string nothing checked — so a typo produced - // a download row belonging to a machine that had never existed. + // A foreign key. It was a bare `text` column — the one place a host was + // referred to by a string nothing checked — so a typo produced a download + // row belonging to a machine that had never existed. ref(d-0048) hostId: ulid('host_id') .notNull() .references(() => MachineTable.id, { onDelete: 'cascade' }), diff --git a/packages/core/src/game/download.test.ts b/packages/core/src/game/download.test.ts index 12efa1f0..0a2bbf26 100644 --- a/packages/core/src/game/download.test.ts +++ b/packages/core/src/game/download.test.ts @@ -9,9 +9,9 @@ import { GameDownload } from './download.js'; const sql = testDb(); /** - * Real registered hosts, not `hst_…` strings. + * Real registered hosts, not made-up strings. * - * These were literals until 0048 made `host_id` a foreign key. The old values + * These were literals until `host_id` became a foreign key. The old values * were the bug the key exists to prevent — a download row attributed to a host * that had never registered — so the test that used them was asserting against * a state the database now refuses. diff --git a/packages/core/src/machine/index.ts b/packages/core/src/machine/index.ts index 710f2fa0..b37ece6c 100644 --- a/packages/core/src/machine/index.ts +++ b/packages/core/src/machine/index.ts @@ -133,11 +133,10 @@ export namespace Machine { /** * Move a host to a different team. * - * There is no "out of a team" any more: `teamId` is notNull since - * [0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md), - * so a host always belongs to exactly one, and the single-operator case is a - * team of one rather than a null. What used to be *unscope* is now *move to - * my personal team*, which the caller names explicitly. + * There is no "out of a team" any more: `teamId` is notNull, so a host always + * belongs to exactly one and the single-operator case is a team of one rather + * than a null. What used to be *unscope* is now *move to my personal team*, + * which the caller names explicitly. ref(d-0048) * * Scoped to the owner in the query itself, so a machine belonging to * someone else is a miss rather than a permission check that could be diff --git a/packages/core/src/machine/machine.sql.ts b/packages/core/src/machine/machine.sql.ts index 184f0153..f565aad4 100644 --- a/packages/core/src/machine/machine.sql.ts +++ b/packages/core/src/machine/machine.sql.ts @@ -23,8 +23,8 @@ export const MachineTable = pgTable( .references(() => UserTable.id, { onDelete: 'cascade' }), // Every user gets a personal team at signup, so there is always one to // point at and the single-operator case is a team of one rather than a - // special case in every query. This was nullable until 0048, which cost - // a `teamId ?? ownerUserId` branch at each call site instead. + // special case in every query. This was nullable, which cost a + // `teamId ?? ownerUserId` branch at each call site instead. ref(d-0048) teamId: ulid('team_id') .notNull() .references(() => TeamTable.id, { onDelete: 'restrict' }), diff --git a/packages/core/src/machine/machine.test.ts b/packages/core/src/machine/machine.test.ts index 8ad5a3d9..ed8c7489 100644 --- a/packages/core/src/machine/machine.test.ts +++ b/packages/core/src/machine/machine.test.ts @@ -62,8 +62,8 @@ describe('Machine registration', () => { test('a host always has a team, so registering without one is impossible', async () => { const owner = await newOwner('mch-team'); - // `teamId` is notNull since 0048 and required by the schema, so this is a - // validation failure rather than a row with a null team. + // `teamId` is notNull and required by the schema, so this is a validation + // failure rather than a row with a null team. // // `toThrow` and not `rejects.toThrow`: `fn()` parses its input // synchronously, before any promise exists, so a bad argument never diff --git a/packages/core/src/session/index.ts b/packages/core/src/session/index.ts index 98e178a6..fbb0f3f9 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session/index.ts @@ -136,9 +136,9 @@ export namespace Session { /** * Publish the current ticket. * - * Overwrites, deliberately: the vsock contract describes the ticket as *"a - * stream, not one value"*, so a later ticket for the same session is a - * better address for the same thing and not a second session. + * Overwrites, deliberately: a ticket is republished as addresses are + * discovered, so a later one for the same session is a better address for + * the same thing and not a second session. */ export const setTicket = fn(Info.pick({ id: true, ticket: true }), async (input) => { return Database.use(async (tx) => { diff --git a/packages/core/src/session/session.sql.ts b/packages/core/src/session/session.sql.ts index c45b6431..71b03b9b 100644 --- a/packages/core/src/session/session.sql.ts +++ b/packages/core/src/session/session.sql.ts @@ -10,7 +10,8 @@ import { LinkedAccountTable } from '../user/linked-account.sql.js'; * * `requested` is written by `POST /session` before anything has been placed, * which is what makes the row the job: the control plane picks a machine and - * `neslet` takes it from here. `live` is the only state that costs money. + * the agent on it takes over from there. `live` is the only state that costs + * money. */ export const SessionState = pgEnum('session_state', [ 'requested', @@ -23,14 +24,12 @@ export const SessionState = pgEnum('session_state', [ /** * One live run of one box, and the thing that gets billed. * - * Separate from `box` for two reasons - * ([0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md)): - * a box is a durable thing somebody owns while a session is what costs money - * and what [`limits.md`](../../../../.nestri/contracts/limits.md) burns - * session-hours against — and because the connect ticket **changes after bind - * as addresses are discovered.** The vsock contract calls it *"a stream, not - * one value"*, so `ticket` is a column that gets rewritten in place while the - * session is starting, and a client polls it rather than receiving it once. + * Separate from `box` for two reasons. A box is a durable thing somebody owns, + * while a session is what costs money and what quota is measured in + * session-hours against. And the connect ticket **changes after bind as + * addresses are discovered** — it is republished rather than issued once, so + * `ticket` is a column rewritten in place while the session starts and a + * client polls it instead of receiving a final value. ref(d-0048) */ export const SessionTable = pgTable( 'session', @@ -43,9 +42,9 @@ export const SessionTable = pgTable( gameId: ulid('game_id') .notNull() .references(() => GameTable.id, { onDelete: 'restrict' }), - // Which Steam account this run is playing as. A user may have up to four - // linked, and *which one* is the question the "who's playing?" screen - // asks — so it belongs on the session and not on the box. + // Which Steam account this run is playing as. A user may link several, + // and *which one* is the question the "who's playing?" screen asks — so + // it belongs on the session and not on the box. // // `restrict`, because unlinking a Steam account must not erase the // billing history of what it played. @@ -67,8 +66,8 @@ export const SessionTable = pgTable( (t) => [ index('session_box_idx').on(t.boxId), index('session_state_idx').on(t.state), - // Metering reads "sessions in this window"; per 0048 this table is what - // billing sums, so the time index is not speculative. + // Metering reads "sessions in this window", and this table is what + // billing sums, so the time index is not speculative. ref(d-0048) index('session_started_idx').on(t.timeStarted) ] ); diff --git a/packages/core/src/session/session.test.ts b/packages/core/src/session/session.test.ts index ae154ea0..12206713 100644 --- a/packages/core/src/session/session.test.ts +++ b/packages/core/src/session/session.test.ts @@ -87,8 +87,8 @@ describe('Session', () => { expect((await Session.setTicket({ id: session.id, ticket: 'ticket-one' }))?.ticket).toBe( 'ticket-one' ); - // The vsock contract calls the ticket "a stream, not one value" — a second - // ticket is a better address for the same session, not a new session. + // A ticket is republished as addresses are discovered — a second one is a + // better address for the same session, not a new session. expect((await Session.setTicket({ id: session.id, ticket: 'ticket-two' }))?.ticket).toBe( 'ticket-two' ); diff --git a/packages/core/src/team/index.ts b/packages/core/src/team/index.ts index 585390a3..33e43687 100644 --- a/packages/core/src/team/index.ts +++ b/packages/core/src/team/index.ts @@ -119,7 +119,7 @@ export namespace Team { * only thing that mints a team at signup — so the first one is the personal * one and any later ones were made deliberately. This is a convention, not a * column: adding an `isPersonal` flag would let the two disagree, and there - * is nothing yet that needs them to. + * is nothing yet that needs them to. ref(d-0048) */ export const personalFor = fn(Info.shape.ownerId, async (ownerId) => { return Database.use(async (tx) => { @@ -136,11 +136,10 @@ export namespace Team { /** * The personal team, made if it is not there. * - * Every user has needed one since [0048](../../../../.nestri/decisions/0048-email-is-the-root-identity-and-a-box-is-a-row.md) - * made `machine.teamId` notNull, so signup calls this and so does anything - * that needs somewhere to put a host. Idempotent, because it runs on every - * login rather than only on the first one — a user created before 0048 has - * no team and gets one the next time they appear. + * Every user has needed one since `machine.teamId` became notNull, so signup + * calls this and so does anything that needs somewhere to put a host. + * Idempotent, because it runs on every login rather than only on the first + * one — an older user with no team gets one the next time they appear. */ export const ensurePersonal = fn(z.object({ displayName: z.string() }), async (input) => { const existing = await personalFor(Actor.userID); diff --git a/packages/core/src/team/team.test.ts b/packages/core/src/team/team.test.ts index 68ca5c4f..7c9a75e9 100644 --- a/packages/core/src/team/team.test.ts +++ b/packages/core/src/team/team.test.ts @@ -54,8 +54,8 @@ describe('Team.ensurePersonal', () => { }); test('a user who predates the personal team gets one on next login', async () => { - // The legacy row the 0007 migration and this call between them repair: a - // user created by Steam sign-in before `ensurePersonal` was ever wired up. + // The legacy row the migration and this call between them repair: a user + // created by Steam sign-in before `ensurePersonal` was ever wired up. const userId = Identifier.ascending('user'); createdUserIds.push(userId); await sql`insert into "user" (id, name, email) values (${userId}, ${'legacy'}, ${`legacy-${userId}@example.test`})`;