@jayyuen66/dsh-plugin-shared 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -11,8 +11,8 @@
11
11
  <a href="https://www.npmjs.com/package/@jayyuen66/dsh-plugin-shared"><img src="https://img.shields.io/npm/d18m/@jayyuen66/dsh-plugin-shared?style=flat-square" alt="npm downloads (total)" /></a>
12
12
  <a href="https://github.com/JayYuen666/dsh-plugin-shared/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/JayYuen666/dsh-plugin-shared/ci.yml?branch=main&style=flat-square&label=ci" alt="ci" /></a>
13
13
  <img src="https://img.shields.io/badge/license-MIT-4c1?style=flat-square" alt="MIT" />
14
- <img src="https://img.shields.io/badge/node-%3E%3D22.12-4c1?style=flat-square&logo=nodedotjs&logoColor=white" alt="node" />
15
- <img src="https://img.shields.io/badge/dsh-%5E0.2.1--rc.3-658EFF?style=flat-square" alt="dsh" />
14
+ <img src="https://img.shields.io/badge/node-%3E%3D24.9-4c1?style=flat-square&logo=nodedotjs&logoColor=white" alt="node" />
15
+ <img src="https://img.shields.io/badge/dsh-%5E0.2.2--rc.1-658EFF?style=flat-square" alt="dsh" />
16
16
  <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by_dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered by dsh" /></a>
17
17
  </p>
18
18
 
@@ -31,24 +31,29 @@
31
31
 
32
32
  ## <img src="https://api.iconify.design/lucide/file-text.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Contents
33
33
 
