Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Contributing

Curfew is a personal habit tracker with evidence. Read `CLAUDE.md` first: it
carries the invariants, the conventions and the voice, and breaking an invariant
is a bug even when the tests pass.

## Getting it running

```
bun install
bun run local          # docker Postgres, mock data, no sign-in
bun run test           # domain core, no database
bun run typecheck
```

Three environment files, all gitignored, all carrying the same keys in the same
order. `.env.example` is the key list. A key missing from one does not fall back
to a default: it leaks in from `.env.local`, which Next loads on every
non-production run, so add a key to one and add it to all three.

| File | Database | Used by |
|---|---|---|
| `.env.local` | docker Postgres, `LOCAL_MODE=1` | `bun run local`, `local:*` |
| `.env.preview` | `curfew-apac-dev` | `bun run dev`, `migrate`, `verify` |
| `.env.production` | the live project | `migrate:production` |

## Adding an activity type

This is the walkthrough, and it is deliberately short. **Adding a type never
edits the engine.** If you find yourself changing anything outside your own
file, the interface got something wrong; say so rather than working around it.

An activity type is one file, no React, under `src/domain/<key>/index.ts`.

### 1. Write the module

```ts
export const pushupsActivity: ActivityType<PushupsConfig, PushupsEvidence> = {
  key: "pushups",
  name: "Pushups",
  description: "A set count, every day you say",
  icon: "gym",
  defaults: { schedule: EVERY_DAY, dayBoundary: "midnight", grace: 2,
              config: { target: 50 } },
  configSchema, evidenceSchema,
  evidence: { level: "optional", source: "live", detail: "Live camera." },
  checkin: { kind: "number" },
  chart: { kind: "numeric", heading: "PUSHUPS A DAY",
           valueField: "done", targetField: "target" },
  fields(config) { ... },   // what the configure screen draws
  steps(config) { ... },    // what can be checked in, and when
  windows(config, periodStart, timezone) { ... },
  evaluate(input) { ... },  // did the period pass
};
```

What each piece decides:

- **`fields`** is what the configure screen draws. `configSchema` says what is
  *valid*; this says what it *looks like*. Four control kinds exist and adding a
  fifth is an engine change, so try to want one of them.
- **`steps`** is what the check-in screen offers, its own words, and any numbers
  it asks for. `repeats: true` means the day can take several.
- **`windows`** resolves those steps to absolute instants for one period. Use
  `oneWindow` unless your type genuinely has several.
- **`evaluate`** returns `{ passed, detail }`. `detail` is yours; nothing
  outside your module ever reads it.
- **`hint`** is optional, and it is the line under the fields on the check-in
  screen. Only your module can write "1180 so far today. The limit is 2000."
- **`countsNow`** is optional and answers one question: would another press of
  this step change anything right now? Default yes. Gym says no once today's
  session is logged, because it counts one a calendar day. The engine uses it
  to hide the button and refuse the press, so a control that cannot do anything
  is never offered. If your `evaluate` ignores some check-ins, this is how you
  say so.
- **`chart`** names which of four charts the stats screen draws, and nothing
  else. `kind` is one of the four; adding a fifth is an engine change.
  `heading` is the label over it, in your words and in caps, because
  "LAST 30 PERIODS" tells a reader nothing about what the bars are. A
  `numeric` or `weekly` chart also names `valueField` and `targetField`: which
  keys of *your own* `detail` carry the plotted number and the line it is
  measured against. The engine used to guess through a chain of field names,
  which is the engine knowing what your type means. It no longer does, so a
  chart with an unnamed field simply plots zero, and `registry.test.ts` fails
  the type before it ships.

### 2. Register it

```ts
// src/domain/index.ts
register(pushupsActivity);
```

### 3. Sync and enable it

```
bun run sync:activities
```

That inserts a row in `activity_types`, **disabled**. A type is only offered
once an admin enables it under Controls, which is what stops a half-finished
type reaching anyone. CI runs `sync:activities --check` and fails if a
registered module has no row.

### 4. Test it

Add cases to `src/domain/catalog.test.ts`, and if your type says anything on a
screen, to `configure.test.ts` or `checkin.test.ts`, which assert the words
verbatim against the mocks.

The one thing that will fail: `src/domain/no-db-imports.test.ts`. Domain code
never imports the database. If your type needs a fact that is not in its config
or its check-ins, that is a design problem worth raising.

## The rules that are not style

`CLAUDE.md` has the full list. The four that catch people out:

- **`events` is the only source of truth.** Everything else is rebuildable, and
  `bun run verify` proves it. If you need a fact that is not derivable from
  events, say so rather than adding mutable state.
- **Scoring reads only `checkin.*` events.** Never sessions, never `last_seen`.
  If ambient telemetry could affect a fine, the app rewards not opening it.
- **Resolve config as it stood on the period being scored**, never as it stands
  now. This applies to `/verify` too, or it reports all history as drift after
  any config change.
- **Nothing outside a module knows what a type means.** No `switch` on a type
  key outside the registry.

## Before you open a PR

```
bun run typecheck
bun run lint          # --max-warnings=0, so a warning is a failure
bun run test
bun run build
bun run verify        # no drift against the preview database
bun run break-in      # the security round; exits non-zero if anything gives
bun run browser       # every screen, against a running server
```

`bun run browser` needs `bun run local:seed` and `bun run local` first, and a
fixture seeded on a different calendar day is refused rather than run: every
fixture anchored on the real clock calls the day it was seeded "today".

If you touched anything a signed-out visitor loads, or `src/middleware.ts`, run
`bun run check:signin -- --http=<a deployment>`. It is the only check that
exercises the sign-in gate at all: LOCAL_MODE makes the middleware stand aside
in every job that serves the app, so the gate is the one path the suite never
takes, and the sign-in page's own photographs and font have both been broken by
it.

If you touched a screen, open it beside its artboard in `.design/` and tick its
row in `.planning/v4/SCREENS.md` in the same commit. If the screen and the mock
disagree, that is drift: fix the code, or amend the mock and the row
deliberately. A screenshot test would only lock in whatever was built, which is
why the gate is a person.

## Voice

Applies to commits, comments and every string that ships. No em-dashes. No
emoji. Curfew is a clerk, not a coach: it states facts and consequences.
"Window closes 7:45 AM. Miss it and today does not count." is the register.

## The licence

Curfew is under the [PolyForm Noncommercial License 1.0.0](./LICENSE). Read it,
fork it, change it, run your own copy for yourself, and send a pull request:
all of that is allowed. Using it for a commercial purpose is not, which
includes running it as a product or a service for other people.

By opening a pull request you license your contribution under the same terms.

The licence covers the code and not the idea. Habit tracking with photo
evidence is not something a licence can reserve, and anybody is free to build
their own.