@nurix/etna 0.19.31 → 0.19.33

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "db921503525c",
2
+ "fingerprint": "de36ada5f0dc",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.5.0",
4
+ "version": "3.5.2",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "7281074d495c"
10
+ "digest": "ca685ff63d77"
11
11
  }
@@ -1,8 +1,6 @@
1
1
  # Changelog
2
2
 
3
- Every unit of work leaves one record: `docs/history/changelog/YYYY-MM-DD-{three-words}.md`. The three words name the work, not the commit. Format, field rules, and the closed exemption list are the `documentation` skill's `templates/changelog-entry.md`.
4
-
5
- An entry is not a summary line. It carries the refinement context only the working session holds — the ask in the user's own words, what was rejected, which skills survived or were overridden, what running the thing taught, what now guards it.
3
+ Every unit of work leaves one record — `docs/history/changelog/YYYY-MM-DD-{three-words}.md` (the three words name the work, not the commit) — plus its row in `docs/history/CHANGELOG.md`, appended in the same pass so the index never trails the folder. Entry format, field rules, and the closed exemption list are the `documentation` skill's `templates/changelog-entry.md`; this rule owns when an entry is owed and how it lives. An entry is never a commit summary — it carries the refinement context only the working session holds, which no diff reconstructs.
6
4
 
7
5
  ## When an entry is owed
8
6
 
@@ -10,28 +8,12 @@ An entry is not a summary line. It carries the refinement context only the worki
10
8
  | --- | --- |
11
9
  | A plan reaches `Status: Complete` | A draft entry for that plan, linked to it — written now, while the session still holds the context |
12
10
  | The user asks for a changelog | The entry for the work in hand. No commit required |
13
- | A commit is made | Every non-exempt commit is covered by an entry — the commit skill writes or completes it |
14
-
15
- **Exempt** (closed list): release and bot commits, harness installs and bumps, lockfile-only changes, pure renames and moves, and changelog housekeeping itself.
11
+ | A commit is made | Every non-exempt commit is covered by an entry — the `commit` skill writes or completes it |
16
12
 
17
- ## The entry's life
18
-
19
- | State | Meaning |
20
- | --- | --- |
21
- | **Draft** | Created before its commit exists. Mutable — fields filled as far as the session can |
22
- | **Complete** | Missing fields filled in from the diff and the session |
23
- | **Frozen** | The moment it rides in a commit. Never edited again |
13
+ ## Lifecycle
24
14
 
25
- **Immutability begins at commit, not at creation.** A draft in the working tree is meant to be edited — that is what lets a completed plan record its context before anything is committed. Once an entry is committed it is history: a correction is a new entry linking back, never an edit.
15
+ Draft (created before its commit exists) → complete (missing fields filled from the diff and the session) → frozen the moment it rides in a commit. **Immutability begins at commit, not at creation** — a draft is meant to be edited, which is what lets a completed plan record its context before anything is committed; a committed entry is history, and a correction is a new entry linking back, **never** an edit.
26
16
 
27
17
  ## Granularity
28
18
 
29
- A commit is the smallest unit an entry may describe. Above that the mapping is judgment — four commits may be four entries or one, whichever reads better as history.
30
-
31
- An entry spanning several commits rides in the **last** of them and names the earlier ones in `Commits:`. An entry can never carry its own commit's SHA, so that field lists predecessors only and is omitted when one entry maps to one commit.
32
-
33
- When the work done exceeds what a draft describes, do not stretch the draft — write a second entry. The first stays an honest account of the plan; the second records the work beyond it.
34
-
35
- ## The index
36
-
37
- Every entry appends one row to `docs/history/CHANGELOG.md` — date, link, one-line summary — in the same pass that writes the entry, so the table never trails the folder.
19
+ A commit is the smallest unit an entry may describe; above that the mapping is judgment — four commits may be four entries or one, whichever reads better as history. An entry spanning several commits rides in the **last** of them, naming the predecessors in `Commits:`. When work exceeds what a draft describes, write a second entry rather than stretching the draft — the first stays an honest account of the plan.
@@ -1,12 +1,12 @@
1
1
  # Database
2
2
 
3
- Fixed conventions for relational schemas — table/column/index **names**, the **key vocabulary** (`id` / `slug` / `label`), column types, and Drizzle mirrors — so the same concept carries the same name in **every** Nurix project. This is a cross-project consistency rule: **one concept, one name, everywhere** — never `slug` in one repo and `handle` / `key` / `code` in another. General code naming (variables, files, casing) is [`nomenclature.md`](nomenclature.md); this rule owns the database.
3
+ Fixed conventions for relational schemas in **every** Nurix project — what earns a table, the key vocabulary, foreign-key doctrine, naming, and column types. A cross-project consistency rule: **one concept, one name, everywhere**. General code naming (variables, files, casing) is [`nomenclature.md`](nomenclature.md); this rule owns the database.
4
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`).
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
6
 
7
7
  ## What earns a table
8
8
 
9
- A table is the most expensive home for data — it costs a migration to change its shape, a join to read, and a mirror to keep in sync. Default to **not** a table. A concept earns its own table only when it needs at least one property in the left column; lacking all of them, the data belongs in the cheaper home on the right.
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
10
 
11
11
  | A table is earned by… | Lacking all of these, the data belongs in… |
12
12
  | -------------------------------------------------------------------------- | ------------------------------------------------------- |
@@ -20,47 +20,104 @@ A table is the most expensive home for data — it costs a migration to change i
20
20
  | process state that is dead once the request completes | a **log or workflow engine**, never the domain DB |
21
21
  | owned by another system-of-record | a **reference column** — store the id, not the data |
22
22
 
23
- Each routing rule is one question:
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
24
 
25
- - **Static vocabulary or a fixed graph → config, not a table.** Dev-authored, same for every tenant, never FK'd at runtime ⇒ a constants file validated at build, surfaced 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 once into an append-only versions table; current state is a derived projection of the newest entry. _Test: is this table just "old rows of that table"? → collapse it into the snapshot._
27
- - **Process state is not domain state.** _Test: if it were deleted the instant the request finished, would a product feature break? No ⇒ it is a log line or a workflow-engine concern._
28
- - **Derived output is a cache, not truth.** _Test: is it exactly recomputable from other rows + code? Yes ⇒ recompute and cache; never persist as first-class state._
29
- - **Un-addressed detail is a column.** _Test: is this row ever SELECTed / JOINed / FK'd alone? No ⇒ it is a JSON field on its parent._
30
- - **Another system's record → keep the pointer.** _Test: does a specialized service already own this — chat, telemetry, object store? ⇒ store its id, not a copy._
31
- - **A surviving table names the invariant it buys.** Each kept structure pays for one guarantee — 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; keep the requirement and the structure is the price a blob cannot pay.
25
+ - **Static vocabulary or a fixed graph → config, not a table.** 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.
32
28
 
33
- Table count tracks **requirements, not domain richness** — a rich domain does not entitle a schema to a table per noun.
29
+ Table count tracks **requirements, not domain richness** — a rich domain is not entitled to a table per noun.
30
+
31
+ ## Scope, not parallel tables
32
+
33
+ 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.
34
+
35
+ - **A table shared by several families carries the union of their columns** — 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.
36
+ - **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.
37
+ - **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.
38
+ - **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.
39
+ - **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.
40
+
41
+ ## Copying rows between scopes
42
+
43
+ - **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.
44
+ - **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.
45
+ - **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.
46
+ - **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.
34
47
 
35
48
  ## Keys & the shared vocabulary
36
49
 
37
50
  Three column roles are fixed across every table and every project:
38
51
 
39
- - **`id`** — the primary key, **always a UUID** (`TEXT PRIMARY KEY` on D1, `uuid` on Postgres). Never a synthetic or natural key (`type_page_button`, `human:default` are ❌). Runtime rows mint a UUIDv7; seed rows derive a deterministic UUIDv5 of the natural key so re-seeds stay stable. Never mix synthetic and UUID ids in one table.
40
- - **`slug`** — the **stable key other rows reference**: `UNIQUE`, lowercase `snake_case`. **Foreign keys point at `slug`, not `id`.** One concept = one slug name across every table and repo. ❌ Never rename this role `handle`, `key`, `code`, or `name` in a different project — it is always `slug`.
41
- - **`label`** — the **human display name**. Always `label` — ❌ never `name`, `title`, or `display_name`.
52
+ - **`id`** — the primary key, **always a UUID** (`TEXT PRIMARY KEY` on D1, `uuid` on Postgres), never a synthetic or natural key (❌ `type_page_button`, `human:default`). Runtime rows mint a UUIDv7; seed rows derive a deterministic UUIDv5 of the natural key so re-seeds stay stable. **Never** mix synthetic and UUID ids in one table.
53
+ - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping (§ Copying rows between scopes). ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
54
+ - **`label`** — the **human display name** — ❌ never `name`, `title`, or `display_name`.
42
55
 
43
56
  Reference columns name what they point at: `<entity>_id` for a UUID-PK reference (`user_id`, `app_id`), `<entity>_slug` for a slug reference (`source_slug`).
44
57
 
45
- - **Enforced** (the referenced row must exist) → a real `FOREIGN KEY (<x>_slug) REFERENCES <table>(slug)`.
58
+ - **Enforced, same scope** (the referenced row must exist) → a real `FOREIGN KEY (<x>_id) REFERENCES <table>(id)`.
59
+ - **Cross-scope** (the reference must survive being copied) → `<x>_slug` as a plain column, **no** FK — an FK would bind the row to one scope's copy of the target, which is the remapping the slug exists to avoid.
46
60
  - **Drift-tolerant** (a controlled-vocabulary value that may fall off-list) → a plain value column, **no** FK, paired with an `is_*` boolean recording whether the value is in the canonical set.
47
61
 
62
+ **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.
63
+
64
+ ## A junction spells many-to-many — nothing else
65
+
66
+ **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:
67
+
68
+ | Relationship | Shape |
69
+ | --- | --- |
70
+ | **1:M** — the child has exactly one parent | a **reference column on the child** (`agents.routing_slug`), plus an index |
71
+ | **M:N** — both sides have many | a **`<a>_<b>_map` junction** with a composite unique |
72
+
73
+ - **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.
74
+ - **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.
75
+ - **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.
76
+ - **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.
77
+ - **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).
78
+
79
+ ## Foreign keys into and out of append-only tables
80
+
81
+ **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:
82
+
83
+ | Action | Why it fails on an append-only table |
84
+ | --- | --- |
85
+ | `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. |
86
+ | `CASCADE` | Deletes history to delete a person. The log exists precisely to outlive its subjects. |
87
+ | `NO ACTION` | Makes anyone who ever appeared in the log undeletable — the hub defect above. |
88
+
89
+ - **`SET NULL` is the trap** — it reads like a safe relaxation of `NO ACTION` and passes review as one.
90
+ - **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.
91
+ - **`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.
92
+ - **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.
93
+
48
94
  ## Naming
49
95
 
50
96
  - **Tables** — `snake_case`, plural noun for what they hold (`users`, `apps`, `devices`).
51
- - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it.
97
+ - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N (§ A junction spells many-to-many).
52
98
  - **Columns** — `snake_case`; booleans `is_*` / `has_*`; timestamps `created_at` / `updated_at`; foreign keys `<entity>_id`.
53
99
  - **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`).