34
- - [What it does](#what-it-does)
35
- - [Install](#install)
36
- - [Verify it is live](#verify-it-is-live)
37
- - [What it does not provide](#what-it-does-not-provide)
38
- - [Modules](#modules)
39
- - [The config exports](#the-config-exports)
40
- - [Known boundaries](#known-boundaries)
41
- - [Data and privacy](#data-and-privacy)
42
- - [Versioning and compatibility](#versioning-and-compatibility)
43
- - [FAQ](#faq)
44
- - [Development](#development)
45
- - [License](#license)
34
+ - [ Contents](#-contents)
35
+ - [ What it does](#-what-it-does)
36
+ - [What belongs here](#what-belongs-here)
37
+ - [ Install](#-install)
38
+ - [Dependency requirements](#dependency-requirements)
39
+ - [Shared version floor](#shared-version-floor)
40
+ - [What ships](#what-ships)
41
+ - [ Verify it is live](#-verify-it-is-live)
42
+ - [ What it does not provide](#-what-it-does-not-provide)
43
+ - [ Modules](#-modules)
44
+ - [ The config exports](#-the-config-exports)
45
+ - [ Known boundaries](#-known-boundaries)
46
+ - [ Data and privacy](#-data-and-privacy)
47
+ - [ Versioning and compatibility](#-versioning-and-compatibility)
48
+ - [ FAQ](#-faq)
49
+ - [ Development](#-development)
50
+ - [ License](#-license)
46
51
 
47
52
  ## <img src="https://api.iconify.design/lucide/shield.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> What it does
48
53
 
49
- - **It is a library, not a plugin.** `package.json` carries no `dsh` field and the package has no `cordis.patch.yml`, so a copy in `node_modules` is never registered as a plugin. There is no settings card, no client side and no route of its own.
54
+ - **It is a library plus one client row, not a standalone bundle.** The host half is imported by the plugins; `package.json` declares `dsh.client` and exports `./client`, which each consumer enables from its own `cordis.patch.yml` with an id-less entry. It carries no `dsh.bundle.patch`, so installing the package alone adds no profile layer and no settings card or route. The id-less entry keeps the row out of the plugin page's per-component switches: switching it off would leave every settings card without its component supplier.
50
55
  - **It holds the parts the plugins repeated verbatim:**
51
- - The tool-event record table (`./lib/tool-events`): one scan from `SessionEvent[]` to call and result tables, PTC sub-calls included, plus `editPathOf` for the edit targets the record can see.
56
+ - The tool-event record table (`./lib/tool-events`): one scan from `SessionEvent[]` to call and result tables, PTC sub-calls included, plus `editPathOf` for the edit targets the record can see and `applyPatchTargetsOf` / `editPathsOf` for every write target in a multi-file `apply_patch` input, parsed by the official `dsh-fs-patch`.
52
57
  - Client card base components (`./lib/card-apply`, `./lib/card-diff`): an apply idempotency guard across HMR, and a form-value diff that treats `null`, `undefined` and an absent key as the same value.
53
58
  - Runtime utilities (`./lib/project-key`, `./lib/locale`, `./lib/lesson-bus`, `./lib/text`, `./lib/record`, `./lib/errors`, `./lib/jsonl`, `./lib/job-outcome`): the small logic every plugin would otherwise rewrite.
54
59
  - Build-time and check baselines (`./lib/canonicalize-region-paths`, `./config/*`): artifact path normalization plus the oxlint, vitest and tsconfig starting points for every consumer.
@@ -78,8 +83,8 @@ End users do not install or enable it separately: any consumer package pulls it
78
83
 
79
84
  | Requirement | Value |
80
85
  | --- | --- |
81
- | Node.js | `>=22.12.0` — the publish-side floor the plugin family shares, so a package declaring the same `engines.node` never rejects this library |
82
- | dsh | `^0.2.1-rc.3 || ^0.2.2-alpha.2`, declared as an **optional** peer (importing this package does not need a host) |
86
+ | Node.js | `>=24.9.0` — the publish-side floor the plugin family shares, so a package declaring the same `engines.node` never rejects this library |
87
+ | dsh | `^0.2.2-rc.1`, declared as an **optional** peer (importing this package does not need a host) |
83
88
  | Module format | ESM only. `require()` works only where Node supports `require(esm)` |
84
89
  | Tree shaking | `"sideEffects": false` |
85
90
 
@@ -87,12 +92,13 @@ End users do not install or enable it separately: any consumer package pulls it
87
92
 
88
93
  ### Dependency requirements
89
94
 
90
- `dependencies` is empty; every `@deepseek-ai/*` entry lives only in `peerDependencies` and `devDependencies`. `test/publish-manifest.test.ts` enforces this, and the reason is a boot-time failure rather than style:
95
+ `dependencies` contains exactly one exception, `@deepseek-ai/dsh-fs-patch`; every other `@deepseek-ai/*` entry lives only in `peerDependencies` and `devDependencies`. `test/publish-manifest.test.ts` enforces this, and the reason is a boot-time failure rather than style:
91
96
 
97
+ - **`@deepseek-ai/dsh-fs-patch` is the one allowed runtime dependency.** It is a stateless pure parser: it holds no service instance or module-level identity, so a duplicate copy cannot split a `Symbol`. Shipping it makes `apply_patch` syntax resolvable on every host in the supported window. Every other host package stays in peer/dev.
92
98
  - **A host package under `dependencies` disables the consumer's host.** Profiles install with `nodeLinker: hoisted` and `autoInstallPeers: false`, so a listed host package is materialized as a real directory next to the copy the host process already loaded. `@deepseek-ai/dsh-tools` keys its tool-runtime scheduler with a plain `Symbol` (not `Symbol.for`), so symbol identity is per module instance: the host reads the scheduler from the duplicate and gets `undefined`, and every native tool call throws. Build, typecheck and tests pass up to that point; the invariant has to be stated in the manifest.
93
99
  - **`@deepseek-ai/dsh-output-retention` is the one required peer.** It is a value import (`truncateEnd`), provided by the host at runtime, and marked `"optional": false` in `peerDependenciesMeta` rather than left implicit — a peer left out of that table is a hard requirement for npm 7+.
94
- - **The remaining peers are optional.** Except for the bare `@deepseek-ai/dsh` peer, which only declares the host line, they carry type declarations only. `devDependencies` holds the exact development line, including `@deepseek-ai/dsh-llm` and `@deepseek-ai/dsh-util-values`, whose types the local PTC declarations name, and `@deepseek-ai/dsh-tool-executor-ptc`, whose types only the parity check reads. Profiles never install peers, so no duplicate appears.
95
- - **Version requirements separate the install gate from the build baseline.** Every `@deepseek-ai/dsh-*` peer carries the same window as `engines.dsh`, and that window names two host lines: a caret's prerelease comparison only covers its own patch tuple, so a single `^0.2.1-rc.3` does not admit `0.2.2-alpha.*` — measured with this workspace's semver, `satisfies('0.2.2-alpha.3','^0.2.1-rc.3')` is false and `{includePrerelease:true}` turns it true, and that flag is exactly what the host's own comparison passes. The cost is measured and belongs here: npm resolves each peer on its own, while the host family pins its own line inside every member (a member of the `0.2.1-rc` line asks cordis `~4.0.5`, the `0.2.2-alpha.2` host line asks `~4.0.6-alpha.1`, and `0.2.2-alpha.3` asks `~4.0.6-alpha.2`), so a consumer that pins the older host line can still stop at `ERESOLVE`, while the same tree resolves on the newest line. The union also admits later prereleases in the same `0.2.2` tuple, such as `0.2.2-rc.1`; that is range semantics, not a compatibility certification. The verified baseline for this release remains exactly `0.2.2-alpha.3`; moving to `rc.1` requires moving the dev pins and re-running the host parity check. pnpm — the installer this workspace supports, and the one behind the host's `dsh plugin add` — resolves both shapes and only prints a peer warning. The loading gate is unaffected either way, because the host compares `engines.dsh` with `includePrerelease: true`. `devDependencies` pin the exact line this repository compiles and tests against (`0.2.2-alpha.3`) and must stay uniform: one `dsh-session` version per tree keeps the nominal types recognizable. `@deepseek-ai/cordis` stays exact in `devDependencies` (`4.0.6-alpha.2`) and its peer range `^4.0.5||^4.0.6-alpha.1` covers both host lines, because one package cannot mix lines the way the host family does: alpha.2 requires `~4.0.6-alpha.1`, alpha.3 requires `~4.0.6-alpha.2`, and the range also admits `4.1.0`. The cordis union carries no spaces around `||`; the install-matrix probe reads one specifier per line now, while the dsh-family window keeps spaces. `test/publish-manifest.test.ts` checks each of these separately.
100
+ - **The remaining peers are optional.** Except for the bare `@deepseek-ai/dsh` peer, which only declares the host line, most carry type declarations only. Two subpath-scoped value peers are the exceptions: `react` is value-imported by `./lib/card-ui`, and `@deepseek-ai/dsh-typert-protocol` by `./lib/remote-marker`; both stay optional, so only a consumer of that subpath needs the package present. `devDependencies` holds the exact development line, including `@deepseek-ai/dsh-llm` and `@deepseek-ai/dsh-util-values`, whose types the local PTC declarations name, and `@deepseek-ai/dsh-tool-executor-ptc`, whose types only the parity check reads. Profiles never install peers, so no duplicate appears.
101
+ - **Version requirements separate the install gate from the build baseline.** Every `@deepseek-ai/dsh-*` peer carries the same window as `engines.dsh` — `^0.2.2-rc.1`, a single host line. A caret's prerelease comparison only covers its own patch tuple, so `^0.2.2-rc.1` admits `0.2.2-rc.1`, later `0.2.2` prereleases and `0.2.2`/`0.2.x`, but not the older `0.2.1` line or `0.2.2-alpha.*`; the host's own comparison passes `{includePrerelease:true}`, which is what lets same-tuple prereleases participate. The cost is measured and belongs here: npm resolves each peer on its own, while the host family pins its own line inside every member (`0.2.2-rc.1` asks cordis `~4.1.0` and schemastery `~3.19.0`), so a consumer that pins an older host line can still stop at `ERESOLVE`, while the same tree resolves on the supported line. pnpm — the installer this workspace supports, and the one behind the host's `dsh plugin add` — resolves both shapes and only prints a peer warning. The loading gate is unaffected either way, because the host compares `engines.dsh` with `includePrerelease: true`. `devDependencies` pin the exact line this repository compiles and tests against (`0.2.2-rc.2`) and must stay uniform: one `dsh-session` version per tree keeps the nominal types recognizable. `@deepseek-ai/cordis` stays exact in `devDependencies` (`4.1.0`); its peer range `^4.0.5||^4.0.6-alpha.1` is retained unchanged and admits `4.1.0`. The cordis union carries no spaces around `||`; the install-matrix probe reads one specifier per line now. `test/publish-manifest.test.ts` checks each of these separately.
96
102
  - Known upstream limitation: `@deepseek-ai/dsh-llm`'s declarations import `@deepseek-ai/dsh-attachment`, which that package does not declare. It is reached through `dsh-session`, so a consumer running with `skipLibCheck: false` sees `TS2307` from that package, not from this one. The published `./lib/tool-events` declaration also names `@deepseek-ai/dsh-util-values`, which is reached through `dsh-session` / `dsh-tools`; a consumer running with `skipLibCheck: false` must have that package resolvable as well.
97
103
 
98
104
  ### Shared version floor
@@ -106,19 +112,19 @@ A consumer profile can lock this package with a pnpm `overrides` entry. Lock it
106
112
  - `publishConfig` carries `access` and `registry` and nothing else. Entry fields there are a trap: pnpm merges them onto the top level when publishing and npm does not, so one publisher would ship a package whose entry points name files that are not in the tarball.
107
113
  - `build` deletes `dist/` first, then emits the runtime artifacts, the config artifacts, the type declarations and the generated barrel declaration. Both builders minify; `test/dist-size.test.ts` keeps the runtime and config artifacts inside their budgets.
108
114
  - `prepare` runs `build`. It does not run when a consumer installs from the registry; it exists so packing, publishing and workspace links always ship a fresh `dist/`.
109
- - Adding a module means touching five places: the barrel in `lib/index.ts`, `exports`, the `facets` array in `build-host.mjs`, `include` in `tsconfig.build.json`, and a `test/<module>.test.ts`. The consistency checks in Development fail if one is missed.
115
+ - Adding a runtime module means touching the barrel in `lib/index.ts`, `exports`, the `facets` array in `build-host.mjs`, `include` in `tsconfig.build.json`, and a `test/<module>.test.ts`. A subpath-only runtime module (like `remote-marker` or `card-ui`) is registered in `entries` and marked barrel-exempt instead; a build-time module (like `decorator-lowering`) goes into `buildFacets`. The consistency checks in Development fail if one is missed.
110
116
 
111
117
  ## <img src="https://api.iconify.design/lucide/circle-check.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> Verify it is live
112
118
 
113
119
  1. **A subpath resolves.** From the consuming package, `node -e 'import("@jayyuen66/dsh-plugin-shared/lib/record").then((m) => console.log(typeof m.isRecord))'` prints `function`. `ERR_MODULE_NOT_FOUND` means the dependency is not installed; `does not provide an export named ...` means the installed copy is older than the subpath you import.
114
- 2. **The barrel resolves.** The same check against the bare package name returns the runtime modules. `canonicalize-region-paths` is the one module deliberately absent from the barrel; import it by subpath when a build script needs it.
120
+ 2. **The barrel resolves.** The same check against the bare package name returns the runtime modules. `canonicalize-region-paths` (build-time), `remote-marker` (value-imports typert), and `card-ui` (client-only) are deliberately absent from the barrel; import them by subpath.
115
121
  3. **A consumer host boots.** Start the profile that declares the plugin. A boot log containing `N entries did not activate` and `does not provide an export named ...` means the profile's `overrides` version is below the version that contains the subpath the consumer imports — raise the version, do not patch the plugin.
116
122
  4. **Local source edits reach the consumer.** Consumers load `dist/`, not `lib/`. Run `pnpm run build` after editing; `test/build-host.test.ts` compares the runtime artifacts on disk against an in-memory build and fails when they are stale. "The code changed but nothing happened" is almost always a stale artifact, not a caching bug.
117
123
 
118
124
  ## <img src="https://api.iconify.design/lucide/circle-slash.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> What it does not provide
119
125
 
120
- - **No plugin registration.** No `dsh` field, no `cordis.patch.yml`, no layer in `--dump-config`; `dsh plugin add` is the wrong action.
121
- - **No settings card or client side.** The package has no browser entry and no client-side state.
126
+ - **No bundle declaration.** It declares `dsh.client` for the client row but no `dsh.bundle.patch`; a copy in `node_modules` is not a profile layer by itself. Consumers enable the row from their own patch.
127
+ - **No settings card of its own.** The client row exports card components; the package registers no card, owns no client state and serves no route.
122
128
  - **No HTTP route and no request-trust checks.** The host's `/api` gateway owns authentication, the Host / Origin checks and body limits for the plugins that do register a route.
123
129
  - **No CLI subcommand and no model tool.**
124
130
  - **No domain judgement.** It does not know which tool counts as an edit, which path is secret, how large a budget is, or when a check should fail. Those belong to the calling plugin.
@@ -129,11 +135,13 @@ The subpath column is the in-package specifier; value imports prefix it with the
129
135
 
130
136
  | Subpath | Provides |
131
137
  | --- | --- |
132
- | `.` | Barrel over the runtime modules below. `canonicalize-region-paths` deliberately stays out, so a runtime consumer does not add an import for it; the client-only `card-ui` stays out for the same reason plus one more: the root barrel must not bring `react` into every consumer. |
133
- | `./lib/tool-events` | `scanToolEvents` returns the `calls` / `results` tables, PTC sub-calls included; `toolEventRowsOf` handles a single event and already carries normalized `arguments` and `badArguments`. `editPathOf` returns a `kind`-discriminated `EditTarget` with only `write` / `read-view`; the path is `undefined` when it cannot be read. Record fields are `readonly`. Re-exports the official `SessionEvent`; `PtcDispatchStartEventData` / `PtcDispatchEventData` are declared in this package and augment `SessionEventMap` locally, because the host moved that augmentation between packages inside the supported window. |
138
+ | `.` | Barrel over the runtime modules below, served from `dist/plugin.js`: the same names plus `name` and a no-op `apply`, so the host loader can also take this package as a row. `canonicalize-region-paths` (build-time), `remote-marker` (value-imports typert), and `card-ui` (client-only) deliberately stay out; the root barrel must not pull an unused package into every consumer. |
139
+ | `./lib/tool-events` | `scanToolEvents` returns the `calls` / `results` tables, PTC sub-calls included; `toolEventRowsOf` handles a single event and already carries normalized `arguments` and `badArguments`. `editPathOf` returns a `kind`-discriminated `EditTarget` with only `write` / `read-view`; the path is `undefined` when it cannot be read. `applyPatchTargetsOf` parses the ordered operations of an `apply_patch` input with the official `dsh-fs-patch`; `editPathsOf` expands every write target, including both sides of a move. Record fields are `readonly`. Re-exports the official `SessionEvent`; `PtcDispatchStartEventData` / `PtcDispatchEventData` are declared in this package and augment `SessionEventMap` locally, because the host moved that augmentation between packages inside the supported window. |
134
140
  | `./lib/card-apply` | `claimApply` + `CardApplyCtx`: an apply idempotency guard built on an **own** flag on `globalThis` (a symbol, or `true` written by an outside holder) with `ctx.effect` cleanup. The flag is process-wide, so a second context on the same page is rejected by design. |
135
141
  | `./lib/card-diff` | `diffTouched`: which touched fields really differ from the snapshot, compared by form value. `undefined`, `null` and an absent key are one value; arrays and objects compare element by element; cycles terminate. |
136
- | `./lib/card-ui` | Client-only settings-card kit: `CardForm`, `Section`, `FieldGrid`, `StatusBar`, `Field`, `FieldLabel`, `InfoTip`, `ToggleField`, `NumberField`, `TextField`, `ListField`, `SelectField`, `SaveBar`, `splitHint` and the `CARD_UI_CSS` string. Built with `createElement` only; `react` is an optional peer and the subpath never enters the root barrel. React 18 runtime APIs only (`useId`, `useState`, `useEffect`, refs). |
142
+ | `./lib/card-ui` | Client-only settings-card kit: `CardForm`, `Section`, `FieldGrid`, `StatusBar`, `Field`, `FieldLabel`, `InfoTip`, `ToggleField`, `NumberField`, `TextField`, `ListField`, `SelectField`, `SaveBar`, `splitHint` and the `CARD_UI_CSS` string. Built with `createElement` only; `react` is an optional peer and the subpath never enters the root barrel. The declared peer floor is `^19.3.0`; the kit uses only APIs that already existed in React 18 (`useId`, `useState`, `useEffect`, refs). |
143
+ | `./client` | Browser client row: the `__ModuleLoader__` bundle the host module graph serves once. Consumers externalize this specifier and enable the row from their own patch with an id-less entry. |
144
+ | `./client-node` | Node/ESM face of the same client surface, used by tests and type-aware Node consumers. |
137
145
  | `./lib/project-key` | `deriveProjectKey` + `ProjectKeyOptions`: cwd → `last-segment-<first 8 hex of sha256(norm)>`, with `default` for a non-string, an empty string or a normalized root. `realpathSync` resolves symlinks; a failure falls back to the resolved path. Windows `\` and `/` produce the same key. |
138
146
  | `./lib/locale` | `Locale` (an alias of the official `BuiltInLocaleId`), `DEFAULT_LOCALE`, `resolveLocale`, `resolveLocalePreference`, `messagesFor`, `MessagesCatalog`, plus `LOCALE_SETTINGS_NAMESPACE` / `LOCALE_PREFERENCE_FIELD`. Host-side dictionaries only; the client uses the official locale package. |
139
147
  | `./lib/lesson-bus` | `settleLessonCall`: routes synchronous throws and asynchronous rejections into one `onFailure` exit. Function-valued thenables count as awaitable; an `onFailure` that itself throws or rejects is handled in the same place, so it never splits into "thrown at the caller" or "unhandled rejection". |
@@ -142,9 +150,13 @@ The subpath column is the in-package specifier; value imports prefix it with the
142
150
  | `./lib/errors` | `errorText` follows the official `@deepseek-ai/dsh-llm` `errorChain` semantics (cause chain, `AggregateError` members, empty message → `name`, cross-realm `message`, malicious getters render as a placeholder) in a zero-dependency local implementation. `isSettingsConflict` recognizes the host's settings-conflict pattern from `code` / `name`, so a cross-realm error is still recognized. |
143
151
  | `./lib/jsonl` | `shrinkJsonlTail`: pure decision logic for JSONL tail shrinking. The trigger policy and error handling stay with the caller; the byte budget is the caller's responsibility to keep finite. |
144
152
  | `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome` map a finished subprocess onto the official job registry's outcome; `killProc` + `KillableProcess` release a child handle and ignore a `kill()` that throws or rejects (for example, on an already-reaped process). |
153
+ | `./lib/remote-marker` | `markRemote(prototype, methodName)`: the programmatic equivalent of the official `@Remote` method decorator. It applies the same decorator function to a hand-built TC39 context, so a source file can avoid decorator syntax and needs no lowering pipeline. It value-imports `@deepseek-ai/dsh-typert-protocol` (an optional peer) and stays out of the root barrel. |
154
+ | `./lib/decorator-lowering` | Build/test helper for standard decorators: `needsDecoratorLowering`, `lowerStandardDecorators`, `rolldownDecorators`, `viteDecorators`. It runs the caller-injected `tsc` entry on one file with `--noResolve`, preserves module specifiers, returns code plus a source map, and (on the Vite side) prunes injected plumbing from coverage mappings. Build-time only, never in the runtime barrel. |
145
155
  | `./lib/canonicalize-region-paths` | Build-time string helper that rewrites rolldown's `//#region <path>` markers into a `process.cwd()`-independent form. Exported as a subpath so build scripts can import it without putting it in the runtime barrel. |
146
156
 
147
- `card-ui` is the one runtime module that is deliberately subpath-only: adding it to the barrel would make every host-side importer resolve `react`.
157
+ `card-ui` (value-imports `react`) and `remote-marker` (value-imports `@deepseek-ai/dsh-typert-protocol`) are the runtime modules that are deliberately subpath-only: putting them in the barrel would make every host-side importer resolve a package it does not use.
158
+
159
+ The client row is enabled by the consuming plugin, not by installing this package alone. The consumer writes `- name: "@jayyuen66/dsh-plugin-shared"` without an `id`; the host still loads it, while the plugin page does not expose it as a separately disableable component. An `id` would add that switch, and switching it off would break every settings card that requires the row's components.
148
160
 
149
161
  ## <img src="https://api.iconify.design/lucide/sliders-horizontal.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> The config exports
150
162
 
@@ -153,7 +165,7 @@ These are lint/test baselines for plugin authors, not runtime code. They pull in
153
165
  | Subpath | Provides | You must install |
154
166
  | --- | --- | --- |
155
167
  | `./config/oxlint` | `definePluginConfig`, `makeTestFileOverrides`, `OFF_JUSTIFICATIONS` and the shared rule baseline | `oxlint`, `eslint-plugin-sonarjs`, `eslint-plugin-regexp`, `eslint-plugin-zod` (requires zod 4), `@e18e/eslint-plugin` |
156
- | `./config/vitest.base` | `definePackageConfig`: four 100% coverage thresholds, a 20-second test timeout, the shared coverage exclusions and the test include globs | `vitest` and `@vitest/coverage-v8` (the baseline sets `coverage.provider: "v8"`) |
168
+ | `./config/vitest.base` | `definePackageConfig`: four 100% coverage thresholds, a 20-second test timeout, the shared coverage exclusions, the test include globs, and the `@jayyuen66/dsh-plugin-shared/client` → `@jayyuen66/dsh-plugin-shared/client-node` alias (the UMD row reads `window` at load time, so a Node test cannot execute it) | `vitest` and `@vitest/coverage-v8` (the baseline sets `coverage.provider: "v8"`) |
157
169
  | `./config/tsconfig.base.json`, `./config/tsconfig.client.base.json` | Plain JSON for `extends` | Nothing |
158
170
 
159
171
  Those toolchain packages are **not** declared as `peerDependencies`: npm 7+ tries to satisfy optional peers too, and `oxlint`'s own `peerOptional vite-plus` locks `vitest` to a version disjoint from `>=5.0.2`, which made a plain `npm install` of this package fail with `ERESOLVE`. The requirement is stated here instead of in the manifest.
@@ -203,10 +215,10 @@ The package is imported into a host process; the table describes run time.
203
215
 
204
216
  | Question | Answer |
205
217
  | --- | --- |
206
- | Should I `dsh plugin add` it? | No. It has neither a config layer nor a client side, so there is nothing to enable. Declare it as a dependency of the plugin package. |
218
+ | Should I `dsh plugin add` it? | No. It declares `dsh.client` for the row consumers enable, but carries no `dsh.bundle.patch`, so adding it as a plugin installs no profile layer and enables nothing by itself. Declare it as a dependency of the plugin package, and insert the id-less row in your own `cordis.patch.yml`. |
207
219
  | A consumer boots with `does not provide an export named ...` and `N entries did not activate`. | The profile is serving an older copy of this package than the consumer imports. Raise the profile's `overrides` entry to a version that contains the subpath the consumer imports; check this package's `exports` and CHANGELOG. No per-repository CI run can see this state. |
208
220
  | I changed `lib/*.ts` and the consumer still runs the old behavior. | Consumers load `dist/`. Run `pnpm run build`; `test/build-host.test.ts` fails if the runtime artifact is stale. A workspace link does not bypass the artifact. |
209
- | Why is `@deepseek-ai/dsh-output-retention` a required peer while the rest are optional? | It is the only host package imported for value (`truncateEnd`). The rest are type-only, except the bare `@deepseek-ai/dsh` peer, which only declares the host line. |
221
+ | Why is `@deepseek-ai/dsh-output-retention` a required peer while the rest are optional? | It is the only **required** host package imported for value (`truncateEnd`). `@deepseek-ai/dsh-typert-protocol` is also value-imported, but only by `./lib/remote-marker`, so it stays optional; the remaining dsh peers are type-only. The bare `@deepseek-ai/dsh` peer only declares the host line, and `react` is a value peer for `./lib/card-ui`. |
210
222
  | Why isn't `oxlint` a `peerDependency`? | npm 7+ would try to satisfy it, and `oxlint`'s optional `vite-plus` peer locks `vitest` outside the range this baseline needs, producing `ERESOLVE` for a plain install. The requirement is documented instead. |
211
223
  | Want a helper added here? | First confirm the body repeats verbatim in two or more packages. Domain judgement stays in the plugins. |
212
224
 
@@ -216,7 +228,7 @@ The package is imported into a host process; the table describes run time.
216
228
  pnpm install
217
229
  pnpm run check # typecheck → lint → build → test → format check
218
230
  pnpm test # vitest with coverage
219
- pnpm run build # runtime artifacts, config artifacts, declarations
231
+ pnpm run build # runtime artifacts, config artifacts, declarations, client row
220
232
  ```
221
233
 
222
234
  - The suite splits by the class of defect a stub cannot see:
@@ -227,7 +239,7 @@ pnpm run build # runtime artifacts, config artifacts, declarations
227
239
  - **Log records**: `test/setup-logs.ts` takes over `console.*`; every `[shared/*]` line must match `test/log-templates.ts` or the run fails. `test/log-templates.test.ts` checks both directions, so adding or deleting a log without updating the record list fails.
228
240
  - **Published shape**: `test/publish-manifest.test.ts` requires every export target to be inside `files`, entry points to be artifacts rather than source, `publishConfig` to carry no entry fields, every peer to be stated in `peerDependenciesMeta`, every `dsh-*` development pin to be exact and uniform, every dsh peer range to equal `engines.dsh`, and the source, docs and artifacts to carry no dates, absolute paths, home-directory references or collaboration-only wording.
229
241
  - Coverage thresholds are **100%** across lines, statements, functions and branches, so a new branch needs a case that reaches it. `test/vitest-base.test.ts` refuses an empty `coverageInclude` because that would make the four thresholds pass over nothing.
230
- - CI runs the checks on Linux, Windows and macOS across a Node 22/24 matrix, with a weekly scheduled run to catch upstream changes. The install matrix packs with both npm and pnpm, installs into a clean directory without peer-resolution flags, imports every subpath and type-checks a small consumer import under both `skipLibCheck` settings.
242
+ - CI runs the checks on Linux, Windows and macOS on Node 24.9, with a weekly scheduled run to catch upstream changes. The install matrix packs with both npm and pnpm, installs into a clean directory without peer-resolution flags, imports every subpath and type-checks a small consumer import under both `skipLibCheck` settings.
231
243
 
232
244
  ## <img src="https://api.iconify.design/lucide/scale.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> License
233
245
 
package/README.zh-CN.md CHANGED
@@ -11,8 +11,8 @@
11
11
  <a href="https://www.npmjs.com/package/@jayyuen66/dsh-plugin-shared"><img src="https://img.shields.io/npm/d18m/@jayyuen66/dsh-plugin-shared?style=flat-square" alt="npm downloads (total)" /></a>
12
12
  <a href="https://github.com/JayYuen666/dsh-plugin-shared/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/JayYuen666/dsh-plugin-shared/ci.yml?branch=main&style=flat-square&label=ci" alt="ci" /></a>
13
13
  <img src="https://img.shields.io/badge/license-MIT-4c1?style=flat-square" alt="MIT" />
14
- <img src="https://img.shields.io/badge/node-%3E%3D22.12-4c1?style=flat-square&logo=nodedotjs&logoColor=white" alt="node" />
15
- <img src="https://img.shields.io/badge/dsh-%5E0.2.1--rc.3-658EFF?style=flat-square" alt="dsh" />
14
+ <img src="https://img.shields.io/badge/node-%3E%3D24.9-4c1?style=flat-square&logo=nodedotjs&logoColor=white" alt="node" />
15
+ <img src="https://img.shields.io/badge/dsh-%5E0.2.2--rc.1-658EFF?style=flat-square" alt="dsh" />
16
16
  <a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/powered_by_dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white" alt="powered by dsh" /></a>
17
17
  </p>
18
18
 
@@ -31,24 +31,29 @@
31
31
 
32
32
  ## <img src="https://api.iconify.design/lucide/file-text.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 目录
33
33
 
34
- - [它做什么](#它做什么)
35
- - [安装](#安装)
36
- - [确认它在正常工作](#确认它在正常工作)
37
- - [它不提供什么](#它不提供什么)
38
- - [模块清单](#模块清单)
39
- - [config 导出](#config-导出)
40
- - [已知边界](#已知边界)
41
- - [数据与隐私](#数据与隐私)
42
- - [版本与兼容](#版本与兼容)
43
- - [常见问题](#常见问题)
44
- - [开发](#开发)
45
- - [许可](#许可)
34
+ - [ 目录](#-目录)
35
+ - [ 它做什么](#-它做什么)
36
+ - [收录边界](#收录边界)
37
+ - [ 安装](#-安装)
38
+ - [依赖要求](#依赖要求)
39
+ - [shared 的版本下限](#shared-的版本下限)
40
+ - [随包发什么](#随包发什么)
41
+ - [ 确认它在正常工作](#-确认它在正常工作)
42
+ - [ 它不提供什么](#-它不提供什么)
43
+ - [ 模块清单](#-模块清单)
44
+ - [ config 导出](#-config-导出)
45
+ - [ 已知边界](#-已知边界)
46
+ - [ 数据与隐私](#-数据与隐私)
47
+ - [ 版本与兼容](#-版本与兼容)
48
+ - [ 常见问题](#-常见问题)
49
+ - [ 开发](#-开发)
50
+ - [ 许可](#-许可)
46
51
 
47
52
  ## <img src="https://api.iconify.design/lucide/shield.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 它做什么
48
53
 
49
- - **它是库,不是插件。** `package.json` 里没有 `dsh` 字段,包目录下也没有 `cordis.patch.yml`,所以位于 `node_modules` 的副本不会被宿主登记成插件。它没有设置卡,没有客户端部分,也没有自己的路由。
54
+ - **它是库加一条客户端行,不是独立组合包。** 宿主半由各插件 import;`package.json` 声明 `dsh.client` 并导出 `./client`,由各消费者在自己的 `cordis.patch.yml` 里以**无 id** 条目启用。它没有 `dsh.bundle.patch`,所以单独安装本包不会新增 profile 层、设置卡或路由。无 id 条目让插件页不把这条行列为可单独停用的组件:关掉它会让所有设置卡失去组件供应。
50
55
  - **它收录的是各插件逐字重复的部分:**
51
- - 工具事件记录表(`./lib/tool-events`):一次扫描把 `SessionEvent[]` 变成调用表与结果表,PTC 子调用一并合并;`editPathOf` 提取其中可见的编辑目标。
56
+ - 工具事件记录表(`./lib/tool-events`):一次扫描把 `SessionEvent[]` 变成调用表与结果表,PTC 子调用一并合并;`editPathOf` 提取其中可见的编辑目标,`applyPatchTargetsOf` / `editPathsOf` 用官方 `dsh-fs-patch` 解析多文件补丁并展开全部写目标。
52
57
  - 客户端卡片基础组件(`./lib/card-apply`、`./lib/card-diff`):跨 HMR 保证 apply 幂等的保护逻辑,以及把 `null`、`undefined` 与键缺失当成同一种值的表单差异比较。
53
58
  - 运行期工具(`./lib/project-key`、`./lib/locale`、`./lib/lesson-bus`、`./lib/text`、`./lib/record`、`./lib/errors`、`./lib/jsonl`、`./lib/job-outcome`):各个插件原本都要重写一遍的小逻辑。
54
59
  - 构建期与检查基线(`./lib/canonicalize-region-paths`、`./config/*`):产物路径规范化,加上给每个消费方的 oxlint、vitest 与 tsconfig 起点。
@@ -74,12 +79,12 @@ pnpm add @jayyuen66/dsh-plugin-shared
74
79
  import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
75
80
  ```
76
81
 
77
- 终端用户不需要单独安装或启用它:任一消费包安装时都会自动包含它。它缺失时的失败是 `ERR_MODULE_NOT_FOUND`(说明符指向 `@jayyuen66/dsh-plugin-shared/...`),不会静默变成其他行为。本包没有 default 导出;请从 barrel 或子路径使用具名导入。
82
+ 终端用户不需要单独安装或启用它:任一消费包安装时都会自动包含它。它缺失时的失败是 `ERR_MODULE_NOT_FOUND`(那条说明符指向 `@jayyuen66/dsh-plugin-shared/...`),不会静默变成其他行为。本包没有 default 导出;请从 barrel 或子路径使用具名导入。
78
83
 
79
84
  | 要求 | 值 |
80
85
  | --- | --- |
81
- | Node.js | `>=22.12.0`——插件家族共用的发布侧下限,声明同一个 `engines.node` 的包不会因为这个库而被拒 |
82
- | dsh | `^0.2.1-rc.3 || ^0.2.2-alpha.2`,声明为 **optional** peer(import 本包不需要宿主存在) |
86
+ | Node.js | `>=24.9.0`——插件家族共用的发布侧下限,声明同一个 `engines.node` 的包不会因为这个库而被拒 |
87
+ | dsh | `^0.2.2-rc.1`,声明为 **optional** peer(import 本包不需要宿主存在) |
83
88
  | 模块形态 | 仅 ESM。`require()` 只在支持 `require(esm)` 的 Node 上可用 |
84
89
  | Tree shaking | `"sideEffects": false` |
85
90
 
@@ -87,17 +92,18 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
87
92
 
88
93
  ### 依赖要求
89
94
 
90
- `dependencies` 为空;所有 `@deepseek-ai/*` 只出现在 `peerDependencies` 与 `devDependencies` 里。`test/publish-manifest.test.ts` 固定了这条要求,理由是一次启动期故障,不是风格偏好:
95
+ `dependencies` 只有 `@deepseek-ai/dsh-fs-patch` 一枚纯解析器例外;其余 `@deepseek-ai/*` 只出现在 `peerDependencies` 与 `devDependencies` 里。`test/publish-manifest.test.ts` 固定了这条要求,理由是一次启动期故障,不是风格偏好:
91
96
 
97
+ - **`@deepseek-ai/dsh-fs-patch` 是唯一允许的普通依赖。** 它是无状态纯解析器,不承载服务实例或模块级身份,多一份不会分裂 `Symbol`;随包安装是为了让宿主窗口内没有该包的版本也能解析 `apply_patch` 语法。其余宿主包仍然必须留在 peer/dev。
92
98
  - **宿主包写进 `dependencies` 会让消费方的宿主无法工作。** profile 用 `nodeLinker: hoisted` 与 `autoInstallPeers: false` 安装,于是列在 `dependencies` 里的宿主包会被安装成真实目录,与宿主进程已加载的那份并存。`@deepseek-ai/dsh-tools` 的工具运行期调度键是普通 `Symbol`(不是 `Symbol.for`),符号身份按模块实例计算:宿主从副本上读那个键得到 `undefined`,于是每一次原生工具调用都抛错。构建、类型检查与测试在此之前全部通过;这条不变式只能写在 manifest 上。
93
99
  - **`@deepseek-ai/dsh-output-retention` 是唯一必需 peer。** 它是值导入(`truncateEnd`),运行期由宿主提供;在 `peerDependenciesMeta` 里明写成 `"optional": false`,不靠"没写就等于必需",因为没写进那张表对 npm 7+ 就是硬要求。
94
- - **其余 peer 都是 optional。** 除裸 `@deepseek-ai/dsh`(只声明宿主版本线)外,它们只承载类型声明。`devDependencies` 保存精确的开发线,其中包括本包 PTC 声明所引用的 `@deepseek-ai/dsh-llm` 与 `@deepseek-ai/dsh-util-values`,以及只有对照断言会读其类型的 `@deepseek-ai/dsh-tool-executor-ptc`。profile 不装 peer,所以不会产生副本。
95
- - **版本要求把装载门与构建基线分开。** 每条 `@deepseek-ai/dsh-*` peer 与 `engines.dsh` 写同一条窗口,而这条窗口点明两条宿主线:caret 的预发布比较只吃自己那条 patch 元组,单写 `^0.2.1-rc.3` 收不进 `0.2.2-alpha.*`——用本工作区的 semver 实测 `satisfies('0.2.2-alpha.3','^0.2.1-rc.3')` 为 false,带 `{includePrerelease:true}` 才为 true,而宿主自己那道比较正是带这一档。代价是量出来写在这里的:npm 逐条 peer 独立解析,宿主家族每一枚包内部又把自己的那条线精确钉住(`0.2.1-rc` 线上的成员要求 cordis `~4.0.5`,`0.2.2-alpha.2` 宿主线要求 `~4.0.6-alpha.1`,`0.2.2-alpha.3` 要求 `~4.0.6-alpha.2`),所以消费者把宿主钉在旧那条线时仍可能止步 `ERESOLVE`,同一条树落在最新线上则可解。并集窗口也收得进同一 `0.2.2` 元组里更晚的预发布,例如 `0.2.2-rc.1`;那只是范围语义,不是兼容性取证。本版实际取证的基线仍是 `0.2.2-alpha.3`,要抬到 `rc.1` 必须同步移动 dev 钉版并重跑宿主对照断言。pnpm——本工作区支持、也是宿主 `dsh plugin add` 在用的安装器——两种形状都解得出,只打一条 peer 告警。装载门两边都不受影响。`devDependencies` 精确锁定本仓编译与测试用的那一线(`0.2.2-alpha.3`),且必须统一:一棵树里只有一份 `dsh-session`,nominal 类型才互相认得。`@deepseek-ai/cordis` 在 `devDependencies` 里精确锁定 `4.0.6-alpha.2`,peer 范围 `^4.0.5||^4.0.6-alpha.1` 同时覆盖 rc 线与 alpha 线——单独一枚包不存在宿主家族那种混线问题:alpha.2 要求 `~4.0.6-alpha.1`,alpha.3 要求 `~4.0.6-alpha.2`,该范围也收得下 `4.1.0`。cordis 这条并集在 `||` 两侧不带空格;类型面 peer 安装现已按行逐枚读取,dsh 家族窗口保留空格。`test/publish-manifest.test.ts` 分别检查这三件事。
100
+ - **其余 peer 都是 optional。** 除裸 `@deepseek-ai/dsh`(只声明宿主版本线)外,它们大多只承载类型声明;两个按子路径收窄的值导入 peer 是例外:`react` 被 `./lib/card-ui` 按值导入,`@deepseek-ai/dsh-typert-protocol` 被 `./lib/remote-marker` 按值导入。两者仍是 optional,只有取用对应子路径的消费方才需要它们在场。`devDependencies` 保存精确的开发线,其中包括本包 PTC 声明所引用的 `@deepseek-ai/dsh-llm` 与 `@deepseek-ai/dsh-util-values`,以及只有对照断言会读其类型的 `@deepseek-ai/dsh-tool-executor-ptc`。profile 不装 peer,所以不会产生副本。
101
+ - **版本要求把装载门与构建基线分开。** 每条 `@deepseek-ai/dsh-*` peer 与 `engines.dsh` 写同一条窗口——`^0.2.2-rc.1`,单条宿主线。caret 的预发布比较只吃自己那条 patch 元组,所以 `^0.2.2-rc.1` 收得进 `0.2.2-rc.1`、同元组更晚的预发布以及 `0.2.2`/`0.2.x`,收不进更旧的 `0.2.1` 线或 `0.2.2-alpha.*`;宿主自己那道比较带 `{includePrerelease:true}`,正是这一档让同元组预发布参与。代价是量出来写在这里的:npm 逐条 peer 独立解析,宿主家族每一枚包内部又把自己的那条线精确钉住(`0.2.2-rc.1` 要求 cordis `~4.1.0`、schemastery `~3.19.0`),所以消费者把宿主钉在旧线时仍可能止步 `ERESOLVE`,同一条树落在受支持的线上则可解。pnpm——本工作区支持、也是宿主 `dsh plugin add` 在用的安装器——两种形状都解得出,只打一条 peer 告警。装载门两边都不受影响,因为宿主比较 `engines.dsh` 时带 `includePrerelease: true`。`devDependencies` 精确锁定本仓编译与测试用的那一线(`0.2.2-rc.2`),且必须统一:一棵树里只有一份 `dsh-session`,nominal 类型才互相认得。`@deepseek-ai/cordis` 在 `devDependencies` 里精确锁定 `4.1.0`;peer 范围 `^4.0.5||^4.0.6-alpha.1` 保持不变,收得下 `4.1.0`。cordis 这条并集在 `||` 两侧不带空格;install-matrix 探针现已按行逐枚读取。`test/publish-manifest.test.ts` 分别检查这几件事。
96
102
  - 已知上游限制:`@deepseek-ai/dsh-llm` 的声明文件引用了它自己没声明的 `@deepseek-ai/dsh-attachment`。它经 `dsh-session` 传递进来,所以开 `skipLibCheck: false` 的消费方会看到来自那个包的 `TS2307`,不是来自本包。发布出去的 `./lib/tool-events` 声明还引用了 `@deepseek-ai/dsh-util-values`,它经 `dsh-session` / `dsh-tools` 传递进来;开 `skipLibCheck: false` 的消费方同样必须能解析到这个包。
97
103
 
98
104
  ### shared 的版本下限
99
105
 
100
- 消费者的 profile 可以用 pnpm `overrides` 把本包固定在某个版本。固定得低于某个 `./lib/...` 子路径首次出现的版本,宿主启动时就会报 `N entries did not activate` 与 `does not provide an export named ...`:包装得上、永远不加载,而任何单仓 CI 都看不见。要固定版本请先对照本包的 `exports` 与 CHANGELOG。
106
+ 消费者的 profile 可以用 pnpm `overrides` 把本包钉在某个版本。钉得低于某个 `./lib/...` 子路径首次出现的版本,宿主启动时就会报 `N entries did not activate` 与 `does not provide an export named ...`:包装得上、永远不加载,而任何单仓 CI 都看不见。要钉版本请先对照本包的 `exports` 与 CHANGELOG。
101
107
 
102
108
  ### 随包发什么
103
109
 
@@ -106,19 +112,19 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
106
112
  - `publishConfig` 只带 `access` 与 `registry`。把入口字段放在那里容易出错:pnpm 发布时会把它合并到顶层,npm 不会——换一个发布器就会发出一个入口指向 tarball 里不存在文件的包。
107
113
  - `build` 先删除 `dist/`,再产出运行期产物、配置产物、类型声明与生成的 barrel 声明。两个构建脚本都压缩;`test/dist-size.test.ts` 把运行期产物与配置产物各自限制在预算内。
108
114
  - `prepare` 指向 `build`。消费方从 registry 安装时它不执行;它存在是为了让打包、发布与 workspace link 拿到的都是新鲜 `dist/`。
109
- - 新增模块要同时改五处:`lib/index.ts` 的 barrel、`exports`、`build-host.mjs` 的 `facets` 数组、`tsconfig.build.json` 的 `include`,外加一份 `test/<模块>.test.ts`。漏一处会被「开发」一节的一致性检查判定失败。
115
+ - 新增运行期模块要同时改:`lib/index.ts` 的 barrel、`exports`、`build-host.mjs` 的 `facets` 数组、`tsconfig.build.json` 的 `include`,外加一份 `test/<模块>.test.ts`。只走子路径的运行期模块(如 `remote-marker`、`card-ui`)改登记进 `entries` 并在 barrel 检查里豁免;构建期模块(如 `decorator-lowering`)进 `buildFacets`。漏一处会被「开发」一节的一致性检查判定失败。
110
116
 
111
117
  ## <img src="https://api.iconify.design/lucide/circle-check.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 确认它在正常工作
112
118
 
113
119
  1. **子路径能解析。** 在消费包里运行 `node -e 'import("@jayyuen66/dsh-plugin-shared/lib/record").then((m) => console.log(typeof m.isRecord))'`,输出 `function`。`ERR_MODULE_NOT_FOUND` 表示依赖没装上;`does not provide an export named ...` 表示装到的那份比你 import 的子路径旧。
114
- 2. **barrel 能解析。** 换成裸包名做同一件事,应能取到运行期模块。`canonicalize-region-paths` 是唯一刻意不在 barrel 里的模块;构建脚本需要时按子路径取。
120
+ 2. **barrel 能解析。** 换成裸包名做同一件事,应能取到运行期模块。`canonicalize-region-paths`(构建期)、`remote-marker`(值导入 typert)与 `card-ui`(客户端专用)刻意不在 barrel 里;需要时按子路径取。
115
121
  3. **消费方宿主能启动。** 启动声明该插件的 profile。若启动日志出现 `N entries did not activate` 与 `does not provide an export named ...`,说明 profile 的 `overrides` 固定得比包含该子路径的版本低——调高固定版本,不要改插件。
116
122
  4. **本地源码改动能反映到消费方。** 消费方载入的是 `dist/`,不是 `lib/`。改完先运行 `pnpm run build`;`test/build-host.test.ts` 会把磁盘上的运行期产物与内存构建逐字节比对,产物过期即报错。"代码改了却没生效"多半是产物过期,不是缓存问题。
117
123
 
118
124
  ## <img src="https://api.iconify.design/lucide/circle-slash.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 它不提供什么
119
125
 
120
- - **不注册插件。** 没有 `dsh` 字段,没有 `cordis.patch.yml`,`--dump-config` 里没有它这一层;`dsh plugin add` 是错误动作。
121
- - **没有设置卡,也没有客户端部分。** 本包没有浏览器入口,也没有客户端状态。
126
+ - **不声明组合包。** 它为客户端行声明 `dsh.client`,但没有 `dsh.bundle.patch`;`node_modules` 里的副本本身不是 profile 层。客户端行由消费者自己的 patch 启用。
127
+ - **没有自己的设置卡。** 客户端行导出卡片组件;本包不注册卡片、不持有客户端状态,也没有路由。
122
128
  - **没有 HTTP 路由,也没有请求信任检查。** 真正注册路由的插件由宿主的 `/api` 网关负责认证、Host / Origin 校验与 body 上限。
123
129
  - **没有 CLI 子命令,也没有模型工具。**
124
130
  - **不做领域判定。** 它不知道哪些工具算编辑、哪条路径是密钥、预算多大、检查何时该报错。这些属于调用方的插件。
@@ -129,11 +135,13 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
129
135
 
130
136
  | 子路径 | 提供什么 |
131
137
  | --- | --- |
132
- | `.` | 下面列出的运行期模块的 barrel。`canonicalize-region-paths` 刻意不进,运行期消费方不必为它增加一次导入;客户端专用的 `card-ui` 同样不进,理由还多一条:根 barrel 不得把 `react` 带进每个消费方 |
133
- | `./lib/tool-events` | `scanToolEvents` 返回 `calls` / `results` 两张表,PTC 子调用一并合并;`toolEventRowsOf` 处理单个事件,交回的记录已带归一后的 `arguments` 与 `badArguments`。`editPathOf` 返回按 `kind` 判别的 `EditTarget`,只有 `write` / `read-view` 两个分支;路径取不到时为 `undefined`。记录字段一律 `readonly`。同时再导出官方 `SessionEvent`;`PtcDispatchStartEventData` / `PtcDispatchEventData` 在本包内声明并自行增强 `SessionEventMap`,因为宿主在支持窗口内换过承载这份增强的包 |
138
+ | `.` | 下面列出的运行期模块的 barrel,由 `dist/plugin.js` 提供:同样的导出名,再加 `name` 与一条空操作的 `apply`,宿主 Loader 因此也能把本包当作一行载入。`canonicalize-region-paths`(构建期)、`remote-marker`(值导入 typert)与 `card-ui`(客户端专用)刻意不进;根 barrel 不得把用不到的包带进每个消费方 |
139
+ | `./lib/tool-events` | `scanToolEvents` 返回 `calls` / `results` 两张表,PTC 子调用一并合并;`toolEventRowsOf` 处理单个事件,交回的记录已带归一后的 `arguments` 与 `badArguments`。`editPathOf` 返回按 `kind` 判别的 `EditTarget`,只有 `write` / `read-view` 两个分支;路径取不到时为 `undefined`。`applyPatchTargetsOf` 用官方 `dsh-fs-patch` 解析 `apply_patch` 的有序操作;`editPathsOf` 展开全部写目标(move 的源与目的都算)。记录字段一律 `readonly`。同时再导出官方 `SessionEvent`;`PtcDispatchStartEventData` / `PtcDispatchEventData` 在本包内声明并自行增强 `SessionEventMap`,因为宿主在支持窗口内换过承载这份增强的包 |
134
140
  | `./lib/card-apply` | `claimApply` + `CardApplyCtx`:保证 apply 幂等的保护逻辑,建立在 `globalThis` 上的**自有**标记(symbol,或外部写入的 `true`)加 `ctx.effect` 清理。标记是进程级的,所以同一页面的第二个 ctx 会被拒绝——这是有意的设计 |
135
141
  | `./lib/card-diff` | `diffTouched`:touched 层里哪些字段真的与快照不同,按表单值比较。`undefined`、`null` 与键缺失是同一种值;数组与对象逐元素比较;循环引用也能正常结束 |
136
- | `./lib/card-ui` | 客户端专用设置卡组件:`CardForm`、`Section`、`FieldGrid`、`StatusBar`、`Field`、`FieldLabel`、`InfoTip`、`ToggleField`、`NumberField`、`TextField`、`ListField`、`SelectField`、`SaveBar`、`splitHint` 与样式字符串 `CARD_UI_CSS`。只用 `createElement`;`react` 是 optional peer,该子路径永不进根 barrel。只用 React 18 运行时 API(`useId`、`useState`、`useEffect`、refs) |
142
+ | `./lib/card-ui` | 客户端专用设置卡组件:`CardForm`、`Section`、`FieldGrid`、`StatusBar`、`Field`、`FieldLabel`、`InfoTip`、`ToggleField`、`NumberField`、`TextField`、`ListField`、`SelectField`、`SaveBar`、`splitHint` 与样式字符串 `CARD_UI_CSS`。只用 `createElement`;`react` 是 optional peer,该子路径永不进根 barrel。声明的 peer 下限是 `^19.3.0`;组件只用 React 18 就已存在的 API(`useId`、`useState`、`useEffect`、refs) |
143
+ | `./client` | 浏览器客户端行:宿主模块图只提供一份的 `__ModuleLoader__` bundle。消费者外置这个说明符,并在自己的 patch 里以无 id 条目启用这一行 |
144
+ | `./client-node` | 同一份客户端接口的 Node/ESM 形态,供测试与类型感知的 Node 消费方使用 |
137
145
  | `./lib/project-key` | `deriveProjectKey` + `ProjectKeyOptions`:cwd → `尾目录名-<sha256(norm) 前 8 位 hex>`;非字符串、空串或归一后的根目录归入 `default`。`realpathSync` 会解析软链接,失败则退回解析后的路径。Windows 的 `\` 与 `/` 产生同一个键 |
138
146
  | `./lib/locale` | `Locale`(官方 `BuiltInLocaleId` 的别名)、`DEFAULT_LOCALE`、`resolveLocale`、`resolveLocalePreference`、`messagesFor`、`MessagesCatalog`,加 `LOCALE_SETTINGS_NAMESPACE` 与 `LOCALE_PREFERENCE_FIELD`。只负责宿主侧字典;客户端用官方 locale 包 |
139
147
  | `./lib/lesson-bus` | `settleLessonCall`:同步抛错与异步 rejection 归到同一个 `onFailure` 出口。函数型 thenable 也算可等待;`onFailure` 自己抛错或拒绝时被统一处理,不会分成"抛给调用方"或"未处理拒绝"两种结果 |
@@ -142,9 +150,13 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
142
150
  | `./lib/errors` | `errorText` 对齐官方 `@deepseek-ai/dsh-llm` 的 `errorChain` 语义(cause 链、`AggregateError` 成员、空 message 回退 `name`、跨 realm 取 `message`、恶意 getter 改用占位文本),本地零依赖实现。`isSettingsConflict` 由 `code` / `name` 识别宿主的设置冲突特征,跨 realm 抛来的错误也能识别 |
143
151
  | `./lib/jsonl` | `shrinkJsonlTail`:JSONL 尾部裁剪的纯逻辑模块。触发策略与错误处理留给调用方;字节预算是否有限由调用方保证 |
144
152
  | `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome` 把已结束的子进程映射成官方作业注册表的结局;`killProc` + `KillableProcess` 释放子进程句柄,对抛错或拒绝的 `kill()` 一律忽略(例如进程已被回收) |
153
+ | `./lib/remote-marker` | `markRemote(prototype, methodName)`:官方 `@Remote` 方法装饰器的程序化等价物。它把同一个装饰器函数作用在手写的 TC39 context 上,源码因此不必写装饰器语法,也不需要降级流水线。按值导入 `@deepseek-ai/dsh-typert-protocol`(optional peer),且不进根 barrel |
154
+ | `./lib/decorator-lowering` | 标准装饰器的构建/测试辅助:`needsDecoratorLowering`、`lowerStandardDecorators`、`rolldownDecorators`、`viteDecorators`。它用调用方注入的 `tsc` 入口单文件编译(`--noResolve`,模块说明符逐字保留),交回代码与源映射;Vite 侧还会把注入样板从覆盖率映射里摘掉。仅构建期使用,绝不进运行期 barrel |
145
155
  | `./lib/canonicalize-region-paths` | 构建期字符串处理:把 rolldown 写下的 `//#region <路径>` 改写成与 `process.cwd()` 无关的形态。只提供子路径,是为了让构建脚本取得到它,同时不把运行期 barrel 增大 |
146
156
 
147
- `card-ui` 是唯一刻意只走子路径的运行期模块:把它加进 barrel 会让每个 host 侧 importer 都去解析 `react`。
157
+ `card-ui`(值导入 `react`)与 `remote-marker`(值导入 `@deepseek-ai/dsh-typert-protocol`)是刻意只走子路径的运行期模块:放进 barrel 会让每个 host 侧 importer 都去解析一枚它用不到的包。
158
+
159
+ 客户端行由消费插件启用,不由单独安装本包启用。消费者写 `- name: "@jayyuen66/dsh-plugin-shared"` 且**不写 id**;宿主照常装载它,插件页不会把它暴露成可单独停用的组件。写 id 会多出那个开关,关闭后所有需要该行组件的设置卡都会失效。
148
160
 
149
161
  ## <img src="https://api.iconify.design/lucide/sliders-horizontal.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> config 导出
150
162
 
@@ -153,7 +165,7 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
153
165
  | 子路径 | 提供什么 | 你需要装什么 |
154
166
  | --- | --- | --- |
155
167
  | `./config/oxlint` | `definePluginConfig`、`makeTestFileOverrides`、`OFF_JUSTIFICATIONS` 与共享规则基线 | `oxlint`、`eslint-plugin-sonarjs`、`eslint-plugin-regexp`、`eslint-plugin-zod`(需 zod 4)、`@e18e/eslint-plugin` |
156
- | `./config/vitest.base` | `definePackageConfig`:四项 100% 覆盖率阈值、20 秒测试超时、共享覆盖率排除表与用例匹配范围 | `vitest` 与 `@vitest/coverage-v8`(基线把 `coverage.provider` 固定为 `"v8"`) |
168
+ | `./config/vitest.base` | `definePackageConfig`:四项 100% 覆盖率阈值、20 秒测试超时、共享覆盖率排除表、用例匹配范围,以及 `@jayyuen66/dsh-plugin-shared/client` → `@jayyuen66/dsh-plugin-shared/client-node` 的别名(UMD 行在装载时就读 `window`,Node 测试执行不了它) | `vitest` 与 `@vitest/coverage-v8`(基线把 `coverage.provider` 固定为 `"v8"`) |
157
169
  | `./config/tsconfig.base.json`、`./config/tsconfig.client.base.json` | 给 `extends` 用的纯 JSON | 什么都不用 |
158
170
 
159
171
  这些工具链包**不**写进 `peerDependencies`:npm 7+ 连 optional peer 都会去满足,而 `oxlint` 自己的 `peerOptional vite-plus` 把 `vitest` 锁定在一个与 `>=5.0.2` 无交集的版本上,结果是本包的普通 `npm install` 直接 `ERESOLVE` 失败。这条约束因此写在文档里,而不是 manifest 里。
@@ -203,10 +215,10 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
203
215
 
204
216
  | 问题 | 回答 |
205
217
  | --- | --- |
206
- | 要不要 `dsh plugin add` 它? | 不要。它没有配置层也没有客户端部分,启用不了任何东西。正确做法是声明成插件包的依赖。 |
218
+ | 要不要 `dsh plugin add` 它? | 不要。它声明的是 `dsh.client`(消费者要启用的那一行),但没有 `dsh.bundle.patch`,单独添加不会装出配置层,也启用不了任何东西。正确做法是声明成插件包的依赖,再在自己的 `cordis.patch.yml` 里插入那条不带 id 的行。 |
207
219
  | 消费方启动时报 `does not provide an export named ...` 与 `N entries did not activate`。 | profile 提供的那份本包比消费方 import 的版本旧。把 profile 的 `overrides` 版本调高到包含该子路径的版本;对照本包的 `exports` 与 CHANGELOG。任何单仓 CI 都看不见这个状态。 |
208
220
  | 改了 `lib/*.ts`,消费方还是旧行为。 | 消费方载入的是 `dist/`。先运行 `pnpm run build`;`test/build-host.test.ts` 会在运行期产物过期时判定失败。workspace link 不会绕过产物。 |
209
- | 为什么 `@deepseek-ai/dsh-output-retention` 是必需 peer,其余都是 optional? | 它是唯一按值导入的宿主包(`truncateEnd`)。其余都是类型声明,唯一的例外是裸 `@deepseek-ai/dsh` peer——它只声明宿主版本线。 |
221
+ | 为什么 `@deepseek-ai/dsh-output-retention` 是必需 peer,其余都是 optional? | 它是唯一**必需**且按值导入的宿主包(`truncateEnd`)。`@deepseek-ai/dsh-typert-protocol` 也按值导入,但只被 `./lib/remote-marker` 取用,所以仍是 optional;其余 dsh peer 只承载类型。裸 `@deepseek-ai/dsh` peer 只声明宿主版本线,`react` 则是 `./lib/card-ui` 的值导入 peer。 |
210
222
  | 为什么 `oxlint` 不写成 `peerDependency`? | npm 7+ 会去满足它,而 `oxlint` 的 optional `vite-plus` peer 把 `vitest` 锁定在基线所需范围之外,普通安装会得到 `ERESOLVE`。这条要求因此写在文档里。 |
211
223
  | 想把某个 helper 收录进来? | 先确认那份函数体在两个以上包里逐字重复。带领域判断的部分留在插件里。 |
212
224
 
@@ -216,7 +228,7 @@ import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
216
228
  pnpm install
217
229
  pnpm run check # typecheck → lint → build → test → 格式检查
218
230
  pnpm test # vitest,带覆盖率
219
- pnpm run build # 运行期产物、配置产物、声明文件
231
+ pnpm run build # 运行期产物、配置产物、声明文件、客户端行
220
232
  ```
221
233
 
222
234
  - 测试按「stub 覆盖不到的那类缺陷」分层:
@@ -227,7 +239,7 @@ pnpm run build # 运行期产物、配置产物、声明文件
227
239
  - **日志记录**:`test/setup-logs.ts` 接管 `console.*`;每条 `[shared/*]` 日志都要匹配 `test/log-templates.ts` 里的模板片段,否则测试立即失败。`test/log-templates.test.ts` 双向校验,增删日志却不改记录清单都会失败。
228
240
  - **发布形态**:`test/publish-manifest.test.ts` 要求入口目标都在 `files` 覆盖内、入口指向产物而不是源码、`publishConfig` 不带入口字段、每条 peer 都在 `peerDependenciesMeta` 里明确标注、`dsh-*` 开发钉版精确且统一、dsh peer 范围与 `engines.dsh` 相同,并要求源码、文档与产物里没有日期、绝对路径、家目录指涉或只在协作当时成立的措辞。
229
241
  - 覆盖率四项阈值都是 **100%**(lines / statements / functions / branches),所以新增分支必须有能走到它的用例。`test/vitest-base.test.ts` 拒绝空 `coverageInclude`,因为那会让四项阈值在空统计范围上全部通过。
230
- - CI 在 Linux、Windows 与 macOS 上运行同一组检查,Node 版本矩阵覆盖 22 与 24,并有一周一次的定时运行来发现上游变化。安装矩阵会分别用 npm 与 pnpm 打包,在干净目录里不带 peer 解析选项安装,逐子路径 import,并在两种 `skipLibCheck` 设置下对一小段消费方导入代码做类型检查。
242
+ - CI 在 Linux、Windows 与 macOS 上以 Node 24.9 运行同一组检查,并有一周一次的定时运行来发现上游变化。安装矩阵会分别用 npm 与 pnpm 打包,在干净目录里不带 peer 解析选项安装,逐子路径 import,并在两种 `skipLibCheck` 设置下对一小段消费方导入代码做类型检查。
231
243
 
232
244
  ## <img src="https://api.iconify.design/lucide/scale.svg?color=%23658EFF&height=22" height="22" width="22" align="absmiddle" alt="" /> 许可
233
245
 
@@ -1 +1,8 @@
1
- import{randomUUID as e}from"node:crypto";import{realpathSync as t}from"node:fs";import{mkdir as n,readFile as r,rename as i,rm as a,writeFile as o}from"node:fs/promises";import{setTimeout as s}from"node:timers/promises";import c from"node:path";function l(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function u(e){return l(e)?e:{}}async function d(e){let t=c.join(e,`package.json`);try{return JSON.parse(await r(t,`utf8`))}catch(e){throw Error(`读 ${t} 失败:${String(e)}`,{cause:e})}}function f(e){let t=e.indexOf(`/`);if(!e.startsWith(`@`))return t===-1?e:e.slice(0,t);let n=t===-1?-1:e.indexOf(`/`,t+1);return n===-1?e:e.slice(0,n)}function p(e){return e?.type===`chunk`&&e.isEntry===!0&&typeof e.code==`string`&&e.code.trim()!==``}function m(e,t){if(e.length!==1){let n=e.map(e=>`${e.type}:${e.fileName}`).join(`, `);throw Error(`rolldown 为 ${t} 产出了 ${e.length} 项输出(期望恰好一个 entry chunk):${n}`)}let n=e.at(0);if(!p(n))throw Error(`rolldown 为 ${t} 产出的不是单个带正文的 entry chunk`);return n}const h=/* @__PURE__ */ new Set([`EPERM`,`EBUSY`,`EACCES`]),g=[50,100,200];async function _(e,t,n=0){try{await i(e,t)}catch(r){let i=typeof r==`object`&&r&&`code`in r?r.code:void 0;if(typeof i!=`string`||!h.has(i))throw r;let a=g.at(n);if(a===void 0)throw r;await s(a),await _(e,t,n+1)}}async function v(t,r){let i=c.join(c.dirname(t),`.tmp`);await n(i,{recursive:!0});let s=c.join(i,`${c.basename(t)}.${process.pid}.${e()}.tmp`);try{await o(s,r,`utf8`),await _(s,t)}catch(e){try{await a(s,{force:!0})}catch{}throw Error(`写产物失败:${t}(草稿 ${s}):${String(e)}`,{cause:e})}}async function y(e){try{await e.close()}catch(e){console.error(`释放 rolldown bundle 失败(不影响上面那条构建错因):`,e)}}function b(e){let n=process.argv.at(1);if(n===void 0||n.length===0||c.basename(n)!==c.basename(e))return!1;try{return t(n)===t(e)}catch(e){throw Error(`无法解析本脚本自身的路径(argv[1]=${n}):构建未发生,不要当成成功。`,{cause:e})}}export{u as asRecord,y as closeQuietly,b as invokedDirectly,l as isRecord,f as packageNameOf,d as readManifest,m as singleEntryChunk,v as writeArtifact};
1
+ import{randomUUID as e}from"node:crypto";import{realpathSync as t}from"node:fs";import{mkdir as n,readFile as r,rename as i,rm as a,writeFile as o}from"node:fs/promises";import{setTimeout as s}from"node:timers/promises";import c from"node:path";function l(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function u(e){return l(e)?e:{}}async function d(e){let t=c.join(e,`package.json`);try{return JSON.parse(await r(t,`utf8`))}catch(e){throw Error(`读 ${t} 失败:${String(e)}`,{cause:e})}}function f(e){let t=e.indexOf(`/`);if(!e.startsWith(`@`))return t===-1?e:e.slice(0,t);let n=t===-1?-1:e.indexOf(`/`,t+1);return n===-1?e:e.slice(0,n)}function p(e){return e?.type===`chunk`&&e.isEntry===!0&&typeof e.code==`string`&&e.code.trim()!==``}function m(e,t){if(e.length!==1){let n=e.map(e=>`${e.type}:${e.fileName}`).join(`, `);throw Error(`rolldown 为 ${t} 产出了 ${e.length} 项输出(期望恰好一个 entry chunk):${n}`)}let n=e.at(0);if(!p(n))throw Error(`rolldown 为 ${t} 产出的不是单个带正文的 entry chunk`);return n}const h=/* @__PURE__ */ new Set([`EPERM`,`EBUSY`,`EACCES`]),g=[50,100,200];async function _(e,t,n=0){try{await i(e,t)}catch(r){let i=typeof r==`object`&&r&&`code`in r?r.code:void 0;if(typeof i!=`string`||!h.has(i))throw r;let a=g.at(n);if(a===void 0)throw r;await s(a),await _(e,t,n+1)}}async function v(t,r){let i=c.join(c.dirname(t),`.tmp`);await n(i,{recursive:!0});let s=c.join(i,`${c.basename(t)}.${process.pid}.${e()}.tmp`);try{await o(s,r,`utf8`),await _(s,t)}catch(e){try{await a(s,{force:!0})}catch{}throw Error(`写产物失败:${t}(草稿 ${s}):${String(e)}`,{cause:e})}}function y(e,t,n){let r=e.charAt(t);n.push(r);let i=t+1;for(;i<e.length;){let t=e.charAt(i);if(t===`\\`)n.push(e.charAt(i+1)===`
2
+ `?`
3
+ `:` `),i+=2;else if(t===r)return n.push(t),i+1;else n.push(t===`
4
+ `?`
5
+ `:` `),i+=1}return i}function b(e){let t=[],n=0,r=(n,r)=>{for(let i=n;i<r;i+=1)t.push(e.charAt(i)===`
6
+ `?`
7
+ `:` `)};for(;n<e.length;){let i=e.charAt(n);if(i===`"`||i===`'`||i==="`")n=y(e,n,t);else if(e.startsWith(`//`,n)){let t=e.indexOf(`
8
+ `,n),i=t===-1?e.length:t;r(n,i),n=i}else if(e.startsWith(`/*`,n)){let t=e.indexOf(`*/`,n+2),i=t===-1?e.length:t+2;r(n,i),n=i}else t.push(i),n+=1}return t.join(``)}async function x(e){try{await e.close()}catch(e){console.error(`释放 rolldown bundle 失败(不影响上面那条构建错因):`,e)}}function S(e){let n=process.argv.at(1);if(n===void 0||n.length===0||c.basename(n)!==c.basename(e))return!1;try{return t(n)===t(e)}catch(e){throw Error(`无法解析本脚本自身的路径(argv[1]=${n}):构建未发生,不要当成成功。`,{cause:e})}}export{u as asRecord,x as closeQuietly,S as invokedDirectly,l as isRecord,b as maskNonCode,f as packageNameOf,d as readManifest,_ as renameWithRetry,m as singleEntryChunk,v as writeArtifact};
@@ -1,2 +1,2 @@
1
- import{asRecord as e,closeQuietly as t,packageNameOf as n,readManifest as r,singleEntryChunk as i}from"./build-artifacts.js";import{t as a}from"./canonicalize-region-paths-DowoRhQK.js";import{readFile as o}from"node:fs/promises";import s from"node:path";const c=[`react`,`react/jsx-runtime`,`react-dom`,`react-dom/client`],l=/^\/\/(?:#region | )?(?:\.\.?\/)*node_modules\/\.pnpm\/[^/]+\/node_modules\//u,u=/node_modules\/\.pnpm\//u,d=/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;function f(e){let t=e.replaceAll(/^\/\/(?:#region .*| .*)$/gmu,e=>e.replaceAll(`\\`,`/`).replace(l,e=>e.startsWith(`//#region`)?`//#region node_modules/`:`// node_modules/`)),n=u.exec(t);if(n!==null){let e=Math.max(0,n.index-40);throw Error(`产物里仍有 pnpm store 路径未折叠(出现在 …${JSON.stringify(t.slice(e,e+100))}…)。折叠判据与 rolldown 写下的形式不再匹配,client 产物的字节会随安装布局变化。`)}return t}async function p(e){return o(e,`utf8`)}function m(e){let t=e.name;if(typeof t!=`string`||t===``)throw Error(`package.json 的 name 缺失或非字符串。client 产物的 id 必须等于 dsh client 模块表的键(bundle 包名)。`);if(!d.test(t))throw Error(`package.json 的 name ${JSON.stringify(t)} 不符合 npm 包名字符集。`);return t}function h(e){let t=new Set(e);return e=>{if(t.has(e))return!0;let r=n(e);if(r===`react`||r===`react-dom`)throw Error(`${e} 是 react 家族说明符但不在外置清单里。外置会得到宿主模块表里没有的键(加载期报错),内联会得到第二份 React(hooks 静默失效);真要引入子路径,先把它加进外置清单并确认宿主模块表里有同名项。`);return!1}}async function g(n){let o=m(e(await r(n.root))),l=n.entry??s.join(`src`,`client-entry.ts`),u=n.minify??!1,d=n.comments??!u,g={format:`cjs`,...u?{minify:!0}:{},...d?{}:{comments:!1}},_=await n.rolldown({input:s.join(n.root,l),external:h(n.externals??c),platform:`browser`,cwd:n.root,resolve:{extensions:[`.tsx`,`.ts`,`.jsx`,`.js`,`.mjs`,`.json`]}}),v;try{let{output:e}=await _.generate(g),t=i(e,l);v=f(a(t.code.trimEnd(),n.root,process.cwd()))}catch(e){throw await t(_),e}return await _.close(),await n.verifyName?.({pkgName:o,body:v,readSource:e=>p(s.join(n.root,e))}),[n.header,`window.__ModuleLoader__.load({`,` id: ${JSON.stringify(o)},`,` factory: (require) => {`,` 'use strict';`,` const module = { exports: {} };`,` let exports = module.exports;`,v,` return module.exports;`,` },`,`});`,``].join(`
1
+ import{t as e}from"./canonicalize-region-paths-DowoRhQK.js";import{asRecord as t,closeQuietly as n,packageNameOf as r,readManifest as i,singleEntryChunk as a}from"./build-artifacts.js";import{readFile as o}from"node:fs/promises";import s from"node:path";const c=[`react`,`react/jsx-runtime`,`react-dom`,`react-dom/client`],l=/^\/\/(?:#region | )?(?:\.\.?\/)*node_modules\/\.pnpm\/[^/]+\/node_modules\//u,u=/node_modules\/\.pnpm\//u,d=/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;function f(e){let t=e.replaceAll(/^\/\/(?:#region .*| .*)$/gmu,e=>e.replaceAll(`\\`,`/`).replace(l,e=>e.startsWith(`//#region`)?`//#region node_modules/`:`// node_modules/`)),n=u.exec(t);if(n!==null){let e=Math.max(0,n.index-40);throw Error(`产物里仍有 pnpm store 路径未折叠(出现在 …${JSON.stringify(t.slice(e,e+100))}…)。折叠判据与 rolldown 写下的形式不再匹配,client 产物的字节会随安装布局变化。`)}return t}async function p(e){return o(e,`utf8`)}function m(e){let t=e.name;if(typeof t!=`string`||t===``)throw Error(`package.json 的 name 缺失或非字符串。client 产物的 id 必须等于 dsh client 模块表的键(bundle 包名)。`);if(!d.test(t))throw Error(`package.json 的 name ${JSON.stringify(t)} 不符合 npm 包名字符集。`);return t}function h(e){let t=new Set(e);return e=>{if(t.has(e))return!0;let n=r(e);if(n===`react`||n===`react-dom`)throw Error(`${e} 是 react 家族说明符但不在外置清单里。外置会得到宿主模块表里没有的键(加载期报错),内联会得到第二份 React(hooks 静默失效);真要引入子路径,先把它加进外置清单并确认宿主模块表里有同名项。`);return!1}}async function g(r){let o=m(t(await i(r.root))),l=r.entry??s.join(`src`,`client-entry.ts`),u=r.minify??!1,d=r.comments??!u,g={format:`cjs`,...u?{minify:!0}:{},...d?{}:{comments:!1}},_=await r.rolldown({input:s.join(r.root,l),external:h(r.externals??c),platform:`browser`,cwd:r.root,resolve:{extensions:[`.tsx`,`.ts`,`.jsx`,`.js`,`.mjs`,`.json`]}}),v;try{let{output:t}=await _.generate(g),n=a(t,l);v=f(e(n.code.trimEnd(),r.root,process.cwd()))}catch(e){throw await n(_),e}return await _.close(),await r.verifyName?.({pkgName:o,body:v,readSource:e=>p(s.join(r.root,e))}),[r.header,`window.__ModuleLoader__.load({`,` id: ${JSON.stringify(o)},`,` factory: (require) => {`,` 'use strict';`,` const module = { exports: {} };`,` let exports = module.exports;`,v,` return module.exports;`,` },`,`});`,``].join(`
2
2
  `)}export{c as REACT_RUNTIME_MODULES,g as buildClientBundle,h as clientExternalPredicate,f as collapseStorePaths};
@@ -1 +1 @@
1
- import{asRecord as e,closeQuietly as t,isRecord as n,packageNameOf as r,readManifest as i,singleEntryChunk as a}from"./build-artifacts.js";import{t as o}from"./canonicalize-region-paths-DowoRhQK.js";import s from"node:path";function c(e,t){let r=e[t];if(r===void 0)return[];if(!n(r))throw TypeError(`package.json 的 ${t} 不是对象(实为 ${Array.isArray(r)?`array`:typeof r})。折成空集会让外部化名单失效、依赖全部内联进产物,所以拒绝继续。`);return Object.keys(r).map(e=>{if(typeof r[e]!=`string`)throw TypeError(`package.json 的 ${t}.${e} 不是字符串范围(收到 ${typeof r[e]})。静默跳过会让这个包被当成内部依赖整包内联,而内联恰好复制它的模块级状态。`);return e})}function l(e,t){let n=[...c(e,`dependencies`),...c(e,`peerDependencies`)];return t&&n.push(...c(e,`optionalDependencies`)),new Set(n)}function u(e){return Array.isArray(e)&&e.length>0&&e.every(e=>typeof e==`string`&&e.length>0)}async function d(n){let c=l(e(await i(n.root)),n.externalizeOptional??!0);if(c.size===0)throw Error(`package.json 的 dependencies / peerDependencies 都取不到字符串条目,外部化集合为空——依赖会被整包内联。`);let d=e=>c.has(r(e)),f=n.resolveExtensions??[`.ts`,`.mjs`,`.js`],p=n.minify??!1,m=n.comments??!p,h={format:`esm`,...p?{minify:!0}:{},...m?{}:{comments:!1}},g=async e=>{let r=await n.rolldown({input:s.join(n.root,e.source),external:d,platform:`node`,cwd:n.root,resolve:{extensions:[...f]},...n.plugins===void 0?{}:{plugins:n.plugins}}),i;try{let{output:t}=await r.generate(h),c=a(t,e.source);if(!u(c.moduleIds))throw TypeError(`rolldown 为 ${e.source} 产出的 entry chunk 的 moduleIds 不是"非空字符串数组"`);let l=o(c.code.trimEnd(),n.root,process.cwd());i={source:e.source,output:e.output,file:s.join(n.root,e.output),code:`${e.header}${l}\n`,inlinedModules:c.moduleIds}}catch(e){throw await t(r),e}return await r.close(),i};return Promise.all(n.entries.map(e=>g(e)))}export{d as buildHostEntries};
1
+ import{t as e}from"./canonicalize-region-paths-DowoRhQK.js";import{asRecord as t,closeQuietly as n,isRecord as r,packageNameOf as i,readManifest as a,singleEntryChunk as o}from"./build-artifacts.js";import s from"node:path";function c(e,t){let n=e[t];if(n===void 0)return[];if(!r(n))throw TypeError(`package.json 的 ${t} 不是对象(实为 ${Array.isArray(n)?`array`:typeof n})。折成空集会让外部化名单失效、依赖全部内联进产物,所以拒绝继续。`);return Object.keys(n).map(e=>{if(typeof n[e]!=`string`)throw TypeError(`package.json 的 ${t}.${e} 不是字符串范围(收到 ${typeof n[e]})。静默跳过会让这个包被当成内部依赖整包内联,而内联恰好复制它的模块级状态。`);return e})}function l(e,t){let n=[...c(e,`dependencies`),...c(e,`peerDependencies`)];return t&&n.push(...c(e,`optionalDependencies`)),new Set(n)}function u(e){return Array.isArray(e)&&e.length>0&&e.every(e=>typeof e==`string`&&e.length>0)}async function d(r){let c=l(t(await a(r.root)),r.externalizeOptional??!0);if(c.size===0)throw Error(`package.json 的 dependencies / peerDependencies 都取不到字符串条目,外部化集合为空——依赖会被整包内联。`);let d=e=>c.has(i(e)),f=r.resolveExtensions??[`.ts`,`.mjs`,`.js`],p=r.minify??!1,m=r.comments??!p,h={format:`esm`,...p?{minify:!0}:{},...m?{}:{comments:!1}},g=async t=>{let i=await r.rolldown({input:s.join(r.root,t.source),external:d,platform:`node`,cwd:r.root,resolve:{extensions:[...f]},...r.plugins===void 0?{}:{plugins:r.plugins}}),a;try{let{output:n}=await i.generate(h),c=o(n,t.source);if(!u(c.moduleIds))throw TypeError(`rolldown 为 ${t.source} 产出的 entry chunk 的 moduleIds 不是"非空字符串数组"`);let l=e(c.code.trimEnd(),r.root,process.cwd());a={source:t.source,output:t.output,file:s.join(r.root,t.output),code:`${t.header}${l}\n`,inlinedModules:c.moduleIds}}catch(e){throw await n(i),e}return await i.close(),a};return Promise.all(r.entries.map(e=>g(e)))}export{d as buildHostEntries};
@@ -1,2 +1,2 @@
1
- import{readFile as e,readdir as t,rename as n,rm as r,writeFile as i}from"node:fs/promises";import a from"node:path";import{execFileSync as o}from"node:child_process";const s=/(?<=\bfrom\s*)["']\.{1,2}\/[^"'\n]*?(?<!\.d)\.(?:mts|cts|ts)["']/gu,c=/(?<=\bimport\(\s*)["']\.{1,2}\/[^"'\n]*?(?<!\.d)\.(?:mts|cts|ts)["']/gu,l=/(?<=\bdeclare\s+module\s*)["']\.{1,2}\/[^"'\n]*?(?<!\.d)\.(?:mts|cts|ts)["']/gu,u={cts:`cjs`,mts:`mjs`,ts:`js`},d=/\.(?<extension>mts|cts|ts)(?=["']$)/u;function f(e){return e.replace(d,(e,t)=>`.${u[t]}`)}function p(e){return e.replaceAll(s,f).replaceAll(c,f).replaceAll(l,f)}function m(e){return typeof e==`string`?e:Buffer.isBuffer(e)?e.toString(`utf8`):``}function h(e,t,n){try{o(e,t,{cwd:n,stdio:[`ignore`,`pipe`,`pipe`],encoding:`utf8`})}catch(e){let t=(e instanceof Error&&`stdout`in e&&`stderr`in e?[e.stdout,e.stderr]:[]).map(e=>m(e)).join(`
2
- `).trim();throw Error(t===``?String(e):t,{cause:e})}}async function g(e,n){let r=await t(e,{withFileTypes:!0}),i=r.filter(e=>!e.isDirectory()&&e.name.endsWith(`.d.ts`)).map(e=>`${n}${e.name}`),o=await Promise.all(r.filter(e=>e.isDirectory()).map(t=>g(a.join(e,t.name),`${n}${t.name}/`)));return[...i,...o.flat()]}async function _(t){let o=t.scratch??a.join(t.root,`.tmp`,`types`),s=t.outDir??a.join(t.root,`types`),c=t.tsconfig??`tsconfig.types.json`;await r(o,{recursive:!0,force:!0});try{h(process.execPath,[t.tscEntry,`-p`,c,`--outDir`,o],t.root);let l=await g(o,``);return await Promise.all(l.map(async t=>{let n=a.join(o,t),r=await e(n,`utf8`);await i(n,p(r),`utf8`)})),await r(s,{recursive:!0,force:!0}),await n(o,s),l}catch(e){throw await r(o,{recursive:!0,force:!0}),e}}export{_ as buildDeclarations,p as canonicalizeDeclarationSpecifiers,g as listDeclarations,h as runTool,m as textOf};
1
+ import{maskNonCode as e,renameWithRetry as t}from"./build-artifacts.js";import{randomUUID as n}from"node:crypto";import{existsSync as r}from"node:fs";import{readFile as i,readdir as a,rm as o,writeFile as s}from"node:fs/promises";import c from"node:path";import{execFileSync as l}from"node:child_process";const u=/(?<=\bfrom\s*)["']\.{1,2}\/[^"'\n]*?(?<!\.d)\.(?:mts|cts|ts)["']/gu,d=/(?<=\bimport\(\s*)["']\.{1,2}\/[^"'\n]*?(?<!\.d)\.(?:mts|cts|ts)["']/gu,f=/(?<=\bdeclare\s+module\s*)["']\.{1,2}\/[^"'\n]*?(?<!\.d)\.(?:mts|cts|ts)["']/gu,p={cts:`cjs`,mts:`mjs`,ts:`js`},m=/\.(?<extension>mts|cts|ts)(?=["']$)/u;function h(e){return e.replace(m,(e,t)=>`.${p[t]}`)}function g(t){let n=e(t),r=(e,t)=>n.charAt(t)===`"`||n.charAt(t)===`'`?h(e):e;return t.replaceAll(u,r).replaceAll(d,r).replaceAll(f,r)}function _(e){return typeof e==`string`?e:Buffer.isBuffer(e)?e.toString(`utf8`):``}function v(e,t,n){try{l(e,t,{cwd:n,stdio:[`ignore`,`pipe`,`pipe`],encoding:`utf8`})}catch(e){let t=(e instanceof Error&&`stdout`in e&&`stderr`in e?[e.stdout,e.stderr]:[]).map(e=>_(e)).join(`
2
+ `).trim();throw Error(t===``?String(e):t,{cause:e})}}async function y(e,t){let n=await a(e,{withFileTypes:!0}),r=n.filter(e=>!e.isDirectory()&&e.name.endsWith(`.d.ts`)).map(e=>`${t}${e.name}`),i=await Promise.all(n.filter(e=>e.isDirectory()).map(n=>y(c.join(e,n.name),`${t}${n.name}/`)));return[...r,...i.flat()]}async function b(e){let a=e.scratch??c.join(e.root,`.tmp`,`types`),l=e.outDir??c.join(e.root,`types`),u=e.tsconfig??`tsconfig.types.json`;await o(a,{recursive:!0,force:!0});try{v(process.execPath,[e.tscEntry,`-p`,u,`--outDir`,a],e.root);let d=await y(a,``);await Promise.all(d.map(async e=>{let t=c.join(a,e),n=await i(t,`utf8`);await s(t,g(n),`utf8`)}));let f=e.rename??t,p=null;r(l)&&(p=`${l}.backup-${process.pid}-${n()}`,await f(l,p));try{await f(a,l)}catch(e){if(p===null)throw e;try{await f(p,l)}catch(t){throw Error(`声明产物换入失败,且备份目录未能恢复(备份留在 ${p}):${String(e)};恢复失败:${String(t)}`,{cause:t})}throw e}return p!==null&&await o(p,{recursive:!0,force:!0}),d}catch(e){throw await o(a,{recursive:!0,force:!0}),e}}export{b as buildDeclarations,g as canonicalizeDeclarationSpecifiers,y as listDeclarations,v as runTool,_ as textOf};
@@ -0,0 +1,5 @@
1
+ import{maskNonCode as e}from"./build-artifacts.js";import{mkdtempSync as t,readFileSync as n,rmSync as r}from"node:fs";import i from"node:path";import{execFileSync as a}from"node:child_process";import{tmpdir as o}from"node:os";const s=/(?:^|[\s{;(,=:[?!&|>\]})])@[A-Za-z_$][\w$]*/u,c=/\.d\.[cm]?ts$/u,l=/\.[cm]?ts$/u,u=/^\s*if \(_metadata\) Object\.defineProperty\([A-Za-z_$][\w$]*, Symbol\.metadata,/u,d=[`__esDecorate(`,`__runInitializers(`];function f(t,n){return l.test(t)&&!c.test(t)&&s.test(e(n))}function p(e){return e.endsWith(`.mts`)?`.mjs`:e.endsWith(`.cts`)?`.cjs`:`.js`}function m(e,t,n){let r=[],i=[],a=0;for(let o of e.split(`
2
+ `)){let e=n?.[a]??``;if(a+=1,u.test(o)&&!o.trimEnd().endsWith(`);`))throw Error(`dsh-plugin-shared: Symbol.metadata 发布语句不是单行(${o.trim().slice(0,80)}…):按行裁剪会留下残片,产物会是非法 JS。`);let s=u.test(o)||o.startsWith(`//# sourceMappingURL=`),c=d.some(e=>o.includes(e));r.push(s?``:o),i.push(s||t&&c?``:e)}return{code:r.join(`
3
+ `),mapLines:n===null?null:i}}function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e,s,c,u=!1){if(e.length===0)return{code:e,map:null};let d=t(i.join(o(),`dsh-decorators-`)),f=``;try{try{a(process.execPath,[c.tscEntry,`--ignoreConfig`,`--noResolve`,`--target`,`es2024`,`--module`,`preserve`,`--sourceMap`,`--outDir`,d,s],{stdio:[`ignore`,`ignore`,`pipe`],encoding:`utf8`})}catch(e){
4
+ /* v8 ignore next 3 -- 见上:否定支在当前调用方式下不可达 */
5
+ e instanceof Error&&`stderr`in e&&typeof e.stderr==`string`&&(f=e.stderr.trim())}let t=i.join(d,`${i.basename(s).replace(l,``)}${p(s)}`),r;try{r=n(t,`utf8`)}catch(e){let n=f===``?`(tsc 没有输出 stderr)`:f;throw Error(`dsh-plugin-shared: 装饰器降级没有产出 ${i.basename(t)}(${s})。tsc stderr:${n}`,{cause:e})}if(r.length===0)throw Error(`dsh-plugin-shared: 装饰器降级没有产出任何字节(${s})`);let o=null;try{let r=JSON.parse(n(`${t}.map`,`utf8`));o=h(r)?r:null,o!==null&&(o.sources=[s],o.sourcesContent=[e])}catch{}let g=o?.mappings,_=g===void 0?null:g.split(`;`),v=m(r,u,_);return o!==null&&v.mapLines!==null&&(o.mappings=v.mapLines.join(`;`)),{code:v.code,map:o}}finally{r(d,{recursive:!0,force:!0})}}function _(e){return e.includes(`?`)?e.slice(0,e.indexOf(`?`)):e}function v(e){return{name:`dsh-plugin-shared:standard-decorators`,transform(t,n){let r=_(n);return f(r,t)?g(t,r,e):null}}}function y(e){return{name:`dsh-plugin-shared:standard-decorators`,enforce:`pre`,transform(t,n){let r=_(n);return f(r,t)?g(t,r,e,!0):void 0}}}export{g as lowerStandardDecorators,f as needsDecoratorLowering,v as rolldownDecorators,y as viteDecorators};