@qvac/harness 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/harness.bundle +65 -12
- package/dist/prebuilds-manifest.json +8 -0
- package/dist-lib/index.d.ts +2 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/lib/capabilities/completion.d.ts +35 -2
- package/dist-lib/lib/capabilities/completion.js +84 -18
- package/dist-lib/lib/connect.js +1 -0
- package/dist-lib/lib/contract-types.d.ts +1 -0
- package/dist-lib/lib/serve.js +1 -0
- package/dist-lib/lib/wire.js +28 -0
- package/dist-lib/schema/harness.js +5 -2
- package/dist-lib/spec/hrpc/messages.js +22 -7
- package/dist-lib/spec/hyperschema/index.js +22 -7
- package/dist-lib/spec/hyperschema/schema.json +14 -1
- package/dist-lib/spec/hyperschema/types.d.ts +3 -2
- package/package.json +6 -6
- package/.claude/skills/asana/SKILL.md +0 -86
- package/.claude/skills/gh-stack/SKILL.md +0 -181
- package/.claude/skills/gh-stack/references/commands.md +0 -177
- package/.claude/skills/gh-stack/references/stack-design.md +0 -97
- package/.claude/skills/gh-stack/references/troubleshooting.md +0 -159
- package/.claude/skills/js-house-style/SKILL.md +0 -671
- package/.claude/skills/kvcache/SKILL.md +0 -171
- package/.claude/skills/pr-create/SKILL.md +0 -180
- package/.claude/skills/pr-review/SKILL.md +0 -414
- package/CLAUDE.md +0 -39
- package/docs/decisions/2026-05-29-kvcache-tool-turns.md +0 -75
- package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +0 -127
- package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +0 -126
- package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +0 -88
- package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +0 -79
- package/docs/decisions/2026-08-11-pyodide-worker-thread.md +0 -68
- package/docs/decisions/2026-08-17-harness-hook-pipeline.md +0 -84
- package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +0 -176
- package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +0 -95
- package/docs/decisions/2026-08-20-kvcache-static-placement.md +0 -109
- package/docs/decisions/2026-08-21-vendored-python-wheels.md +0 -85
- package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +0 -64
- package/docs/decisions/2026-08-27-writable-skills-overlay.md +0 -56
- package/docs/decisions/2026-09-01-skill-operation-manifests.md +0 -53
- package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +0 -78
- package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +0 -136
- package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +0 -63
- package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +0 -70
- package/docs/decisions/INDEX.md +0 -40
- package/docs/decisions/TEMPLATE.md +0 -32
- package/docs/kvcache-reference.md +0 -655
|
@@ -1,671 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: js-house-style
|
|
3
|
-
description: House style for writing, structuring, reviewing, or refactoring JavaScript — for Bare, Node, or both. Use for essentially any authoring or review task in a JS package: writing a module, refactoring, code review, judging structure and layering, making something composable, weighing a dependency, or adding tests. Also governs comments, whitespace, and PR shape everywhere in the repo. Encodes how we build — small single-responsibility modules composed on primitives, do-more-with-less, no defensive programming — and the judgment behind it.
|
|
4
|
-
version: 1.1.0
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# JS house style
|
|
8
|
-
|
|
9
|
-
How we write JavaScript — for Bare, for Node, for both. Small modules. One thing each. Do more with less. Compose tiny pieces through a few shared conventions so that many small modules behave like one coherent system.
|
|
10
|
-
|
|
11
|
-
The most important rule comes first because it governs all the others:
|
|
12
|
-
|
|
13
|
-
**Don't follow rules without thinking. Make the best code you can, always.**
|
|
14
|
-
|
|
15
|
-
Everything below is a default with a reason attached. Defaults exist so you don't re-derive them every time, and so a codebase reads consistently. But a default you can't justify in the moment is a default you should question. Important things are made well and thoughtfully — the judgment about _when a rule earns its place_ is the actual skill. The rest is scaffolding for that judgment.
|
|
16
|
-
|
|
17
|
-
If you internalize five things:
|
|
18
|
-
|
|
19
|
-
1. **One module, one responsibility — perfect at one thing, composed with the rest.** If you can't describe it in one sentence without "and", it's two modules. Build on the layer below rather than reimplementing it (compact-encoding → hyperschema → hrpc; hypercore → hyperbee → hyperdb → autobee). Build the smallest piece that validates the rest first.
|
|
20
|
-
2. **Make construction cheap; do real work lazily; tear down explicitly.** No async in constructors. Open on first use, close cleanly. `ready-resource` / `ready-guard` give you this when it fits — use them where lifecycle matters, skip them where it doesn't.
|
|
21
|
-
3. **Streams for flows, events for notifications, no defensive guards.** `streamx` for data that flows. Events for "something happened." Don't program defensively — let caller bugs crash; surface real operational failure on the resource. The README, tests, and semver are the contract, not runtime guard code.
|
|
22
|
-
4. **Write for Bare and Node at once.** `b4a` instead of the `Buffer` global; `compact-encoding` for wire and disk formats. These cost nothing on Node and make the same code run on Bare. That portability is the default because we target both.
|
|
23
|
-
5. **Every dependency is a liability you pay for forever.** Each must earn its place in one sentence. A 40-line module you understand beats a 4000-line one you don't. Reach for the small ecosystem primitives before inventing or importing.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## Scope
|
|
28
|
-
|
|
29
|
-
This is the repo's single code-style document. Everything about how code is shaped lives here — structure, layering, naming, typing, comments, whitespace, tests. If you're writing or reviewing code in this repo, this is the style.
|
|
30
|
-
|
|
31
|
-
It is **not** the place for workflow. Commit, branch-naming, and push/PR-delivery rules live in [`CLAUDE.md`](../../../CLAUDE.md).
|
|
32
|
-
|
|
33
|
-
**TypeScript everywhere.** Bare runs TS natively now, so the old "plain JS for Bare, TS for the app" split is gone — there is no longer a runtime reason to drop types. No new raw `.js`/`.cjs` product sources.
|
|
34
|
-
|
|
35
|
-
For anything Bare loads, set **`"erasableSyntaxOnly": true`** in the tsconfig (already the case in this repo's `tsconfig.json`). Bare strips types rather than compiling them, so only erasable syntax survives: no `enum`, no `namespace`, no parameter properties (`constructor (private x)`), no `declare` class fields. `erasableSyntaxOnly` makes `tsc` reject those at build time instead of letting Bare fail to load the module at runtime — which presents as an opaque load failure you have to bisect file-by-file. Use plain `const` objects (`as const`) instead of `enum`, and assign fields in the constructor body.
|
|
36
|
-
|
|
37
|
-
Exports follow what the code _is_, not which runtime it targets:
|
|
38
|
-
|
|
39
|
-
- **Product source** — `lib/`, `worker/`, `schema/` — **named exports**, one concept per file.
|
|
40
|
-
- **Standalone publishable modules** — a holepunch-style module that stands on its own and could be `npm i`-ed — a **single default export**, per the module anatomy below.
|
|
41
|
-
|
|
42
|
-
When in doubt you are writing product source; the default export is for the module you'd publish. Everything else in this document applies to both without exception.
|
|
43
|
-
|
|
44
|
-
## The method: work backwards from ambition, iterate like hell
|
|
45
|
-
|
|
46
|
-
Before structure comes direction. The way we approach a problem (mafintosh's framing, which the whole way of working is built on):
|
|
47
|
-
|
|
48
|
-
- Start from the **great ambition** — the real thing you want to exist.
|
|
49
|
-
- Plan _backwards_ into small, self-contained pieces. Sort them topologically.
|
|
50
|
-
- Build the **smallest piece that most efficiently validates the rest** first. Scope it down until the first iteration is a couple of days. Then execute immediately — never stall, never wait for a grand plan to be complete.
|
|
51
|
-
- Lineage example: "unstoppable P2P file sharing" → Hypercore → "address Merkle-tree nodes efficiently" → `flat-tree`, built day 0. Each layer is a small publishable module that stands on its own.
|
|
52
|
-
|
|
53
|
-
Two failure modes this guards against, both worth catching in review:
|
|
54
|
-
|
|
55
|
-
- **Working hard in the wrong direction.** Effort mimics progress — tasks closed, bugs fixed — while the direction is wrong. Run the complexity check continuously: _is this the right way, and is there a simpler way to execute the ambition?_ Sunk cost is real. Cut early.
|
|
56
|
-
- **Scope creep.** It kills projects. Small modules are the antidote — each is independently finishable, testable, replaceable.
|
|
57
|
-
|
|
58
|
-
On using AI to do this work (heed it, since this skill is often _for_ an AI): AI with human comprehension is powerful; without it, quickly destructive. **Don't generate a subsystem you can't evaluate.** If you don't know what _good_ looks like for the thing (the canonical cautionary tale: a BBR congestion-control implementation that looked fine and wasn't), generating it costs more than it saves. Build the comprehension first, or get it from someone who has it, then generate.
|
|
59
|
-
|
|
60
|
-
Practical upshot: bias to the smallest correct thing, make it composable, move on.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## Build on primitives — let each module be perfect at one thing
|
|
65
|
-
|
|
66
|
-
This is the heart of the style. A module should do **one thing perfectly** and expose it cleanly. The next capability up doesn't reimplement that thing, doesn't reach around it, doesn't fork it — it **composes** it. The ecosystem is a stack of layers where each one is a thin, sharp module sitting on the one below.
|
|
67
|
-
|
|
68
|
-
The two canonical lineages:
|
|
69
|
-
|
|
70
|
-
```
|
|
71
|
-
compact-encoding → hyperschema → hrpc
|
|
72
|
-
(the wire) (typed (RPC over
|
|
73
|
-
structs on typed messages
|
|
74
|
-
the wire) on the wire)
|
|
75
|
-
|
|
76
|
-
hypercore → hyperbee → hyperdb → autobee
|
|
77
|
-
(append- (B-tree (typed, (multiwriter
|
|
78
|
-
only log) KV on the indexed hyperbee via
|
|
79
|
-
log) collections autobase)
|
|
80
|
-
on the bee)
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Read those left to right: each arrow is "built _on_, not instead of." `hyperschema` doesn't invent its own serialization — it generates `compact-encoding` codecs. `hrpc` doesn't invent a message format — it speaks `hyperschema`. `hyperbee` doesn't invent storage — it's a B-tree over a `hypercore`. `hyperdb` doesn't invent a tree — it's collections and indexes over a `hyperbee`. `autobee` doesn't invent consensus from scratch — it's a `hyperbee` made multiwriter through `autobase`.
|
|
84
|
-
|
|
85
|
-
What this means when you write code:
|
|
86
|
-
|
|
87
|
-
- **Find the lowest layer that already solves your sub-problem and build the thin next layer on it.** Before writing storage, ask which of hypercore/hyperbee/hyperdb already gives you 90% of it. You are almost never starting from zero — you're adding one sharp capability to an existing primitive.
|
|
88
|
-
- **Each layer is independently perfect and independently usable.** Someone who needs just the wire format uses `compact-encoding` and never sees `hrpc`. Don't bundle concerns upward — a lower layer must never depend on a higher one, and must be useful on its own.
|
|
89
|
-
- **Don't reach around a layer.** If you're on `hyperdb` and find yourself poking at the underlying `hypercore` directly to do something the bee/db layer should express, that's a smell: either the layer is missing a capability (add it _there_, in its own module) or you're at the wrong layer.
|
|
90
|
-
- **Don't collapse layers "for convenience."** A module that does encoding _and_ storage _and_ RPC is the thing this style exists to prevent. Three modules, each perfect, composed. The seams are the feature — they're where reuse, testing, and replacement happen.
|
|
91
|
-
- **Composition is by passing the lower thing in.** A higher module takes the primitive it builds on as a constructor argument (the `store`, the `core`, the `bee`), it doesn't construct it secretly or hard-wire it. That's what keeps layers swappable and testable.
|
|
92
|
-
|
|
93
|
-
When you reach for a new capability, the instinct is: _which primitive is this one honest step above, and what is the smallest module that adds exactly that step?_
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Module anatomy
|
|
98
|
-
|
|
99
|
-
A well-shaped module is a single default export, an options bag, cheap construction, and — where it has resources — a clean teardown.
|
|
100
|
-
|
|
101
|
-
```js
|
|
102
|
-
const b4a = require('b4a')
|
|
103
|
-
|
|
104
|
-
module.exports = class Thing {
|
|
105
|
-
constructor(store, opts = {}) {
|
|
106
|
-
this.store = store
|
|
107
|
-
this.foo = opts.foo ?? defaultFoo
|
|
108
|
-
this._internal = null // declare every field here, even if null
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async open() {
|
|
112
|
-
this._internal = await this.store.get()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async doWork(arg) {
|
|
116
|
-
// ...
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
The conventions, each with its reason:
|
|
122
|
-
|
|
123
|
-
- **One thing per module, exported once.** A standalone publishable module is a single default export; product source uses named exports, one concept per file. Either way, a second unrelated export is usually a sign you have two modules. _Reason: composition is by importing the thing, and one-concept modules slot together predictably._
|
|
124
|
-
- **`(required, opts = {})` constructor.** Positional for the one or two things the module fundamentally _is_ (the store, the key, the core); everything else in the options bag with `??` defaults. _Reason: the signature documents what's essential vs. configurable._
|
|
125
|
-
- **Declare all instance fields in the constructor**, even `null` ones. _Reason: a reader sees the whole shape in one place, and the engine keeps a stable hidden class._
|
|
126
|
-
- **No async work in the constructor.** Construction is synchronous and cheap; async setup happens in an `open`/`ready` step, lazily. _Reason: constructors can't await, and an object that's secretly half-built is a footgun._
|
|
127
|
-
- **Opts pass down.** An options bag lets a caller configure without touching the signature, and lets a parent hand its opts to the child it constructs.
|
|
128
|
-
```js
|
|
129
|
-
function aThing(somethingSuperImportantLikeACorestore, opts = {}) {
|
|
130
|
-
const { signal } = opts
|
|
131
|
-
this._otherThing = new OtherThing(opts)
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
- **Functions under ~20 lines**, one responsibility, early returns over nesting. Flat over nested; group by feature, not by type. No `utils.ts` dumping grounds.
|
|
135
|
-
- **Split on domains, never on line count.** Splitting before there's a real seam buys import graphs, indirection, and shared-state plumbing in exchange for nothing. But a long file is only legitimate once everything separable has actually been taken out of it.
|
|
136
|
-
|
|
137
|
-
`@qvac/core → lib/core/mesh.ts` is the worked example. It's ~680 lines, and it should stay one file — but only because each domain it touches already lives somewhere else:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import { syncChanges } from './sync/index.ts' // per-domain sync: knowledge, chunks, devices, chats
|
|
141
|
-
import { registerRoutes } from './contract/index.ts' // the RPC surface
|
|
142
|
-
import { createSyncActivity } from './sync-status.ts' // activity tracking
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
`sync/` in turn is five files of 8–122 lines, one per domain, because keeping them together would blur independent implementations into one.
|
|
146
|
-
|
|
147
|
-
In this repo the same shape is `lib/capabilities/` and `lib/models/`: `index.ts` composes, each sibling owns one capability.
|
|
148
|
-
|
|
149
|
-
So the 443 lines are **residual, not undivided** — what's left is mesh lifecycle itself, which has no seam that isn't arbitrary. That's the test in both directions: a long file is fine if you can't name a domain still trapped inside it, and a split is right the moment you can name one.
|
|
150
|
-
|
|
151
|
-
**Split by domain, not by operation.** The same file's `blob-sweep.ts` is the counter-example — it carves out one _verb_ on blobs, so blob logic ends up spread across files that each own a fraction of it. Blobs are one domain: `blobs.ts`, holding sweep alongside the rest of blob handling. If the filename is a verb phrase, you probably split an operation off its domain rather than splitting two domains apart.
|
|
152
|
-
|
|
153
|
-
Corollary: a file growing past what you can hold in your head is a prompt to ask whether a second domain crept in — not a mandate to cut it at a line number.
|
|
154
|
-
|
|
155
|
-
- **Match the surrounding project on CommonJS vs ESM.** Read the package and be consistent within it. Don't mix in one package without a reason.
|
|
156
|
-
|
|
157
|
-
### Parameters
|
|
158
|
-
|
|
159
|
-
The constructor rule above — one or two positional things, everything else in an options bag — is not special to constructors. It is how every function takes arguments.
|
|
160
|
-
|
|
161
|
-
Positional is for the one or two things the function fundamentally acts _on_. Everything else goes in an object, because at the call site an object labels itself and a position doesn't.
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
// no — eight positional args, and three adjacent numbers that all accept each other
|
|
165
|
-
frameProgress(update, mainSrc, srcs[i], i, srcs.length, completedBytes, expectedSize, totalBytes)
|
|
166
|
-
|
|
167
|
-
// yes — the subject stays positional, the rest is named
|
|
168
|
-
frameProgress(update, {
|
|
169
|
-
setKey: mainSrc,
|
|
170
|
-
currentFile: srcs[i],
|
|
171
|
-
fileIndex: i,
|
|
172
|
-
totalFiles: srcs.length,
|
|
173
|
-
completedBytes,
|
|
174
|
-
expectedSize,
|
|
175
|
-
totalBytes
|
|
176
|
-
})
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**The trigger is ambiguity, not arity.** Three parameters of three distinct types read fine. _Two_ adjacent parameters of the same type are already a hazard — swap them and it compiles, runs, and quietly produces a wrong answer that no test you thought to write will catch. Count makes it worse; sameness makes it dangerous.
|
|
180
|
-
|
|
181
|
-
- **Same-typed neighbours go in an object.** Especially numbers, and especially sizes/offsets/indexes, which are indistinguishable to the compiler and to the reader.
|
|
182
|
-
- **Never take a positional boolean.** `render(node, true, false)` is unreadable at the call site. Use named options — and prefer that the flag not exist at all.
|
|
183
|
-
- **Loop-invariant values don't belong in a per-iteration signature.** If half the arguments are the same on every call, bind them once in a closure or a small factory and pass only what varies. That usually shrinks the function more than the object would.
|
|
184
|
-
- **Object properties are also where defaults live** (`?? fallback`), so adding an option later doesn't churn every call site.
|
|
185
|
-
|
|
186
|
-
Same instinct as the rest of the style: make the wrong call hard to write, rather than adding a check that catches it later.
|
|
187
|
-
|
|
188
|
-
### Naming
|
|
189
|
-
|
|
190
|
-
- Readable prose: `inviteDevice`, not `inv`. `ServiceResult`, not `SvcRes`.
|
|
191
|
-
- File names describe content. No abbreviations unless universally understood (`id`, `url`, `http`).
|
|
192
|
-
- No magic strings for domain identifiers — enums, schemas, or `as const` constants.
|
|
193
|
-
|
|
194
|
-
### Typing
|
|
195
|
-
|
|
196
|
-
Types are the cheapest contract you get. Keep them honest at the boundary and the interior needs no guards.
|
|
197
|
-
|
|
198
|
-
- **Zero `as` casts.** Parse untrusted data with Zod (or the package's typed boundary) and derive types with `z.infer<>`. A cast is you telling the compiler to stop helping.
|
|
199
|
-
- **Zero `any`.** If a type is genuinely unknown, narrow it at the boundary.
|
|
200
|
-
- **Minimize `unknown`** — acceptable only at a true boundary (IPC handler, `JSON.parse`), narrowed immediately.
|
|
201
|
-
- **Explicit return types on exported functions.** `readonly` on object properties, `ReadonlyArray<T>` over `T[]`.
|
|
202
|
-
- Test files may use narrow casts for mock wiring or intentionally invalid fixtures. Keep them local; never copy into production code.
|
|
203
|
-
|
|
204
|
-
This is the typed counterpart of _don't program defensively_ below — the type checker enforces the contract statically, so you don't re-check it at runtime.
|
|
205
|
-
|
|
206
|
-
### Comments and whitespace
|
|
207
|
-
|
|
208
|
-
Well-structured code in small modules with honest names does not need narration. Comments are the exception, not the habit.
|
|
209
|
-
|
|
210
|
-
- **Prefer no comment.** If a comment explains _what_ the code does, delete it and fix the name. The API, the README, and the tests are the documentation.
|
|
211
|
-
- **One line, maximum.** A comment that needs a second line is a sign the code needs a seam, not a paragraph. Never write a comment block.
|
|
212
|
-
- **Only ever explain _why_** — a non-obvious constraint, a workaround with a reason, why an operational error is safe to swallow.
|
|
213
|
-
- **Long AI-generated comment blocks are an anti-pattern.** They restate the obvious, drift out of date silently, and mislead the next reader. Don't emit them.
|
|
214
|
-
|
|
215
|
-
### Whitespace groups the code
|
|
216
|
-
|
|
217
|
-
Blank lines are the structure a reader navigates by. They are not padding, and removing them is not tidiness — an unbroken wall of statements is _harder_ to read than the same code grouped, because nothing signals where one thought ends and the next begins.
|
|
218
|
-
|
|
219
|
-
Group by what the code is doing: setup, then action, then the assertions or effects that follow from it. A new group starts wherever the reader's question changes.
|
|
220
|
-
|
|
221
|
-
```js
|
|
222
|
-
// no — one wall, no seam between setup, the four scenarios, or their assertions
|
|
223
|
-
const { ragSearch } = await toolModeApi({ ragEnabled: false })
|
|
224
|
-
const tool = await ragSearch({
|
|
225
|
-
chatId: 'chat-1',
|
|
226
|
-
query: 'alpha fuel pressure',
|
|
227
|
-
mode: 'tool',
|
|
228
|
-
limit: 5
|
|
229
|
-
})
|
|
230
|
-
t.ok(tool.enabled, 'the toggle does not gate tool mode')
|
|
231
|
-
t.ok((tool.hits?.length ?? 0) > 0, 'hits found on a toggle-off chat')
|
|
232
|
-
const injection = await ragSearch({ chatId: 'chat-1', query: 'alpha fuel pressure' })
|
|
233
|
-
t.absent(injection.enabled, 'injection mode still honors the toggle')
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
```js
|
|
237
|
-
// yes — setup, then one group per scenario
|
|
238
|
-
const { ragSearch } = await toolModeApi({ ragEnabled: false })
|
|
239
|
-
|
|
240
|
-
const tool = await ragSearch({
|
|
241
|
-
chatId: 'chat-1',
|
|
242
|
-
query: 'alpha fuel pressure',
|
|
243
|
-
mode: 'tool',
|
|
244
|
-
limit: 5
|
|
245
|
-
})
|
|
246
|
-
t.ok(tool.enabled, 'the toggle does not gate tool mode')
|
|
247
|
-
t.ok((tool.hits?.length ?? 0) > 0, 'hits found on a toggle-off chat')
|
|
248
|
-
t.absent(tool.targetDocumentId, 'tool mode never resolves a target document')
|
|
249
|
-
|
|
250
|
-
const injection = await ragSearch({ chatId: 'chat-1', query: 'alpha fuel pressure' })
|
|
251
|
-
t.absent(injection.enabled, 'injection mode still honors the toggle')
|
|
252
|
-
|
|
253
|
-
const missing = await ragSearch({ chatId: 'chat-404', query: 'alpha', mode: 'tool', limit: 3 })
|
|
254
|
-
t.absent(missing.enabled, 'an unknown chat never searches, even in tool mode')
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Split a group further when it grows its own phases — _set up this case_, _wait for the state_, _check it_ — rather than letting one group run long. The grouping is what a comment would otherwise have to say, which is most of why the comment isn't needed.
|
|
258
|
-
|
|
259
|
-
What isn't grouping, and shouldn't appear: banner or section-divider comments, columns aligned with padding spaces, a blank line immediately after `{` or before `}`, and two blank lines where one does the job.
|
|
260
|
-
|
|
261
|
-
### Cleanliness
|
|
262
|
-
|
|
263
|
-
Say the thing once, in the simplest form that expresses it. Two habits to hold:
|
|
264
|
-
|
|
265
|
-
**Let `undefined` propagate — never conditionally spread.**
|
|
266
|
-
|
|
267
|
-
```js
|
|
268
|
-
// no
|
|
269
|
-
{ ...(runStream.signal ? { signal: runStream.signal } : {}) }
|
|
270
|
-
|
|
271
|
-
// yes
|
|
272
|
-
{ signal: runStream.signal }
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
The conditional spread adds a branch, a nested object, and a second reading of the same value to express nothing — an absent key and a key set to `undefined` behave identically at every consumer that matters. For an optional field, type it `signal?: AbortSignal | undefined` and assign it plainly.
|
|
276
|
-
|
|
277
|
-
**No dead scaffolding.** No commented-out code, no unused parameter kept "for symmetry", no option nothing reads, no re-export that only forwards. Delete it — git remembers.
|
|
278
|
-
|
|
279
|
-
### Derived state
|
|
280
|
-
|
|
281
|
-
Every value that exists twice will eventually disagree. Most "impossible" bugs are two things that were supposed to stay in sync and didn't.
|
|
282
|
-
|
|
283
|
-
**One source of truth per value.** If a number is already available, don't carry a second copy of it beside the first. A consumer then has to know which to read, and the day they diverge nobody will know which was right.
|
|
284
|
-
|
|
285
|
-
**But check the contract before you call it duplication.** Two fields holding the same value are sometimes two _channels_ that are documented to mean different things, and which happen to agree right now. Then the defect isn't the copy — it's that a value was written into the wrong channel, and deleting a field would be the wrong fix.
|
|
286
|
-
|
|
287
|
-
Find the contract first: the type declaration and its comments, and the order a consumer reads the fields in. A reader that prefers one field over another _is_ the contract, even where nothing is written down.
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
// looks like duplication — the same number in four places
|
|
291
|
-
{ downloaded, total, percentage, fileSetInfo: { overallDownloaded, overallTotal, overallPercentage } }
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Here the type said top-level is _this asset_ and `fileSetInfo` is _the whole transfer_, and the consumer read `fileSetInfo` in preference. So the real defect was writing the aggregate over the per-asset fields — a contract violation, fixed by putting each value in its own channel, not by deleting either.
|
|
295
|
-
|
|
296
|
-
The two diagnoses lead to opposite fixes, so get the diagnosis right:
|
|
297
|
-
|
|
298
|
-
- **Distinct documented meanings** → put each value in its own channel. Both fields stay.
|
|
299
|
-
- **No contract distinguishing them** → genuine duplication. Emit it once; give a nested shape only the fields that add information.
|
|
300
|
-
|
|
301
|
-
**Precompute the invariant; don't maintain it incrementally.** A running accumulator has to be advanced correctly at every step, and every future edit to the loop can break it. A table computed once cannot drift:
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
// no — a mutable offset the loop must remember to advance, at exactly the right moment
|
|
305
|
-
let completedBytes = 0
|
|
306
|
-
for (...) { /* use completedBytes */ ; completedBytes += size }
|
|
307
|
-
|
|
308
|
-
// yes — offsets computed once, correct by construction
|
|
309
|
-
const offsets = []
|
|
310
|
-
let running = 0
|
|
311
|
-
for (const src of srcs) { offsets.push(running); running += sizes[src] }
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
The second version is the same arithmetic with the mutation confined to where it's obviously right, and the loop body doesn't participate in keeping it true.
|
|
315
|
-
|
|
316
|
-
**Don't launder a bad value into a plausible one.** `Math.max(x, 0)`, `?? 0`, and `|| 0` on something that should never have been negative, null, or absent don't fix anything — they convert a loud failure into a wrong number that flows onward and gets debugged three layers away. Clamping for a _presentational_ guarantee (a progress bar that must not go backwards) is legitimate; clamping because you aren't sure what the producer sends is not. Know which one you're writing.
|
|
317
|
-
|
|
318
|
-
Note the repo's own trap here: an optional `uint` decodes to `0` rather than `null` when absent, so `??` won't catch a missing timestamp and `||` is the correct operator.
|
|
319
|
-
|
|
320
|
-
### README shape
|
|
321
|
-
|
|
322
|
-
Ship the same structure on every module — OSS, private, or throwaway. Consistency is the point:
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
# module-name
|
|
326
|
-
One-line description.
|
|
327
|
-
## Installation npm i module-name
|
|
328
|
-
## Usage the smallest runnable real example
|
|
329
|
-
## API constructor + each method
|
|
330
|
-
## License match the project
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
Keep Usage runnable and minimal — it's the first thing anyone copies.
|
|
334
|
-
|
|
335
|
-
---
|
|
336
|
-
|
|
337
|
-
## Lifecycle: a pattern, used where it earns its place
|
|
338
|
-
|
|
339
|
-
When a thing owns resources — a file, a core, a socket, a watcher — it needs orderly setup and teardown. `ready-resource` and `ready-guard` give you that, and most of our stateful code uses them. But this is a **pattern, not a mandate**. A pure function or a stateless helper doesn't need a lifecycle; bolting one on is over-engineering. Use it where lifecycle genuinely matters; skip it where it doesn't. Same judgment applies to every primitive below — reach for it because it fits, not because it's on a list.
|
|
340
|
-
|
|
341
|
-
When it does fit, `ready-resource` gives you:
|
|
342
|
-
|
|
343
|
-
- `ready()` — idempotent; runs `_open()` exactly once, concurrent callers await the same promise. Safe to call on every method.
|
|
344
|
-
- `close()` — idempotent; runs `_close()` once. Safe before `ready()`, safe twice.
|
|
345
|
-
- `opened` / `closed` / `opening` / `closing` state, and a `'close'` event.
|
|
346
|
-
|
|
347
|
-
The rules when you use it:
|
|
348
|
-
|
|
349
|
-
- **Implement `_open`/`_close`; never call them directly.** `ready()`/`close()` are the public surface.
|
|
350
|
-
- **`_open` opens dependencies in order; `_close` reverses it.** Close the watcher before the core it watches.
|
|
351
|
-
- **Enter public async methods behind the opened guard:**
|
|
352
|
-
|
|
353
|
-
```js
|
|
354
|
-
if (!this.opened) await this.ready()
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
Not a bare `await this.ready()`. `ready()` is idempotent and returns an already-resolved promise once open, but awaiting it still costs a microtask tick on _every_ call — on a hot path that's a tick per operation for no work. The guard makes the common case synchronous and only awaits the first time. This is the laziness contract: construct, then call, and the first call opens.
|
|
358
|
-
|
|
359
|
-
- **Make every async step interruptible.** Check `closing` before and after each suspension point, in `_open` and in any long-running method:
|
|
360
|
-
|
|
361
|
-
```js
|
|
362
|
-
async _open () {
|
|
363
|
-
this.core = await this.store.get()
|
|
364
|
-
if (this.closing) return
|
|
365
|
-
this.watcher = this.core.watch()
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
Without that, a `close()` landing mid-setup leaves a half-built resource — the watcher gets created after teardown already ran, and nothing will ever close it.
|
|
370
|
-
|
|
371
|
-
- **Set state before you await, not after.** The state field is the guard other calls read; if you flip it after the await, a concurrent caller sees stale state and does the work twice. Don't add a parallel `parked`/`busy` boolean beside the real one — one source of truth.
|
|
372
|
-
- **Teardown must actually finish.** `_close` releases everything `_open` took: streams destroyed, watchers closed, locks unlocked in `finally`, timers cleared, listeners removed. A process that won't exit is a `_close` that missed something.
|
|
373
|
-
|
|
374
|
-
If you're writing an `init()` method, stop — that's `ready()`, and you're reimplementing it.
|
|
375
|
-
|
|
376
|
-
### Fix the source, not the symptom
|
|
377
|
-
|
|
378
|
-
Lifecycle bugs are where this matters most, because the symptom is almost always cheaper to paper over than the cause — and papering over is how the cause becomes permanent.
|
|
379
|
-
|
|
380
|
-
- A process that doesn't exit is a resource you didn't close. **Don't** add a force-exit, a kill timer, or logic that reaps a stale lock holder at next launch. Find what's still open.
|
|
381
|
-
- A value that's occasionally missing is a race. **Don't** retry it or wait longer. Wait on the signal from the layer that knows.
|
|
382
|
-
- A test that's flaky is telling you the code is order-dependent. **Don't** extend the timeout or add a stabilisation sleep. Gate on the actual state you need.
|
|
383
|
-
|
|
384
|
-
Each of those workarounds converts a reproducible bug into an intermittent one, and moves the cost from you now to everyone later.
|
|
385
|
-
|
|
386
|
-
### Bare safety
|
|
387
|
-
|
|
388
|
-
Bare is not Node, and two differences bite specifically around lifecycle:
|
|
389
|
-
|
|
390
|
-
- **`AbortSignal` listeners fire synchronously.** Fire-and-forget work in an abort listener is fine; anything that blocks or awaits inside one can deadlock, because the abort is still unwinding. Signal the intent and let the async work happen outside the listener.
|
|
391
|
-
- **Type stripping, not compiling.** See the `erasableSyntaxOnly` note in [Scope](#scope) — a non-erasable construct fails at module load as an opaque error you have to bisect for, not as a type error.
|
|
392
|
-
|
|
393
|
-
Everything in [Buffers and wire formats](#buffers-and-wire-formats-bare--node) applies for the same reason: write it once, run it on both.
|
|
394
|
-
|
|
395
|
-
---
|
|
396
|
-
|
|
397
|
-
## Streams, events, errors
|
|
398
|
-
|
|
399
|
-
### Flows → streamx
|
|
400
|
-
|
|
401
|
-
Use `streamx`, not Node core `stream`. _Reason: lighter, predictable backpressure, runs on Bare, and it's what the ecosystem speaks._ `Readable`/`Writable`/`Duplex`/`Transform` plus `pipeline`.
|
|
402
|
-
|
|
403
|
-
```js
|
|
404
|
-
const { Readable } = require('streamx')
|
|
405
|
-
|
|
406
|
-
class Query extends Readable {
|
|
407
|
-
constructor(db, range) {
|
|
408
|
-
super()
|
|
409
|
-
this.db = db
|
|
410
|
-
this.range = range
|
|
411
|
-
this._it = null
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
_open(cb) {
|
|
415
|
-
this._it = this.db.iterator(this.range)
|
|
416
|
-
cb(null)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
async _read(cb) {
|
|
420
|
-
const { done, value } = await this._it.next()
|
|
421
|
-
if (done) this.push(null)
|
|
422
|
-
else this.push(value)
|
|
423
|
-
cb(null)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
_destroy(cb) {
|
|
427
|
-
this._it?.close().then(() => cb(null), cb)
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
streamx's stream hooks (`_open`/`_read`/`_write`/`_destroy`, callback-style) are not ReadyResource's promise-based `_open`/`_close` — don't conflate them. Prefer returning a stream over building a giant array when results are large or unbounded; `for await (const x of stream)` is the consumer idiom.
|
|
433
|
-
|
|
434
|
-
### Notifications → events
|
|
435
|
-
|
|
436
|
-
EventEmitter (or streamx's emitter) for "something happened": `'close'`, `'update'`, `'peer-add'`. Fire-and-forget signals, not data pipes, not request/response.
|
|
437
|
-
|
|
438
|
-
### Errors: don't program defensively
|
|
439
|
-
|
|
440
|
-
We do **not** program defensively. We don't litter methods with arg-type checks, null guards, or "did you call this in the right order" assertions. _Reason: defensive code is complexity you write, read, and pay for at runtime forever — to handle a case that is a caller bug, not a real condition._ The contract is the README and the tests; semver protects it. A dev who uses the API totally wrong gets a native crash, and that is the correct outcome — fail fast, fail loud, fix the call site.
|
|
441
|
-
|
|
442
|
-
- **Let misuse fail.** Pass a string where a buffer goes? It throws from deep in the stack. Fine. That's a bug in their code, surfaced immediately, not a state your module should anticipate or absorb.
|
|
443
|
-
- **Don't validate inputs you've documented.** The documented type _is_ the validation. Re-checking it in code is redundant with the README and the tests.
|
|
444
|
-
- **Operational failure is different — that travels with the resource.** A dropped peer, a replication error, a disk hiccup are real runtime conditions, not caller bugs → emit `'error'`, reject the in-flight promise, or expose state. Don't take the process down for a network blip.
|
|
445
|
-
- **`safety-catch`** rethrows real bugs while swallowing benign teardown rejections, so a genuine programmer error still crashes loudly rather than getting silently eaten:
|
|
446
|
-
```js
|
|
447
|
-
const safetyCatch = require('safety-catch')
|
|
448
|
-
somePromise().catch(safetyCatch)
|
|
449
|
-
```
|
|
450
|
-
- **Never swallow silently.** If you catch and ignore an _operational_ error, leave a comment on why it's safe.
|
|
451
|
-
|
|
452
|
-
The line to hold: **caller bug → let it crash. Runtime condition → handle it on the resource.** Don't blur the two by defending against the first.
|
|
453
|
-
|
|
454
|
-
---
|
|
455
|
-
|
|
456
|
-
## Concurrency: scope-lock
|
|
457
|
-
|
|
458
|
-
Async interleaving is the recurring tax. The answer is `scope-lock` — concurrency semantics around entering a scope — to serialize access to a shared resource. It's `lock()` / `unlock()`, not acquire-a-release-fn.
|
|
459
|
-
|
|
460
|
-
```js
|
|
461
|
-
const ScopeLock = require('scope-lock')
|
|
462
|
-
|
|
463
|
-
const l = new ScopeLock()
|
|
464
|
-
|
|
465
|
-
async function append(data) {
|
|
466
|
-
await l.lock()
|
|
467
|
-
try {
|
|
468
|
-
await reallyAppend(data)
|
|
469
|
-
} finally {
|
|
470
|
-
l.unlock()
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
`lock()` queues — callers run in order, one at a time. Always `unlock()` in `finally`; a lock you never release deadlocks the scope silently.
|
|
476
|
-
|
|
477
|
-
It also has a **debounced** mode, for when you don't need every queued entry to run — only the first and the last. Coalescing a burst of "something changed, re-sync" calls into one leading and one trailing run is the classic use:
|
|
478
|
-
|
|
479
|
-
```js
|
|
480
|
-
const d = new ScopeLock({ debounce: true })
|
|
481
|
-
|
|
482
|
-
async function resync() {
|
|
483
|
-
if (await d.lock()) {
|
|
484
|
-
// only the entries that actually get the scope return true
|
|
485
|
-
try {
|
|
486
|
-
await doResync()
|
|
487
|
-
} finally {
|
|
488
|
-
d.unlock()
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
await d.flush() // wait for all current work in the scope to drain
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
Use a plain lock when every call must run; use debounce when intermediate calls are redundant and only the latest state matters. Don't reach for either when the operation is already atomic, and don't build an elaborate queue when a scope lock suffices.
|
|
497
|
-
|
|
498
|
-
### Closures capture the variable, not its value
|
|
499
|
-
|
|
500
|
-
A callback that outlives the statement that created it reads whatever the variable holds _when it fires_, not when it was passed. Combine that with a mutation after an `await` and you have a bug that only appears under real timing:
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
let completedBytes = 0
|
|
504
|
-
for (let i = 0; i < srcs.length; i++) {
|
|
505
|
-
const onProgress = (u) => report(completedBytes + u.downloaded) // reads it later
|
|
506
|
-
await download(srcs[i], { onProgress })
|
|
507
|
-
completedBytes += sizes[srcs[i]] // moved by then
|
|
508
|
-
}
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
A progress event that lands after its download resolves — a straggler over IPC, a queued microtask, an event flushed on close — sees the _next_ offset and double-counts. It won't reproduce locally, and clamping the output only hides it.
|
|
512
|
-
|
|
513
|
-
- **Don't let a callback close over state that a later step mutates.** Pass the value the callback needs, or compute the whole table up front (see [Derived state](#derived-state)).
|
|
514
|
-
- **`for (let i …)` is per-iteration; `let x` outside the loop is not.** The loop index is safe to capture, the accumulator beside it isn't.
|
|
515
|
-
- **After every `await`, assume anything not local may have changed** — including fields on `this`, which is the same hazard the `closing` checks exist for.
|
|
516
|
-
|
|
517
|
-
### Never sleep to synchronise
|
|
518
|
-
|
|
519
|
-
**Zero `setTimeout` in product code.** No arbitrary delays, no sleep-polling. Wait on the signal, promise, event, or callback from the layer that actually knows when the thing is ready. A timeout that "usually works" is a race you've decided to ship — and it fails on the slowest machine, which is CI.
|
|
520
|
-
|
|
521
|
-
If nothing exposes readiness, that's the bug: add the event or the promise to the layer that owns the state, and wait on it. Tests may use a bounded `setTimeout` only when exercising genuinely external timing with no deterministic primitive to wait on.
|
|
522
|
-
|
|
523
|
-
---
|
|
524
|
-
|
|
525
|
-
## Buffers and wire formats (Bare + Node)
|
|
526
|
-
|
|
527
|
-
### b4a everywhere
|
|
528
|
-
|
|
529
|
-
**Don't use the `Buffer` global.** _Reason: it may not exist on Bare. `b4a` is identical behavior, runs on both._ This is the default precisely because we target Bare and Node from the same source.
|
|
530
|
-
|
|
531
|
-
```js
|
|
532
|
-
const b4a = require('b4a')
|
|
533
|
-
|
|
534
|
-
b4a.from('hello') // not Buffer.from
|
|
535
|
-
b4a.alloc(32) // not Buffer.alloc
|
|
536
|
-
b4a.equals(a, b) // not a.equals(b)
|
|
537
|
-
b4a.toString(buf, 'hex') // not buf.toString('hex')
|
|
538
|
-
b4a.concat([a, b])
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
A single `Buffer.from` is a portability bug in code meant to run on Bare.
|
|
542
|
-
|
|
543
|
-
### compact-encoding for the wire
|
|
544
|
-
|
|
545
|
-
Wire and on-disk formats use `compact-encoding` — positional, length-prefixed, no field tags.
|
|
546
|
-
|
|
547
|
-
```js
|
|
548
|
-
const c = require('compact-encoding')
|
|
549
|
-
|
|
550
|
-
const message = {
|
|
551
|
-
preencode(state, m) {
|
|
552
|
-
c.uint.preencode(state, m.seq)
|
|
553
|
-
c.string.preencode(state, m.name)
|
|
554
|
-
},
|
|
555
|
-
encode(state, m) {
|
|
556
|
-
c.uint.encode(state, m.seq)
|
|
557
|
-
c.string.encode(state, m.name)
|
|
558
|
-
},
|
|
559
|
-
decode(state) {
|
|
560
|
-
return { seq: c.uint.decode(state), name: c.string.decode(state) }
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
Carved-in-stone rule: **fields are positional — new fields append at the END; existing fields are never removed, reordered, or retyped.** Any data already on disk or replicated to a peer is frozen. Plan the format before shipping. For typed records, prefer `hyperschema`/`hyperdb` codegen over hand-rolling (see the hyperdb-hyperschema skill).
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
## Dependency discipline
|
|
570
|
-
|
|
571
|
-
Each dependency is maintained forever — its bugs, breaking changes, security surface, and weight on Bare become yours.
|
|
572
|
-
|
|
573
|
-
- **Prefer the small ecosystem primitive** first: `b4a`, `streamx`, `ready-resource`, `ready-guard`, `scope-lock`, `safety-catch`, `compact-encoding`, `hypercore-crypto`, `sodium-universal`. Small, audited, Bare-safe.
|
|
574
|
-
- **A small dependency you understand beats a large one you don't.** Need 40 lines of behavior from a 4000-line library? Write the 40 lines.
|
|
575
|
-
- **Vet for Bare-compatibility.** Anything touching the `Buffer` global, Node `stream`, raw `fs`, or unported native addons is suspect.
|
|
576
|
-
- **Justify every `package.json` entry in one sentence.** If you can't, it shouldn't be there.
|
|
577
|
-
- **Don't import to avoid an obvious function.** `isOdd` is the cautionary tale.
|
|
578
|
-
|
|
579
|
-
The governing question is the complexity check again: _is there a simpler way to execute the ambition?_ Often it's no dependency at all.
|
|
580
|
-
|
|
581
|
-
---
|
|
582
|
-
|
|
583
|
-
## Testing
|
|
584
|
-
|
|
585
|
-
Tests are the contract. Because we don't program defensively, the test suite plus the README plus semver are what keep the API safe to depend on — there's no guard code doing it at runtime. So tests carry real weight: **test the API the way a dev actually uses it.**
|
|
586
|
-
|
|
587
|
-
**Tests are as real as possible.** For anything P2P, bootstrap a real `hyperdht` testnet rather than mocking the transport — a mocked swarm proves your mock works. Don't assert that a function was called; assert the behavior it was supposed to produce. If a unit test can only verify a spy, it has no value and shouldn't exist.
|
|
588
|
-
|
|
589
|
-
`brittle` is the standard — flat, assertion-driven, TAP-style. (Older repos use `tape`/`test`; match the repo.)
|
|
590
|
-
|
|
591
|
-
```js
|
|
592
|
-
const test = require('brittle')
|
|
593
|
-
const Thing = require('./')
|
|
594
|
-
|
|
595
|
-
test('basic append and read', async function (t) {
|
|
596
|
-
const thing = new Thing(store)
|
|
597
|
-
await thing.ready()
|
|
598
|
-
t.teardown(() => thing.close())
|
|
599
|
-
|
|
600
|
-
await thing.doWork('a')
|
|
601
|
-
t.is(thing.length, 1)
|
|
602
|
-
t.alike(await thing.get(0), expected)
|
|
603
|
-
})
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
What "test the API" means here:
|
|
607
|
-
|
|
608
|
-
- **Tests exercise the public surface as a consumer would call it** — construct, `ready()`, call the documented methods, assert the documented behavior. They're the executable version of the README's Usage section. If a method is documented, there's a test that uses it the documented way.
|
|
609
|
-
- **Test how the thing is _meant_ to be used, not how it's built.** Assert behavior through the public API (`t.is`, `t.alike`, `t.ok`), never reach into internals. A refactor that keeps the API stable must keep the tests green — that's the whole point.
|
|
610
|
-
- **You do not test misuse.** We don't program defensively, so there's nothing to assert about passing garbage in — that path is a native crash by design. Don't write tests that pin down the crash behavior of unsupported input; it isn't part of the contract and pinning it makes the contract accidentally wider.
|
|
611
|
-
- **Test the real failure paths that _are_ part of the contract** — close-during-open, the race you just fixed, the documented bounds that legitimately return nothing, operational errors surfacing on the resource. These are behaviors you promise; prove them.
|
|
612
|
-
- **`t.teardown()` for cleanup** — closes resources even when assertions fail. Use it for everything you open.
|
|
613
|
-
- **Flat tests, one `test()` per behavior.** The name states the assertion. No deep describe/it nesting.
|
|
614
|
-
- **Semver follows the tests.** A change that breaks an existing API test is a breaking change — major bump. Tests are how you know. Even small throwaway modules get CI, lint, and basic API tests; same checklist every time.
|
|
615
|
-
- **Keep helpers out of the way.** A helper used by one test goes inside that test; a helper used by the file goes at the end of it. Big reusable ones move to `test/helpers/`. Never let scaffolding sit between the reader and the first assertion.
|
|
616
|
-
|
|
617
|
-
For a suite that builds real multi-peer structure, put the setup behind one factory that owns the testnet and the teardown:
|
|
618
|
-
|
|
619
|
-
```js
|
|
620
|
-
const { things } = await create(t) // testnet + teardown registered
|
|
621
|
-
const thing1 = await things.start() // takes opts, awaits ready, wires the testnet in
|
|
622
|
-
const t1 = await things.restart(0) // close and re-open thing1
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
The test then reads as the scenario it describes, and lifecycle correctness lives in one place instead of being re-derived per test.
|
|
626
|
-
|
|
627
|
-
---
|
|
628
|
-
|
|
629
|
-
## Review checklist
|
|
630
|
-
|
|
631
|
-
Apply to any module, regardless of size or visibility. When reviewing, walk it and report violations tersely with the fix, in dependency order — don't rewrite wholesale unless asked. And remember the first rule: a flagged item is a prompt to think, not an automatic defect. If the code broke a default _for a good reason_, that's good code.
|
|
632
|
-
|
|
633
|
-
- [ ] One responsibility, describable without "and"; built on the layer below, not reimplementing or reaching around it.
|
|
634
|
-
- [ ] Composed by passing the lower primitive in (store/core/bee), not constructing it secretly; no layer depends upward.
|
|
635
|
-
- [ ] File boundaries fall on domains — a long single-focus file left whole, a multi-domain one split; no split made on line count alone.
|
|
636
|
-
- [ ] Exports match the kind of code (named for product source, one default for a publishable module); `(required, opts = {})` constructor; all fields declared.
|
|
637
|
-
- [ ] Functions take one or two positional subjects and an object for the rest — no same-typed neighbours, no positional booleans, no loop-invariants re-passed per call.
|
|
638
|
-
- [ ] TypeScript with no `as`, no `any`, `unknown` only at boundaries; `erasableSyntaxOnly` respected for anything Bare loads.
|
|
639
|
-
- [ ] Comments: none unless they explain _why_, one line max, no comment blocks, no banner dividers.
|
|
640
|
-
- [ ] Blank lines group the code into setup / action / assertion — neither an unbroken wall nor padding that groups nothing.
|
|
641
|
-
- [ ] No conditional spreads — optional values assigned plainly and allowed to be `undefined`; no commented-out code or unused scaffolding.
|
|
642
|
-
- [ ] One source of truth per value — nothing carried twice or computed twice in the same payload, and where two fields do hold the same value, each is in the channel its type documents.
|
|
643
|
-
- [ ] Invariants precomputed rather than accumulated in a loop; no callback closing over state a later step mutates.
|
|
644
|
-
- [ ] No `Math.max(x, 0)` / `?? 0` / `|| 0` laundering a value that should never be wrong.
|
|
645
|
-
- [ ] No `setTimeout` to synchronise — waits on a real readiness signal.
|
|
646
|
-
- [ ] Cheap construction, no async in constructor; lifecycle (ready/close) where resources warrant it, skipped where they don't.
|
|
647
|
-
- [ ] `if (!this.opened) await this.ready()` on public async methods, not a bare await; `closing` checked at every suspension point; state set before awaiting.
|
|
648
|
-
- [ ] `_close` releases everything `_open` took — no force-exit, kill timer, or stale-lock reaping standing in for a real teardown.
|
|
649
|
-
- [ ] Root cause fixed, not the symptom — no retry, longer wait, or stabilisation sleep hiding a race.
|
|
650
|
-
- [ ] `b4a` for buffers, no `Buffer` global; runs on Bare and Node.
|
|
651
|
-
- [ ] `streamx` for flows; events for notifications; no defensive guards — caller bugs crash, operational failures surface on the resource; `safety-catch` where teardown rejections are expected.
|
|
652
|
-
- [ ] Shared-resource concurrency serialized (scope-lock) where interleaving would corrupt; not over-applied.
|
|
653
|
-
- [ ] Wire/disk format append-only and planned; codegen where it fits.
|
|
654
|
-
- [ ] Every dependency justified in one sentence; ecosystem primitives preferred.
|
|
655
|
-
- [ ] `brittle` tests that exercise the public API the way a consumer calls it, with `t.teardown`, covering the contracted failure paths; no tests pinning misuse behavior.
|
|
656
|
-
- [ ] API documented in the README and versioned with semver; breaking an API test means a major bump.
|
|
657
|
-
- [ ] README: install / usage (runnable) / API / license.
|
|
658
|
-
- [ ] CI + lint wired, even if tiny.
|
|
659
|
-
- [ ] Nothing generated that the author can't evaluate.
|
|
660
|
-
|
|
661
|
-
---
|
|
662
|
-
|
|
663
|
-
## Sibling skills
|
|
664
|
-
|
|
665
|
-
This skill governs the shape of the code. The others cover surfaces and workflow around it:
|
|
666
|
-
|
|
667
|
-
- Writing the PR itself (title, body, type) → **pr-create**.
|
|
668
|
-
- Reviewing a PR or a local diff against these rules → **pr-review**.
|
|
669
|
-
- The KV cache, the context window and the overflow ladder → **kvcache**.
|
|
670
|
-
|
|
671
|
-
Commit and branch rules live in [`CLAUDE.md`](../../../CLAUDE.md); how a PR itself is shaped and delivered is **pr-create**.
|