54
100
  - **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.
55
101
 
102
+ ## Uniqueness with a nullable column
103
+
104
+ **`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:
105
+
106
+ ```sql
107
+ CREATE UNIQUE INDEX <t>_parent_slug_unique ON <t> (parent_id, slug) WHERE child_id IS NULL;
108
+ CREATE UNIQUE INDEX <t>_child_slug_unique ON <t> (child_id, slug) WHERE child_id IS NOT NULL;
109
+ ```
110
+
111
+ **Prove a uniqueness claim by inserting the duplicate it forbids** — a schema review cannot tell a firing index from a silent one by reading.
112
+
56
113
  ## Column types (SQLite / D1)
57
114
 
58
115
  - **Timestamps** — `created_at` + `updated_at`, both `TEXT NOT NULL DEFAULT (datetime('now'))`, on **every** table — except append-only logs, which carry only `created_at`.
59
- - **Booleans** — `INTEGER`, named `is_*` / `has_*` (Drizzle `integer(…, { mode: "boolean" })`).
116
+ - **Booleans** — `INTEGER` (Drizzle `integer(…, { mode: "boolean" })`); the `is_*` / `has_*` names are § Naming.
60
117
  - **JSON** — `TEXT` (Drizzle `text(…, { mode: "json" })`).
61
118
 
62
119
  ## Vocabulary / lookup tables
63
120
 
64
- A controlled vocabulary earns a **table** only when a user row FKs into it and reads its `label` / `description` at runtime; a controlled vocabulary that is static, dev-authored config nobody references by FK stays **config** (§ What earns a table) — enumerable is not the same as tabular, and a `type`-style column validated against the constants file is preferred over an FK to a seeded mirror. When a table is earned, it follows a fixed shape: `id` (UUID) · `slug` (UNIQUE) · `label` · optional `description` · `status` · timestamps.
121
+ A controlled vocabulary earns a **table** only when a user row FKs into it and reads its `label` / `description` at runtime; static dev-authored config nobody references by FK stays **config** (§ What earns a table) — 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.
65
122
 
66
- - **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 is ❌ — regenerate it.
123
+ - **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.
@@ -4,6 +4,8 @@ Express 5.x server setup and the JSON response envelope every Nurix HTTP service
4
4
 
5
5
  ## Express 5.x
6
6
 
7
+ Canonical middleware stack:
8
+
7
9
  ```javascript
8
10
  app.use(cors({ origin: process.env.FRONTEND_URL }));
9
11
  app.use(compression());
@@ -11,10 +13,13 @@ app.use(express.json({ limit: '10mb' }));
11
13
  app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
12
14
  ```
13
15
 
14
- - Middleware order: body parsers → custom middleware → routes → error handlers (centralized, last). Auth via middleware. Use Express Router for modular routes.
16
+ - **Middleware order is fixed**: body parsers → custom middleware → routes → error handlers (centralized, **always** last) — no ad-hoc per-route error responses.
17
+ - Auth is middleware, not inline route checks. Routes are modular via Express Router.
15
18
 
16
19
  ## API Response Format
17
20
 
21
+ 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:
22
+
18
23
  ```json
