@nurix/etna 0.19.54 → 0.19.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/harness-index.json +1 -1
- package/data/harnesses/apollo/CLAUDE.md +29 -30
- package/data/harnesses/apollo/harness.json +1 -1
- package/data/harnesses/apollo/hooks/approved-packages.md +42 -0
- package/data/harnesses/apollo/hooks/compliance.mjs +11 -6
- package/data/harnesses/apollo/hooks/lib/ownership.mjs +11 -1
- package/data/harnesses/apollo/rules/changelog.md +7 -1
- package/data/harnesses/apollo/rules/cloudflare.md +167 -0
- package/data/harnesses/apollo/rules/database.md +1 -125
- package/data/harnesses/apollo/rules/frontend.md +2 -4
- package/data/harnesses/apollo/rules/general.md +20 -42
- package/data/harnesses/apollo/rules/nomenclature.md +2 -6
- package/data/harnesses/apollo/rules/pnpm.md +2 -2
- package/data/harnesses/apollo/rules/testing.md +44 -3
- package/data/harnesses/apollo/skills/documentation/references/instruction-style.md +1 -0
- package/data/harnesses/apollo/skills/documentation/templates/plan.md +4 -4
- package/data/harnesses/apollo/skills/schema-design/SKILL.md +171 -0
- package/data/harnesses/apollo/skills/test/SKILL.md +104 -0
- package/data/harnesses/apollo/skills/testing/SKILL.md +100 -29
- package/data/harnesses/code/CLAUDE.md +29 -30
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/code/hooks/approved-packages.md +42 -0
- package/data/harnesses/code/hooks/compliance.mjs +11 -6
- package/data/harnesses/code/hooks/lib/ownership.mjs +11 -1
- package/data/harnesses/code/rules/changelog.md +7 -1
- package/data/harnesses/code/rules/cloudflare.md +167 -0
- package/data/harnesses/code/rules/database.md +1 -125
- package/data/harnesses/code/rules/frontend.md +2 -4
- package/data/harnesses/code/rules/general.md +20 -42
- package/data/harnesses/code/rules/nomenclature.md +2 -6
- package/data/harnesses/code/rules/pnpm.md +2 -2
- package/data/harnesses/code/rules/testing.md +44 -3
- package/data/harnesses/code/skills/documentation/references/instruction-style.md +1 -0
- package/data/harnesses/code/skills/documentation/templates/plan.md +4 -4
- package/data/harnesses/code/skills/schema-design/SKILL.md +171 -0
- package/data/harnesses/code/skills/test/SKILL.md +104 -0
- package/data/harnesses/code/skills/testing/SKILL.md +100 -29
- package/data/harnesses/delivery/CLAUDE.md +13 -1
- package/data/harnesses/delivery/harness.json +1 -1
- package/data/harnesses/delivery/hooks/generate-ownership-index.mjs +39 -4
- package/data/harnesses/delivery/rules/changelog.md +7 -1
- package/data/harnesses/delivery/rules/nomenclature.md +2 -6
- package/data/harnesses/delivery/skills/documentation/references/instruction-style.md +1 -0
- package/data/harnesses/delivery/skills/documentation/templates/plan.md +4 -4
- package/data/harnesses/discover/CLAUDE.md +14 -1
- package/data/harnesses/discover/harness.json +1 -1
- package/data/harnesses/discussion/CLAUDE.md +4 -2
- package/data/harnesses/discussion/harness.json +1 -1
- package/data/harnesses/etna/CLAUDE.md +14 -2
- package/data/harnesses/etna/harness.json +1 -1
- package/data/harnesses/etna/hooks/generate-ownership-index.mjs +39 -4
- package/data/harnesses/etna/rules/changelog.md +7 -1
- package/data/harnesses/etna/rules/general.md +20 -42
- package/data/harnesses/etna/rules/nomenclature.md +2 -6
- package/data/harnesses/etna/rules/testing.md +44 -3
- package/data/harnesses/etna/skills/documentation/references/instruction-style.md +1 -0
- package/data/harnesses/etna/skills/documentation/templates/plan.md +4 -4
- package/data/harnesses/hr/CLAUDE.md +8 -3
- package/data/harnesses/hr/harness.json +1 -1
- package/data/harnesses/kb-agi/CLAUDE.md +8 -2
- package/data/harnesses/kb-agi/harness.json +1 -1
- package/package.json +2 -2
- package/data/harnesses/apollo/rules/express.md +0 -33
- package/data/harnesses/apollo/rules/verification.md +0 -67
- package/data/harnesses/apollo/skills/test-plan/SKILL.md +0 -85
- package/data/harnesses/code/rules/express.md +0 -33
- package/data/harnesses/code/rules/verification.md +0 -67
- package/data/harnesses/code/skills/test-plan/SKILL.md +0 -85
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Cloudflare Worker
|
|
2
|
+
|
|
3
|
+
Conventions for Cloudflare Workers — `wrangler.jsonc` setup, vars and secrets, and code shape (dispatch, errors, modules, WebSockets). Schema design doctrine is the `schema-design` skill.
|
|
4
|
+
|
|
5
|
+
## 1. Project & wrangler.jsonc Setup
|
|
6
|
+
|
|
7
|
+
### 1.1 Wrangler version
|
|
8
|
+
|
|
9
|
+
- Use Wrangler **v4** — v3's bundled config schema is missing newer keys (e.g. `secrets_store_secrets`), so editors flag valid config as invalid.
|
|
10
|
+
- **No `package.json`** (dep-free worker): use `npx wrangler@4` in every command and doc — the `@4` is the pin. Don't create a `package.json` just to record it.
|
|
11
|
+
- **`package.json` exists**: pin wrangler to the exact version tested against (not `^4.0.0`), reinstall, and verify with `wrangler deploy --dry-run` before pushing.
|
|
12
|
+
|
|
13
|
+
### 1.2 Config format
|
|
14
|
+
|
|
15
|
+
- Use `wrangler.jsonc` (not `wrangler.toml`).
|
|
16
|
+
- Include `"$schema": "node_modules/wrangler/config-schema.json"` so the editor validates against the exact installed wrangler version.
|
|
17
|
+
|
|
18
|
+
### 1.3 Comments in jsonc
|
|
19
|
+
|
|
20
|
+
None by default. Keep a comment only when it captures a non-obvious **WHY** — a hidden constraint, workaround, or account-specific quirk. Never narrate what a key already says.
|
|
21
|
+
|
|
22
|
+
### 1.4 workers.dev URLs
|
|
23
|
+
|
|
24
|
+
- Set `"workers_dev": false` once a custom domain is wired up, so the custom domain is the only entry point.
|
|
25
|
+
- Pair with a `routes` entry using `custom_domain: true` so Cloudflare manages the DNS record and cert.
|
|
26
|
+
|
|
27
|
+
### 1.5 Observability
|
|
28
|
+
|
|
29
|
+
Default block for every Worker:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
"observability": {
|
|
33
|
+
"logs": {
|
|
34
|
+
"enabled": false,
|
|
35
|
+
"invocation_logs": true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`enabled: false` avoids per-log billing; `invocation_logs: true` keeps the cheap per-request audit trail. **Bootstrap override:** a new Worker (especially cron-only) runs with `enabled: true` until a few real runs look clean — with logs off, `console.log` is visible only live via `npx wrangler tail` and nothing persists after a failed run. Flip back once stable.
|
|
41
|
+
|
|
42
|
+
### 1.6 Project structure & dependencies
|
|
43
|
+
|
|
44
|
+
- Main entry file: `index.ts`. Reusable helpers: `helper.ts`.
|
|
45
|
+
- TypeScript under `strict: true` — wrangler bundles it natively, no separate build step; a minimal `tsconfig.json` ships even in dep-free Workers.
|
|
46
|
+
- Don't run `npm run build` or `npm run deploy` in examples.
|
|
47
|
+
- **`package.json` / `package-lock.json` are opt-in, not default.** Create them only when code under `src/` imports an npm package at runtime — a Worker on built-in Web/Workers APIs alone needs no manifest; run wrangler, linters, and one-offs via `npx <tool>`. Add both files the moment the first real dep lands. `node_modules/` stays in `.gitignore` either way.
|
|
48
|
+
|
|
49
|
+
### 1.7 Environment & configuration
|
|
50
|
+
|
|
51
|
+
- Use the deployed Cloudflare URL in docs/tests, not `localhost`.
|
|
52
|
+
- Prefer SQLite-based Durable Objects over in-memory-only designs; add classes under `migrations.new_sqlite_classes`.
|
|
53
|
+
|
|
54
|
+
### 1.8 Cloudflare Secrets Store
|
|
55
|
+
|
|
56
|
+
Use when the user mentions it. Use the same `binding` name as the `secret_name`.
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
"secrets_store_secrets": [
|
|
60
|
+
{ "binding": "CLICKUP_KEY", "store_id": "792206780c0c46e8ba8ade3f21b44eac", "secret_name": "CLICKUP_KEY" }
|
|
61
|
+
]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Access in code: `await env.CLICKUP_KEY.get()`.
|
|
65
|
+
|
|
66
|
+
### 1.9 Naming
|
|
67
|
+
|
|
68
|
+
- Repo/dir: `cf-<name>` (e.g. `cf-extract-design`).
|
|
69
|
+
- Worker name: `<name>` — repo name minus the `cf-` prefix.
|
|
70
|
+
- R2 bucket, D1 database, and KV namespace names: equal to the worker name. One name, one resource set.
|
|
71
|
+
|
|
72
|
+
### 1.10 Vars & secrets
|
|
73
|
+
|
|
74
|
+
**Non-secret vars live in the `vars` block in `wrangler.jsonc`** — checked in, one source of truth. Vars are configuration, not runtime state; do not move them to the dashboard. A single-tenant application Worker (one deployment — e.g. Apollo) declares them at the top level; a multi-tenant Worker declares per-tenant vars under each `env.<tenant>` block (§11).
|
|
75
|
+
|
|
76
|
+
**Secrets never touch git.** Local dev: a gitignored `.dev.vars` (`KEY=VALUE` dotenv, auto-loaded by `wrangler dev`) with a committed `.dev.vars.example` listing the keys and placeholder values. Production: `wrangler secret put <NAME>`, a gitignored `.secrets` file pushed with `wrangler secret bulk .secrets` (or shipped with a deploy via `wrangler deploy --secrets-file .secrets`), or the Secrets Store (§1.8). Never log or echo a secret value.
|
|
77
|
+
|
|
78
|
+
**No `keep_vars`.** Leave it unset (default `false`) — each deploy reconciles vars to exactly what the config declares. `keep_vars` only guards plaintext vars set out-of-band in the dashboard; it never touches secrets — `wrangler secret put` secrets survive every deploy regardless. Only the multi-tenant pattern sets `keep_vars: true` (§11).
|
|
79
|
+
|
|
80
|
+
**Never name a deployed-value file `.env` or `.env.<anything>`** — `wrangler dev` auto-loads a root `.env` as local dev vars, so a prod-valued `.env` silently becomes the local environment on a fresh clone or in CI. Committed `*.example` files carry keys with placeholder values only.
|
|
81
|
+
|
|
82
|
+
`.gitignore`: `.dev.vars`, `.secrets`, `.secrets.*`, `.env`, `.env.*`
|
|
83
|
+
|
|
84
|
+
## 2. Code & Helpers
|
|
85
|
+
|
|
86
|
+
- Use `generateUuidV7()` in `helper.ts` for IDs (time-sortable UUIDv7).
|
|
87
|
+
- Only `await` what's needed (e.g. `request.json()`, `do.fetch(...)`).
|
|
88
|
+
|
|
89
|
+
## 3. fetch() Structure
|
|
90
|
+
|
|
91
|
+
`validateRequest()` first, then `switch (path)` for dispatch. Single `try/catch` wrapping the entire handler. No if-chains, no regex in fetch.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
export default {
|
|
95
|
+
async fetch(request, env, ctx) {
|
|
96
|
+
try {
|
|
97
|
+
const route = validateRequest(request);
|
|
98
|
+
switch (route.path) {
|
|
99
|
+
case "monitors":
|
|
100
|
+
return await handleMonitors({ ...route, env, ctx });
|
|
101
|
+
default:
|
|
102
|
+
throw { code: 404, message: "Route not found" };
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Unhandled error in fetch()", error);
|
|
106
|
+
return createErrorResponse(
|
|
107
|
+
error.message || "Internal server error",
|
|
108
|
+
error.code || 500,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
No nested `try/catch` except around WebSocket `send`/`close` (see §9).
|
|
116
|
+
|
|
117
|
+
## 4. validateRequest()
|
|
118
|
+
|
|
119
|
+
Lives in `helper.ts`. Parses the URL in one place and returns `{ path, method, query, resourceId, subPath }`; throws `{ code: 400 }` when a path requires a resource ID and none is present.
|
|
120
|
+
|
|
121
|
+
## 5. Validation
|
|
122
|
+
|
|
123
|
+
Fail fast with thrown `{ code, message }` (`400` bad input, `401` missing/invalid auth). Validate required fields explicitly — no optional chaining for required fields.
|
|
124
|
+
|
|
125
|
+
## 6. Errors & Responses
|
|
126
|
+
|
|
127
|
+
- Throw structured errors: `{ code, message }`. Always `console.error()` with context (path, user, etc.). No empty catch blocks.
|
|
128
|
+
- Response envelope `{ status: 1|0, message, data }` — the shared cross-service contract.
|
|
129
|
+
- `createResponse(data, message, code)` in `helper.ts` bakes CORS in, returns a null body for 204/304, and uses proper HTTP codes; `createErrorResponse(message, code)` delegates to it.
|
|
130
|
+
- CORS is passive: a `CORS_HEADERS` const in `helper.ts`, spread into `createResponse()` — no explicit OPTIONS handler, no wrapper functions. Origin-restricted CORS builds the headers from an allowlist (plus `localhost` in dev), still inside `createResponse()`.
|
|
131
|
+
|
|
132
|
+
## 7. Handler Signatures
|
|
133
|
+
|
|
134
|
+
Single destructured object — never positional args. Compose by spreading route with env/ctx: `handleGetStats({ request, env, resourceId })`, not `handleGetStats(request, id, env)`.
|
|
135
|
+
|
|
136
|
+
## 8. Module Structure
|
|
137
|
+
|
|
138
|
+
Feature-based folders with `handle.ts` + `helper.ts` pairs. `index.ts` only does dispatch.
|
|
139
|
+
|
|
140
|
+
| Path | Role |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `src/index.ts` | Entry point — dispatch only |
|
|
143
|
+
| `src/helper.ts` | Shared CORS, responses, validation, utilities |
|
|
144
|
+
| `src/database.ts` | Shared D1 queries |
|
|
145
|
+
| `src/<feature>/handle.ts` | Route handlers |
|
|
146
|
+
| `src/<feature>/helper.ts` | Feature-specific helpers & DB ops |
|
|
147
|
+
|
|
148
|
+
## 9. WebSockets
|
|
149
|
+
|
|
150
|
+
Standard `WebSocketPair` handshake (`server.accept()`, return 101 with `webSocket: client`). Check `readyState === WebSocket.OPEN` before sending. The only acceptable nested `try/catch`: wrap `send`; in its catch, `close(1011)` inside a bare inner try, then evict the session.
|
|
151
|
+
|
|
152
|
+
## 10. D1 Schema
|
|
153
|
+
|
|
154
|
+
Follows the shared database naming and column-type vocabulary; what earns a table and the rest of the design doctrine is the `schema-design` skill.
|
|
155
|
+
|
|
156
|
+
## 11. Multi-tenant Workers (native environments)
|
|
157
|
+
|
|
158
|
+
Rare — only when **one Worker codebase ships as several independent tenants** (separate Workers, domains, and data stores from a single `src/`, e.g. `~/dev/auth` → `auth-nuplay`, `auth-nustack`). Use **native wrangler environments**, never one Worker that switches tenant at runtime. For a single-tenant application Worker, §1.10 applies and this section does not.
|
|
159
|
+
|
|
160
|
+
- **One config, `env.<tenant>` blocks.** The top-level block is **local dev** (`wrangler dev`): a `*-dev` name and placeholder D1/KV ids, simulated locally. Each `env.<tenant>` block is **one production tenant**; deploy it with `wrangler deploy --env <tenant>`.
|
|
161
|
+
- **Named environments never inherit** `vars` / `d1_databases` / `kv_namespaces` / `routes` / `secrets_store_secrets` / `assets` from the top level — **each `env.<tenant>` declares them in full**. Dropping a binding from an env block disables that capability for that one tenant.
|
|
162
|
+
- **Per-tenant non-secret vars live in `env.<tenant>.vars`** — still checked in; one file holds every tenant's config side by side.
|
|
163
|
+
- **Local-dev vars** come from a gitignored `.vars.local`, injected as `--var` by the dev script (the top-level block carries no in-config `vars`).
|
|
164
|
+
- **Per-tenant secrets, never in git** — `wrangler secret put <NAME> --env <tenant>`, or a gitignored per-tenant `.secrets` (e.g. `worker/tenants/<tenant>/.secrets`) pushed with `wrangler secret bulk --env <tenant>`, plus a committed `.secrets.example`. Secrets Store bindings are declared inside each env block.
|
|
165
|
+
- **`keep_vars: true`** (top level) keeps a per-tenant deploy **additive**: tenants inject their non-secret vars via `--var` (or the dashboard), and without it a deploy would wipe them. On secrets it changes nothing (§1.10).
|
|
166
|
+
|
|
167
|
+
`.gitignore`: `.vars.local`, `.secrets`, `.secrets.*`, `.env`, `.env.*`
|
|
@@ -1,84 +1,6 @@
|
|
|
1
1
|
# Database
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Written for SQLite / Cloudflare D1 + Drizzle (the concrete types below). The names and key vocabulary are engine-agnostic — a Postgres project keeps the same column names and semantics with native types (`uuid`, `timestamptz`, `boolean`).
|
|
6
|
-
|
|
7
|
-
## What earns a table
|
|
8
|
-
|
|
9
|
-
A table is the most expensive home for data — a migration to change its shape, a join to read, a mirror to keep in sync. Default to **not** a table — a concept earns one only with at least one property in the left column.
|
|
10
|
-
|
|
11
|
-
| A table is earned by… | Lacking all of these, the data belongs in… |
|
|
12
|
-
| -------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
13
|
-
| identity minted at **runtime** — a session, an upload | — |
|
|
14
|
-
| variation **per tenant / user** — a workspace's theme | — |
|
|
15
|
-
| **independent addressing** — queried, indexed, or FK'd in isolation | a **JSON column** on its parent row |
|
|
16
|
-
| **concurrency-safe** writes — racing writers must not clobber a shared blob | — |
|
|
17
|
-
| a **DB-enforced invariant** — immutability trigger, unique, atomic boundary | — |
|
|
18
|
-
| dev-authored, published on deploy, identical for every tenant — plan tiers | **config / a constants file**, validated at build |
|
|
19
|
-
| exactly recomputable from other rows — a tenant's usage total | a **cache** (KV / edge), recomputed on demand |
|
|
20
|
-
| process state dead once the request completes — a retry count | a **log or workflow engine**, never the domain DB |
|
|
21
|
-
| owned by another system-of-record — the payment processor's invoice | a **reference column** — store the id, not the data |
|
|
22
|
-
|
|
23
|
-
Each right-column route is one test: process state — would a product feature break if this vanished the instant the request finished?; derived output — exactly recomputable from other rows + code?; un-addressed detail — is the row ever `SELECT`ed / `JOIN`ed / FK'd alone?; another system's record — does a specialized service (chat, telemetry, object store) already own it?
|
|
24
|
-
|
|
25
|
-
- **Static vocabulary or a fixed graph → config, not a table** — HTTP status labels, a fixed onboarding funnel. Surface it in code as a constant/type; reifying it into tables plus a seed makes the file and the tables a two-copy sync problem. The refined boundary is § Vocabulary / lookup tables.
|
|
26
|
-
- **History → one immutable log + a mutable head.** Never shatter versioning into per-entity `*_revisions` / `*_revision_sections` tables — snapshot the coherent set into one append-only versions table; current state is a projection of the newest entry. A table that is just "old rows of another table" collapses into the snapshot.
|
|
27
|
-
- **A surviving table names the invariant it buys** — content-addressed immutable rows buy reproducibility, a monotonic sequence buys ordering/sync, an atomic write boundary buys multi-row atomicity. Drop the requirement and the table drops with it.
|
|
28
|
-
|
|
29
|
-
Table count tracks **requirements, not domain richness** — a rich domain is not entitled to a table per noun.
|
|
30
|
-
|
|
31
|
-
## State, rules, and history
|
|
32
|
-
|
|
33
|
-
**One fact has one home, chosen by what kind of fact it is.** Confusing these produces the most common redundant table in a schema — a queue whose rows restate state the entity already carries.
|
|
34
|
-
|
|
35
|
-
| The fact | Home | Example |
|
|
36
|
-
| --- | --- | --- |
|
|
37
|
-
| What the entity **is now** | a **column on the entity's own row** | `documents.state = 'blocked'` |
|
|
38
|
-
| What may be **done** about that state | **code** — the transition map, the UI's affordances | which inputs unblock a document |
|
|
39
|
-
| What **was done**, and by whom | the **append-only audit log** | an `audit_log` row: `document.override_applied` |
|
|
40
|
-
| What a consumer must **replay in order** | the **sequence log** | an `app_events` row keyed `(app_id, sequence)` |
|
|
41
|
-
|
|
42
|
-
- **State becomes a column the moment anything selects on it.** `WHERE state = 'blocked'` takes an index; `WHERE json_extract(metadata, '$.state') = 'blocked'` scans the table and is invisible to every schema check. JSON keeps only what § What earns a table leaves it — detail nothing addresses alone.
|
|
43
|
-
- **A pending-work table mirroring row state is the anti-pattern** — `*_actions`, `*_inbox`, `*_flags`. The tell: resolving one of its rows also updates the entity, so two rows answer one question and are free to disagree. The *open question* is a column beside the state (at most one per row ⇒ a column, § A junction spells many-to-many); the *answer given* is an audit-log row.
|
|
44
|
-
- **Legal transitions live in code, never as rows** — a `status_transitions` table is a state machine the database cannot enforce and the code must re-implement anyway to use.
|
|
45
|
-
|
|
46
|
-
**The two logs are not substitutes**, and no one table serves both:
|
|
47
|
-
|
|
48
|
-
| | Sequence log | Audit log |
|
|
49
|
-
| --- | --- | --- |
|
|
50
|
-
| Answers | what happened here, **in order** | who did what, **and whether it succeeded** |
|
|
51
|
-
| Ordinal | a **gapless per-entity `sequence`**, allocated from a counter | none — nothing replays it |
|
|
52
|
-
| Scope | one entity, always | any — **including none**: a sign-up predates every tenant |
|
|
53
|
-
| Records | only what **happened** | **attempts too** — `denied`, `failure` |
|
|
54
|
-
| Read by | cursors replaying forward | investigations, at any age |
|
|
55
|
-
|
|
56
|
-
The bottom two rows are why neither can hold the other's rows: an event at a scope with no entity has no sequence to take, and a refused attempt in a replay stream would have consumers apply something that never happened.
|
|
57
|
-
|
|
58
|
-
### The audit log's own shape
|
|
59
|
-
|
|
60
|
-
**An audit log outlives every row it names, so it stores what it needs to stay readable without them.** Both consequences are missing from most first drafts:
|
|
61
|
-
|
|
62
|
-
- **Denormalize the display at write time** — `actor_label`, `target_label` beside `actor_id` / `target_id`. Ids alone leave the log intact but unreadable once the person or record is deleted, which is the one thing it exists to survive.
|
|
63
|
-
- **Record the attempt, not the success** — `outcome` (`success | failure | denied`) plus a `reason`. Refused sign-ins and permission denials are most of what anyone opens an audit log for; without the column they cannot be written at all.
|
|
64
|
-
- **Keep the columns product-neutral** — `actor_*` · `action` · `outcome` · `target_*` · `scope_path` · `channel` · `source_ip` · `correlation_id` · `changes_json` serve any domain. Per-level tenancy columns tie the log to today's model; a materialized `scope_path` (`workspace:abc/app:def`) makes "everything under this tenant" one prefix scan and survives a new level appearing — at the price of enforcing the hierarchy in code rather than a `CHECK`.
|
|
65
|
-
|
|
66
|
-
## Scope, not parallel tables
|
|
67
|
-
|
|
68
|
-
One entity that exists at several scopes is **one table with a nullable scope column** — never one table per scope, and never one table shared across entity families.
|
|
69
|
-
|
|
70
|
-
- **A table shared by several families carries the union of their columns** — one `settings` table for users, apps, and workspaces. Almost nothing can be `NOT NULL`, the payload lands in JSON beyond every constraint, index, and FK, and a new family widens a table other features already depend on. One table per family.
|
|
71
|
-
- **Two tables declared to have "the same columns" are two schemas maintained by hand** — the drift is silent, surfacing as an incomplete copy, never as an error. Collapse them and discriminate by column.
|
|
72
|
-
- **The scope column is the discriminator** (`app_id IS NULL` = parent scope, set = child scope). Copying a scope is then `INSERT … SELECT` over one table with one column rewritten, so source and target cannot disagree about shape.
|
|
73
|
-
- **A nullable owner column is how a row exists before its owner does** — a pending enrolment or unclaimed record is creatable before the owner is known, which is what lets a `pending_*` / `*_requests` table collapse into the real one.
|
|
74
|
-
- **A reserved row can own the shipped defaults.** A fixed-id "system" parent makes the defaults and a tenant's copy the same kind of row, through the same code. It must then be excluded from every tenant-scoped query — state that invariant where the id is defined.
|
|
75
|
-
|
|
76
|
-
## Copying rows between scopes
|
|
77
|
-
|
|
78
|
-
- **A copied junction row is already correct with no remapping** — its endpoints are `slug` references (§ Keys & the shared vocabulary), and a slug means the same thing in every scope. Rewriting ids through a source→copy map is the most error-prone step in a multi-level copy, and a partial failure leaves wiring pointed into another scope rather than raising.
|
|
79
|
-
- **Provenance is a soft reference — no FK.** `source_id` must outlive its referent; an FK would either block the source's deletion or erase the lineage.
|
|
80
|
-
- **Store the source's version at copy time** — `version` + `source_version` make "is an upgrade available" a comparison of two stored values, not a content diff.
|
|
81
|
-
- **A copy is a row, never a live read.** Nothing at the child scope reads the parent's row at runtime; propagation happens only through an explicit re-copy, so a local edit is never silently overwritten.
|
|
3
|
+
Naming and the fixed column vocabulary for relational schemas in **every** Nurix project — one concept, one name, everywhere. Written for SQLite / Cloudflare D1 + Drizzle; a Postgres project keeps the same column names and semantics with native types (`uuid`, `timestamptz`, `boolean`). **Design doctrine — what earns a table, state vs. logs, junctions, FK policy, uniqueness traps — is the `schema-design` skill; consult it before any schema change.**
|
|
82
4
|
|
|
83
5
|
## Keys & the shared vocabulary
|
|
84
6
|
|
|
@@ -96,36 +18,6 @@ Reference columns name what they point at: `<entity>_id` for a UUID-PK reference
|
|
|
96
18
|
|
|
97
19
|
**Every inbound FK to a hub table declares a deliberate `ON DELETE`.** A hub (`users`, `devices`, anything several modules point at) collects edges from teams that never meet, and a single `NO ACTION` among them turns deleting the parent into a constraint error naming neither table. Decide per edge: the child dies with the row (`CASCADE`) or survives it (`SET NULL`, which needs a nullable column) — `NO ACTION` on a hub is a deferred outage, not a default.
|
|
98
20
|
|
|
99
|
-
## A junction spells many-to-many — nothing else
|
|
100
|
-
|
|
101
|
-
**A `_map` table is the schema's notation for M:N. Using it for 1:M is a category error** — the junction's own uniqueness admits many parents per child, so the "one parent" rule stops being enforced the moment it is written this way. Cardinality decides between the only two shapes:
|
|
102
|
-
|
|
103
|
-
| Relationship | Shape |
|
|
104
|
-
| --- | --- |
|
|
105
|
-
| **1:M** — the child has exactly one parent | a **reference column on the child** (`agents.routing_slug`), plus an index |
|
|
106
|
-
| **M:N** — both sides have many | a **`<a>_<b>_map` junction** with a composite unique |
|
|
107
|
-
|
|
108
|
-
- **The test is one question about the child**: can this row ever belong to two parents at once? No ⇒ column. A junction "in case it becomes M:N later" ships the unenforced model today to buy a migration you may never run.
|
|
109
|
-
- **A junction for a 1:M also** makes the association a row that can go missing independently of the child — a child can exist correctly while its wiring does not — and forces a join onto the most common read in the system.
|
|
110
|
-
- **A junction carrying only its two endpoints and timestamps is the tell** — a real M:N junction usually earns its own columns (a role, an order, a status); one that never does is a 1:M waiting to be collapsed into a column.
|
|
111
|
-
- **Collapsing deletes a copy step** — the association travels inside the row it belongs to: no second pass to remap, no copy landing with its wiring half-applied.
|
|
112
|
-
- **A pair already recorded elsewhere needs no table at all.** If an append-only log stores `(parent, child)` on every write, the association table is `SELECT DISTINCT` over that log — a cache free to disagree with the log it derives from (§ What earns a table).
|
|
113
|
-
|
|
114
|
-
## Foreign keys into and out of append-only tables
|
|
115
|
-
|
|
116
|
-
**An append-only table can afford no `ON DELETE` action at all, so its references to mutable tables are soft — no FK.** Every option is closed to it:
|
|
117
|
-
|
|
118
|
-
| Action | Why it fails on an append-only table |
|
|
119
|
-
| --- | --- |
|
|
120
|
-
| `SET NULL` | It is an **UPDATE**. An append-only trigger aborts it, so the parent's deletion fails — with an error naming the trigger, not the constraint. |
|
|
121
|
-
| `CASCADE` | Deletes history to delete a person. The log exists precisely to outlive its subjects. |
|
|
122
|
-
| `NO ACTION` | Makes anyone who ever appeared in the log undeletable — the hub defect above. |
|
|
123
|
-
|
|
124
|
-
- **`SET NULL` is the trap** — it reads like a safe relaxation of `NO ACTION` and passes review as one.
|
|
125
|
-
- **Record the actor as a plain column** — the same soft-reference treatment provenance gets (`source_id`, `thread_id`). The log keeps naming who acted after the account is gone, which is what an audit trail is for.
|
|
126
|
-
- **`CASCADE` from the append-only side's own parent is still fine** — a cascade is a DELETE, so a `BEFORE UPDATE` trigger never sees it: an App's log dying with the App is legitimate, distinct from erasing a log to delete a person.
|
|
127
|
-
- **State the exemption where it exists.** A catalog whose rows must not disappear under a referencing row (a service definition, a vocabulary) keeps `NO ACTION` deliberately — that guarantee is worth one line saying so.
|
|
128
|
-
|
|
129
21
|
## Naming
|
|
130
22
|
|
|
131
23
|
- **Tables** — `snake_case`, plural noun for what they hold (`users`, `apps`, `devices`).
|
|
@@ -134,25 +26,9 @@ Reference columns name what they point at: `<entity>_id` for a UUID-PK reference
|
|
|
134
26
|
- **Indexes** — `<table>_<col(s)>_idx`; unique `<table>_<col(s)>_unique`; a junction's composite unique reads `<table>_<colA>_<colB>_unique` (`app_user_map_app_user_unique`).
|
|
135
27
|
- **Drizzle mirror** — the exported object is the camelCase table name (`app_user_map` → `appUserMap`); column props are the camelCase of the column (`created_at` → `createdAt`). The `sqliteTable("…")` string and every index name must match the DDL (`init.sql`) exactly — the schema-parity boundary.
|
|
136
28
|
|
|
137
|
-
## Uniqueness with a nullable column
|
|
138
|
-
|
|
139
|
-
**`UNIQUE (a, b)` does not constrain rows where `b` is `NULL`** — SQL treats every `NULL` as distinct, so a composite unique over a nullable discriminator silently permits exactly the duplicates it was introduced to keep apart. Write one partial index per scope instead:
|
|
140
|
-
|
|
141
|
-
```sql
|
|
142
|
-
CREATE UNIQUE INDEX <t>_parent_slug_unique ON <t> (parent_id, slug) WHERE child_id IS NULL;
|
|
143
|
-
CREATE UNIQUE INDEX <t>_child_slug_unique ON <t> (child_id, slug) WHERE child_id IS NOT NULL;
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
**Prove a uniqueness claim by inserting the duplicate it forbids** — a schema review cannot tell a firing index from a silent one by reading.
|
|
147
|
-
|
|
148
29
|
## Column types (SQLite / D1)
|
|
149
30
|
|
|
150
31
|
- **Timestamps** — `created_at` + `updated_at`, both `TEXT NOT NULL DEFAULT (datetime('now'))`, on **every** table — except append-only logs, which carry only `created_at`.
|
|
151
32
|
- **Booleans** — `INTEGER` (Drizzle `integer(…, { mode: "boolean" })`); the `is_*` / `has_*` names are § Naming.
|
|
152
33
|
- **JSON** — `TEXT` (Drizzle `text(…, { mode: "json" })`).
|
|
153
34
|
|
|
154
|
-
## Vocabulary / lookup tables
|
|
155
|
-
|
|
156
|
-
A controlled vocabulary earns a **table** only when a user row FKs into it and reads its `label` / `description` at runtime — a service catalog rendered in a picker; static dev-authored config nobody references by FK stays **config** (§ What earns a table) — a `doc_type` value list. Enumerable is not tabular, and a `type`-style column validated against the constants file beats an FK to a seeded mirror. An earned table has a fixed shape: `id` (UUID) · `slug` (UNIQUE) · `label` · optional `description` · `status` · timestamps.
|
|
157
|
-
|
|
158
|
-
- **Seeds are generated, never hand-written** — the source of truth is a CSV or a shared package; a generator emits the `*.seed.sql` with UUIDv5 ids and an idempotent `ON CONFLICT(slug)` upsert that preserves live counts / admin-tuned columns. ❌ Hand-editing seed SQL — regenerate it.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Frontend
|
|
2
2
|
|
|
3
|
-
Tailwind v4 + shadcn-based UI conventions for **web** apps — styling, the `@nurix/components` library, the `design.md` token artifact, and design-skill routing — plus the TypeScript conventions every package compiles under.
|
|
3
|
+
Tailwind v4 + shadcn-based UI conventions for **web** apps — styling, the `@nurix/components` library, the `design.md` token artifact, and design-skill routing — plus the TypeScript conventions every package compiles under.
|
|
4
4
|
|
|
5
5
|
- **Build every UI through the `design` skill** — never hand-roll layouts; the page archetypes, the `design.md` identity system with its token artifact (`design.tokens.css`), and chrome wiring live in the skill.
|
|
6
6
|
- **The brief's stated theme binds** — dark boots dark when the brief says so (recorded in the project's `design.md`); light is the default only when the brief is silent.
|
|
7
|
-
- **`@nurix/components`** is the UI library — import by subpath (`@nurix/components/button`)
|
|
7
|
+
- **`@nurix/components`** is the UI library — import by subpath (`@nurix/components/button`). **Never** `npx shadcn add`, no `components.json` scaffold — consume the prebuilt library (`.tsx`, not `.jsx`).
|
|
8
8
|
- Vite: `@tailwindcss/vite` + `@vitejs/plugin-react-swc`. Pin `react` / `react-dom` / `react/jsx-runtime` / `react/jsx-dev-runtime` to the app's single copy via `resolve.alias` — `@nurix/*` ship nested react, and two copies means two hook dispatchers.
|
|
9
9
|
- Tailwind v4 is CSS-only — no `tailwind.config.js`, no PostCSS.
|
|
10
10
|
- CSS entry `globals.css`, imports in order: `@import "./fonts.css"` (`@fontsource` registrations) → the project's `design.tokens.css` (derived from the repo-base `design.md` by the design skill's validator — **never** hand-edited) → `@import "tailwindcss"` → the library's `styles.css` — then `@theme inline {…}` mapping vars to tokens.
|
|
@@ -28,5 +28,3 @@ Shared libraries (`@nurix/components`, etc.) are generic on purpose — that's w
|
|
|
28
28
|
- **Bind a repeated factory once** (preferred): `const col = defineColumn<BriefRow>;` → `col({...})`. Canonical: `modules/briefs/BriefsListPage.tsx`.
|
|
29
29
|
- **Infer JSX type args from props:** `<Table data={rows} columns={columns} />`, not `<Table<BriefRow> …>`.
|
|
30
30
|
- **Anchor with a collection annotation** when nothing else can: `const columns: ColumnDefinition<BriefRow>[] = [...]`.
|
|
31
|
-
|
|
32
|
-
> Monorepo TS policy (`tsconfig.base.json` + thin per-package `extends`, source-first internal packages, `peerDependencies`, pnpm `catalog:`) lives in [`pnpm.md`](pnpm.md) §2 — it travels with package boundaries.
|
|
@@ -1,54 +1,32 @@
|
|
|
1
1
|
# General Standards
|
|
2
2
|
|
|
3
|
-
Runtime versions, the
|
|
3
|
+
Runtime versions, package policy, the API response envelope, and GitHub Actions pins — the workspace baseline.
|
|
4
4
|
|
|
5
5
|
## Runtime Versions
|
|
6
6
|
|
|
7
|
-
- **Node.js:**
|
|
7
|
+
- **Node.js:** the version pinned in `.nvmrc` / `package.json` `engines` — **never** a hardcoded number, **never** the runner's default. Workflows use `node-version-file: .nvmrc`.
|
|
8
8
|
- **Package manager:** detect from the lockfile — **never** hardcode one.
|
|
9
9
|
- **PostgreSQL:** 17 · **Redis:** 7.x (latest)
|
|
10
10
|
|
|
11
11
|
## Packages
|
|
12
12
|
|
|
13
|
-
**Only**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
| `sonner` | ^2.0.7 | Toast notifications | `react-toastify` |
|
|
31
|
-
| `react-window` | ^2.2.3 | List virtualization | `react-virtualized` |
|
|
32
|
-
| `nanoid` | ^6.0.0 | ID generation | `uuid` |
|
|
33
|
-
| `express` | ^5.2.1 | HTTP server | `fastify`, `koa` |
|
|
34
|
-
| `drizzle-orm` | ^0.45.2 | Database ORM | `prisma`, `sequelize`, `typeorm` |
|
|
35
|
-
| `drizzle-kit` | ^0.31.10 | Schema tooling | — |
|
|
36
|
-
| `zod` | ^4.2.1 | Schema validation | `yup`, `joi` |
|
|
37
|
-
| `jose` | ^6.1.3 | JWT handling | `jsonwebtoken` |
|
|
38
|
-
| `microdiff` | ^1.5.0 | JSON object diffing | `deep-diff`, `just-diff` |
|
|
39
|
-
| `react18-json-view` | ^0.2.9 | JSON tree view | `react-json-view` |
|
|
40
|
-
| `@tiptap/react` | ^3.11.1 | Rich text editor | `slate`, `quill`, `draft-js` |
|
|
41
|
-
| `shiki` | ^4.3.1 | Syntax highlighting | `prism`, `highlight.js` |
|
|
42
|
-
| `lucide-react` | ^1.26.0 | Icons | `react-icons`, `heroicons` |
|
|
43
|
-
| `@pdfme/generator` | ^6.1.12 | PDF generation | `jspdf`, `pdfkit` |
|
|
44
|
-
| `cmdk` | ^1.1.1 | Command palette | `kbar`, `ninja-keys` |
|
|
45
|
-
| `@monaco-editor/react` | ^4.7.0 | Code editor | `codemirror`, `ace` |
|
|
46
|
-
| `localforage` | ^1.10.0 | Client-side storage | `idb`, `dexie` |
|
|
47
|
-
| `react-markdown` | ^10.1.0 | Markdown rendering | `marked`, `markdown-it` |
|
|
48
|
-
| `remark-gfm` | ^4.0.1 | GFM (tables, tasklists) | `remark-github`, custom |
|
|
49
|
-
| `fast-deep-equal` | ^3.1.3 | Deep equality checks | `lodash.isEqual` |
|
|
50
|
-
| `@ngneat/falso` | latest | Fake data generation | `@faker-js/faker`, `chance` |
|
|
51
|
-
| `pino` | ^10.1.0 | Logging | `winston` |
|
|
13
|
+
**Only packages in the approved table** — `.claude/hooks/approved-packages.md`, enforced by the PreToolUse install gate, which names the file on a block. The table changes only by explicit user decision. Test packages install at `latest`; fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance`.
|
|
14
|
+
|
|
15
|
+
## API Response Envelope
|
|
16
|
+
|
|
17
|
+
Success and error share **one** envelope — a cross-service contract clients branch on, so a per-service "improvement" (`success: true`, a boolean `status`, a bare payload) breaks every consumer:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"status": 1,
|
|
22
|
+
"message": "Message sent successfully",
|
|
23
|
+
"data": { "delivered": [{ "user@example.com": 2 }] }
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Error: `{ "status": 0, "message": "Error description", "data": null }`
|
|
28
|
+
|
|
29
|
+
HTTP codes, default list: `200`, `201` (created), `400`, `401` (not authenticated), `403` (not authorized), `404`, `500` — plus `204`/`304` (null-body responses), `409` (conflicting write), `429` (rate limit), `503` (unhealthy dependency) where the semantics call for them. Middleware- and protocol-emitted codes are exempt from the envelope.
|
|
52
30
|
|
|
53
31
|
## GitHub Actions
|
|
54
32
|
|
|
@@ -59,4 +37,4 @@ Pin the major, and check it against the action's releases — a major behind sti
|
|
|
59
37
|
| `actions/checkout` | v7 | Repo checkout |
|
|
60
38
|
| `actions/setup-node` | v7 | Node toolchain + npm cache |
|
|
61
39
|
|
|
62
|
-
Set `node-version`
|
|
40
|
+
Set `node-version` via `node-version-file: .nvmrc`, **never** a hardcoded number and **never** the runner's default.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nomenclature
|
|
2
2
|
|
|
3
|
-
Naming and casing for code — identifiers, files, folders. The rationale throughout is consistency: one concept, one name, one casing shape, in every repo that loads this rule.
|
|
3
|
+
Naming and casing for code — identifiers, files, folders. The rationale throughout is consistency: one concept, one name, one casing shape, in every repo that loads this rule.
|
|
4
4
|
|
|
5
5
|
Names state intent in full words — clarity over brevity; no abbreviations unless universally understood (`id`, `url` ✅ · `cfg`, `btn`, `idx` ❌).
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Names state intent in full words — clarity over brevity; no abbreviations unle
|
|
|
12
12
|
| `PascalCase` | classes, type definitions, components; the file exporting one | `PaymentProcessor`, `AppsListPage.tsx` |
|
|
13
13
|
| `UPPER_SNAKE_CASE` | constants, environment variables | `MAX_RETRY_COUNT` |
|
|
14
14
|
| `kebab-case` | URLs, CSS class names, HTML IDs, multi-word folders | `user-profile`, `workspace-provisioning/` |
|
|
15
|
-
| `snake_case` | database tables, columns, indexes
|
|
15
|
+
| `snake_case` | database tables, columns, indexes | `created_at` |
|
|
16
16
|
|
|
17
17
|
**A file exporting one component carries that component's name exactly**, so an import names the file it came from; a single-word folder is plain lowercase (`lib`, `surfaces`). ❌ Never `snake_case` a file or folder — that casing belongs to the database.
|
|
18
18
|
|
|
@@ -28,7 +28,3 @@ Names state intent in full words — clarity over brevity; no abbreviations unle
|
|
|
28
28
|
| Event handler | `handle` or `on` prefix | `handleSubmit`, `onPageLoad` |
|
|
29
29
|
| Interface | `I` prefix acceptable; type aliases stay plain | `IConfigOptions`, `AuthResponse` |
|
|
30
30
|
| Async function | verb phrase; `Async` suffix acceptable | `loadConfigAsync` |
|
|
31
|
-
|
|
32
|
-
## Database
|
|
33
|
-
|
|
34
|
-
Table, column, index, and key naming for relational schemas — the `id` / `slug` / `label` vocabulary, junction `_map` tables, and Drizzle mirrors — lives in [`database.md`](database.md), which routes general code naming back here.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
|
|
2
2
|
|
|
3
|
-
How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here.
|
|
3
|
+
How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ Every workspace package is scoped **`@nurix/`**; the dot vs. hyphen after the sc
|
|
|
22
22
|
|
|
23
23
|
## 2. Monorepo & package boundaries
|
|
24
24
|
|
|
25
|
-
- **One `tsconfig.base.json` at the repo root owns TS policy** — the strict set
|
|
25
|
+
- **One `tsconfig.base.json` at the repo root owns TS policy** — the strict set, `target`, and everything uniform across packages (here, `module`/`moduleResolution`). Each package's `tsconfig.json` `extends` the base and adds only its coordinates (`rootDir`/`outDir`/`include`) and genuine environment overrides (`lib`/`types`/`jsx`; `module`/`moduleResolution` only when the package's module system truly differs). **Never** re-declare or weaken a policy option locally. One exception: a framework that owns its own base (an Astro app on `astro/tsconfigs/strict`).
|
|
26
26
|
- **Internal packages are source-first.** A `private: true` package exposes its API via `exports` pointing at source (`"types"`/`"default"` → `./src/index.ts`) — no build step, and no bare `main`/`module` on new packages. One typed subpath per public surface; consumers import the subpath, **never** a deep relative path into another package's `src/`. `types` comes **first** in every subpath block — resolvers take the first matching condition, so a `types` listed after `default` is never reached.
|
|
27
27
|
- **Crossing the publish boundary is the `nustack` skill** — `private: false`, the published manifest, the pipeline.
|
|
28
28
|
- **Single-source shared dep versions with pnpm `catalog:`** — `typescript`, `zod`, `vitest`, `react`, type packages — so every package resolves the same copy and its types stay mutually assignable.
|
|
@@ -1,9 +1,44 @@
|
|
|
1
|
-
# Testing
|
|
1
|
+
# Testing & Verification
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**When** work is proven and **which technology** proves it — the standing constraints that bind on every turn. The full lifecycle procedure — deriving a plan's Test Obligations, placing tests in strata, executing the verification gate — is the `testing` skill, invoked at those moments; this rule never restates it.
|
|
4
|
+
|
|
5
|
+
## Two regimes
|
|
6
|
+
|
|
7
|
+
**Deferral is a property of plans, not of code.** A plan carries a Test Obligations ledger and a gate that discharges it; nothing else does, so nothing else may defer.
|
|
8
|
+
|
|
9
|
+
| Regime | During the work | Proven |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| **Plan-driven build** | No new test files. Code only — the loop stays fast | At the verification gate, before `Status: Complete` |
|
|
12
|
+
| **Bug fix** — a defect in shipped behavior | The regression test ships with the fix | Immediately — it fails before the change, passes after |
|
|
13
|
+
| **Ad-hoc / non-plan change** | The test ships with the change **when it alters promised behavior** — layout, copy, and config tweaks owe none | Immediately, same discipline as a bug fix |
|
|
14
|
+
|
|
15
|
+
- **A bug fix cannot defer** — "fails before, passes after" is provable afterward only by reverting to the pre-fix ref, a cost nobody pays; capture the property at fix time or lose it.
|
|
16
|
+
- **Ad-hoc work cannot defer — there is no ledger to defer into.** A non-plan change that alters no promised behavior — a pure refactor, a rename — owes no test.
|
|
17
|
+
|
|
18
|
+
## During the loop
|
|
19
|
+
|
|
20
|
+
Rules for the plan regime between the plan gate and the verification gate:
|
|
21
|
+
|
|
22
|
+
- **No new test files** — obligations wait in the plan's ledger (written at the plan gate by the `testing` skill).
|
|
23
|
+
- **Existing tests broken by the plan's intended behavior change are updated to the new contract before any mid-plan commit** — the shared branch never carries a red suite, and an update is never a weakening (§ Discipline).
|
|
24
|
+
- **A defect in the plan's own in-flux code is the loop iterating** — no test, no ledger row. **A pre-existing bug in shipped behavior is the bug-fix regime even mid-plan**: fix it with its regression test now — deferring it to the gate loses "fails before" — or send it to the backlog via Out-of-Scope Discovery. Never fix silently.
|
|
25
|
+
- **The ledger is amendable while the plan is open** — the gate discharges whatever it holds at gate time.
|
|
26
|
+
- **A mid-plan commit is a checkpoint, not a verification claim.**
|
|
27
|
+
|
|
28
|
+
## The gate
|
|
29
|
+
|
|
30
|
+
**A discrete, user-triggered step.** When implementation is complete, report it and offer the gate — never run it unbidden, and never flip `Status: Complete` without it. An explicit user acceptance of finished plan work — "ship it", "commit", "looks good" — counts as the trigger; open the gate without asking again. A plan with outstanding obligations stays open, whatever the code does. The procedure — checklist, drive-then-pin ladder, row discharge, what earns a durable test — is the `testing` skill (gate mode).
|
|
31
|
+
|
|
32
|
+
- **Precondition: the ledger exists.** A plan with no Test Obligations section is blocked at the gate — run the `testing` skill (plan-gate mode) on it first; an empty ledger passes vacuously and the gap surfaces only after the file is frozen.
|
|
33
|
+
- **The verdict is never delegated** (the root policy file § Delegation), and where browser work is main-session-only (its § Runtime Notes), the driving is too. Fan-out may prepare fixtures or draft test bodies; the driving and the verdict stay inline.
|
|
34
|
+
- **The gate needs no stronger tier than the session default** — a standard-tier session suffices whatever tier built the plan; the plan and its ledger carry all the state the gate needs, so gating in a fresh session loses nothing.
|
|
35
|
+
- **Report what happened, including failures.** A gate that reports green while a suite is red is worse than no gate; state the failing test and its output.
|
|
36
|
+
- **Abandonment does not erase debt.** Before `Status: Abandoned` freezes a plan whose code was committed, convert its undischarged rows into backlog items; uncommitted code owes nothing.
|
|
4
37
|
|
|
5
38
|
## Frameworks by stack
|
|
6
39
|
|
|
40
|
+
Framework choice is **never** ad hoc and never the user's problem mid-task: detect the stack, use its row; if the repo already has a runner, that runner wins — **never** introduce a second framework beside it.
|
|
41
|
+
|
|
7
42
|
| Stack | Unit / integration | Component | E2E | HTTP mocking | Don't use |
|
|
8
43
|
| --- | --- | --- | --- | --- | --- |
|
|
9
44
|
| Node + Vite / React (web) | `vitest` | `@testing-library/react` (+ `@testing-library/user-event`, `happy-dom`) | `@playwright/test` | `msw` | `jest`, `mocha`, `jasmine`, `cypress`, `enzyme` |
|
|
@@ -12,7 +47,7 @@ Test technology and placement — framework per stack, suite layout, mocking pol
|
|
|
12
47
|
| Cloudflare Workers | `vitest` + `@cloudflare/vitest-pool-workers` | — | `@playwright/test` against `wrangler dev` | `msw` | `jest` |
|
|
13
48
|
| Rust | `cargo test` (built-in) | — | — | — | — |
|
|
14
49
|
|
|
15
|
-
Test packages install at `latest` —
|
|
50
|
+
Test packages install at `latest` — outside the approved-packages pin set. Fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance`.
|
|
16
51
|
|
|
17
52
|
## Conventions
|
|
18
53
|
|
|
@@ -21,3 +56,9 @@ Test packages install at `latest` — they sit outside [`general.md`](general.md
|
|
|
21
56
|
- **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob.
|
|
22
57
|
- **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
|
|
23
58
|
- **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone and in any order.
|
|
59
|
+
|
|
60
|
+
## Discipline
|
|
61
|
+
|
|
62
|
+
- **Never weaken, skip, or delete a test to mask an unexplained failure.** When a plan or LLD intentionally changes behavior, updating the affected tests to the new contract is part of that change.
|
|
63
|
+
- **A behavior that is compiler-guarded owes no test** — if it cannot ship green-but-broken, a test adds cost and no safety.
|
|
64
|
+
- **Throwaway checks are not tests.** Temporary scripts written to drive a change live in scratch space and are deleted — never committed into the test tree.
|
|
@@ -24,6 +24,7 @@ Policy files (`CLAUDE.md`, rules) are declarative — assertion bullets and tabl
|
|
|
24
24
|
## Routing
|
|
25
25
|
|
|
26
26
|
- **Link, don't copy — one canonical source per topic.** `CLAUDE.md` is a **router, not a manual**: per workflow, when to enter, what to dispatch, where the output lands — mechanics live in the agent or skill file, which loads only when it runs.
|
|
27
|
+
- **A rule never references a sibling rule.** Rules co-load, so pointing at one from another routes a reader to text already in front of them — noise that also drifts the moment either file moves. State what the rule owns, self-contained; the only outward links a rule carries are to skills and agents, whose bodies do *not* co-load. Routing downward from `CLAUDE.md` to a rule is fine — that is the router doing its job.
|
|
27
28
|
- **Cut generic engineering advice.** Agents already know how to code; keep only what is true of *this* stack and not derivable by looking at it.
|
|
28
29
|
- **Keep load-bearing rules even when project-specific.** Genericizing is for reference material, not guardrails — deleting a hard rule removes a constraint the agent will otherwise violate.
|
|
29
30
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Plan (docs/history/plans/YYYY-MM-DD-feature-name.md)
|
|
2
2
|
|
|
3
|
-
Record. Use for: every implementation plan. Plans are created here directly with `Status: Draft`; after approval the plan gates (`boundary`, then `
|
|
3
|
+
Record. Use for: every implementation plan. Plans are created here directly with `Status: Draft`; after approval the plan gates (`boundary`, then `testing` plan-gate mode) run against the still-`Draft` file, the status flips to `Status: In Progress` at the first implementation step, and the file freezes at `Status: Complete` or `Status: Abandoned`. Once frozen, never edit — write a new dated plan if scope changes. The plan records the journey; the truth it implemented lives on in the feature doc and any AD it touched.
|
|
4
4
|
|
|
5
|
-
The `boundary` skill parses the **Proposed Changes** sections to check boundary impact before execution, and the `
|
|
5
|
+
The `boundary` skill parses the **Proposed Changes** sections to check boundary impact before execution, and the `testing` skill (plan-gate mode) writes a **Test Obligations** section into the plan at the same gate. Keep that structure stable — section headings and bullet shape are load-bearing for the parsers, and the Test Obligations table is the ledger the verification gate discharges before the plan may reach `Status: Complete`.
|
|
6
6
|
|
|
7
|
-
The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `
|
|
7
|
+
The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. Never hand-edit the line to skip a gate.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
# {Feature Name}
|
|
@@ -52,7 +52,7 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
|
|
|
52
52
|
|
|
53
53
|
## Test Obligations
|
|
54
54
|
|
|
55
|
-
<!-- Written by the `
|
|
55
|
+
<!-- Written by the `testing` skill (plan-gate mode) at the plan gate (after `boundary`). One row per
|
|
56
56
|
promised behavior: layer · target file · oracle · fixtures. Amendable while the
|
|
57
57
|
plan is open; discharged at the verification gate. Do not hand-author. -->
|
|
58
58
|
|