@treeseed/dsh-chapters 0.1.6 → 0.1.7

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.
@@ -69,8 +69,8 @@ scripts/bootstrap-dev-profile.sh --with-probe # scripted rounds only (probe se
69
69
  | unit | `npm test` (`tests/unit`) | node --test, no build, no network | pure core: registry, render, archive, notice, engine-core, tools, commands |
70
70
  | integration | (today: probe rounds — see spikes/probe/README pattern) | boots real hosts in scratch homes | host wiring, realm mounts, provider economics |
71
71
  | e2e (tape — the default loop) | `npm run test:e2e:replay` | same boots with `E2E_MODEL=replay`; model turns answered from `tests/fixtures/model-tape/<project>` via the record/replay proxy (`tests/e2e/model-proxy.ts`) — seconds, deterministic |
72
- | e2e (distillation — occasional) | `npm run test:e2e:record` | `E2E_MODEL=record`: forwards every exchange to the real Local model and appends the tape. Run once per scenario; wipe `var/model-tape` first for a clean corpus; tapes are isolated per project (seven: suite/heavy/arrival/fanout/enrich/rules/fork); re-record when scenarios, prompts, or thresholds change. The GPU model is the DISTILLER, never the test oracle (user directive 2026-09-19) |
73
- | e2e | `npm run test:e2e` (`tests/e2e`, SEVEN projects: suite/heavy/arrival/fanout/enrich/rules/fork) | Playwright + workspace-cached chromium vs a throwaway `var/e2e-home-<port>` rebuilt per boot (port-keyed so no boot can stomp another's state) (isolated from live agents — see FINDINGS 2026-09-19) | the browser: identity, fork button + switch, knowledge loop end-to-end, the oversized turn (mid-turn compactions), the subagent fan-out, arrival-time artifacting (low-floor project) |
72
+ | e2e (distillation — occasional) | `npm run test:e2e:record` | `E2E_MODEL=record`: forwards every exchange to the real Local model and appends the tape. Run once per scenario; wipe `var/model-tape` first for a clean corpus; tapes are isolated per project (eight: suite/heavy/arrival/fanout/enrich/rules/fork/plans); re-record when scenarios, prompts, or thresholds change. The GPU model is the DISTILLER, never the test oracle (user directive 2026-09-19) |
73
+ | e2e | `npm run test:e2e` (`tests/e2e`, EIGHT projects: suite/heavy/arrival/fanout/enrich/rules/fork/plans) | Playwright + workspace-cached chromium vs a throwaway `var/e2e-home-<port>` rebuilt per boot (port-keyed so no boot can stomp another's state) (isolated from live agents — see FINDINGS 2026-09-19) | the browser: identity, fork button + switch, knowledge loop end-to-end, the oversized turn (mid-turn compactions), the subagent fan-out, arrival-time artifacting (low-floor project) |
74
74
 
75
75
  The e2e layer exists because client-plane facts were being GUESSED (services, methods, tooltips,
76
76
  switching) and every guess cost the user a browser round-trip. Playwright answers those in seconds
@@ -134,7 +134,7 @@ Re-record discipline (learned the hard way 2026-09-22/23):
134
134
  sweep files rather than by lowering the bar.
135
135
  - `npm run coverage` — the same deterministic suite under node's native
136
136
  `--experimental-test-coverage` (a quick second opinion; no gate).
137
- - `npm run coverage:e2e` — replays the seven browser segments with
137
+ - `npm run coverage:e2e` — replays the eight browser segments with
138
138
  `E2E_COVERAGE=1`, which makes every boot write NODE_V8_COVERAGE profiles
139
139
  under `var/e2e-cov/<port>` (flushed on the boot's clean exit), then runs
140
140
  `scripts/e2e-coverage.mjs` → **c8 with real source maps** (tsconfig
@@ -169,10 +169,15 @@ ship; `0.1.0` itself carries no attestation.
169
169
 
170
170
  - **`ci.yml`** — two jobs. `test`: `npm ci` (all deps public on
171
171
  registry.npmjs.org — **no install secret**), typecheck, build,
172
- `coverage:ci` (the 85/85/85/85 gate). `e2e-replay`: installs the pinned
172
+ `bash scripts/ci-gate.sh` (the 85/85/85/85 gate — since 0.1.6 the gate
173
+ ANNOTATES its own failure: metric lines, the pass/fail tally, up to 24
174
+ failing-test lines and c8's threshold verdicts as `::error::`, because on the
175
+ 0.1.6 tag a red gate left nothing behind but "exit code 1" while every local
176
+ reproduction of the same SHA was green; the summary step now runs
177
+ `if: always()`). `e2e-replay`: installs the pinned
173
178
  host (`npm i -g @deepseek-ai/dsh@0.1.5-rc.1` — the SAME version as the
174
179
  gate line in `scripts/ci-replay.sh`, which fails loudly on mismatch) plus
175
- chromium's apt libs, then `scripts/ci-replay.sh` — seven browser segments
180
+ chromium's apt libs, then `scripts/ci-replay.sh` — eight browser segments
176
181
  served entirely from the committed tapes. **No LLM, no key, no GPU:** in
177
182
  replay mode `localModelUp()` short-circuits to true (specs RUN, they do not
178
183
  skip) and a tape miss is a loud proxy 503, so a scenario that legitimately
@@ -218,8 +218,43 @@ Registry note: **do not** use `state.plans` — that name is the compaction mani
218
218
  product prompt *is* edited (personas, tool descriptions, notice text), the rule stands: re-record
219
219
  **whole projects**, never one spec of a shared boot.
220
220
  - New e2e project `plans` (own boot, own `E2E_PORT`, throwaway home `var/e2e-home-<port>`, one journey =
221
- one boot — the fork- and fanout-carve-out lesson), recorded once with `npm run test:e2e:record`, then
222
- replayed. Prompts must be tool-free **in words the model obeys** (the RR2 OOM lesson).
221
+ one boot — the fork- and fanout-carve-out lesson), recorded once then replayed. Prompts must be
222
+ tool-free **in words the model obeys** (the RR2 OOM lesson). **LANDED 2026-10-10 as the eighth chain
223
+ project** — but NOT from the live model: the box's `:8080` server closed the stream mid-turn
224
+ (`turn/end` `reason {kind:"error", code:"TRANSPORT"}`), so the tape was distilled through the scripted
225
+ upstream `tests/e2e/stub-model.ts` (wire format copied form-for-form from recorded llama.cpp traffic).
226
+ `tests/fixtures/model-tape/plans/PROVENANCE.md` states what that proves (the whole carriage, in a real
227
+ browser, against the real review channel) and what it does not (a model choosing to plan), and carries
228
+ the re-record command that retires the stub.
229
+
230
+ ### What the browser plane corrected (this is why it exists)
231
+
232
+ Every item below was an assumption the deterministic plane had encoded — and the live run said otherwise:
233
+
234
+ - **`planMode:` at a plan's chapter end is `inactive`, not `active`.** Approving a plan EXITS plan mode,
235
+ and the host appends that flip at the next accepted pre-step — inside the same turn, before
236
+ `turn/end`. My expectation said `active`; the archived chapter said `inactive`, and both mode flips
237
+ printed. The fold was right; my belief was wrong.
238
+ - **The toolbar chip is an EXIT control only.** `PlanModeControl` renders when
239
+ `plan.pending ? !plan.active : plan.active` (`dsh-client-ui-plan/lib/client.js:39-41`), so there is no
240
+ "press to turn plan mode ON". Entry is `/plan <message>` in the composer, which appends
241
+ `plan/mode {active:true}` before the turn and steers the text as one real user message. Learning that
242
+ cost one 40-minute timeout on a locator that could never appear.
243
+ - **`text=Approve this plan…` hangs; `getByRole('region', {name: /Approve this plan/})` does not.** The
244
+ question is the review REGION's accessible name, not matchable element text — read off the failure
245
+ snapshot, which is the only honest way to find this out.
246
+ - **A seeded event read out of the session log is JSON-escaped.** `expect(raw).toMatch(/ACTIVE PLAN: "x"/)`
247
+ can pass as a substring and fail as a regex; parse the event and assert on `data.content[0].text`.
248
+ - **`expect.poll(..., {intervals: 3000})` throws a TypeError** — intervals is an array. Trivial, but it
249
+ killed the first two recording runs.
250
+ - **A scripted upstream that string-matches the message array lies confidently.** The stub's first
251
+ `decide()` searched all messages for `exit_plan_mode`/`approved` — both words live in the SYSTEM PROMPT
252
+ (tool catalog + plan policy), so every turn answered as if the review were done and the review card
253
+ never opened. The honest signal is the transcript's SHAPE (`role:'tool'` exists only once a tool ran).
254
+ Pinned by `tests/unit/stub-model.test.ts`, because a stub that answers wrongly records a tape of a
255
+ session that never planned.
256
+ - **Acceptance assertions must be scoped to the session tree the run created.** Scanning the whole store
257
+ lets a previous run's plan satisfy the next run — a green that proves nothing.
223
258
  - Coverage: `npm run coverage:ci` gate (≥85 all four) must stay green; `src/plans.ts` is pure and gets a
224
259
  full-branch row like the other planners, and the new `render.ts` arms need the success/refusal
225
260
  surface covered the way the 2026-09-23 sweep did it.
@@ -253,7 +288,8 @@ Registry note: **do not** use `state.plans` — that name is the compaction mani
253
288
  | 4 | `plansSection` in `assembleNotice` + wiring + config register | done — P5/P8/P9 |
254
289
  | 5 | Site docs (§9 anatomy, §16 tools, §17 configuration, §18 notice) | done in the same release |
255
290
  | — | Seven-segment tape replay | green WITHOUT re-recording (2026-10-09, `npm run test:e2e:replay`, EXIT=0) |
256
- | — | `plans` browser journey | spec written as the `plans-dev` project; **awaits its tape** (no live Local model on this box — it self-skips, and is deliberately outside the CI chain until recorded) |
291
+ | — | `plans` browser journey | LANDED as the eighth chain project (2026-10-10): `tests/e2e/plans.spec.ts` passes in 13 s against `tests/fixtures/model-tape/plans/`, and the full eight-project chain is `CHAIN_EXIT=0` / 9 tests. Its tape is scripted-upstream distilled — see § Tapes and `PROVENANCE.md` |
292
+ | — | Follow-up, not blocking | re-record the `plans` tape from the live model on the GPU box (command in `PROVENANCE.md`); the carriage evidence does not change, only the claim about model behaviour |
257
293
 
258
294
  Deterministic ledger at landing: **549 unit+integration rows, zero failures** with the TreeDX container
259
295
  up (80 of them new, across `plans`, `render-plans`, `continue-plans`, `plans-segmentation`, `plans-tools`,
package/docs/verify.md CHANGED
@@ -150,7 +150,7 @@ renderer, the segmentation tool, and the child's notice.
150
150
  | P8 | A plan-less path is byte-identical to pre-feature | no `## Plans in this chapter` block, no `plans:`/`planMode:` frontmatter lines, no `## Plans` in the notice, and `chapters_segment` emits no `planHint` — the whole feature is conditional on there being a plan | unit `render-plans.test.ts`, `continue-plans.test.ts`, `plans-segmentation.test.ts`, `plans-tools.test.ts`; integration `plans-carriage.test.ts` |
151
151
  | P9 | Both carriage paths agree | the CONTINUATION path (`chapters_continue`) and the ENGINE path (in-place `summarize`/finalize) render the same block and the same record facts, including a verdict that lands past a composed chapter boundary — which is why `renderChapter` scans the whole event list, not its own range | unit `plans-engine.test.ts` (5) + `render-plans.test.ts`; zod round-trip of `plans`/`planMode` through the registry in `plans-engine.test.ts`, legacy rows gaining nothing |
152
152
  | P10 | Artifact citations resolve wherever they were written | a chapter's `artifacts/aa/<sha>.txt` reference resolves whether the blob sits at the STORE ROOT (arrival stubs) or inside the ANCESTRY TREE (`<store>/<root>/artifacts/…`, where `writeArchive` puts deferred results and every plan) — before this fix the second form was unreadable through `chapters_artifact`, which is what "every byte remains retrievable" actually promised | `artifact-query.test.ts` rows + integration `plans-carriage.test.ts` (`resolveArtifactWithMirror` over the real tree) |
153
- | P11 | The browser plane, once recorded | `/plan <message>` → model submits → user approves in the plan-review card → fork → the child's own log carries `ACTIVE PLAN: … APPROVED` and the plan blob exists with its full bytes | `tests/e2e/plans.spec.ts` (project `plans-dev`) — SELF-SKIPS without the live Local model; NOT in the CI chain until its tape is recorded on the GPU box (docs/plans-carriage.md § Tapes) |
153
+ | P11 | The browser plane RUNS in the chain | `/plan <message>` → a real `exit_plan_mode` call → the host's review card opened and answered by a CLICK → fork → the child's ONE seeded event calls the plan out, the chapter carries the block, the artifact holds the plan byte-for-byte, and the folded `planMode:` is `inactive` because approving EXITS plan mode | `tests/e2e/plans.spec.ts` = chain project `plans` (eighth), replayed from `tests/fixtures/model-tape/plans/` in 13 s with no LLM. **Provenance: that tape was distilled from the scripted upstream `tests/e2e/stub-model.ts`, not the live model** (the box's `:8080` dropped the stream mid-journey) — `tests/fixtures/model-tape/plans/PROVENANCE.md` says precisely what it does and does not prove, plus the re-record command that retires the stub. The stub's own turn policy is pinned by `tests/unit/stub-model.test.ts` (4 rows): a stub that answers wrongly records a tape of a session that never planned |
154
154
 
155
155
  ## The Cache Metric, Before You Trust a Number
156
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/dsh-chapters",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "dsh plugin: verbatim chapter archive + zero-token index for small-context models",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -45,7 +45,7 @@
45
45
  "build": "tsc -p tsconfig.json && tsdown -c tsdown.client.config.ts",
46
46
  "build:client": "tsdown -c tsdown.client.config.ts",
47
47
  "test:unit": "node --test tests/unit/*.test.ts",
48
- "test:e2e": "PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=suite playwright test -c tests/e2e/playwright.config.ts --project=suite && E2E_PORT=41733 E2E_THRESHOLD=0.5 E2E_FLOOR=1000000 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=heavy playwright test -c tests/e2e/playwright.config.ts --project=heavy && E2E_PORT=41732 E2E_THRESHOLD=0.9 E2E_FLOOR=500 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=arrival playwright test -c tests/e2e/playwright.config.ts --project=arrival && E2E_PORT=41734 E2E_THRESHOLD=0.9 E2E_ENRICH=1 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=enrich playwright test -c tests/e2e/playwright.config.ts --project=enrich && E2E_PORT=41736 E2E_THRESHOLD=0.75 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=fanout playwright test -c tests/e2e/playwright.config.ts --project=fanout && E2E_PORT=41735 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=rules playwright test -c tests/e2e/playwright.config.ts --project=rules && E2E_PORT=41737 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=fork playwright test -c tests/e2e/playwright.config.ts --project=fork",
48
+ "test:e2e": "PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=suite playwright test -c tests/e2e/playwright.config.ts --project=suite && E2E_PORT=41733 E2E_THRESHOLD=0.5 E2E_FLOOR=1000000 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=heavy playwright test -c tests/e2e/playwright.config.ts --project=heavy && E2E_PORT=41732 E2E_THRESHOLD=0.9 E2E_FLOOR=500 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=arrival playwright test -c tests/e2e/playwright.config.ts --project=arrival && E2E_PORT=41734 E2E_THRESHOLD=0.9 E2E_ENRICH=1 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=enrich playwright test -c tests/e2e/playwright.config.ts --project=enrich && E2E_PORT=41736 E2E_THRESHOLD=0.75 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=fanout playwright test -c tests/e2e/playwright.config.ts --project=fanout && E2E_PORT=41735 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=rules playwright test -c tests/e2e/playwright.config.ts --project=rules && E2E_PORT=41737 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=fork playwright test -c tests/e2e/playwright.config.ts --project=fork && E2E_PORT=41738 E2E_THRESHOLD=0.9 PLAYWRIGHT_BROWSERS_PATH=$PWD/var/ms-playwright E2E_TAPE=plans playwright test -c tests/e2e/playwright.config.ts --project=plans",
49
49
  "test:all": "npm test && npm run test:e2e",
50
50
  "test:integration": "node --test tests/integration/*.test.ts",
51
51
  "test:e2e:record": "E2E_MODEL=record npm run test:e2e",