@qvac/harness 0.2.6 → 0.2.8
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/.claude/skills/gh-stack/SKILL.md +24 -24
- package/.claude/skills/gh-stack/references/commands.md +2 -2
- package/.claude/skills/js-house-style/SKILL.md +89 -68
- package/.claude/skills/kvcache/SKILL.md +7 -7
- package/.claude/skills/pr-create/SKILL.md +58 -15
- package/.claude/skills/pr-review/SKILL.md +307 -41
- package/CLAUDE.md +3 -0
- package/README.md +26 -4
- package/dist/harness.bundle +12 -10
- package/dist-lib/index.js +58 -58
- package/dist-lib/lib/audio-transcode-bare.js +2 -2
- package/dist-lib/lib/capabilities/completion.js +30 -30
- package/dist-lib/lib/capabilities/download.js +2 -2
- package/dist-lib/lib/capabilities/speech-text.js +1 -1
- package/dist-lib/lib/capabilities/tool-prompt-log.js +1 -1
- package/dist-lib/lib/capabilities/tool-selection.js +2 -2
- package/dist-lib/lib/capabilities/tool-turns.js +1 -1
- package/dist-lib/lib/capabilities/usage-breakdown.js +1 -1
- package/dist-lib/lib/capabilities/voice.js +3 -3
- package/dist-lib/lib/connect.js +11 -11
- package/dist-lib/lib/context-window.js +1 -1
- package/dist-lib/lib/docx/index.d.ts +5 -0
- package/dist-lib/lib/docx/index.js +49 -0
- package/dist-lib/lib/hardware/capability-probe.js +3 -3
- package/dist-lib/lib/hardware/gpu-probe-addon.js +2 -2
- package/dist-lib/lib/hardware/gpu-probe-entry.js +1 -1
- package/dist-lib/lib/helpers/markup-stream-filter.js +1 -1
- package/dist-lib/lib/helpers/run-outcome.js +1 -1
- package/dist-lib/lib/helpers/spawn-path.d.ts +1 -1
- package/dist-lib/lib/helpers/thinking.js +2 -2
- package/dist-lib/lib/helpers/tool-intent.js +1 -1
- package/dist-lib/lib/helpers/tool-label.js +3 -3
- package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -1
- package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -1
- package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -1
- package/dist-lib/lib/hooks/built-in/egress.js +2 -2
- package/dist-lib/lib/hooks/built-in/index.js +3 -3
- package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -1
- package/dist-lib/lib/hooks/pipeline.js +4 -4
- package/dist-lib/lib/hooks/registry.js +1 -1
- package/dist-lib/lib/hooks/run.js +1 -1
- package/dist-lib/lib/models/model-catalog.js +4 -4
- package/dist-lib/lib/models/model-delete.js +1 -1
- package/dist-lib/lib/models/model-handle.js +1 -1
- package/dist-lib/lib/models/model-list.js +1 -1
- package/dist-lib/lib/models/model-pool.js +8 -8
- package/dist-lib/lib/models/model-source.js +1 -1
- package/dist-lib/lib/models/music-catalog.d.ts +1 -1
- package/dist-lib/lib/no-redirect-fetch-bare.js +3 -3
- package/dist-lib/lib/no-redirect-fetch-impl.js +1 -1
- package/dist-lib/lib/no-redirect-fetch-node.js +2 -2
- package/dist-lib/lib/resolution-detector.js +1 -1
- package/dist-lib/lib/sdk-loader.js +2 -2
- package/dist-lib/lib/sdk-storage.js +1 -1
- package/dist-lib/lib/serve.js +10 -10
- package/dist-lib/lib/spawn.js +3 -3
- package/dist-lib/lib/staging.js +1 -1
- package/dist-lib/lib/tools/catalog.js +1 -1
- package/dist-lib/lib/tools/create-documents.js +2 -2
- package/dist-lib/lib/tools/edit-documents.js +2 -2
- package/dist-lib/lib/tools/edit-image.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/attachments.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -6
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +3 -3
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +5 -5
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -2
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/cli-process.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
- package/dist-lib/lib/tools/exec/obsidian/command.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/route.js +7 -7
- package/dist-lib/lib/tools/exec/obsidian/vault-create.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +6 -6
- package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-read.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/vault-write.js +2 -2
- package/dist-lib/lib/tools/exec/runner.js +1 -1
- package/dist-lib/lib/tools/exec.js +13 -13
- package/dist-lib/lib/tools/file-glob.js +3 -3
- package/dist-lib/lib/tools/fs/local.js +1 -1
- package/dist-lib/lib/tools/fs/remote.js +1 -1
- package/dist-lib/lib/tools/fs/walk.js +2 -2
- package/dist-lib/lib/tools/generate-image.js +4 -4
- package/dist-lib/lib/tools/generate-music.js +6 -6
- package/dist-lib/lib/tools/grep.js +6 -6
- package/dist-lib/lib/tools/http-request/truncate-response.js +1 -1
- package/dist-lib/lib/tools/http-request.js +9 -9
- package/dist-lib/lib/tools/mcp/utils.js +1 -1
- package/dist-lib/lib/tools/mcp-call.js +8 -8
- package/dist-lib/lib/tools/operations/builders.d.ts +1 -1
- package/dist-lib/lib/tools/operations/builders.js +1 -1
- package/dist-lib/lib/tools/operations/compile.js +1 -1
- package/dist-lib/lib/tools/operations/manifest.d.ts +16 -16
- package/dist-lib/lib/tools/operations/manifest.js +1 -1
- package/dist-lib/lib/tools/outbound-guard.js +1 -1
- package/dist-lib/lib/tools/rag-search.js +1 -1
- package/dist-lib/lib/tools/read-files.js +3 -3
- package/dist-lib/lib/tools/skill.js +8 -8
- package/dist-lib/lib/tools/skills/auth-resolver.js +2 -2
- package/dist-lib/lib/tools/skills/bin-check.js +1 -1
- package/dist-lib/lib/tools/skills/bundled-dir.js +2 -2
- package/dist-lib/lib/tools/skills/catalog.js +10 -10
- package/dist-lib/lib/tools/skills/cli-schema-check.js +1 -1
- package/dist-lib/lib/tools/skills/cli-schema-load.js +1 -1
- package/dist-lib/lib/tools/skills/cli-schema.js +4 -4
- package/dist-lib/lib/tools/skills/credential-check.js +3 -3
- package/dist-lib/lib/tools/skills/credential-grants.js +1 -1
- package/dist-lib/lib/tools/skills/credential-health.js +2 -2
- package/dist-lib/lib/tools/skills/frontmatter-parser.js +1 -1
- package/dist-lib/lib/tools/skills/index.js +5 -5
- package/dist-lib/lib/tools/skills/mention.js +1 -1
- package/dist-lib/lib/tools/skills/overlay-crud.js +1 -1
- package/dist-lib/lib/tools/skills/setup-routes.js +1 -1
- package/dist-lib/lib/tools/skills/skill-settings.js +1 -1
- package/dist-lib/lib/tools/skills/skill-state-constants.d.ts +8 -8
- package/dist-lib/lib/tools/skills/skill-state.js +9 -9
- package/dist-lib/lib/tools/toolsets.d.ts +1 -1
- package/dist-lib/lib/tools/url-guard.js +2 -2
- package/dist-lib/lib/tools/weather-lookup/place.d.ts +11 -0
- package/dist-lib/lib/tools/weather-lookup/place.js +152 -0
- package/dist-lib/lib/tools/weather-lookup/report.d.ts +37 -0
- package/dist-lib/lib/tools/weather-lookup/report.js +56 -0
- package/dist-lib/lib/tools/weather-lookup.d.ts +8 -0
- package/dist-lib/lib/tools/weather-lookup.js +110 -0
- package/dist-lib/lib/tools/web-fetch.js +10 -10
- package/dist-lib/lib/tools/web-search.js +2 -2
- package/dist-lib/lib/transport.js +1 -1
- package/dist-lib/lib/validating-lookup.js +1 -1
- package/dist-lib/lib/web-fetch/embedded-state.js +1 -1
- package/dist-lib/lib/web-fetch/extract.js +1 -1
- package/dist-lib/lib/web-fetch/images.js +1 -1
- package/dist-lib/lib/web-fetch/video.js +1 -1
- package/dist-lib/lib/web-search/engine-registry.js +7 -7
- package/dist-lib/lib/web-search/engines/aol.js +3 -3
- package/dist-lib/lib/web-search/engines/bing.js +4 -4
- package/dist-lib/lib/web-search/engines/brave.js +2 -2
- package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -3
- package/dist-lib/lib/web-search/engines/google.js +4 -4
- package/dist-lib/lib/web-search/engines/wikipedia.js +2 -2
- package/dist-lib/lib/web-search/engines/yahoo.js +3 -3
- package/dist-lib/lib/web-search/fetch-html.js +1 -1
- package/dist-lib/lib/web-search/merge-results.js +1 -1
- package/dist-lib/lib/web-search/orchestrator.js +3 -3
- package/dist-lib/schema/baseline.js +1 -1
- package/dist-lib/schema/build.js +3 -3
- package/dist-lib/schema/release-tags.d.ts +2 -1
- package/dist-lib/schema/release-tags.js +9 -7
- package/docs/decisions/2026-05-29-kvcache-tool-turns.md +75 -0
- package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +127 -0
- package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +126 -0
- package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +88 -0
- package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +79 -0
- package/docs/decisions/2026-08-11-pyodide-worker-thread.md +68 -0
- package/docs/decisions/2026-08-17-harness-hook-pipeline.md +84 -0
- package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +176 -0
- package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +95 -0
- package/docs/decisions/2026-08-20-kvcache-static-placement.md +109 -0
- package/docs/decisions/2026-08-21-vendored-python-wheels.md +85 -0
- package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +64 -0
- package/docs/decisions/2026-08-27-writable-skills-overlay.md +56 -0
- package/docs/decisions/2026-09-01-skill-operation-manifests.md +53 -0
- package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +78 -0
- package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +136 -0
- package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +63 -0
- package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +70 -0
- package/docs/decisions/INDEX.md +40 -0
- package/docs/decisions/TEMPLATE.md +32 -0
- package/package.json +21 -4
- package/worker/harness-sidecar.mjs +2 -0
|
@@ -7,7 +7,7 @@ description: >
|
|
|
7
7
|
branch layers, dependent PRs, or gh stack; or when a stack is checked out.
|
|
8
8
|
metadata:
|
|
9
9
|
author: github
|
|
10
|
-
version:
|
|
10
|
+
version: '0.1.0'
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# gh-stack
|
|
@@ -45,16 +45,16 @@ Agent harnesses differ, so always pass the flags below instead of relying on tha
|
|
|
45
45
|
`--remote <name>` unless `remote.pushDefault` is configured. `checkout` and `trunk` have no
|
|
46
46
|
`--remote` flag and require the config.
|
|
47
47
|
|
|
48
|
-
| Always run
|
|
49
|
-
|
|
50
|
-
| `gh stack view --json`
|
|
51
|
-
| `gh stack submit --auto`
|
|
52
|
-
| `gh stack merge <target> --yes`
|
|
53
|
-
| `gh stack init <branch>...`
|
|
54
|
-
| `gh stack add <branch>`
|
|
55
|
-
| `gh stack checkout <target>`
|
|
56
|
-
| `gh stack up` / `down` / `top` / `bottom` | `gh stack switch`
|
|
57
|
-
| —
|
|
48
|
+
| Always run | Never run bare | Why |
|
|
49
|
+
| ----------------------------------------- | ------------------- | --------------------------------------------- |
|
|
50
|
+
| `gh stack view --json` | `gh stack view` | opens a TUI under a PTY |
|
|
51
|
+
| `gh stack submit --auto` | `gh stack submit` | prompts for a title per new PR |
|
|
52
|
+
| `gh stack merge <target> --yes` | `gh pr merge` | `gh pr merge` cannot merge a stack |
|
|
53
|
+
| `gh stack init <branch>...` | `gh stack init` | prompts for branch names |
|
|
54
|
+
| `gh stack add <branch>` | `gh stack add` | prompts for a name, and fails even when piped |
|
|
55
|
+
| `gh stack checkout <target>` | `gh stack checkout` | opens a selection menu |
|
|
56
|
+
| `gh stack up` / `down` / `top` / `bottom` | `gh stack switch` | `switch` is menu-only |
|
|
57
|
+
| — | `gh stack modify` | TUI-only, no non-interactive path |
|
|
58
58
|
|
|
59
59
|
- `view --short` is safe in both modes, but it is formatted for humans. Use `--json` to parse.
|
|
60
60
|
- **`checkout <pr>` when a different local stack already covers those branches** cannot be forced.
|
|
@@ -138,19 +138,19 @@ an ancestor of the branch.
|
|
|
138
138
|
|
|
139
139
|
## Exit codes
|
|
140
140
|
|
|
141
|
-
| Code | Meaning
|
|
142
|
-
|
|
143
|
-
| 0
|
|
144
|
-
| 1
|
|
145
|
-
| 2
|
|
146
|
-
| 3
|
|
147
|
-
| 4
|
|
148
|
-
| 5
|
|
149
|
-
| 6
|
|
150
|
-
| 7
|
|
151
|
-
| 8
|
|
152
|
-
| 9
|
|
153
|
-
| 10
|
|
141
|
+
| Code | Meaning | Recovery |
|
|
142
|
+
| ---- | -------------------------- | ---------------------------------------------------------- |
|
|
143
|
+
| 0 | Success | — |
|
|
144
|
+
| 1 | Generic error | Read stderr |
|
|
145
|
+
| 2 | Not in a stack | `gh stack init`, or `gh stack checkout <target>` |
|
|
146
|
+
| 3 | Rebase conflict | Follow the Exit 3 recovery below |
|
|
147
|
+
| 4 | GitHub API failure | Check `gh auth status`, retry |
|
|
148
|
+
| 5 | Invalid arguments | Fix the invocation; see `<command> --help` |
|
|
149
|
+
| 6 | Disambiguation required | Branch is in several stacks; check out a non-shared branch |
|
|
150
|
+
| 7 | Rebase already in progress | `gh stack rebase --continue` or `--abort` |
|
|
151
|
+
| 8 | Stack file locked | Another `gh stack` process is writing; retry after ~5s |
|
|
152
|
+
| 9 | Stacked PRs unavailable | Not enabled on the repository; tell the user |
|
|
153
|
+
| 10 | Modify recovery required | `gh stack modify --abort` |
|
|
154
154
|
|
|
155
155
|
**Exit 3 recovery:**
|
|
156
156
|
|
|
@@ -67,7 +67,7 @@ first non-merged ancestor, then links them into a Stack on GitHub.
|
|
|
67
67
|
the title and its body as the PR body. A branch with multiple commits humanizes the branch name
|
|
68
68
|
(hyphens and underscores become spaces). There is no flag for a custom title or body; use
|
|
69
69
|
`gh pr edit` afterwards.
|
|
70
|
-
- `--open` marks new
|
|
70
|
+
- `--open` marks new _and existing_ PRs ready for review; without it new PRs are drafts.
|
|
71
71
|
- Requires stacked PRs to be enabled on the repository. If not, `submit` exits **9** when
|
|
72
72
|
non-interactive (under a TTY it offers to create ordinary unstacked PRs instead).
|
|
73
73
|
|
|
@@ -172,6 +172,6 @@ Removes the stack **grouping** only. It never deletes pull requests or branches.
|
|
|
172
172
|
|
|
173
173
|
`up`, `down`, `top`, `bottom`, and `trunk` are always non-interactive. `up` and `down` accept a
|
|
174
174
|
count (`gh stack up 3`). Movement clamps at the stack bounds, and merged branches are skipped when
|
|
175
|
-
navigating from an active branch, so `bottom` lands on the lowest
|
|
175
|
+
navigating from an active branch, so `bottom` lands on the lowest _unmerged_ branch.
|
|
176
176
|
|
|
177
177
|
`gh stack switch` is a selection menu with no non-interactive path. Use the commands above instead.
|
|
@@ -12,7 +12,7 @@ The most important rule comes first because it governs all the others:
|
|
|
12
12
|
|
|
13
13
|
**Don't follow rules without thinking. Make the best code you can, always.**
|
|
14
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
|
|
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
16
|
|
|
17
17
|
If you internalize five things:
|
|
18
18
|
|
|
@@ -34,7 +34,7 @@ It is **not** the place for workflow. Commit, branch-naming, and push/PR-deliver
|
|
|
34
34
|
|
|
35
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
36
|
|
|
37
|
-
Exports follow what the code
|
|
37
|
+
Exports follow what the code _is_, not which runtime it targets:
|
|
38
38
|
|
|
39
39
|
- **Product source** — `lib/`, `worker/`, `schema/` — **named exports**, one concept per file.
|
|
40
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.
|
|
@@ -46,16 +46,16 @@ When in doubt you are writing product source; the default export is for the modu
|
|
|
46
46
|
Before structure comes direction. The way we approach a problem (mafintosh's framing, which the whole way of working is built on):
|
|
47
47
|
|
|
48
48
|
- Start from the **great ambition** — the real thing you want to exist.
|
|
49
|
-
- Plan
|
|
49
|
+
- Plan _backwards_ into small, self-contained pieces. Sort them topologically.
|
|
50
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
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
52
|
|
|
53
53
|
Two failure modes this guards against, both worth catching in review:
|
|
54
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:
|
|
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
56
|
- **Scope creep.** It kills projects. Small modules are the antidote — each is independently finishable, testable, replaceable.
|
|
57
57
|
|
|
58
|
-
On using AI to do this work (heed it, since this skill is often
|
|
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
59
|
|
|
60
60
|
Practical upshot: bias to the smallest correct thing, make it composable, move on.
|
|
61
61
|
|
|
@@ -80,17 +80,17 @@ hypercore → hyperbee → hyperdb → autobee
|
|
|
80
80
|
on the bee)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
Read those left to right: each arrow is "built
|
|
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
84
|
|
|
85
85
|
What this means when you write code:
|
|
86
86
|
|
|
87
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
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
|
|
90
|
-
- **Don't collapse layers "for convenience."** A module that does encoding
|
|
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
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
92
|
|
|
93
|
-
When you reach for a new capability, the instinct is:
|
|
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
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
@@ -102,17 +102,17 @@ A well-shaped module is a single default export, an options bag, cheap construct
|
|
|
102
102
|
const b4a = require('b4a')
|
|
103
103
|
|
|
104
104
|
module.exports = class Thing {
|
|
105
|
-
constructor
|
|
105
|
+
constructor(store, opts = {}) {
|
|
106
106
|
this.store = store
|
|
107
107
|
this.foo = opts.foo ?? defaultFoo
|
|
108
108
|
this._internal = null // declare every field here, even if null
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
async open
|
|
111
|
+
async open() {
|
|
112
112
|
this._internal = await this.store.get()
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
async doWork
|
|
115
|
+
async doWork(arg) {
|
|
116
116
|
// ...
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -120,13 +120,13 @@ module.exports = class Thing {
|
|
|
120
120
|
|
|
121
121
|
The conventions, each with its reason:
|
|
122
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.
|
|
124
|
-
- **`(required, opts = {})` constructor.** Positional for the one or two things the module fundamentally
|
|
125
|
-
- **Declare all instance fields in the constructor**, even `null` ones.
|
|
126
|
-
- **No async work in the constructor.** Construction is synchronous and cheap; async setup happens in an `open`/`ready` step, lazily.
|
|
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
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
128
|
```js
|
|
129
|
-
function aThing
|
|
129
|
+
function aThing(somethingSuperImportantLikeACorestore, opts = {}) {
|
|
130
130
|
const { signal } = opts
|
|
131
131
|
this._otherThing = new OtherThing(opts)
|
|
132
132
|
}
|
|
@@ -134,12 +134,12 @@ The conventions, each with its reason:
|
|
|
134
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
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
136
|
|
|
137
|
-
`@qvac/core
|
|
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
138
|
|
|
139
139
|
```ts
|
|
140
|
-
import { syncChanges } from './sync/index.ts'
|
|
141
|
-
import { registerRoutes } from './contract/index.ts'
|
|
142
|
-
import { createSyncActivity } from './sync-status.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
143
|
```
|
|
144
144
|
|
|
145
145
|
`sync/` in turn is five files of 8–122 lines, one per domain, because keeping them together would blur independent implementations into one.
|
|
@@ -148,16 +148,17 @@ The conventions, each with its reason:
|
|
|
148
148
|
|
|
149
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
150
|
|
|
151
|
-
**Split by domain, not by operation.** The same file's `blob-sweep.ts` is the counter-example — it carves out one
|
|
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
152
|
|
|
153
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
|
+
|
|
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.
|
|
155
156
|
|
|
156
157
|
### Parameters
|
|
157
158
|
|
|
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.
|
|
159
160
|
|
|
160
|
-
Positional is for the one or two things the function fundamentally acts
|
|
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.
|
|
161
162
|
|
|
162
163
|
```ts
|
|
163
164
|
// no — eight positional args, and three adjacent numbers that all accept each other
|
|
@@ -175,7 +176,7 @@ frameProgress(update, {
|
|
|
175
176
|
})
|
|
176
177
|
```
|
|
177
178
|
|
|
178
|
-
**The trigger is ambiguity, not arity.** Three parameters of three distinct types read fine.
|
|
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.
|
|
179
180
|
|
|
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.
|
|
181
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.
|
|
@@ -200,26 +201,32 @@ Types are the cheapest contract you get. Keep them honest at the boundary and th
|
|
|
200
201
|
- **Explicit return types on exported functions.** `readonly` on object properties, `ReadonlyArray<T>` over `T[]`.
|
|
201
202
|
- Test files may use narrow casts for mock wiring or intentionally invalid fixtures. Keep them local; never copy into production code.
|
|
202
203
|
|
|
203
|
-
This is the typed counterpart of
|
|
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.
|
|
204
205
|
|
|
205
206
|
### Comments and whitespace
|
|
206
207
|
|
|
207
208
|
Well-structured code in small modules with honest names does not need narration. Comments are the exception, not the habit.
|
|
208
209
|
|
|
209
|
-
- **Prefer no comment.** If a comment explains
|
|
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.
|
|
210
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.
|
|
211
|
-
- **Only ever explain
|
|
212
|
+
- **Only ever explain _why_** — a non-obvious constraint, a workaround with a reason, why an operational error is safe to swallow.
|
|
212
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
|
+
|
|
213
215
|
### Whitespace groups the code
|
|
214
216
|
|
|
215
|
-
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
|
|
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.
|
|
216
218
|
|
|
217
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.
|
|
218
220
|
|
|
219
221
|
```js
|
|
220
222
|
// no — one wall, no seam between setup, the four scenarios, or their assertions
|
|
221
223
|
const { ragSearch } = await toolModeApi({ ragEnabled: false })
|
|
222
|
-
const tool = await ragSearch({
|
|
224
|
+
const tool = await ragSearch({
|
|
225
|
+
chatId: 'chat-1',
|
|
226
|
+
query: 'alpha fuel pressure',
|
|
227
|
+
mode: 'tool',
|
|
228
|
+
limit: 5
|
|
229
|
+
})
|
|
223
230
|
t.ok(tool.enabled, 'the toggle does not gate tool mode')
|
|
224
231
|
t.ok((tool.hits?.length ?? 0) > 0, 'hits found on a toggle-off chat')
|
|
225
232
|
const injection = await ragSearch({ chatId: 'chat-1', query: 'alpha fuel pressure' })
|
|
@@ -230,7 +237,12 @@ t.absent(injection.enabled, 'injection mode still honors the toggle')
|
|
|
230
237
|
// yes — setup, then one group per scenario
|
|
231
238
|
const { ragSearch } = await toolModeApi({ ragEnabled: false })
|
|
232
239
|
|
|
233
|
-
const tool = await ragSearch({
|
|
240
|
+
const tool = await ragSearch({
|
|
241
|
+
chatId: 'chat-1',
|
|
242
|
+
query: 'alpha fuel pressure',
|
|
243
|
+
mode: 'tool',
|
|
244
|
+
limit: 5
|
|
245
|
+
})
|
|
234
246
|
t.ok(tool.enabled, 'the toggle does not gate tool mode')
|
|
235
247
|
t.ok((tool.hits?.length ?? 0) > 0, 'hits found on a toggle-off chat')
|
|
236
248
|
t.absent(tool.targetDocumentId, 'tool mode never resolves a target document')
|
|
@@ -242,7 +254,7 @@ const missing = await ragSearch({ chatId: 'chat-404', query: 'alpha', mode: 'too
|
|
|
242
254
|
t.absent(missing.enabled, 'an unknown chat never searches, even in tool mode')
|
|
243
255
|
```
|
|
244
256
|
|
|
245
|
-
Split a group further when it grows its own phases —
|
|
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.
|
|
246
258
|
|
|
247
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.
|
|
248
260
|
|
|
@@ -270,16 +282,16 @@ Every value that exists twice will eventually disagree. Most "impossible" bugs a
|
|
|
270
282
|
|
|
271
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.
|
|
272
284
|
|
|
273
|
-
**But check the contract before you call it duplication.** Two fields holding the same value are sometimes two
|
|
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.
|
|
274
286
|
|
|
275
|
-
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
|
|
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.
|
|
276
288
|
|
|
277
289
|
```ts
|
|
278
290
|
// looks like duplication — the same number in four places
|
|
279
291
|
{ downloaded, total, percentage, fileSetInfo: { overallDownloaded, overallTotal, overallPercentage } }
|
|
280
292
|
```
|
|
281
293
|
|
|
282
|
-
Here the type said top-level is
|
|
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.
|
|
283
295
|
|
|
284
296
|
The two diagnoses lead to opposite fixes, so get the diagnosis right:
|
|
285
297
|
|
|
@@ -301,7 +313,7 @@ for (const src of srcs) { offsets.push(running); running += sizes[src] }
|
|
|
301
313
|
|
|
302
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.
|
|
303
315
|
|
|
304
|
-
**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
|
|
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.
|
|
305
317
|
|
|
306
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.
|
|
307
319
|
|
|
@@ -342,7 +354,7 @@ The rules when you use it:
|
|
|
342
354
|
if (!this.opened) await this.ready()
|
|
343
355
|
```
|
|
344
356
|
|
|
345
|
-
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
|
|
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.
|
|
346
358
|
|
|
347
359
|
- **Make every async step interruptible.** Check `closing` before and after each suspension point, in `_open` and in any long-running method:
|
|
348
360
|
|
|
@@ -386,32 +398,32 @@ Everything in [Buffers and wire formats](#buffers-and-wire-formats-bare--node) a
|
|
|
386
398
|
|
|
387
399
|
### Flows → streamx
|
|
388
400
|
|
|
389
|
-
Use `streamx`, not Node core `stream`.
|
|
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`.
|
|
390
402
|
|
|
391
403
|
```js
|
|
392
404
|
const { Readable } = require('streamx')
|
|
393
405
|
|
|
394
406
|
class Query extends Readable {
|
|
395
|
-
constructor
|
|
407
|
+
constructor(db, range) {
|
|
396
408
|
super()
|
|
397
409
|
this.db = db
|
|
398
410
|
this.range = range
|
|
399
411
|
this._it = null
|
|
400
412
|
}
|
|
401
413
|
|
|
402
|
-
_open
|
|
414
|
+
_open(cb) {
|
|
403
415
|
this._it = this.db.iterator(this.range)
|
|
404
416
|
cb(null)
|
|
405
417
|
}
|
|
406
418
|
|
|
407
|
-
async _read
|
|
419
|
+
async _read(cb) {
|
|
408
420
|
const { done, value } = await this._it.next()
|
|
409
421
|
if (done) this.push(null)
|
|
410
422
|
else this.push(value)
|
|
411
423
|
cb(null)
|
|
412
424
|
}
|
|
413
425
|
|
|
414
|
-
_destroy
|
|
426
|
+
_destroy(cb) {
|
|
415
427
|
this._it?.close().then(() => cb(null), cb)
|
|
416
428
|
}
|
|
417
429
|
}
|
|
@@ -425,17 +437,17 @@ EventEmitter (or streamx's emitter) for "something happened": `'close'`, `'updat
|
|
|
425
437
|
|
|
426
438
|
### Errors: don't program defensively
|
|
427
439
|
|
|
428
|
-
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.
|
|
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.
|
|
429
441
|
|
|
430
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.
|
|
431
|
-
- **Don't validate inputs you've documented.** The documented type
|
|
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.
|
|
432
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.
|
|
433
445
|
- **`safety-catch`** rethrows real bugs while swallowing benign teardown rejections, so a genuine programmer error still crashes loudly rather than getting silently eaten:
|
|
434
446
|
```js
|
|
435
447
|
const safetyCatch = require('safety-catch')
|
|
436
448
|
somePromise().catch(safetyCatch)
|
|
437
449
|
```
|
|
438
|
-
- **Never swallow silently.** If you catch and ignore an
|
|
450
|
+
- **Never swallow silently.** If you catch and ignore an _operational_ error, leave a comment on why it's safe.
|
|
439
451
|
|
|
440
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.
|
|
441
453
|
|
|
@@ -450,7 +462,7 @@ const ScopeLock = require('scope-lock')
|
|
|
450
462
|
|
|
451
463
|
const l = new ScopeLock()
|
|
452
464
|
|
|
453
|
-
async function append
|
|
465
|
+
async function append(data) {
|
|
454
466
|
await l.lock()
|
|
455
467
|
try {
|
|
456
468
|
await reallyAppend(data)
|
|
@@ -467,8 +479,9 @@ It also has a **debounced** mode, for when you don't need every queued entry to
|
|
|
467
479
|
```js
|
|
468
480
|
const d = new ScopeLock({ debounce: true })
|
|
469
481
|
|
|
470
|
-
async function resync
|
|
471
|
-
if (await d.lock()) {
|
|
482
|
+
async function resync() {
|
|
483
|
+
if (await d.lock()) {
|
|
484
|
+
// only the entries that actually get the scope return true
|
|
472
485
|
try {
|
|
473
486
|
await doResync()
|
|
474
487
|
} finally {
|
|
@@ -484,18 +497,18 @@ Use a plain lock when every call must run; use debounce when intermediate calls
|
|
|
484
497
|
|
|
485
498
|
### Closures capture the variable, not its value
|
|
486
499
|
|
|
487
|
-
A callback that outlives the statement that created it reads whatever the variable holds
|
|
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:
|
|
488
501
|
|
|
489
502
|
```ts
|
|
490
503
|
let completedBytes = 0
|
|
491
504
|
for (let i = 0; i < srcs.length; i++) {
|
|
492
|
-
const onProgress = (u) => report(completedBytes + u.downloaded)
|
|
505
|
+
const onProgress = (u) => report(completedBytes + u.downloaded) // reads it later
|
|
493
506
|
await download(srcs[i], { onProgress })
|
|
494
|
-
completedBytes += sizes[srcs[i]]
|
|
507
|
+
completedBytes += sizes[srcs[i]] // moved by then
|
|
495
508
|
}
|
|
496
509
|
```
|
|
497
510
|
|
|
498
|
-
A progress event that lands after its download resolves — a straggler over IPC, a queued microtask, an event flushed on close — sees the
|
|
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.
|
|
499
512
|
|
|
500
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)).
|
|
501
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.
|
|
@@ -513,15 +526,15 @@ If nothing exposes readiness, that's the bug: add the event or the promise to th
|
|
|
513
526
|
|
|
514
527
|
### b4a everywhere
|
|
515
528
|
|
|
516
|
-
**Don't use the `Buffer` global.**
|
|
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.
|
|
517
530
|
|
|
518
531
|
```js
|
|
519
532
|
const b4a = require('b4a')
|
|
520
533
|
|
|
521
|
-
b4a.from('hello')
|
|
522
|
-
b4a.alloc(32)
|
|
523
|
-
b4a.equals(a, b)
|
|
524
|
-
b4a.toString(buf, 'hex')
|
|
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')
|
|
525
538
|
b4a.concat([a, b])
|
|
526
539
|
```
|
|
527
540
|
|
|
@@ -535,9 +548,17 @@ Wire and on-disk formats use `compact-encoding` — positional, length-prefixed,
|
|
|
535
548
|
const c = require('compact-encoding')
|
|
536
549
|
|
|
537
550
|
const message = {
|
|
538
|
-
preencode
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
}
|
|
541
562
|
}
|
|
542
563
|
```
|
|
543
564
|
|
|
@@ -555,7 +576,7 @@ Each dependency is maintained forever — its bugs, breaking changes, security s
|
|
|
555
576
|
- **Justify every `package.json` entry in one sentence.** If you can't, it shouldn't be there.
|
|
556
577
|
- **Don't import to avoid an obvious function.** `isOdd` is the cautionary tale.
|
|
557
578
|
|
|
558
|
-
The governing question is the complexity check again:
|
|
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.
|
|
559
580
|
|
|
560
581
|
---
|
|
561
582
|
|
|
@@ -585,9 +606,9 @@ test('basic append and read', async function (t) {
|
|
|
585
606
|
What "test the API" means here:
|
|
586
607
|
|
|
587
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.
|
|
588
|
-
- **Test how the thing is
|
|
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.
|
|
589
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.
|
|
590
|
-
- **Test the real failure paths that
|
|
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.
|
|
591
612
|
- **`t.teardown()` for cleanup** — closes resources even when assertions fail. Use it for everything you open.
|
|
592
613
|
- **Flat tests, one `test()` per behavior.** The name states the assertion. No deep describe/it nesting.
|
|
593
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.
|
|
@@ -596,9 +617,9 @@ What "test the API" means here:
|
|
|
596
617
|
For a suite that builds real multi-peer structure, put the setup behind one factory that owns the testnet and the teardown:
|
|
597
618
|
|
|
598
619
|
```js
|
|
599
|
-
const { things } = await create(t)
|
|
600
|
-
const thing1 = await things.start()
|
|
601
|
-
const t1 = await things.restart(0)
|
|
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
|
|
602
623
|
```
|
|
603
624
|
|
|
604
625
|
The test then reads as the scenario it describes, and lifecycle correctness lives in one place instead of being re-derived per test.
|
|
@@ -607,7 +628,7 @@ The test then reads as the scenario it describes, and lifecycle correctness live
|
|
|
607
628
|
|
|
608
629
|
## Review checklist
|
|
609
630
|
|
|
610
|
-
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
|
|
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.
|
|
611
632
|
|
|
612
633
|
- [ ] One responsibility, describable without "and"; built on the layer below, not reimplementing or reaching around it.
|
|
613
634
|
- [ ] Composed by passing the lower primitive in (store/core/bee), not constructing it secretly; no layer depends upward.
|
|
@@ -615,7 +636,7 @@ Apply to any module, regardless of size or visibility. When reviewing, walk it a
|
|
|
615
636
|
- [ ] Exports match the kind of code (named for product source, one default for a publishable module); `(required, opts = {})` constructor; all fields declared.
|
|
616
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.
|
|
617
638
|
- [ ] TypeScript with no `as`, no `any`, `unknown` only at boundaries; `erasableSyntaxOnly` respected for anything Bare loads.
|
|
618
|
-
- [ ] Comments: none unless they explain
|
|
639
|
+
- [ ] Comments: none unless they explain _why_, one line max, no comment blocks, no banner dividers.
|
|
619
640
|
- [ ] Blank lines group the code into setup / action / assertion — neither an unbroken wall nor padding that groups nothing.
|
|
620
641
|
- [ ] No conditional spreads — optional values assigned plainly and allowed to be `undefined`; no commented-out code or unused scaffolding.
|
|
621
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.
|
|
@@ -647,4 +668,4 @@ This skill governs the shape of the code. The others cover surfaces and workflow
|
|
|
647
668
|
- Reviewing a PR or a local diff against these rules → **pr-review**.
|
|
648
669
|
- The KV cache, the context window and the overflow ladder → **kvcache**.
|
|
649
670
|
|
|
650
|
-
Commit
|
|
671
|
+
Commit and branch rules live in [`CLAUDE.md`](../../../CLAUDE.md); how a PR itself is shaped and delivered is **pr-create**.
|
|
@@ -11,19 +11,19 @@ common wrong turns.
|
|
|
11
11
|
|
|
12
12
|
This repo owns the cache: `lib/capabilities/completion.ts` (the `kvCache` key,
|
|
13
13
|
the round loop, the overflow ladder), `lib/context-window.ts`,
|
|
14
|
-
`lib/models/kv-cache.ts` and `lib/models/model-pool.ts`. The
|
|
15
|
-
folding a chat's history so it fits — is `@qvac/core
|
|
14
|
+
`lib/models/kv-cache.ts` and `lib/models/model-pool.ts`. The _prompt_ side —
|
|
15
|
+
folding a chat's history so it fits — is `@qvac/core → lib/compaction.ts`;
|
|
16
16
|
see its **compaction** skill before proposing anything that shrinks history.
|
|
17
17
|
|
|
18
18
|
## The one thing to internalise
|
|
19
19
|
|
|
20
20
|
Three state sets exist and are **never reconciled**:
|
|
21
21
|
|
|
22
|
-
| Layer
|
|
23
|
-
|
|
|
24
|
-
| caller (`@qvac/core`) | message **list** (shrinks only on an overflow retry) | chat
|
|
25
|
-
| SDK
|
|
26
|
-
| addon
|
|
22
|
+
| Layer | State | Keyed by |
|
|
23
|
+
| --------------------- | ---------------------------------------------------- | --------------- |
|
|
24
|
+
| caller (`@qvac/core`) | message **list** (shrinks only on an overflow retry) | chat |
|
|
25
|
+
| SDK | message **count** (`savedCount`) | cache file path |
|
|
26
|
+
| addon | token **positions** (`nPast`) + the on-disk `.bin` | cache key |
|
|
27
27
|
|
|
28
28
|
Almost every bug here is drift between those three, not context pressure.
|
|
29
29
|
`savedCount` means "already sent", never "still resident": nothing below the
|