19
24
  {
20
25
  "status": 1,
@@ -25,4 +30,4 @@ app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
25
30
 
26
31
  Error: `{ "status": 0, "message": "Error description", "data": null }`
27
32
 
28
- HTTP codes: `200`, `201` (created), `400`, `401` (not authenticated), `403` (not authorized), `404`, `500`
33
+ HTTP codes, **closed list**: `200`, `201` (created), `400`, `401` (not authenticated), `403` (not authorized), `404`, `500`.
@@ -1,35 +1,32 @@
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. Package versions live in [`general.md`](general.md); the Express server and API envelope in [`express.md`](express.md); workspace layout and installing/consuming `@nurix/*` packages in [`pnpm.md`](pnpm.md); React Native styling is out of scope — that's [`react-native.md`](react-native.md).
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. Package versions live in [`general.md`](general.md); the Express server and API envelope in [`express.md`](express.md); workspace layout and consuming `@nurix/*` packages in [`pnpm.md`](pnpm.md); React Native styling is [`react-native.md`](react-native.md).
4
4
 
5
- - **Build every UI through the `design` skill** — route the brief through it instead of hand-rolling layouts; it owns the page archetypes, the design.md identity system and its derived token artifact (`design.tokens.css`), and chrome wiring — `@nurix/components` or the incumbent stack's components, behind its one-line primitives switch.
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** — the UI component library, a restricted-scope npm package (install, auth, and the no-substitutes guard are [`pnpm.md`](pnpm.md) §3); import by subpath, e.g. `@nurix/components/button`.
8
- - No components.json / shadcn CLI scaffold (.tsx, not .jsx) — consume the prebuilt library; never `npx shadcn add`.
9
- - 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 — keep one hook dispatcher).
10
- - Tailwind v4, CSS-only (no tailwind.config.js, no PostCSS): styling runs through the `@tailwindcss/vite` plugin.
11
- - CSS entry `globals.css`, imports in order — `@import "./fonts.css"` (@fontsource registrations, first) → the project's `design.tokens.css` (token source — 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.
12
- - Tokens: CSS vars in `:root, :root[data-theme="light"]` and `:root[data-theme="dark"]` (both specificity 0,2,0 — the armor a later bare `:root` can't defeat), consumed as semantic Tailwind tokens (bg-card, text-foreground). `globals.css` carries the active `@custom-variant dark` line; the theme attribute convention is `data-theme`. The library bundles class-variance-authority, clsx, tailwind-merge.
13
- - Plus general styling rules (spacing in 2/4/6/8, object-cover, truncate, motion, color tokens — never hardcoded hex in components).
7
+ - **`@nurix/components`** is the UI library — import by subpath (`@nurix/components/button`); install, auth, and the no-substitutes guard are [`pnpm.md`](pnpm.md) §3. **Never** `npx shadcn add`, no `components.json` scaffold — consume the prebuilt library (`.tsx`, not `.jsx`).
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
+ - Tailwind v4 is CSS-only — no `tailwind.config.js`, no PostCSS.
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.
11
+ - Tokens: CSS vars in `:root, :root[data-theme="light"]` and `:root[data-theme="dark"]` (both specificity 0,2,0 — armor a later bare `:root` can't defeat), consumed as semantic Tailwind tokens (`bg-card`, `text-foreground`). `globals.css` carries the active `@custom-variant dark` line; the theme attribute is `data-theme`. The library bundles `class-variance-authority`, `clsx`, `tailwind-merge` — don't re-add them.
12
+ - Spacing in 2/4/6/8; `object-cover`; `truncate`; color through tokens — **never** hardcoded hex in components.
14
13
 
15
14
  ## TypeScript
16
15
 
17
- TypeScript conventions. Compiles under `strict: true` and `noUncheckedIndexedAccess: true`.
16
+ Every package compiles under `strict: true` and `noUncheckedIndexedAccess: true`.
18
17
 
19
18
  ### Type Safety
20
19
 
21
- - Never `any` to silence the checker. Unknown value → type `unknown`, then narrow (type guard / `zod` parse).
22
- - Never weaken strictness. No `// @ts-nocheck`; `// @ts-expect-error` only with a same-line reason.
23
- - Infer locals & return types; annotate at boundaries (exports, props, contracts).
20
+ - **Never** `any` to silence the checker. Unknown value → type `unknown`, then narrow (type guard / `zod` parse).
21
+ - **Never** weaken strictness: no `// @ts-nocheck`; `// @ts-expect-error` only with a same-line reason.
22
+ - Infer locals and return types; annotate at boundaries (exports, props, contracts).
24
23
 
25
24
  ### Generics
26
25
 
27
- Shared libraries (`@nurix/components`, etc.) are generic on purpose — that's what types `cell(row)`, checks `slug` against `keyof TData`, and ties `data`/`columns` together. Keep the safety; never widen to `any` to dodge it. State the row type **once per file**, then let inference carry it:
26
+ Shared libraries (`@nurix/components`, etc.) are generic on purpose — that's what types `cell(row)`, checks `slug` against `keyof TData`, and ties `data`/`columns` together; **never** widen to `any` to dodge it. State the row type **once per file** and let inference carry it — repeating `fn<Type>(…)` / `<Comp<Type>>` per call site is cosmetic (same safety, same emitted JS):
28
27
 
29
- - **Bind a repeated factory once** (preferred): `const col = defineColumn<BriefRow>;` → `col({...})`. No `<T>` per call; `row` stays typed. Canonical: `modules/briefs/BriefsListPage.tsx`.
28
+ - **Bind a repeated factory once** (preferred): `const col = defineColumn<BriefRow>;` → `col({...})`. Canonical: `modules/briefs/BriefsListPage.tsx`.
30
29
  - **Infer JSX type args from props:** `<Table data={rows} columns={columns} />`, not `<Table<BriefRow> …>`.
31
30
  - **Anchor with a collection annotation** when nothing else can: `const columns: ColumnDefinition<BriefRow>[] = [...]`.
32
31
 
33
- Don't repeat `fn<Type>(…)` / `<Comp<Type>>` at every call site — cosmetic only (same safety, same emitted JS).
34
-
35
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,61 +1,61 @@
1
1
  # General Standards
2
2
 
3
- Runtime versions and the canonical package list — the workspace baseline every Nurix code harness loads. Web-frontend/styling conventions are [`frontend.md`](frontend.md); the Express server and the API response envelope are [`express.md`](express.md).
3
+ Runtime versions, the canonical package list, and GitHub Actions pins — the workspace baseline. Web-frontend/styling conventions are [`frontend.md`](frontend.md); the Express server and the API response envelope are [`express.md`](express.md).
4
4
 
5
5
  ## Runtime Versions
6
6
 
7
7
  - **Node.js:** 24 LTS
8
- - **Package manager:** detect from lockfile (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm). Don't hardcode one.
8
+ - **Package manager:** detect from the lockfile — **never** hardcode one.
9
9
  - **PostgreSQL:** 17 · **Redis:** 7.x (latest)
10
10
 
11
- ## Recommended Packages
12
-
13
- No alternatives. Just use these.
14
-
15
- | Package | Version | Use | Don't use |
16
- | ------------------------ | --------- | ---------------------- | -------------------------- |
17
- | react | ^19.2.3 | UI components | - |
18
- | vite | ^8.0.0 | Build tool, dev server | webpack, CRA |
19
- | tailwindcss | ^4.1.18 | Styling | styled-components |
20
- | @vitejs/plugin-react-swc | ^4.2.2 | JSX compilation | @vitejs/plugin-react |
21
- | react-router-dom | ^7.11.0 | Routing | - |
22
- | @reduxjs/toolkit | ^2.11.2 | State management | zustand, jotai |
23
- | react-redux | ^9.2.0 | Redux React bindings | - |
24
- | axios | ^1.13.2 | HTTP requests | fetch |
25
- | date-fns | ^4.1.0 | Date formatting | moment, dayjs |
26
- | @dnd-kit/core | ^6.3.1 | Drag and drop | react-beautiful-dnd |
27
- | @xyflow/react | ^12.11.0 | Node-based flow charts | reactflow, react-flow-renderer |
28
- | motion | ^12.23.26 | Animations | react-spring |
29
- | sonner | ^2.0.7 | Toast notifications | react-toastify |
30
- | react-window | ^2.2.3 | List virtualization | react-virtualized |
31
- | nanoid | ^6.0.0 | ID generation | uuid |
32
- | express | ^5.2.1 | HTTP server | fastify, koa |
33
- | drizzle-orm | ^0.45.2 | Database ORM | prisma, sequelize, typeorm |
34
- | drizzle-kit | ^0.31.10 | Schema tooling | - |
35
- | zod | ^4.2.1 | Schema validation | yup, joi |
36
- | jose | ^6.1.3 | JWT handling | jsonwebtoken |
37
- | microdiff | ^1.5.0 | JSON object diffing | deep-diff, just-diff |
38
- | react18-json-view | ^0.2.9 | JSON tree view | react-json-view |
39
- | @tiptap/react | ^3.11.1 | Rich text editor | slate, quill, draft-js |
40
- | shiki | ^4.3.1 | Syntax highlighting | prism, highlight.js |
41
- | lucide-react | ^1.26.0 | Icons | react-icons, heroicons |
42
- | @pdfme/generator | ^6.1.12 | PDF generation | jspdf, pdfkit |
43
- | cmdk | ^1.1.1 | Command palette | kbar, ninja-keys |
44
- | @monaco-editor/react | ^4.7.0 | Code editor | codemirror, ace |
45
- | localforage | ^1.10.0 | Client-side storage | idb, dexie |
46
- | react-markdown | ^10.1.0 | Markdown rendering | marked, markdown-it |
47
- | remark-gfm | ^4.0.1 | GFM (tables, tasklists) | remark-github, custom |
48
- | fast-deep-equal | ^3.1.3 | Deep equality checks | lodash.isEqual |
49
- | @ngneat/falso | latest | Fake data generation | @faker-js/faker, chance |
50
- | pino | ^10.1.0 | Logging | winston |
11
+ ## Packages
12
+
13
+ **Only** these — no alternatives.
14
+
15
+ | Package | Version | Use | Don't use |
16
+ | --- | --- | --- | --- |
17
+ | `react` | ^19.2.3 | UI components | — |
18
+ | `vite` | ^8.0.0 | Build tool, dev server | `webpack`, CRA |
19
+ | `tailwindcss` | ^4.1.18 | Styling | `styled-components` |
20
+ | `@vitejs/plugin-react-swc` | ^4.2.2 | JSX compilation | `@vitejs/plugin-react` |
21
+ | `react-router-dom` | ^7.11.0 | Routing | — |
22
+ | `@reduxjs/toolkit` | ^2.11.2 | State management | `zustand`, `jotai` |
23
+ | `react-redux` | ^9.2.0 | Redux React bindings | — |
24
+ | `axios` | ^1.13.2 | HTTP requests | `fetch` |
25
+ | `date-fns` | ^4.1.0 | Date formatting | `moment`, `dayjs` |
26
+ | `@dnd-kit/core` | ^6.3.1 | Drag and drop | `react-beautiful-dnd` |
27
+ | `@xyflow/react` | ^12.11.0 | Node-based flow charts | `reactflow`, `react-flow-renderer` |
28
+ | `motion` | ^12.23.26 | Animations | `react-spring` |
29
+ | `sonner` | ^2.0.7 | Toast notifications | `react-toastify` |
30
+ | `react-window` | ^2.2.3 | List virtualization | `react-virtualized` |
31
+ | `nanoid` | ^6.0.0 | ID generation | `uuid` |
32
+ | `express` | ^5.2.1 | HTTP server | `fastify`, `koa` |
33
+ | `drizzle-orm` | ^0.45.2 | Database ORM | `prisma`, `sequelize`, `typeorm` |
34
+ | `drizzle-kit` | ^0.31.10 | Schema tooling | — |
35
+ | `zod` | ^4.2.1 | Schema validation | `yup`, `joi` |
36
+ | `jose` | ^6.1.3 | JWT handling | `jsonwebtoken` |
37
+ | `microdiff` | ^1.5.0 | JSON object diffing | `deep-diff`, `just-diff` |
38
+ | `react18-json-view` | ^0.2.9 | JSON tree view | `react-json-view` |
39
+ | `@tiptap/react` | ^3.11.1 | Rich text editor | `slate`, `quill`, `draft-js` |
40
+ | `shiki` | ^4.3.1 | Syntax highlighting | `prism`, `highlight.js` |
41
+ | `lucide-react` | ^1.26.0 | Icons | `react-icons`, `heroicons` |
42
+ | `@pdfme/generator` | ^6.1.12 | PDF generation | `jspdf`, `pdfkit` |
43
+ | `cmdk` | ^1.1.1 | Command palette | `kbar`, `ninja-keys` |
44
+ | `@monaco-editor/react` | ^4.7.0 | Code editor | `codemirror`, `ace` |
45
+ | `localforage` | ^1.10.0 | Client-side storage | `idb`, `dexie` |
46
+ | `react-markdown` | ^10.1.0 | Markdown rendering | `marked`, `markdown-it` |
47
+ | `remark-gfm` | ^4.0.1 | GFM (tables, tasklists) | `remark-github`, custom |
48
+ | `fast-deep-equal` | ^3.1.3 | Deep equality checks | `lodash.isEqual` |
49
+ | `@ngneat/falso` | latest | Fake data generation | `@faker-js/faker`, `chance` |
50
+ | `pino` | ^10.1.0 | Logging | `winston` |
51
51
 
52
52
  ## GitHub Actions
53
53
 
54
54
  Pin the major, and check it against the action's releases — a major behind still runs, so a stale pin never fails, it only warns.
55
55
 
56
- | Action | Version | Use |
57
- | -------------------------- | ------- | -------------------------- |
58
- | actions/checkout | v7 | Repo checkout |
59
- | actions/setup-node | v7 | Node toolchain + npm cache |
56
+ | Action | Version | Use |
57
+ | --- | --- | --- |
58
+ | `actions/checkout` | v7 | Repo checkout |
59
+ | `actions/setup-node` | v7 | Node toolchain + npm cache |
60
60
 
61
- Set `node-version` from the Runtime Versions block above, not from whatever the runner defaults to.
61
+ Set `node-version` from the Runtime Versions block above, **never** from the runner's default.
@@ -1,34 +1,34 @@
1
- # Nomenclature Guidelines
1
+ # Nomenclature
2
2
 
3
- ## Naming Guidelines
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. Database naming is not here — see § Database.
4
4
 
5
- - Use descriptive names that reveal intent, purpose, and usage
6
- - Names should explain why something exists and how it's used
7
- - Favor clarity over brevity
8
- - Maintain consistency within the codebase
9
- - Avoid abbreviations unless they're universally understood
5
+ Names state intent in full words — clarity over brevity; no abbreviations unless universally understood (`id`, `url` ✅ · `cfg`, `btn`, `idx` ❌).
10
6
 
11
- ## Naming Styles
7
+ ## Casing
12
8
 
13
- - Variables : Use nouns or noun phrases. Examples: `userData`, `firstName`, `paymentStatus`, `configOptions`
14
- - Functions & Methods : Use verbs or verb phrases that describe the action. Examples: `calculateTotal`, `getUserInfo`, `validateInput`, `sendNotification`
15
- - Boolean Variables : Prefix with is**, has**, can**, or should**. Examples: `isValid`, `hasPermission`, `canEdit`, `shouldRefresh`
16
- - Arrays : Use plural nouns. Examples: `users`, `productList`, `errorMessages`, `activeConnections`
17
- - Classes & Components : Use singular nouns or noun phrases. Examples: `RequestHandler`, `UserProfile`, `PaymentProcessor`, `DataValidator`
18
- - Constants : Use uppercase with meaningful names. Examples: `DEFAULT_TIMEOUT`, `API_BASE_URL`, `MAX_RETRY_COUNT`, `COLOR_PRIMARY`
19
- - Event Handlers : Use Prefix with handle or on. Examples: `handleSubmit`, `onUserLogin`, `handleInputChange`, `onPageLoad`
20
- - Interfaces & Types : Use Consider prefixing with I for interfaces (if preferred). Examples: `IUserData`, `IConfigOptions`, `AuthResponse`, `RequestParams`
21
- - Promises & Async Functions : Use Consider using future tense or suffixes like Async. Examples: `fetchUserData`, `getUserProfileAsync`, `loadConfigAsync`, `processPayment`
22
- - File & Folder Structure : Use lowercase with descriptive names. Group by feature or function type. Example: `auth/login_service.js`
9
+ | Casing | Used for | Example |
10
+ | --- | --- | --- |
11
+ | `camelCase` | variables, functions, methods, properties; a module file exporting no component | `calculateTotal`, `sessionClient.ts` |
12
+ | `PascalCase` | classes, type definitions, components; the file exporting one | `PaymentProcessor`, `AppsListPage.tsx` |
13
+ | `UPPER_SNAKE_CASE` | constants, environment variables | `MAX_RETRY_COUNT` |
14
+ | `kebab-case` | URLs, CSS class names, HTML IDs, multi-word folders | `user-profile`, `workspace-provisioning/` |
15
+ | `snake_case` | database tables, columns, indexes — § Database | `created_at` |
23
16
 
24
- ## Casing Styles
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.
25
18
 
26
- - camelCase : Used in Variables, function names, method names, property names. Examples: `userData`, `calculateTotal`, `firstName`, `isValidEmail`
27
- - PascalCase : Used in Class names, component names, type definitions. Examples: `UserProfile`, `DataService`, `AuthProvider`, `RequestHandler`
28
- - snake_case : Used in File names, folder names. Examples: `user_service`, `api_utils`, `auth_middleware`, `data_models`
29
- - UPPER_SNAKE_CASE : Used in Constants, environment variables. Examples: `MAX_RETRY_COUNT`, `API_KEY`, `DEFAULT_TIMEOUT`, `COLOR_PRIMARY`
30
- - kebab-case : Used in URLs, CSS class names, HTML IDs. Examples: `nav-container`, `user-profile`, `button-primary`, `form-wrapper`
19
+ ## Identifier shape
20
+
21
+ | Identifier | Shape | Example |
22
+ | --- | --- | --- |
23
+ | Variable | noun phrase | `paymentStatus` |
24
+ | Function / method | verb phrase naming the action | `validateInput` |
25
+ | Boolean | `is` / `has` / `can` / `should` prefix — **never** a bare adjective | `hasPermission` |
26
+ | Array / collection | plural noun | `errorMessages` |
27
+ | Class / component | singular noun phrase | `RequestHandler` |
28
+ | Event handler | `handle` or `on` prefix | `handleSubmit`, `onPageLoad` |
29
+ | Interface | `I` prefix acceptable; type aliases stay plain | `IConfigOptions`, `AuthResponse` |
30
+ | Async function | verb phrase; `Async` suffix acceptable | `loadConfigAsync` |
31
31
 
32
32
  ## Database
33
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), the cross-project database conventions rule. Kept there, not here, so there is one canonical home and the same concept carries the same name in every project.
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,54 +1,57 @@
1
1
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
2
2
 
3
- How a `@nurix/*` repo is **named**, **structured**, and how its published packages are **consumed**. Every repo takes the workspace shape — a frontend with a single package still uses the multi-package layout, so this rule applies everywhere. Publishing a package — the pipeline, tokens, and the publish workflow — lives in the `nustack` skill, not here. TypeScript language conventions are [`frontend.md`](frontend.md).
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. TypeScript language conventions are [`frontend.md`](frontend.md).
4
4
 
5
5
  ---
6
6
 
7
7
  ## 1. Package naming
8
8
 
9
- Every workspace package is scoped to **`@nurix/`**. The segment after the scope encodes **intent** — the dot vs. hyphen is semantic, not cosmetic:
9
+ Every workspace package is scoped **`@nurix/`**; the dot vs. hyphen after the scope is semantic, not cosmetic:
10
10
 
11
- | Form | Meaning | Examples |
12
- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
13
- | `@nurix/workflows-sdk` (hyphen, single segment) | The **published, externally-destined** surface. A clean `@nurix/*` name is a published _contract_ — changing an export or peer dep is a breaking change for consumers outside this repo. | `@nurix/workflows-sdk` |
14
- | `@nurix/workflows.<name>` (dot separator) | **Internal** to the workflows product — `private: true`, refactor freely, no external consumers. | `@nurix/workflows.agent-core`, `.worker`, `.gateway`, `.secrets`, `.connector`, `.playground`, `.playground-tests` |
15
- | `@nurix/<name>` (plain, no `workflows` prefix) | Cross-repo **shared libs** owned _elsewhere_ and merely consumed here — never renamed. | `@nurix/contracts`, `@nurix/components` |
11
+ | Form | Meaning | Examples |
12
+ | --- | --- | --- |
13
+ | `@nurix/workflows-sdk` (hyphen) | **Published, externally-destined** contract | `@nurix/workflows-sdk` |
14
+ | `@nurix/workflows.<name>` (dot) | **Internal** to the product — `private: true`, refactor freely. The default for a new package | `@nurix/workflows.agent-core`, `.worker`, `.playground` |
15
+ | `@nurix/<name>` (plain) | Cross-repo shared lib owned **elsewhere** — never renamed | `@nurix/contracts`, `@nurix/components` |
16
16
 
17
- - The `<name>` segment is always **kebab-case** (`agent-core`, `playground-tests`).
18
- - The dot is **not** a path separator, so subpath exports still resolve: `@nurix/workflows.agent-core/nodes`.
19
- - The dot tracks "internal product package," not the `private` flag exactly — `@nurix/workflows.secrets` is `private: false` because it's extraction-bound (slated to move to a sibling repo/namespace), yet still wears the dotted internal name. Today `-sdk` is the only clean published form.
20
-
21
- **Naming a new package:** internal to this repo? → `@nurix/workflows.<thing>`, `private: true`, kebab-case `<thing>` (the default). Externally consumed contract? → clean `@nurix/<thing>` (rare). Shared lib from another repo? → keep its existing `@nurix/<name>`.
17
+ - `<name>` is always **kebab-case**.
18
+ - The dot is **not** a path separator — subpath exports resolve normally (`@nurix/workflows.agent-core/nodes`).
19
+ - The dot tracks "internal product package," not the `private` flag: `@nurix/workflows.secrets` is `private: false` (extraction-bound) yet dotted. `-sdk` is the only clean published form.
22
20
 
23
21
  ---
24
22
 
25
23
  ## 2. Monorepo & package boundaries
26
24
 
27
- One `tsconfig.base.json` at the repo root is the **single owner of TS policy** — the strict set (see [`frontend.md`](frontend.md)), `target`, and any option uniform across packages (here, `module`/`moduleResolution`). Each package's `tsconfig.json` is thin: it `extends` the base, then adds only its own *coordinates* (`rootDir`/`outDir`/`include`) and genuine *environment* overrides (`lib`/`types`/`jsx`, plus `module`/`moduleResolution` when a package's module system genuinely differs — a Worker, browser, or NodeNext package). That is environment, not policy. Never re-declare or weaken a *policy* option locally (no per-package `strict: false`, no widening `noUncheckedIndexedAccess`). A framework that owns its own base (e.g. an Astro app on `astro/tsconfigs/strict`) is the one allowed exception.
25
+ - **One `tsconfig.base.json` at the repo root owns TS policy** — the strict set ([`frontend.md`](frontend.md)), `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
+ - **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
+ - **Crossing the publish boundary is the `nustack` skill** — `private: false`, the published manifest, the pipeline.
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.
29
+ - **TS project references only when it hurts** — adopt `references` once the typecheck/build graph is slow enough to feel; until then, `pnpm -r typecheck`.
30
+
31
+ ### The catalog has no garbage collector
32
+
33
+ - **Removing the last consumer of a cataloged package removes its catalog entry in the same change** — `pnpm remove` rewrites the manifest and lockfile but **never** `pnpm-workspace.yaml`, and nothing downstream flags the orphan: no install fails, no lint fires.
34
+ - The check is mechanical: an entry no workspace manifest names under any dependency field is orphaned; one absent from `pnpm-lock.yaml` is an orphan with certainty.
35
+ - **A cataloged package is `catalog:` in every manifest** — a literal range (`"zod": "^4.2.1"`) resolves identically today and silently stops tracking the catalog the moment it moves.
28
36
 
29
- - **Internal packages are source-first.** A `private: true` workspace package exposes its API through the `exports` field pointing at TypeScript source (`"types"`/`"default"` → `./src/index.ts`) — consumed across the workspace with **no build step**. Prefer the `exports` field; don't add a bare `main`/`module` to new packages. Add one typed subpath per public surface (`"./nodes": { "types": "./src/nodes/index.ts", "default": "./src/nodes/index.ts" }`); consumers import the subpath, **never** a deep relative path into another package's `src/`.
30
- - **Published packages build to `dist` — never ship raw `src`.** The moment a package crosses a publish boundary (`private: false`), it compiles with `tsc` (`declaration: true`, `declarationMap: true` from the base), ships `files: ["dist"]`, and points `exports`/`types` at `./dist/*` (use `publishConfig.exports` to swap source→dist on publish if the dev entry stays on source). `prepublishOnly` runs the build.
31
- - **Declare host singletons as `peerDependencies`** on published libraries (`react`, `react-dom`, the framework client) so consumers keep one copy; never bundle them. Apps depend on them directly.
32
- - **Single-source shared dep versions with pnpm `catalog:`** — `typescript`, `zod`, `vitest`, `react`, type packages. One version per dependency across the workspace so every package's types resolve identically; no per-manifest version drift.
33
- - **Reach for TS project references only when it hurts** — adopt `references` once the typecheck/build graph is slow enough to feel. Until then, `pnpm -r typecheck` over independent `extends` configs is the correct default.
37
+ ### The workspace root is a hard boundary
38
+
39
+ - **A dependency spec never points outside the folder holding `pnpm-workspace.yaml`** — `devDependencies` included. Inside the root, `workspace:*` between `packages/*` is the normal, correct thing; outside it, nothing — no `file:../…`, no `link:`, no absolute `file:`. A sibling checkout is another repo, and the registry is the only supported way to consume one — a cross-root spec bakes machine-local paths into the lockfile and the published tarball, so `install --frozen-lockfile` fails on any other machine.
40
+ - **A package unreachable from the registry is an auth or publishing bug — fix it there, never with a local path.** Diagnosis order: the scope's registry (§3) → the token → whether it was actually published.
34
41
 
35
42
  ---
36
43
 
37
44
  ## 3. Consuming `@nurix/*` packages
38
45
 
39
- `@nurix/*` packages publish to npm with **`restricted`** access — installing needs read rights to the `nurix` org. (Publishing one is the `nustack` skill.)
46
+ `@nurix/*` packages publish to npm with **`restricted`** access — installing needs read rights to the `nurix` org.
40
47
 
41
- - **Auth** — `npm login` once (the npm user must be a `nurix` member), **or** drop a read token in `.npmrc` (the only CI option; with it you don't need `npm login`):
48
+ - **Registry** — point the scope at npmjs, where the packages are published. A restricted read answers `404`, not `401`, so a mispaired registry is indistinguishable from "never published" — on an `@nurix/*` 404, check the registry pairing **first**, before auth and before assuming the package is missing.
49
+ - **Auth** — `npm login` once (the npm user must be a `nurix` member), or a read token in `.npmrc` (the only CI option):
42
50
  ```ini
43
51
  @nurix:registry=https://registry.npmjs.org/
44
52
  //registry.npmjs.org/:_authToken=${NPM_READ_TOKEN}
45
53
  ```
46
- A `403` on install means auth failed — check `npm whoami` (local) or the `NPM_READ_TOKEN` secret (CI). The read token only needs `Read` on the `nurix` scope. Just attempt the install — the token authenticates ambiently; don't pre-empt or second-guess it. If the install actually fails, flag it to the user — **never** reach for alternatives, a substitute package, or `npm login` swaps.
47
- - **Install** — `npm install @nurix/<name>` (or `pnpm add` / `yarn add`).
48
- - **Import by subpath, never the barrel** — bundlers tree-shake subpaths reliably; root-barrel imports less so. Available subpaths are the package's `exports` keys.
49
- ```tsx
50
- import { Button } from "@nurix/components/button";
51
- import { useMobile } from "@nurix/components/hooks/use-mobile";
52
- ```
53
- - **Styles** — packages that ship CSS need their stylesheet imported **once** at the app entry: `import "@nurix/components/styles.css";`. Unstyled components are almost always a missing styles import. Tailwind v4 doesn't scan `node_modules`, but the shipped stylesheet is precompiled — no `@source` directive needed unless you consume the source via a workspace link.
54
+ The token needs only `Read` on the `nurix` scope. A `403` on install means auth failed — check `npm whoami` (local) or the `NPM_READ_TOKEN` secret (CI). Attempt the install directly — the token authenticates ambiently; don't pre-empt or second-guess it. If it fails, flag it to the user — **never** reach for alternatives, a substitute package, or `npm login` swaps.
55
+ - **Import by subpath, never the barrel** — subpaths tree-shake reliably; the available subpaths are the package's `exports` keys. `import { Button } from "@nurix/components/button";`
56
+ - **Styles** — import a CSS-shipping package's stylesheet **once** at the app entry: `import "@nurix/components/styles.css";`. Unstyled components are almost always a missing styles import. The shipped stylesheet is precompiled — no Tailwind `@source` directive unless consuming source via a workspace link.
54
57
  - **CLIs** ship a `bin` — run directly: `npx @nurix/etna@latest` (or `npm install -g`).
@@ -1,25 +1,23 @@
1
1
  # Testing
2
2
 
3
- 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.
3
+ Test technology and placement — framework per stack, suite layout, mocking policy; the suite-first policy (what gets tests, run-and-report discipline) is the harness `CLAUDE.md` Tests guardrail, not here. 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.
4
4
 
5
5
  ## Frameworks by stack
6
6
 
7
- No alternatives. Just use these.
8
-
9
7
  | Stack | Unit / integration | Component | E2E | HTTP mocking | Don't use |
10
8
  | --- | --- | --- | --- | --- | --- |
11
- | Node + Vite / React (web) | vitest | @testing-library/react (+ @testing-library/user-event, happy-dom) | @playwright/test | msw | jest, mocha, jasmine, cypress, enzyme |
12
- | Express APIs | vitest | — | supertest against the app instance | msw (outbound calls) | jest, chai-http |
13
- | React Native / Expo | jest via the jest-expo preset | @testing-library/react-native | maestro | msw | detox, enzyme |
14
- | Cloudflare Workers | vitest + @cloudflare/vitest-pool-workers | — | @playwright/test against `wrangler dev` | msw | jest |
9
+ | Node + Vite / React (web) | `vitest` | `@testing-library/react` (+ `@testing-library/user-event`, `happy-dom`) | `@playwright/test` | `msw` | `jest`, `mocha`, `jasmine`, `cypress`, `enzyme` |
10
+ | Express APIs | `vitest` | — | `supertest` against the app instance | `msw` (outbound calls) | `jest`, `chai-http` |
11
+ | React Native / Expo | `jest` via the `jest-expo` preset | `@testing-library/react-native` | `maestro` | `msw` | `detox`, `enzyme` |
12
+ | Cloudflare Workers | `vitest` + `@cloudflare/vitest-pool-workers` | — | `@playwright/test` against `wrangler dev` | `msw` | `jest` |
15
13
  | Rust | `cargo test` (built-in) | — | — | — | — |
16
14
 
17
- All npm packages at `latest`. Fake data comes from `@ngneat/falso` (the repo standard — never faker/chance).
15
+ Test packages install at `latest` — they sit outside [`general.md`](general.md)'s pinned table. Fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance` (its row there).
18
16
 
19
17
  ## Conventions
20
18
 
21
- - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses (detect, don't invent) — named `<subject>.test.ts`. Shared fixtures, factories, and helpers live in `tests/helpers/` (or the suite's existing shared location). The harness `CLAUDE.md` Tests guardrail carries the suite-first policy (what gets tests, discipline); this rule owns the technology and placement choices.
22
- - **Wire the canonical command.** `"test"` in package.json runs the whole unit suite (`vitest run`); e2e runs under `"test:e2e"`. A repo where `pnpm test`/`npm test` doesn't run the suite is misconfigured.
23
- - **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.
24
- - **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).
25
- - **Determinism.** No real network, no real time, no shared mutable state between tests; every test must pass alone and in any order.
19
+ - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
20
+ - **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
21
+ - **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
+ - **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
+ - **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone and in any order.
@@ -61,6 +61,6 @@ Segmented control for switching a view-type projection (e.g. table/card), with a
61
61
 
62
62
  ## Other Components
63
63
 
64
- - `icon-renderer` + `icons-data` — `IconRenderer`: `icon`/`family`/`size` [sm|md|lg|xl]/`color`; resolves a remote SVG (`https://static.amoga.io/fa/<family>/<icon>.svg`) tinted via CSS filter, with a built-in fallback icon on load error. `icons-data` exports `iconsData` — `{ name, categories, tags }[]` — for building icon pickers over the local Lucide set (distinct from the remote family above).
64
+ - `icon-renderer` + `icons-data` — `IconRenderer`: `icon`/`family`/`size` [sm|md|lg|xl]/`color`; resolves a remote SVG from the configured icon CDN (`<cdn>/fa/<family>/<icon>.svg`) tinted via CSS filter, with a built-in fallback icon on load error. `icons-data` exports `iconsData` — `{ name, categories, tags }[]` — for building icon pickers over the local Lucide set (distinct from the remote family above).
65
65
  - `live-waveform` + `mic-icon` — `LiveWaveform`: canvas mic-level visualizer; `active`, `processing`, `mode` [static|history|frequency], `barWidth`/`barGap`/`barRadius`/`barColor`, `sensitivity`, `onStreamReady`/`onStreamEnd`/`onError` (draft API — most props are untyped passthroughs). `MicIcon`: animated mic glyph with an imperative ref (`startAnimation`/`stopAnimation`); `size`, `duration`, `isAnimated`.
66
66
  - `shine-border` — `ShineBorder`: absolutely-positioned animated gradient border overlay (parent needs `position: relative`); `borderWidth`, `duration` (seconds), `shineColor` (string or array, cycled through a radial gradient sweep)
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.3.0",
4
+ "version": "1.3.2",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "b1de16d6988e"
10
+ "digest": "c7f202566db9"
11
11
  }
@@ -1,8 +1,6 @@
1
1
  # Changelog
2
2
 
3
- Every unit of work leaves one record: `docs/history/changelog/YYYY-MM-DD-{three-words}.md`. The three words name the work, not the commit. Format, field rules, and the closed exemption list are the `documentation` skill's `templates/changelog-entry.md`.
4
-
5
- An entry is not a summary line. It carries the refinement context only the working session holds — the ask in the user's own words, what was rejected, which skills survived or were overridden, what running the thing taught, what now guards it.
3
+ Every unit of work leaves one record — `docs/history/changelog/YYYY-MM-DD-{three-words}.md` (the three words name the work, not the commit) — plus its row in `docs/history/CHANGELOG.md`, appended in the same pass so the index never trails the folder. Entry format, field rules, and the closed exemption list are the `documentation` skill's `templates/changelog-entry.md`; this rule owns when an entry is owed and how it lives. An entry is never a commit summary — it carries the refinement context only the working session holds, which no diff reconstructs.
6
4
 
7
5
  ## When an entry is owed
8
6
 
@@ -10,28 +8,12 @@ An entry is not a summary line. It carries the refinement context only the worki
10
8
  | --- | --- |
11
9
  | A plan reaches `Status: Complete` | A draft entry for that plan, linked to it — written now, while the session still holds the context |
12
10
  | The user asks for a changelog | The entry for the work in hand. No commit required |
13
- | A commit is made | Every non-exempt commit is covered by an entry — the commit skill writes or completes it |
14
-
15
- **Exempt** (closed list): release and bot commits, harness installs and bumps, lockfile-only changes, pure renames and moves, and changelog housekeeping itself.
11
+ | A commit is made | Every non-exempt commit is covered by an entry — the `commit` skill writes or completes it |
16
12
 
17
- ## The entry's life
18
-
19
- | State | Meaning |
20
- | --- | --- |
21
- | **Draft** | Created before its commit exists. Mutable — fields filled as far as the session can |
22
- | **Complete** | Missing fields filled in from the diff and the session |
23
- | **Frozen** | The moment it rides in a commit. Never edited again |
13
+ ## Lifecycle
24
14
 
25
- **Immutability begins at commit, not at creation.** A draft in the working tree is meant to be edited — that is what lets a completed plan record its context before anything is committed. Once an entry is committed it is history: a correction is a new entry linking back, never an edit.
15
+ Draft (created before its commit exists) → complete (missing fields filled from the diff and the session) → frozen the moment it rides in a commit. **Immutability begins at commit, not at creation** — a draft is meant to be edited, which is what lets a completed plan record its context before anything is committed; a committed entry is history, and a correction is a new entry linking back, **never** an edit.
26
16
 
27
17
  ## Granularity
28
18
 
29
- A commit is the smallest unit an entry may describe. Above that the mapping is judgment — four commits may be four entries or one, whichever reads better as history.
30
-
31
- An entry spanning several commits rides in the **last** of them and names the earlier ones in `Commits:`. An entry can never carry its own commit's SHA, so that field lists predecessors only and is omitted when one entry maps to one commit.
32
-
33
- When the work done exceeds what a draft describes, do not stretch the draft — write a second entry. The first stays an honest account of the plan; the second records the work beyond it.
34
-
35
- ## The index
36
-
37
- Every entry appends one row to `docs/history/CHANGELOG.md` — date, link, one-line summary — in the same pass that writes the entry, so the table never trails the folder.
19
+ A commit is the smallest unit an entry may describe; above that the mapping is judgment — four commits may be four entries or one, whichever reads better as history. An entry spanning several commits rides in the **last** of them, naming the predecessors in `Commits:`. When work exceeds what a draft describes, write a second entry rather than stretching the draft — the first stays an honest account of the plan.
@@ -1,61 +1,61 @@
1
1
  # General Standards
2
2
 
3
- Runtime versions and the canonical package list — the workspace baseline every Nurix code harness loads. Web-frontend/styling conventions are [`frontend.md`](frontend.md); the Express server and the API response envelope are [`express.md`](express.md).
3
+ Runtime versions, the canonical package list, and GitHub Actions pins — the workspace baseline. Web-frontend/styling conventions are [`frontend.md`](frontend.md); the Express server and the API response envelope are [`express.md`](express.md).
4
4
 
5
5
  ## Runtime Versions
6
6
 
7
7
  - **Node.js:** 24 LTS
8
- - **Package manager:** detect from lockfile (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm). Don't hardcode one.
8
+ - **Package manager:** detect from the lockfile — **never** hardcode one.
9
9
  - **PostgreSQL:** 17 · **Redis:** 7.x (latest)
10
10
 
11
- ## Recommended Packages
12
-
13
- No alternatives. Just use these.
14
-
15
- | Package | Version | Use | Don't use |
16
- | ------------------------ | --------- | ---------------------- | -------------------------- |
17
- | react | ^19.2.3 | UI components | - |
18
- | vite | ^8.0.0 | Build tool, dev server | webpack, CRA |
19
- | tailwindcss | ^4.1.18 | Styling | styled-components |
20
- | @vitejs/plugin-react-swc | ^4.2.2 | JSX compilation | @vitejs/plugin-react |
21
- | react-router-dom | ^7.11.0 | Routing | - |
22
- | @reduxjs/toolkit | ^2.11.2 | State management | zustand, jotai |
23
- | react-redux | ^9.2.0 | Redux React bindings | - |
24
- | axios | ^1.13.2 | HTTP requests | fetch |
25
- | date-fns | ^4.1.0 | Date formatting | moment, dayjs |
26
- | @dnd-kit/core | ^6.3.1 | Drag and drop | react-beautiful-dnd |
27
- | @xyflow/react | ^12.11.0 | Node-based flow charts | reactflow, react-flow-renderer |
28
- | motion | ^12.23.26 | Animations | react-spring |
29
- | sonner | ^2.0.7 | Toast notifications | react-toastify |
30
- | react-window | ^2.2.3 | List virtualization | react-virtualized |
31
- | nanoid | ^6.0.0 | ID generation | uuid |
32
- | express | ^5.2.1 | HTTP server | fastify, koa |
33
- | drizzle-orm | ^0.45.2 | Database ORM | prisma, sequelize, typeorm |
34
- | drizzle-kit | ^0.31.10 | Schema tooling | - |
35
- | zod | ^4.2.1 | Schema validation | yup, joi |
36
- | jose | ^6.1.3 | JWT handling | jsonwebtoken |
37
- | microdiff | ^1.5.0 | JSON object diffing | deep-diff, just-diff |
38
- | react18-json-view | ^0.2.9 | JSON tree view | react-json-view |
39
- | @tiptap/react | ^3.11.1 | Rich text editor | slate, quill, draft-js |
40
- | shiki | ^4.3.1 | Syntax highlighting | prism, highlight.js |
41
- | lucide-react | ^1.26.0 | Icons | react-icons, heroicons |
42
- | @pdfme/generator | ^6.1.12 | PDF generation | jspdf, pdfkit |
43
- | cmdk | ^1.1.1 | Command palette | kbar, ninja-keys |
44
- | @monaco-editor/react | ^4.7.0 | Code editor | codemirror, ace |
45
- | localforage | ^1.10.0 | Client-side storage | idb, dexie |
46
- | react-markdown | ^10.1.0 | Markdown rendering | marked, markdown-it |
47
- | remark-gfm | ^4.0.1 | GFM (tables, tasklists) | remark-github, custom |
48
- | fast-deep-equal | ^3.1.3 | Deep equality checks | lodash.isEqual |
49
- | @ngneat/falso | latest | Fake data generation | @faker-js/faker, chance |
50
- | pino | ^10.1.0 | Logging | winston |
11
+ ## Packages
12
+
13
+ **Only** these — no alternatives.
14
+
15
+ | Package | Version | Use | Don't use |
16
+ | --- | --- | --- | --- |
17
+ | `react` | ^19.2.3 | UI components | — |
18
+ | `vite` | ^8.0.0 | Build tool, dev server | `webpack`, CRA |
19
+ | `tailwindcss` | ^4.1.18 | Styling | `styled-components` |
20
+ | `@vitejs/plugin-react-swc` | ^4.2.2 | JSX compilation | `@vitejs/plugin-react` |
21
+ | `react-router-dom` | ^7.11.0 | Routing | — |
22
+ | `@reduxjs/toolkit` | ^2.11.2 | State management | `zustand`, `jotai` |
23
+ | `react-redux` | ^9.2.0 | Redux React bindings | — |
24
+ | `axios` | ^1.13.2 | HTTP requests | `fetch` |
25
+ | `date-fns` | ^4.1.0 | Date formatting | `moment`, `dayjs` |
26
+ | `@dnd-kit/core` | ^6.3.1 | Drag and drop | `react-beautiful-dnd` |
27
+ | `@xyflow/react` | ^12.11.0 | Node-based flow charts | `reactflow`, `react-flow-renderer` |
28
+ | `motion` | ^12.23.26 | Animations | `react-spring` |
29
+ | `sonner` | ^2.0.7 | Toast notifications | `react-toastify` |
30
+ | `react-window` | ^2.2.3 | List virtualization | `react-virtualized` |
31
+ | `nanoid` | ^6.0.0 | ID generation | `uuid` |
32
+ | `express` | ^5.2.1 | HTTP server | `fastify`, `koa` |
33
+ | `drizzle-orm` | ^0.45.2 | Database ORM | `prisma`, `sequelize`, `typeorm` |
34
+ | `drizzle-kit` | ^0.31.10 | Schema tooling | — |
35
+ | `zod` | ^4.2.1 | Schema validation | `yup`, `joi` |
36
+ | `jose` | ^6.1.3 | JWT handling | `jsonwebtoken` |
37
+ | `microdiff` | ^1.5.0 | JSON object diffing | `deep-diff`, `just-diff` |
38
+ | `react18-json-view` | ^0.2.9 | JSON tree view | `react-json-view` |
39
+ | `@tiptap/react` | ^3.11.1 | Rich text editor | `slate`, `quill`, `draft-js` |
40
+ | `shiki` | ^4.3.1 | Syntax highlighting | `prism`, `highlight.js` |
41
+ | `lucide-react` | ^1.26.0 | Icons | `react-icons`, `heroicons` |
42
+ | `@pdfme/generator` | ^6.1.12 | PDF generation | `jspdf`, `pdfkit` |
43
+ | `cmdk` | ^1.1.1 | Command palette | `kbar`, `ninja-keys` |
44
+ | `@monaco-editor/react` | ^4.7.0 | Code editor | `codemirror`, `ace` |
45
+ | `localforage` | ^1.10.0 | Client-side storage | `idb`, `dexie` |
46
+ | `react-markdown` | ^10.1.0 | Markdown rendering | `marked`, `markdown-it` |
47
+ | `remark-gfm` | ^4.0.1 | GFM (tables, tasklists) | `remark-github`, custom |
48
+ | `fast-deep-equal` | ^3.1.3 | Deep equality checks | `lodash.isEqual` |
49
+ | `@ngneat/falso` | latest | Fake data generation | `@faker-js/faker`, `chance` |
50
+ | `pino` | ^10.1.0 | Logging | `winston` |
51
51
 
52
52
  ## GitHub Actions
53
53
 
54
54
  Pin the major, and check it against the action's releases — a major behind still runs, so a stale pin never fails, it only warns.
55
55
 
56
- | Action | Version | Use |
57
- | -------------------------- | ------- | -------------------------- |
58
- | actions/checkout | v7 | Repo checkout |
59
- | actions/setup-node | v7 | Node toolchain + npm cache |
56
+ | Action | Version | Use |
57
+ | --- | --- | --- |
58
+ | `actions/checkout` | v7 | Repo checkout |
59
+ | `actions/setup-node` | v7 | Node toolchain + npm cache |
60
60
 
61
- Set `node-version` from the Runtime Versions block above, not from whatever the runner defaults to.
61
+ Set `node-version` from the Runtime Versions block above, **never** from the runner's default.
@@ -1,13 +1,6 @@
1
- # kb.md — Patterns for Obsidian-like Folders
1
+ # Knowledge-Base Folders
2
2
 
3
- Conventions for any folder that is majority markdown — `docs/`, `research/`, an Obsidian vault, or any knowledge base. Rules grow over time; append new ones below.
3
+ Conventions for any folder that is majority markdown — `docs/`, `research/`, an Obsidian vault, any knowledge base. Folder shape and linking only.
4
4
 
5
- ## Rules
6
-
7
- ### 1. Every topic gets a `Readme.md`
8
-
9
- When you start documenting a topic, create `Readme.md` as the folder's landing page. Don't use `index.md` or unscoped filenames — `Readme.md` is what readers and tools (GitHub, Obsidian) look for first, and it makes the folder self-introducing.
10
-
11
- ### 2. Always link documents to each other
12
-
13
- When one document references an idea, concept, or topic covered in another, link to it. Wikilinks (`[[other-doc]]`) inside Obsidian-native vaults; relative markdown links (`[label](../other-doc.md)`) elsewhere. If you mention it, you link it — that's how the folder becomes a knowledge graph instead of a pile of files.
5
+ - **Every topic folder carries a `Readme.md`**, created with the topic's first document — **never** `index.md` or an unscoped filename. `Readme.md` is what GitHub and Obsidian surface first, so every folder introduces itself.
6
+ - **Every mention is a link** — wikilinks (`[[other-doc]]`) inside Obsidian-native vaults, relative markdown links (`[label](../other-doc.md)`) everywhere else. Unlinked mentions decay a knowledge graph into a pile of files.
@@ -1,34 +1,34 @@
1
- # Nomenclature Guidelines
1
+ # Nomenclature
2
2
 
3
- ## Naming Guidelines
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. Database naming is not here — see § Database.
4
4
 
5
- - Use descriptive names that reveal intent, purpose, and usage
6
- - Names should explain why something exists and how it's used
7
- - Favor clarity over brevity
8
- - Maintain consistency within the codebase
9
- - Avoid abbreviations unless they're universally understood
5
+ Names state intent in full words — clarity over brevity; no abbreviations unless universally understood (`id`, `url` ✅ · `cfg`, `btn`, `idx` ❌).
10
6
 
11
- ## Naming Styles
7
+ ## Casing
12
8
 
13
- - Variables : Use nouns or noun phrases. Examples: `userData`, `firstName`, `paymentStatus`, `configOptions`
14
- - Functions & Methods : Use verbs or verb phrases that describe the action. Examples: `calculateTotal`, `getUserInfo`, `validateInput`, `sendNotification`
15
- - Boolean Variables : Prefix with is**, has**, can**, or should**. Examples: `isValid`, `hasPermission`, `canEdit`, `shouldRefresh`
16
- - Arrays : Use plural nouns. Examples: `users`, `productList`, `errorMessages`, `activeConnections`
17
- - Classes & Components : Use singular nouns or noun phrases. Examples: `RequestHandler`, `UserProfile`, `PaymentProcessor`, `DataValidator`
18
- - Constants : Use uppercase with meaningful names. Examples: `DEFAULT_TIMEOUT`, `API_BASE_URL`, `MAX_RETRY_COUNT`, `COLOR_PRIMARY`
19
- - Event Handlers : Use Prefix with handle or on. Examples: `handleSubmit`, `onUserLogin`, `handleInputChange`, `onPageLoad`
20
- - Interfaces & Types : Use Consider prefixing with I for interfaces (if preferred). Examples: `IUserData`, `IConfigOptions`, `AuthResponse`, `RequestParams`
21
- - Promises & Async Functions : Use Consider using future tense or suffixes like Async. Examples: `fetchUserData`, `getUserProfileAsync`, `loadConfigAsync`, `processPayment`
22
- - File & Folder Structure : Use lowercase with descriptive names. Group by feature or function type. Example: `auth/login_service.js`
9
+ | Casing | Used for | Example |
10
+ | --- | --- | --- |
11
+ | `camelCase` | variables, functions, methods, properties; a module file exporting no component | `calculateTotal`, `sessionClient.ts` |
12
+ | `PascalCase` | classes, type definitions, components; the file exporting one | `PaymentProcessor`, `AppsListPage.tsx` |
13
+ | `UPPER_SNAKE_CASE` | constants, environment variables | `MAX_RETRY_COUNT` |
14
+ | `kebab-case` | URLs, CSS class names, HTML IDs, multi-word folders | `user-profile`, `workspace-provisioning/` |
15
+ | `snake_case` | database tables, columns, indexes — § Database | `created_at` |
23
16
 
24
- ## Casing Styles
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.
25
18
 
26
- - camelCase : Used in Variables, function names, method names, property names. Examples: `userData`, `calculateTotal`, `firstName`, `isValidEmail`
27
- - PascalCase : Used in Class names, component names, type definitions. Examples: `UserProfile`, `DataService`, `AuthProvider`, `RequestHandler`
28
- - snake_case : Used in File names, folder names. Examples: `user_service`, `api_utils`, `auth_middleware`, `data_models`
29
- - UPPER_SNAKE_CASE : Used in Constants, environment variables. Examples: `MAX_RETRY_COUNT`, `API_KEY`, `DEFAULT_TIMEOUT`, `COLOR_PRIMARY`
30
- - kebab-case : Used in URLs, CSS class names, HTML IDs. Examples: `nav-container`, `user-profile`, `button-primary`, `form-wrapper`
19
+ ## Identifier shape
20
+
21
+ | Identifier | Shape | Example |
22
+ | --- | --- | --- |
23
+ | Variable | noun phrase | `paymentStatus` |
24
+ | Function / method | verb phrase naming the action | `validateInput` |
25
+ | Boolean | `is` / `has` / `can` / `should` prefix — **never** a bare adjective | `hasPermission` |
26
+ | Array / collection | plural noun | `errorMessages` |
27
+ | Class / component | singular noun phrase | `RequestHandler` |
28
+ | Event handler | `handle` or `on` prefix | `handleSubmit`, `onPageLoad` |
29
+ | Interface | `I` prefix acceptable; type aliases stay plain | `IConfigOptions`, `AuthResponse` |
30
+ | Async function | verb phrase; `Async` suffix acceptable | `loadConfigAsync` |
31
31
 
32
32
  ## Database
33
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), the cross-project database conventions rule. Kept there, not here, so there is one canonical home and the same concept carries the same name in every project.
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,25 +1,23 @@
1
1
  # Testing
2
2
 
3
- 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.
3
+ Test technology and placement — framework per stack, suite layout, mocking policy; the suite-first policy (what gets tests, run-and-report discipline) is the harness `CLAUDE.md` Tests guardrail, not here. 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.
4
4
 
5
5
  ## Frameworks by stack
6
6
 
7
- No alternatives. Just use these.
8
-
9
7
  | Stack | Unit / integration | Component | E2E | HTTP mocking | Don't use |
10
8
  | --- | --- | --- | --- | --- | --- |
11
- | Node + Vite / React (web) | vitest | @testing-library/react (+ @testing-library/user-event, happy-dom) | @playwright/test | msw | jest, mocha, jasmine, cypress, enzyme |
12
- | Express APIs | vitest | — | supertest against the app instance | msw (outbound calls) | jest, chai-http |
13
- | React Native / Expo | jest via the jest-expo preset | @testing-library/react-native | maestro | msw | detox, enzyme |
14
- | Cloudflare Workers | vitest + @cloudflare/vitest-pool-workers | — | @playwright/test against `wrangler dev` | msw | jest |
9
+ | Node + Vite / React (web) | `vitest` | `@testing-library/react` (+ `@testing-library/user-event`, `happy-dom`) | `@playwright/test` | `msw` | `jest`, `mocha`, `jasmine`, `cypress`, `enzyme` |
10
+ | Express APIs | `vitest` | — | `supertest` against the app instance | `msw` (outbound calls) | `jest`, `chai-http` |
11
+ | React Native / Expo | `jest` via the `jest-expo` preset | `@testing-library/react-native` | `maestro` | `msw` | `detox`, `enzyme` |
12
+ | Cloudflare Workers | `vitest` + `@cloudflare/vitest-pool-workers` | — | `@playwright/test` against `wrangler dev` | `msw` | `jest` |
15
13
  | Rust | `cargo test` (built-in) | — | — | — | — |
16
14
 
17
- All npm packages at `latest`. Fake data comes from `@ngneat/falso` (the repo standard — never faker/chance).
15
+ Test packages install at `latest` — they sit outside [`general.md`](general.md)'s pinned table. Fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance` (its row there).
18
16
 
19
17
  ## Conventions
20
18
 
21
- - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses (detect, don't invent) — named `<subject>.test.ts`. Shared fixtures, factories, and helpers live in `tests/helpers/` (or the suite's existing shared location). The harness `CLAUDE.md` Tests guardrail carries the suite-first policy (what gets tests, discipline); this rule owns the technology and placement choices.
22
- - **Wire the canonical command.** `"test"` in package.json runs the whole unit suite (`vitest run`); e2e runs under `"test:e2e"`. A repo where `pnpm test`/`npm test` doesn't run the suite is misconfigured.
23
- - **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.
24
- - **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).
25
- - **Determinism.** No real network, no real time, no shared mutable state between tests; every test must pass alone and in any order.
19
+ - **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
20
+ - **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
21
+ - **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
+ - **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
+ - **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone and in any order.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "harness": "hr",
3
3
  "name": "hr",
4
- "version": "0.1.11",
4
+ "version": "0.1.12",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
- "digest": "2cd2d7347c4f"
10
+ "digest": "077993d5c509"
11
11
  }
@@ -1,45 +1,45 @@
1
1
  # Code-Eval Batcher
2
2
 
3
- When asked to evaluate and rank **multiple code projects** against the production-readiness + product-quality rubric — candidate take-homes, submissions, a folder of repos — you (the main agent) drive this directly. You can dispatch sub-agents in parallel; a sub-agent cannot, which is why this is a rule you perform, not an agent you delegate to.
4
-
5
- This is distinct from `interview-eval` (which scores how a person *steered a session*). This rule scores the *project's code and product*, across many projects, and ranks them.
3
+ Evaluating and ranking **multiple code projects** — candidate take-homes, submissions, a folder of repos — against the production-readiness + product-quality rubric. The main agent drives this directly: it can dispatch sub-agents in parallel and a sub-agent cannot, so this is a rule to perform, not an agent to delegate to. A single project does **not** need this rule — invoke the `evaluator` skill directly. Scoring how a person *steered a session* is `interview-eval`, not this rule.
6
4
 
7
5
  ## Division of labor
8
6
 
9
- Per-project scoring — the rubric, gates, absolute banding, the 8 sub-criteria, every recipe — belongs to the **`evaluator` skill** (`.claude/skills/evaluator/`). Do not re-derive or re-band it here. This rule owns only what the skill deliberately doesn't: **enumeration, parallel dispatch, weighting, and the cross-project rollup.** Bands are absolute — never re-band a project against the others; only weight and normalize.
7
+ Per-project scoring — the rubric, gates, absolute banding, the 8 sub-criteria, every recipe — belongs to the **`evaluator` skill** (`.claude/skills/evaluator/`); never re-derive it here. This rule owns only what the skill deliberately doesn't: **enumeration, parallel dispatch, weighting, and the cross-project rollup.** **Never re-band a project against the others** — cohort-relative banding is the anti-pattern this design removed; the only cross-project levers live in the weights block.
10
8
 
11
9
  ## Procedure
12
10
 
13
11
  | # | Step |
14
12
  | --- | --- |
15
- | 0 | **Preflight once:** `bash .claude/skills/evaluator/preflight.sh`. Halt on non-zero and report the missing tools — do not score with degraded signals. |
16
- | 1 | **Enumerate** the projects → a list of local paths, one slug each (its dir name). Choose an `out_dir` (default `./code-eval-out`). |
17
- | 2 | **Dispatch in parallel — rolling window of 4.** For each project, `Agent(subagent_type="general-purpose", run_in_background=True, …)` instructing it to invoke the `evaluator` skill on that one project path, write the workspace to `<out_dir>/<slug>/`, validate it (`python3 .claude/skills/evaluator/validate_scorecard.py <out_dir>/<slug>/scorecard.yaml`), and return the workspace path + the scorecard's `normalized`. Fire a replacement the moment any slot frees so 4 are always in flight. |
18
- | 3 | **Collect** every `<out_dir>/*/scorecard.yaml`. Re-dispatch any that fail validation once; on a second failure mark the project `VALIDATION_FAILED` and continue. |
13
+ | 0 | **Preflight once:** `bash .claude/skills/evaluator/preflight.sh`. Halt on non-zero and report the missing tools — never score with degraded signals. |
14
+ | 1 | **Enumerate** the projects → local paths, one slug each (its dir name). Choose an `out_dir` (default `./code-eval-out`). |
15
+ | 2 | **Dispatch in parallel — rolling window of 4.** Per project: `Agent(subagent_type="general-purpose", run_in_background=True, …)` instructing it to invoke the `evaluator` skill on that one path, write the workspace to `<out_dir>/<slug>/`, validate it (`python3 .claude/skills/evaluator/validate_scorecard.py <out_dir>/<slug>/scorecard.yaml`), and return the workspace path + the scorecard's `normalized`. Fire a replacement the moment a slot frees. |
16
+ | 3 | **Collect** every `<out_dir>/*/scorecard.yaml`. Re-dispatch a validation failure once; on the second failure mark the project `VALIDATION_FAILED` and continue. |
19
17
  | 4 | **Weight + normalize** every project (math below) using the weights block. |
20
- | 5 | **Rollup** → `<out_dir>/results/{scorecard.csv, ranking.md, summary.md}`, sorted by rank score desc. Echo the exact weights + normalization used into `summary.md` so the ranking is reproducible. Leave everything uncommitted. |
21
-
22
- A single project does **not** need this rule — invoke the `evaluator` skill directly.
18
+ | 5 | **Rollup** → `<out_dir>/results/{scorecard.csv, ranking.md, summary.md}` (contents below), sorted by rank score desc. Leave everything uncommitted. |
23
19
 
24
20
  ## Weighting math
25
21
 
26
- For each project `p`, read the raw 0–5 `score` of each sub-criterion `s` from its `scorecard.yaml`:
22
+ For each project `p`, read each sub-criterion `s`'s raw 0–5 `score` (the evaluator's absolute band) from its `scorecard.yaml`:
27
23
 
28
- 1. **Weighted (0–100)** over sub-criteria with a non-null band and `weight > 0`:
29
- `weighted_p = 100 × Σ_s (band_{p,s} / 5 × weight_s) / Σ_s weight_s`
24
+ 1. **Weighted (0–100)** over sub-criteria with a non-null `score` and `weight > 0`:
25
+ `weighted_p = 100 × Σ_s (score_{p,s} / 5 × weight_s) / Σ_s weight_s`
30
26
  2. **Gate penalty:** `adjusted_p = max(0, weighted_p − Σ_{g ∈ gates_triggered_p} gate_penalties[g])`.
31
- 3. **Normalize across the set** `{adjusted_p}` per `normalization`:
32
- - `percent_of_max` → `adjusted_p` unchanged (absolute; comparable across runs).
33
- - `min_max` → `100 × (adjusted_p − min) / (max − min)`; if `max == min`, all = 100.
34
- - `z_score` → `clamp(0, 100, 50 + 10 × (adjusted_p − mean) / sd)`; if `sd == 0` or n < 2, fall back to `adjusted_p`.
27
+ 3. **Normalize** across the set `{adjusted_p}` per `normalization`:
28
+ - `percent_of_max` (default) → `adjusted_p` unchanged — absolute, comparable across runs.
29
+ - `min_max` → `100 × (adjusted_p − min) / (max − min)`; `max == min` ⇒ all 100. Spreads a tight set; not comparable across runs.
30
+ - `z_score` → `clamp(0, 100, 50 + 10 × (adjusted_p − mean) / sd)`; `sd == 0` or n < 2 ⇒ fall back to `adjusted_p`.
35
31
 
36
- `VALIDATION_FAILED` projects are excluded from min/max/mean/sd and listed separately. A null sub-criterion drops out of that project's weighted average — don't impute a 0. Compute with a short throwaway script over the scorecards; round reported scores to 2 decimals.
32
+ `VALIDATION_FAILED` projects are excluded from min/max/mean/sd and listed separately. A null sub-criterion drops out of that project's weighted average — never impute a 0. Compute with a short throwaway script over the scorecards; round reported scores to 2 decimals.
37
33
 
38
- `scorecard.csv` columns: `rank, project, <8 bands>, gates, short_circuited, evaluator_total, weighted, penalty, rank_score`. `ranking.md`: a ranked table headed with the normalization method (the sort key). `summary.md`: n scored, the weights/penalties/normalization used, top-5 / bottom-5, gate frequency, any `VALIDATION_FAILED`.
34
+ | Output | Contents |
35
+ | --- | --- |
36
+ | `scorecard.csv` | `rank, project, <8 scores>, gates, short_circuited, evaluator_total, weighted, penalty, rank_score` |
37
+ | `ranking.md` | ranked table headed with the normalization method — the sort key |
38
+ | `summary.md` | n scored, the exact weights / penalties / normalization used, top-5 / bottom-5, gate frequency, any `VALIDATION_FAILED` |
39
39
 
40
40
  ## Weights — edit this block to change the ranking
41
41
 
42
- These are the only cross-project knobs. Edit the values to re-rank; the math above consumes them verbatim. (Edits here are overwritten if you re-install the harness — to persist a custom weighting, drop a `code-eval-batcher.weights.json` with the same shape in your `out_dir` and read that instead.)
42
+ The math above consumes these values verbatim. Edits here are overwritten on a harness re-install — to persist a custom weighting, drop a `code-eval-batcher.weights.json` with the same shape in the `out_dir` and read that instead.
43
43
 
44
44
  ```json
45
45
  {
@@ -61,11 +61,7 @@ These are the only cross-project knobs. Edit the values to re-rank; the math abo
61
61
  }
62
62
  ```
63
63
 
64
- - **`weights`** — relative importance of each sub-criterion; any non-negative numbers (normalized to sum 100 over the present, weighted ones). Set one to `0` to drop that sub-criterion. Defaults (all 12.5) reproduce the evaluator's own equal-weight total.
65
- - **`gate_penalties`** — flat 0–100-scale points docked per triggered gate, on top of the band caps the evaluator already applied. Default 0 = rely only on the evaluator's caps. E.g. `SECRETS: 15` docks 15 from any project with a committed secret.
66
- - **`normalization`** — `percent_of_max` (absolute, default) · `min_max` (spreads a tight set; not comparable across runs) · `z_score` (distance from the set mean).
67
-
68
- ## Discipline
64
+ - **`weights`** — relative importance; any non-negative numbers, normalized to sum 100 over the present, weighted sub-criteria. `0` drops a sub-criterion. The all-12.5 default reproduces the evaluator's own equal-weight total.
65
+ - **`gate_penalties`** — flat 0–100-scale points docked per triggered gate, on top of the band caps the evaluator already applied. Default 0 = rely on the evaluator's caps alone. E.g. `SECRETS: 15` docks 15 from any project with a committed secret.
69
66
 
70
- - **No silent fallback.** A malformed weights block, a failed preflight, or a scorecard that won't validate is reported loudly — never worked around.
71
- - **You weight; the evaluator bands.** Changing a project's band because of how others scored is the cohort-relative anti-pattern this design removed. Your only cross-project levers are the weights block.
67
+ **No silent fallback.** A malformed weights block, a failed preflight, or a scorecard that won't validate is reported loudly — never worked around.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.31",
3
+ "version": "0.19.33",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {