dsh-context-compression-improved 0.1.1 → 0.2.0

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.
Files changed (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +84 -81
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,582 @@
1
+ # Repair log
2
+
3
+ Cross-version ledger of defects that break the **install / boot** path. Every branch and
4
+ version must leave its verdict here (affected / unaffected / fixed) before it ships.
5
+
6
+ Read this file before starting a new line (see `CONTRIBUTING.md`). A defect class already
7
+ recorded here is expected to be *re-checked*, not re-discovered.
8
+
9
+ - One defect, one `D#` id.
10
+ - One entry must contain: symptom / root cause / evidence / affected surface / fix / verification.
11
+ - Evidence means a command and its observed output, not a description of the code.
12
+
13
+ Defects that ship to the **settings surface** rather than the boot path use the `U#` series and
14
+ keep the same six-part shape. They are recorded here because they are reported the same way —
15
+ from a real machine, by someone who cannot tell a gate from a bug.
16
+
17
+ ---
18
+
19
+ ## D1 — Host plugin entry statically imports a sibling package
20
+
21
+ **Symptom.** `dsh web` aborts while loading the plugin tree:
22
+
23
+ ```text
24
+ failed to import loader entry context-compression-improved-bundle
25
+ (dsh-context-compression-improved): Cannot find package
26
+ 'dsh-context-compression-improved-runtime' imported from
27
+ <profile>/node_modules/dsh-context-compression-improved/packages/selector/lib/index.js
28
+ code: 'ERR_MODULE_NOT_FOUND'
29
+ ```
30
+
31
+ **Root cause.** The repository was installed whole via a git dependency, so pnpm only
32
+ installs what the **root** manifest declares. The root manifest declared no
33
+ `dependencies`, so the sibling package was never materialized — and
34
+ `packages/selector/lib/index.js:3` uses a top-level static `import` of it, which throws
35
+ during module evaluation.
36
+
37
+ **Cascade.** The Host entry never loads → `settings.register('context-compression', …)`
38
+ never runs → the browser `SettingsScopeSnapshot.writable` stays at its initial `false` →
39
+ every control in the settings panel renders `disabled`. The panel itself is still visible
40
+ because the client bundle is resolved from `dsh.client` metadata and is unaffected by this
41
+ failure.
42
+
43
+ **Evidence (committed artifacts, not a working tree).**
44
+
45
+ | Branch | `packages/selector/lib/index.js` |
46
+ | --- | --- |
47
+ | `feat/ctx-preset-v2` @ `455f74f` | `L3: import { … } from "dsh-context-compression-improved-runtime";` — the crash site |
48
+ | `compat/0.1.5` @ `d7c592d` | no top-level import; only `modulePath("…-runtime", import.meta.resolve("…-runtime"))` |
49
+
50
+ **Affected.** `feat/ctx-preset-v2` (the crash itself).
51
+
52
+ **Fix.** Merge into a single package: the entry imports `./runtime/config.ts` from inside
53
+ its own package.
54
+
55
+ **Verification.** `grep` over `lib/**/*.js` must find no
56
+ `from "dsh-context-compression-improved-runtime"`. The frozen provenance literal (D5 note
57
+ below) is the only remaining occurrence and is required.
58
+
59
+ **Status.** Fixed on the merge branch.
60
+
61
+ ---
62
+
63
+ ## D2 — Root manifest missing the install contract
64
+
65
+ **Symptom.** `dsh plugin add github:<owner>/<repo>` completes, yet the profile gains no
66
+ usable plugin: no bundle row, no resolvable entry.
67
+
68
+ **Root cause.** A git install reads the **root** manifest. Without `main` / `exports` /
69
+ `dsh.client.inject` / `dsh.bundle.patch` there is nothing for the harness to mount; and if
70
+ the root name is still a private workspace name with `"private": true`, pnpm installs a
71
+ private package nothing can load.
72
+
73
+ **Evidence.**
74
+
75
+ | Branch | Root manifest |
76
+ | --- | --- |
77
+ | `feat/ctx-preset-v2` @ `455f74f` | correct name, has `main`/`exports`/`dsh`; but `dependencies` **empty**, `exports` missing `./pruner` and `./invariant` |
78
+ | `compat/0.1.5` @ `d7c592d` | `dsh-context-compression-improved-workspace`, `private: true`, **no** `main`/`exports`/`dsh`/`dependencies` |
79
+ | `main` / `baseline/pre-god-module-split` @ `e337bf5` | same as compat, plus **zero** `lib/` artifacts committed |
80
+
81
+ **Affected.** `compat/0.1.5`, `baseline/pre-god-module-split`, `main` (fully);
82
+ `feat/ctx-preset-v2` (partially: empty dependencies, incomplete exports).
83
+
84
+ `baseline/pre-god-module-split` is a pre-split baseline, not a distribution: it has no
85
+ install contract and no built artifacts. Do not use it as a rollback target.
86
+
87
+ **Fix.** The root manifest carries the install contract: `name`, `main`, `types`,
88
+ `exports`, `dependencies`, `dsh`. `dependencies` lists the real third-party runtime
89
+ dependencies (`@huggingface/tokenizers`, `js-yaml`).
90
+
91
+ **Verification.** `git show <branch>:package.json` field-by-field; after a real
92
+ `dsh plugin add`, confirm `node_modules/<pkg>` exists and contains `lib/index.js`.
93
+
94
+ **Status.** Fixed on the merge branch for `dependencies` / `./pruner`. `./invariant` was
95
+ added to the root `exports` by the closure commit — the root and the package manifest now
96
+ export the same four subpaths (`.`, `./invariant`, `./pruner`, `./client`). See the
97
+ verification ledger below for the command-level evidence.
98
+
99
+ ---
100
+
101
+ ## D3 — Build chunks not committed: the same crash under a different filename
102
+
103
+ **Symptom.** After the merge, the built entry starts with:
104
+
105
+ ```js
106
+ // packages/selector/lib/index.js:1
107
+ import { … } from "./config.js";
108
+ // packages/selector/lib/invariant.js:1
109
+ import { … } from "./tail-trim.js";
110
+ ```
111
+
112
+ tsdown splits a shared chunk out of every entry that reuses a module.
113
+
114
+ **Root cause.** This repository commits `lib/` because pnpm refuses a `prepare` script on a
115
+ git dependency, so consumers can only install prebuilt artifacts. After the merge the new
116
+ artifacts — `lib/config.js`, `lib/tail-trim.js`, `lib/pruner.js`, `lib/pruner.d.ts`,
117
+ `src/deepseek-v4-tokenizer.ts` — were **untracked** while `lib/index.js` already imported
118
+ them. Pushing in that state makes a git install fetch an entry whose chunk does not exist:
119
+ the identical `ERR_MODULE_NOT_FOUND`, only the specifier changes from the sibling package
120
+ to `./config.js`.
121
+
122
+ A git install fetches exactly the **tracked** tree, so `pnpm test:e2e:packed` cannot catch
123
+ this: `npm pack` builds from the working tree and happily includes untracked files.
124
+
125
+ **Evidence.**
126
+
127
+ - `git ls-files packages/selector/lib` listed only the seven pre-merge files.
128
+ - `git status --porcelain` showed the five untracked artifacts above.
129
+ - Before the gate was added, `pnpm verify:release` printed
130
+ `release verification: OK` — the release gate did not cover the artifact graph.
131
+ - Contrast with history, showing this is merge-introduced: on `feat/ctx-preset-v2` and
132
+ `compat/0.1.5`, `packages/runtime/lib/tail-trim.js` is committed, and the selector entry
133
+ is self-contained with no relative imports.
134
+
135
+ **Affected.** Any release built from the merged single-package layout, i.e. every future
136
+ version of this repository. Treat it as a permanent gate, not a one-off fix.
137
+
138
+ **Fix.**
139
+ 1. Commit `lib/**` together with the sources, including every chunk and entry.
140
+ 2. Gate it: every relative import in a shipped artifact must (a) resolve to an existing
141
+ file, (b) be covered by the package `files` allowlist (packed install), and (c) be
142
+ git-tracked (git install).
143
+
144
+ The gate is scoped to the **import graph**. Unreferenced build by-products such as
145
+ `lib/style.css` are excluded: its rules are already carried inline by `lib/client.js`
146
+ (the `\0dsh-context-compression-css:` region), nothing in the repository reads the file,
147
+ and the `files` allowlist deliberately drops it from the tarball. Requiring it would mean
148
+ widening the allowlist to ship dead weight.
149
+
150
+ **Verification.** `scripts-dist/verify-release.js` (compiled from `scripts/verify-release.ts`). Observed:
151
+
152
+ ```text
153
+ # untracked chunk present
154
+ Error: release verification: packages/selector/lib/config.js is not committed;
155
+ a git install would fetch an incomplete artifact graph
156
+
157
+ # after git add (the gate reads the index, so staging is enough)
158
+ release verification: OK
159
+ ```
160
+
161
+ **Status.** Gate implemented and verified red → green. The root-cause fix is the
162
+ `git add`, applied in the merge commit.
163
+
164
+ ---
165
+
166
+ ## D4 — Consumer profile leftovers
167
+
168
+ **Symptom.** Even with the package fixed, a profile can still install the wrong thing or
169
+ fail to install.
170
+
171
+ **Evidence (`web` profile).**
172
+
173
+ - `package.json` depended on `…-selector-workspace` at
174
+ `github:<owner>/<repo>#baseline/pre-god-module-split` — a spec **without**
175
+ `&path:/packages/selector`, which resolves to the private workspace root and installs
176
+ nothing usable.
177
+ - `pnpm-workspace.yaml` kept an `overrides` entry pointing at the now-deleted
178
+ `packages/runtime` path.
179
+ - `node_modules` held nothing but the useless workspace package.
180
+
181
+ **Affected.** Any profile that installs this plugin.
182
+
183
+ **Fix.** Remove the stale dependency and the dead override; install by the single-package
184
+ spec (no `&path:` needed once the root manifest is the install face).
185
+
186
+ **Correction.** An earlier analysis claimed `allowBuilds` for `@huggingface/tokenizers` was
187
+ a blocker. `@huggingface/tokenizers@0.1.3` has no `install`/`postinstall` script — `dist/`
188
+ ships prebuilt — and the repository's own packed E2E writes a consumer workspace manifest
189
+ of just `packages: []`. Adding the `allowBuilds` entry is precautionary, not required.
190
+
191
+ **Status.** Delivered as a manual script; not executed by the maintainer agent.
192
+
193
+ ---
194
+
195
+ ## D5 — `compat/0.1.5` predates the install contract
196
+
197
+ **Verdict: it must inherit.** `compat/0.1.5` @ `d7c592d` is four commits behind
198
+ `feat/ctx-preset-v2` (`0b20bf2`, `cd03575`, `4635d19`, `455f74f`), so what it needs is not
199
+ merely the merge but the whole install contract:
200
+
201
+ 1. the root manifest contract (D2) — it currently cannot be mounted at all;
202
+ 2. the single-package merge (structural fix for D1) — it is still two packages, with
203
+ neither manifest declaring the other as an installable dependency, so a git install
204
+ cannot materialize the runtime package: the same root cause, a different presentation;
205
+ 3. the artifact-graph gate (D3) and the profile hygiene (D4).
206
+
207
+ Its entry lacks a top-level cross-package import only because it resolves the runtime
208
+ package lazily through `import.meta.resolve`. That is why it fails as "no bundle to load"
209
+ instead of a load-time throw. Same disease, different symptom — do not read it as
210
+ "unaffected".
211
+
212
+ **Replay, not merge.** `compat/0.1.5` carries 0.1.5-rc.2-only adaptations (client slot
213
+ names, `engines.dsh`, pinned overrides, lazy service resolution, estimator host route,
214
+ `/api` route prefix). A `git merge` would drag the old-line adaptations back, so the merge
215
+ is replayed file by file on that branch instead.
216
+
217
+ **Status.** Not started.
218
+
219
+ ---
220
+
221
+ ## D6 — Consumer profile blocks dependency reconciliation (host precondition)
222
+
223
+ **Scope note.** This is *not* a defect of this repository. It is recorded here because it
224
+ blocks the real-machine verification step that every branch needs, and because CI is
225
+ structurally blind to it.
226
+
227
+ **Symptom.** `pnpm install` in the `web` profile fails, at a **different path on every run**:
228
+
229
+ ```text
230
+ cannot create directory at "...\node_modules\katex\node_modules\commander_pacquet-stage_…":
231
+ 拒绝访问。 (os error 5)
232
+
233
+ failed to remove existing directory "...\node_modules\better-sqlite3" prior to swap:
234
+ 拒绝访问。 (os error 5)
235
+ ```
236
+
237
+ Progress advances each run (`added 151 → 159 → 205`). The install is not wedged — it is
238
+ walking a list of blockers, one per run.
239
+
240
+ **Root cause — two independent sources of `os error 5`. Do not conflate them.**
241
+
242
+ 1. **ACL denies write**, at exactly one directory: `node_modules\katex\node_modules` lists
243
+ only `BUILTIN\Users: ReadAndExecute` and does not name the interactive user at all.
244
+ 2. **A mapped native module is held by a live process.** `better-sqlite3` grants the
245
+ interactive user `FullControl` on both itself and its nested `node_modules`, yet deletion
246
+ still fails. `session-query-sqlite` is enabled and `dsh web` is running, so
247
+ `better-sqlite3\build\Release\better_sqlite3.node` is mapped into that process; Windows
248
+ returns `ERROR_ACCESS_DENIED` when asked to delete a mapped image. No ACL change fixes
249
+ this one — only stopping the process does.
250
+
251
+ **Criterion warning — never test this by ownership.** Measured here:
252
+
253
+ | criterion | flagged |
254
+ | --- | --- |
255
+ | owner is not the interactive user | **211** |
256
+ | the interactive user actually lacks write | **0** of 269 package dirs, plus **1** nested |
257
+
258
+ Ownership is `BUILTIN\Administrators` almost everywhere (the profile was evidently produced
259
+ by one elevated install), while the interactive user holds explicit `FullControl` in nearly
260
+ all of them. An ownership-based fix would take ownership of 211 directories to solve one.
261
+ Test the **effective write right** instead.
262
+
263
+ **Affected.** Every real-machine verification on this host, on every branch — including the
264
+ `compat/0.1.5` replay, which cannot be validated anywhere else.
265
+
266
+ **Fix.** Stop `dsh web`; grant write on the one genuinely denied directory; re-run
267
+ `pnpm install --no-frozen-lockfile`; restart. Delivered as
268
+ `DSH-ccp-单包修复-04-node_modules依赖收敛阻塞.ps1` — diagnose / repair / re-verify, dry-run
269
+ capable, and it never stops the service for you.
270
+
271
+ **Verification.** The script's `-DryRun` reports one true target and names the one path.
272
+
273
+ **Status. Resolved** in the maintenance window. The consumer ran a plain `pnpm install` in
274
+ the profile and it completed: `Packages: +256 -16`, `Progress: resolved 256, reused 256`,
275
+ `Done in 11.1s using pnpm v12.4.1`, exit 0.
276
+
277
+ **Resolution evidence (read-only re-check after the install):**
278
+
279
+ | Check | Result |
280
+ | --- | --- |
281
+ | `pnpm-lock.yaml` references `dsh-context-compression-selector-workspace` | **none** — lockfile and `package.json` agree again |
282
+ | `node_modules\dsh-context-compression*` | exactly one directory, `dsh-context-compression-improved` |
283
+ | `*_pacquet-stage_*` residue | none |
284
+ | plugin `packages/selector/lib/**` | all ten artifacts present, `invariant.js` 3927 B — byte-size identical to the source build |
285
+ | `dsh.profile.bundles` row | `dsh-context-compression-improved` still listed |
286
+
287
+ Two things follow for the record. First, the lockfile inconsistency tracked here was a
288
+ **symptom** of the failed installs, not a separate defect: once one install succeeded, it
289
+ cleared itself — no manual lockfile surgery was needed. Second, the blockage cleared **by
290
+ retrying**, without the elevated ACL repair. That does not retire the finding — the `katex`
291
+ ACL hole is still real and will bite the next replacement of that subtree — but it does mean
292
+ the earlier per-run progress (`added 151 → 159 → 205`) was the decisive mechanism: each
293
+ attempt converged further and the last one finished. Record it as *cleared by convergence*,
294
+ not as *fixed by the script*; `-04` remains the tool of record if a future install stalls on
295
+ the same signature.
296
+
297
+ ---
298
+
299
+ ## D7 — The estimator catalog route never registered: a detached method lost `this`
300
+
301
+ **Symptom.** The settings card's host-route dropdowns stayed empty. An authenticated
302
+ `GET /api/dsh-context-compression-improved/estimator-catalog` answered 404 while a sibling
303
+ plugin's `/api/dsh-perm-gate/receiver` answered 200 in the same breath.
304
+
305
+ **Root cause.** `asWebServer` duck-typed the `webServer` service by pulling `register` off it
306
+ and returning a fresh wrapper:
307
+
308
+ ```ts
309
+ const register = value?.register
310
+ return { register } // `this` is now the wrapper
311
+ ```
312
+
313
+ `dsh-host-webserver`'s `register` reads its own route tables:
314
+
315
+ ```js
316
+ const table = route.kind === "exact" ? this.exact : this.prefixes;
317
+ if (table.has(route.path)) … // L178 — `table` is undefined, TypeError
318
+ ```
319
+
320
+ `this` was the wrapper, so `this.exact` / `this.prefixes` were `undefined`, the host threw
321
+ inside `register`, and the plugin's catch-all swallowed it. **The helper had never once
322
+ registered a route since it was written.**
323
+
324
+ **Why it stayed invisible.** On the 0.1.2 host the `dsh web` terminal prints no plugin
325
+ `ctx.logger` output at all — a full boot produced 1350 bytes containing only Node's
326
+ experimental warning and two `dsh web:` lines — and a plugin-load failure travels the same
327
+ logger. A swallowed throw and a plugin that quietly did nothing were observationally
328
+ identical. Once the diagnostic moved to `console` (as `dsh-perm-gate` already does on this
329
+ host), the real frame appeared:
330
+
331
+ ```
332
+ TypeError: Cannot read properties of undefined (reading 'has')
333
+ at Object.register (dsh-host-webserver/lib/index.js:178:13)
334
+ at Object.apply (cordis/lib/index.js:120:36)
335
+ ```
336
+
337
+ **Fix.** `return value as WebServerLike` — pass the service itself; `dsh-perm-gate` works
338
+ for exactly this reason.
339
+
340
+ **Verification.** Cordis does **not** bind service methods: on a bare `Context`, both a
341
+ detached call and a wrapped one lose `this`. On the real host, after installing this fix,
342
+ `/api/dsh-context-compression-improved/estimator-catalog` answers **200** with a full
343
+ catalog (four provider groups), `/endpoint/…` answers 200, perm-gate stays 200, and a
344
+ garbage path stays 401.
345
+
346
+ **Guard.** `packages/selector/tests/estimator-route-registration.host.spec.ts` now mounts a
347
+ stand-in whose `register` reads its tables off `this`, mirroring the host. Against the
348
+ previous implementation it fails **3 of 5** cases with `expected [] to deeply equal […]` —
349
+ the same empty route table the host exhibited. The earlier version of that stand-in recorded
350
+ routes in a closure, so it could never have caught this.
351
+
352
+ **Withdrawn hypotheses.** Everything this ledger recorded before the fix about the cause —
353
+ narrowing the injection gate, isolation scope, and moving to the `connection` service — was
354
+ a false trail produced by the silence. Two of them are worth keeping as *non*-causes:
355
+ isolation is opt-in and this row never opted in, and `connection.rpc` is the wrong transport
356
+ here for reasons the project's own `upgrade-pitfalls` §2.1 records independently.
357
+
358
+ ---
359
+
360
+ ## U1 — The estimator card vanished off TokenPilot-inspired instead of explaining its gate
361
+
362
+ **Symptom.** With any profile other than TokenPilot-inspired selected, the Settings page showed
363
+ no estimator section at all. A reader who had configured nothing could not tell whether the
364
+ feature was missing, broken, or gated — the first real-machine report of Defect A was exactly
365
+ this, and it was filed alongside D7 even though the two have nothing in common.
366
+
367
+ **Root cause.** The render was `current !== 'tokenpilot-inspired' ? null : <EstimatorControls …/>`.
368
+ The condition is right: `runtime/config.ts` merges `presetOptions` over the tokenpilot-inspired
369
+ defaults alone (`mergePresetOptions`), and `resolvePolicy(config, 'balanced').presetOptions` is
370
+ `undefined`, so no other profile can carry an estimator channel. What was wrong is that the gate
371
+ was *rendered as nothing*. The profile card that unlocks the section sits elsewhere on the page,
372
+ and the one element that would have named it was the section being hidden — a gate the reader
373
+ cannot see is indistinguishable from an absent feature.
374
+
375
+ **Fix.** Render `EstimatorInactiveNotice` in place of the controls: same `<section>`, same
376
+ `#context-compression-estimator-title` heading anchor, one paragraph naming the current profile
377
+ and the profile that unlocks the card. Losing the heading was half the defect, so the heading
378
+ stays. **The gate is unchanged** — no estimator control exists off TokenPilot-inspired, and no
379
+ other profile gains `presetOptions`.
380
+
381
+ **Evidence.** `packages/selector/tests/estimator-channel.client.spec.tsx` mounts the section with
382
+ `profile: 'balanced'` and asserts the anchor still reads `Estimator (optional)`, that the notice
383
+ contains the current profile label and `Select TokenPilot-inspired`, and that no channel select,
384
+ no provider input and no `input[list]` exist. Counter-proof: substituting `null` for the notice
385
+ turns that case red (`expected undefined to be 'Estimator (optional)'`) while the other six stay
386
+ green — the guard fails against the pre-fix behaviour, so it is not vacuous.
387
+
388
+ **Affected surface.** Client bundle only: `src/client/CompressionProfileSelector.tsx`,
389
+ `src/client/locales.ts` (both dictionaries; `en` satisfies the full key set), `lib/client.js`,
390
+ `lib/client.d.ts`. No runtime, config, or persistence change, and no change to
391
+ `presetOptions` semantics.
392
+
393
+ **Still open.** The save affordance remains unexplained in the UI — fields commit on change or
394
+ blur with only a transient busy state, so "did that save?" has no answer on screen. That is a
395
+ separate, larger change (explicit save button plus three-state feedback) and is not fixed here.
396
+
397
+ ---
398
+
399
+ ## Verification ledger — `feat/ctx-preset-v2` closure
400
+
401
+ Closure = `935d501` + the root-`exports` completion (`./invariant`). The working tree held
402
+ exactly that one manifest change; `lib/**` rebuilt byte-identically, so the build is
403
+ deterministic.
404
+
405
+ ### Ten gates
406
+
407
+ | # | Command | Result |
408
+ | --- | --- | --- |
409
+ | 1 | `pnpm install` | exit 0 |
410
+ | 2 | `pnpm build` | exit 0 |
411
+ | 3 | `pnpm typecheck` | exit 0 |
412
+ | 4 | `pnpm lint` | exit 0 |
413
+ | 5 | `pnpm test` | exit 1 — nondeterministic Windows set, see below |
414
+ | 6 | `pnpm test:built` | exit 0 |
415
+ | 7 | `pnpm verify:release` | exit 0 |
416
+ | 8 | `pnpm pack:dry-run` | exit 0 |
417
+ | 9 | `pnpm test:e2e:packed` (`DSH_E2E_MODE=dev`) | exit 0 |
418
+ | 10 | `pnpm test:e2e:packed` (release mode) | exit 0 — upgrade leg skipped until the first publish, see below |
419
+
420
+ ### Gate 5 — the failure set is nondeterministic; do not cite it as "4 known failures"
421
+
422
+ A single `pnpm test` run is not a baseline. Eight interleaved runs (four at `455f74f`,
423
+ four at the closure commit), same machine, nothing else running:
424
+
425
+ | Run | `455f74f` failed | closure failed |
426
+ | --- | --- | --- |
427
+ | 1 | 3 | 5 |
428
+ | 2 | 3 | 2 |
429
+ | 3 | 4 | 3 |
430
+ | 4 | 3 | 4 |
431
+
432
+ A ninth run, taken while a packed-install E2E ran concurrently, reported **9** failures at
433
+ `455f74f` — the same commit that reported 3 in three other runs. The count tracks machine
434
+ load, not code.
435
+
436
+ Per test name, over the four quiet runs on each side:
437
+
438
+ | Test | `455f74f` | closure |
439
+ | --- | --- | --- |
440
+ | `preset-overlay.host.spec.ts` > uses owner-only files and starts a new generation after source content changes | 4/4 | 4/4 |
441
+ | `standing-generation.host.spec.ts` > keeps one fully-identical generation under concurrent composition of the same identity | 3/4 | 3/4 |
442
+ | `standing-generation.host.spec.ts` > keeps one generation under concurrent and repeated composition of the same identity | 3/4 | 3/4 |
443
+ | `standing-generation.host.spec.ts` > separates colliding equal-size generations on a whole-second metadata surface before publish | 3/4 | 3/4 |
444
+ | `standing-generation.host.spec.ts` > switches the standing generation for an equal-length source change at a fixed threshold | 0/4 | 1/4 |
445
+
446
+ Four of the five fail on both sides at identical frequency: pre-existing. The fifth failed
447
+ once in four runs on the closure side and never on the baseline side, inside the same
448
+ timing-sensitive `describe` block as three tests that are 3/4 flaky on *both* sides. It is
449
+ recorded as **not excluded**, not as clean.
450
+
451
+ The visible causes are Windows-only: an owner-only permission assertion (`0o700` against
452
+ `0o666`), `fs.rename` `EPERM` during publish under concurrency, and mtime-window
453
+ assertions that assume a coarser clock.
454
+
455
+ **Consequence for review.** Any criterion phrased as "the failure set matches the baseline
456
+ item by item" is unsatisfiable — the baseline has no single failure set. Compare
457
+ *ever-failed* sets over at least three interleaved runs per side instead.
458
+
459
+ ### Gate 10 — a defect in this repository, not an external precondition
460
+
461
+ Release mode builds the upgrade leg of its fixture from the *published* previous release.
462
+ The fixture named that predecessor as the pinned literal `0.1.0-beta.2`, a version that only
463
+ ever existed under this package's pre-rename name: it 404s under the published name and
464
+ appears nowhere in the repository's 12-revision history. Every release-mode run therefore
465
+ aborted with `release gate requires the published previous release for the upgrade leg:
466
+ packument responded 404`.
467
+
468
+ This was recorded here as an external precondition. That was wrong. The literal was a
469
+ fixture defect — exactly the hardcoded-version class this repository's own doctrine names —
470
+ and it is fixed: the predecessor is now whatever the registry actually offers, resolved by
471
+ publication order from `packument.time` (not semver order, which avoids a semver dependency
472
+ and handles prereleases), and `predecessorFound` keys both release-mode gates instead of a
473
+ hardcoded version.
474
+
475
+ | Registry state | `upgradeLeg` | Release outcome |
476
+ | --- | --- | --- |
477
+ | package unpublished (404) | `skipped-package-not-published` | pass, skip recorded |
478
+ | published, no other version | `skipped-no-earlier-release` | pass, skip recorded |
479
+ | published, earlier version exists | `installed` | pass only when the leg installed |
480
+ | any other fetch or tarball failure | — | throws |
481
+
482
+ Only the third row runs the leg. Where a predecessor exists the gate stays fail-closed: a
483
+ leg reporting anything other than `installed` throws. Where none exists the skip is never
484
+ silent — it is emitted as `UPGRADE_LEG_SKIPPED <reason>` in the report, and
485
+ `officialCloneSmoke` falls back to that marker rather than passing vacuous on a package that
486
+ has never shipped.
487
+
488
+ This is a one-time bridge. Once the first version is published, `predecessorFound` is true
489
+ on every subsequent run and the upgrade leg is a hard requirement again — which is the
490
+ state the gate was always meant to enforce.
491
+
492
+ ### Criterion withdrawn
493
+
494
+ The earlier merge plan carried "the overlay identity hash is unchanged across the merge".
495
+ That is unsatisfiable by construction: identity is `sha256(preset.id ‖ source ‖
496
+ JSON.stringify({modules, autoCompactThresholdPercent}))` computed over **absolute** module
497
+ paths, so collapsing two packages into one necessarily changes it. The replacement is
498
+ structural — `canonicalCompressionRows` stays byte-identical and the only permitted
499
+ difference in the generated YAML is the `tool-result-pruner` row's `name` resolving to the
500
+ merged package.
501
+
502
+ ### `invariant.ts` literal decision
503
+
504
+ The merged package keeps **one** invariant companion, and it is the runtime's: `name =
505
+ 'context-compression-selector-runtime-invariant'`, `PACKAGE_NAME =
506
+ 'dsh-context-compression-improved-runtime'`. Both stay verbatim.
507
+
508
+ - The string is the frozen provenance literal (inheritance rule 4) and is written into
509
+ durable session logs, so renaming it would discard historical tail-trim entries.
510
+ - The companion's real checks (`validatePublishedTailTrim`, `sessionEvents`) belong to the
511
+ runtime, so the runtime identity is the semantically correct one.
512
+ - The pre-merge `packages/selector/src/invariant.ts` companion
513
+ (`client-ui-context-compression-selector-invariant`, `PACKAGE_NAME =
514
+ 'dsh-context-compression-improved'`) installed **no** checks (`install = () => {}`) and
515
+ was dropped by the merge. Nothing is lost: its only effect was reserving a name in the
516
+ invariant registry, which is keyed by the installing package either way.
517
+
518
+ ---
519
+
520
+ ## Affected-surface matrix
521
+
522
+ | Branch / version | D1 entry import | D2 install contract | D3 artifact chunks | D4 profile | Verdict |
523
+ | --- | --- | --- | --- | --- | --- |
524
+ | `feat/ctx-preset-v2` @ `455f74f` | **present (crash)** | partial | n/a before merge | needed | affected; D1/D2 fixed on merge branch |
525
+ | merged single package | eliminated | fixed (all four subpaths) | **gated** | delivered | this change |
526
+ | `compat/0.1.5` @ `d7c592d` | no (lazy) | **absent** | n/a | needed | **inherits the whole contract** |
527
+ | `baseline/pre-god-module-split`, `main` @ `e337bf5` | no | **absent, no `lib/`** | n/a | needed | not distributable by design |
528
+ | any future single-package release | structurally impossible | — | **permanent gate** | — | inherits D3 |
529
+ | every branch on this host | — | — | — | — | was **blocked by D6**; the profile install now completes, so real-machine verification is unblocked |
530
+
531
+ ### D7 per branch — the fix must travel one way only
532
+
533
+ `asWebServer` has two different bodies across the branches, and they are not equivalent:
534
+
535
+ | Branch | `asWebServer` returns | Verdict |
536
+ | --- | --- | --- |
537
+ | `compat/0.1.5` @ `d7c592d` | `value as WebServerLike` — the service itself | **correct; never had D7** |
538
+ | `feat/ctx-preset-v2` @ `e588f1c`, `ts/0.1.2+` @ `0eb5183` | `{ register }` — a detached method | **carries D7** |
539
+ | `baseline/pre-god-module-split`, `main` @ `e337bf5` | no such helper | n/a |
540
+
541
+ The defect was introduced on the V2 line, not inherited from the 0.1.x line. That inverts the
542
+ usual direction of these hand-offs: **the 0.1.5 replay must not copy this helper out of
543
+ `feat/ctx-preset-v2`.** Take `compat`'s body, or the fixed one from `00afcfc`; they agree.
544
+
545
+ ## Inheritance rules
546
+
547
+ 1. **The root manifest is the install contract**: `name`, `main`, `types`, `exports`,
548
+ `dependencies`, `dsh` — all six. A new subpath export is declared in both the root and
549
+ the package manifest.
550
+ 2. **No top-level cross-package import inside one package.** `lib/**/*.js` may only
551
+ reference the frozen provenance literal.
552
+ 3. **`lib/**` ships in the same commit as its sources**, and every chunk an entry imports
553
+ must be committed. Enforced by `pnpm verify:release`.
554
+ 4. **The provenance literal `dsh-context-compression-improved-runtime` is frozen.** It is
555
+ already written into durable session logs (`source.plugin` on tail-trim manifests) and
556
+ validated on read, so renaming it silently discards historical tail-trim entries. This
557
+ covers every occurrence, including `src/invariant.ts`'s `PACKAGE_NAME` and companion
558
+ `name` — the merge does **not** rename them to match the merged package name.
559
+ 5. **Profile-side hygiene**: install spec that actually resolves, no dead `overrides`. And
560
+ before trusting any profile-side install result: reconcile dependencies **with the host
561
+ stopped**, and test a directory's write right — never its owner (D6).
562
+ 6. **Never detach a method off a host service.** Duck typing that returns `{ register }`
563
+ instead of the service loses `this`, and `dsh-host-webserver.register` reads its route
564
+ tables off `this`. Pass the service object itself; `dsh-perm-gate` does, which is why it
565
+ serves its routes. The same trap applies to any service whose methods touch instance
566
+ state, so check the contract before narrowing a service to a single method (D7).
567
+ 7. **A conditional the user cannot see is a defect, not a design.** If a section renders only
568
+ under some profile, mode, or capability, the hidden branch must say what is missing and what
569
+ would restore it — and keep its heading and anchor id so the panel is still findable where
570
+ the reader last saw it. Gating *semantics* are not what is on trial here; hiding the *reason*
571
+ is (U1).
572
+
573
+ ## Change log
574
+
575
+ | Date | Entry | Action |
576
+ | --- | --- | --- |
577
+ | 2026-09-14 | D1–D5 | Ledger created; D1 fixed; D2 fixed for `dependencies`/`./pruner`; D3 gate added and verified; D4 delivered as a script; D5 recorded for `compat/0.1.5` |
578
+ | 2026-09-14 | D2 | `./invariant` added to the root `exports`; root and package manifests now agree. Ten-gate closure ledger added, with the gate-5 flakiness evidence and the withdrawn identity-hash criterion |
579
+ | 2026-09-14 | D6 | Profile dependency-reconciliation blocker diagnosed: two independent `os error 5` sources (one ACL-denied directory; mapped native modules held by the live host), plus the ownership-vs-write-right criterion warning. Delivered as a dry-runnable script |
580
+ | 2026-09-15 | D6 | **Resolved.** One plain `pnpm install` in the profile converged (`+256 -16`, exit 0); the lockfile repointed itself and the `_pacquet-stage_` residue is gone. Cleared by convergence over successive attempts, not by the ACL repair — the `katex` denial stays on record |
581
+ | 2026-09-15 | D7 | **The estimator catalog route had never registered at all.** `asWebServer` detached `register` from the service, `this` became the wrapper, the host threw inside `register`, and a catch-all swallowed it. Fixed by passing the service itself; 200 verified on the real host; the injection, isolation and transport hypotheses recorded earlier are withdrawn |
582
+ | 2026-09-15 | U1 | **The estimator card was hidden by its own gate.** Off TokenPilot-inspired the section rendered `null`, so the reader saw a missing feature rather than a gated one. The heading and anchor are now kept and the hidden branch names the profile that unlocks the card; the gate itself is unchanged. Guard added with a counter-proof; the save-affordance question stays open |
@@ -0,0 +1,30 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import tseslint from 'typescript-eslint'
4
+
5
+ export default tseslint.config(
6
+ {
7
+ ignores: ['**/node_modules/**', '**/lib/**', '**/dist/**', '**/scripts-dist/**', '**/coverage/**', 'docs/**'],
8
+ },
9
+ js.configs.recommended,
10
+ ...tseslint.configs.recommended,
11
+ {
12
+ files: ['packages/**/*.{ts,tsx}', 'scripts/**/*.{js,mjs,ts}', 'eslint.config.js'],
13
+ languageOptions: {
14
+ globals: { ...globals.browser, ...globals.node },
15
+ },
16
+ rules: {
17
+ '@typescript-eslint/no-unused-vars': ['error', {
18
+ argsIgnorePattern: '^_',
19
+ varsIgnorePattern: '^_',
20
+ caughtErrorsIgnorePattern: '^_',
21
+ destructuredArrayIgnorePattern: '^_',
22
+ }],
23
+ },
24
+ },
25
+ {
26
+ // Terminal normalization intentionally matches ANSI/BEL control characters.
27
+ files: ['packages/selector/src/runtime/reducers.ts'],
28
+ rules: { 'no-control-regex': 'off' },
29
+ },
30
+ )