@jayyuen66/dsh-plugin-shared 0.1.5-rc → 0.1.6
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 +17 -32
- package/README.zh-CN.md +17 -32
- package/dist/card-apply.js +1 -1
- package/dist/card-diff.js +1 -0
- package/dist/config/oxlint.base.js +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/job-outcome.js +1 -1
- package/dist/record.js +1 -1
- package/dist/tool-events.js +1 -1
- package/dist/types/card-apply.d.ts +6 -2
- package/dist/types/card-diff.d.ts +10 -0
- package/dist/types/errors.d.ts +9 -0
- package/dist/types/job-outcome.d.ts +16 -0
- package/dist/types/jsonl.d.ts +7 -7
- package/dist/types/record.d.ts +11 -1
- package/dist/types/tool-events.d.ts +0 -13
- package/package.json +35 -39
- package/dist/http.js +0 -1
- package/dist/trust.js +0 -1
- package/dist/types/http.d.ts +0 -81
- package/dist/types/trust.d.ts +0 -44
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ pnpm add @jayyuen66/dsh-plugin-shared
|
|
|
23
23
|
Declare it as an ordinary dependency of your plugin package and import it by bare-package subpath:
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
import {
|
|
26
|
+
import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
End users do not install or enable it separately: any consumer package pulls it in. When it is missing the failure is `ERR_MODULE_NOT_FOUND` on a `@jayyuen66/dsh-plugin-shared/...` specifier, never a silent downgrade.
|
|
@@ -32,8 +32,8 @@ End users do not install or enable it separately: any consumer package pulls it
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
| --- | --- |
|
|
35
|
-
| Node.js |
|
|
36
|
-
| dsh |
|
|
35
|
+
| Node.js | `>=22.12.0` — the publish-side floor the nine-package family shares, so a plugin that declares the same `engines.node` never rejects this library |
|
|
36
|
+
| dsh | `^0.2.1-rc.2`, declared as an **optional** peer (this package needs no host at import time) |
|
|
37
37
|
| Module format | ESM only. `require()` works only where Node supports `require(esm)` |
|
|
38
38
|
| Tree shaking | `"sideEffects": false` |
|
|
39
39
|
|
|
@@ -42,7 +42,6 @@ End users do not install or enable it separately: any consumer package pulls it
|
|
|
42
42
|
| Behaviour | Where | Scope |
|
|
43
43
|
| --- | --- | --- |
|
|
44
44
|
| Reads a directory entry through `realpathSync` | `lib/project-key.ts` | Only the path handed to `deriveProjectKey`; a failure (ENOENT/EACCES) falls back to `path.resolve` and never throws |
|
|
45
|
-
| Enumerates this machine's network interfaces via `os.networkInterfaces()` | `lib/trust.ts` | **Only** when the caller passes `servingNonLoopback: true`; used to check whether the request's `Host` is really an address this machine holds. Nothing leaves the process |
|
|
46
45
|
| Reads nothing else | — | No other file access in `lib/**`, no writes at all, no environment variables read, no outbound network requests, no `eval`/`new Function`/dynamic import |
|
|
47
46
|
|
|
48
47
|
`lib/card-apply.ts` reads and writes one `globalThis` key by design — that is what survives a duplicated module instance under HMR.
|
|
@@ -53,32 +52,20 @@ The subpath column is the in-package specifier; value imports must prefix it wit
|
|
|
53
52
|
|
|
54
53
|
| Subpath | Provides |
|
|
55
54
|
| --- | --- |
|
|
56
|
-
| `.` | Barrel over the
|
|
57
|
-
| `./lib/
|
|
58
|
-
| `./lib/
|
|
59
|
-
| `./lib/card-
|
|
55
|
+
| `.` | Barrel over the runtime facets listed below. `canonicalize-region-paths` deliberately stays out, so no runtime consumer pays an import for it |
|
|
56
|
+
| `./lib/tool-events` | `scanToolEvents` → the `calls`/`results` tables, `toolEventRowsOf` (its single-event step, same code; the records it hands back already carry normalized `arguments` and `badArguments`), `editPathOf` and `EditTarget` (discriminated on `kind`, only `write` / `read-view`; `path` is `undefined` when no path could be read); every field of the three ledger interfaces is `readonly`; re-exports the official `SessionEvent` and the two PTC event types |
|
|
57
|
+
| `./lib/card-apply` | `claimApply` + `CardApplyCtx`: apply idempotency guard (an **own** boolean flag on `globalThis` plus `ctx.effect` cleanup) |
|
|
58
|
+
| `./lib/card-diff` | `diffTouched`: which touched fields really differ from the snapshot (value comparison, so `undefined`, `null` and an absent key are one and the same value) |
|
|
60
59
|
| `./lib/project-key` | `deriveProjectKey` + `ProjectKeyOptions`: cwd → `last-segment-<first 8 hex of sha256(norm)>`, fallback bucket `default` |
|
|
61
60
|
| `./lib/locale` | `Locale` (an alias of the official `BuiltInLocaleId`), `DEFAULT_LOCALE`, `resolveLocale`, `resolveLocalePreference`, `messagesFor`, `MessagesCatalog`, plus `LOCALE_SETTINGS_NAMESPACE` / `LOCALE_PREFERENCE_FIELD` |
|
|
62
61
|
| `./lib/lesson-bus` | `settleLessonCall`: routes synchronous throws and asynchronous rejections into one `onFailure` exit; function-thenables count as awaitable (missing them leaves a rejection unhandled); an `onFailure` that itself throws is contained, so it never forks into "thrown at the caller" or "unhandled rejection" |
|
|
63
62
|
| `./lib/text` | `truncateEnd` (delegates to the official `truncateWithoutSplittingSurrogatePair`) and `truncateStart` (back cut; the official surface has no back-cut form) |
|
|
64
|
-
| `./lib/record` | `isRecord`, `fieldOf` |
|
|
65
|
-
| `./lib/errors` | `errorText`: semantics aligned with the official `@deepseek-ai/dsh-llm` `errorChain` (cause chain, `AggregateError` members, empty message falling back to `name`, cross-realm `message` read off the value itself, hostile getters degraded), implemented locally with zero dependencies |
|
|
63
|
+
| `./lib/record` | `isRecord`, `fieldOf`, `hasMethods` (service-surface check: the value is a record **and** every named method is callable) |
|
|
64
|
+
| `./lib/errors` | `errorText`: semantics aligned with the official `@deepseek-ai/dsh-llm` `errorChain` (cause chain, `AggregateError` members, empty message falling back to `name`, cross-realm `message` read off the value itself, hostile getters degraded), implemented locally with zero dependencies; `isSettingsConflict` recognizes the host's settings-conflict shape from `code` / `name`, so a cross-realm error still classifies |
|
|
66
65
|
| `./lib/jsonl` | `shrinkJsonlTail`: the pure decision half of JSONL tail halving; trigger policy and error exit stay with the caller |
|
|
67
|
-
| `./lib/
|
|
68
|
-
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome`: maps a settled subprocess onto the official job registry's outcome |
|
|
66
|
+
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome`: maps a settled subprocess onto the official job registry's outcome; `killProc` + `KillableProcess` terminates a child handle and ignores a `kill()` that throws on an already-reaped process |
|
|
69
67
|
| `./lib/canonicalize-region-paths` | Build-time string helper that folds rolldown's `//#region <path>` markers into a `process.cwd()`-independent form. Exported as a subpath precisely because runtime consumers should not pay an import for it |
|
|
70
68
|
|
|
71
|
-
### Security notes worth knowing
|
|
72
|
-
|
|
73
|
-
`requestTrust` exists because `sec-fetch-site` alone does not survive DNS rebinding: a hostile page that resolves its own hostname to `127.0.0.1` genuinely *is* same-origin as far as the browser is concerned, so both the `sec-fetch-site` leg and the Origin leg pass together. Only the Host leg can deny it. Four deliberate choices:
|
|
74
|
-
|
|
75
|
-
1. A missing `Host` passes only for a loopback peer — HTTP/1.1 forces `Host`, so reaching this branch means HTTP/1.0 or a raw socket, i.e. a local caller.
|
|
76
|
-
2. `sec-fetch-site` is an allow-list (`same-origin`, `none`, absent), not a deny-list, so a same-site-but-different-port request is not waved through.
|
|
77
|
-
3. A non-loopback serving surface is accepted only if the address really belongs to one of this machine's interfaces. `.local`/`.lan` suffixes are never trusted, because an mDNS name can be claimed by any local process.
|
|
78
|
-
4. `guardTrust` refuses to no-op: `sendJson` silently skips once headers are sent, so if someone moves the gate after `await readBody()` it would silently become a pass. It logs instead.
|
|
79
|
-
|
|
80
|
-
`readBody` counts UTF-8 **bytes**, pre-checks `content-length`, and accumulates `Buffer`s before decoding — cutting per chunk would turn a multi-byte character straddling a chunk boundary into U+FFFD while still being valid JSON. A budget that is not a finite non-negative number is refused as `bad-budget` (HTTP 500) rather than silently becoming unlimited: every comparison against `NaN` is false, so an unclamped `NaN` budget disables the limit entirely.
|
|
81
|
-
|
|
82
69
|
## The `config/` facets
|
|
83
70
|
|
|
84
71
|
These are lint/test baselines for plugin authors, not runtime code. They pull in tooling your package must provide itself:
|
|
@@ -97,11 +84,11 @@ The sonarjs entry point is resolved lazily, inside `definePluginConfig()`. Impor
|
|
|
97
84
|
|
|
98
85
|
## Dependencies
|
|
99
86
|
|
|
100
|
-
`dependencies` is empty;
|
|
87
|
+
`dependencies` is empty; every `@deepseek-ai/*` entry lives only in `peerDependencies` and `devDependencies`. `test/publish-manifest.test.ts` enforces this — it is not a style preference:
|
|
101
88
|
|
|
102
89
|
- **Putting them in `dependencies` disables the consumer's host.** Consumers are dsh plugins installed into a profile (`nodeLinker: hoisted`, `autoInstallPeers: false`). Any host package listed under `dependencies` is written into the profile as a **real directory**, coexisting with 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's `dsh-agent-loop` reads a `ToolRuntime` mounted from the duplicate using its own copy's symbol, gets `undefined`, and **every native tool call throws** `Cannot read properties of undefined (reading 'prepare')` — the whole profile loses its tools. This package's build, typecheck and tests all stay green right up to that point, so the invariant has to live in the manifest.
|
|
103
|
-
- `@deepseek-ai/dsh-output-retention` is a **value** import (`truncateEnd`) and is provided by the host at runtime, so it is
|
|
104
|
-
- The
|
|
90
|
+
- `@deepseek-ai/dsh-output-retention` is a **value** import (`truncateEnd`) and is provided by the host at runtime, so it is the one required peer — stated as `"optional": false` in `peerDependenciesMeta` rather than left implicit, because an unlisted peer is a hard requirement for npm 7+ (`ERESOLVE`, not a warning).
|
|
91
|
+
- The remaining peers are referenced only by the published `.d.ts`, so they are optional peers plus devDependencies: the peer keeps the type surface honest — `lib/job-outcome` used to take `ShellProcess` and `JobOutcome` from packages no consumer tree was required to contain, so with `skipLibCheck: true` a wrong `status` or `exitCode` produced no error at all and `jobOutcomeOf`'s return type collapsed to `any` — while devDependencies let this repository compile and test. Profiles never install peers, so no duplicate appears.
|
|
105
92
|
- Versions follow the host: exact pins throughout the `dsh-*` family and for `cordis`, matching what the host bundle itself declares so the same copy is reused rather than duplicated.
|
|
106
93
|
|
|
107
94
|
Known upstream limitation: `@deepseek-ai/dsh-llm`'s own declarations import `@deepseek-ai/dsh-attachment`, which it does not declare. It is reached transitively through `dsh-session`, so a consumer running with `skipLibCheck: false` sees `TS2307` from *that* package, not from this one.
|
|
@@ -114,7 +101,7 @@ Known upstream limitation: `@deepseek-ai/dsh-llm`'s own declarations import `@de
|
|
|
114
101
|
- `files` is `icon.svg`, `dist`, `config/*.json`, `README.zh-CN.md`. npm always adds `package.json`, `LICENSE` and `README.md`; `README.zh-CN.md` is listed explicitly so the Chinese copy ships too.
|
|
115
102
|
- `build` starts by deleting `dist/`, because `files` takes the whole directory and a renamed facet would otherwise keep publishing its orphan artifact.
|
|
116
103
|
- `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/`.
|
|
117
|
-
- Node throws `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` for `.ts` inside `node_modules`, which is exactly where a published package is loaded from, so an artifact is mandatory. The barrel's declaration file is generated by `build-host.mjs` because source-form `export * from "./
|
|
104
|
+
- Node throws `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` for `.ts` inside `node_modules`, which is exactly where a published package is loaded from, so an artifact is mandatory. The barrel's declaration file is generated by `build-host.mjs` because source-form `export * from "./record.ts"` specifiers are not resolvable by consumers.
|
|
118
105
|
- Adding a facet means touching three places: `exports`, the `facets` array in `build-host.mjs`, `include` in `tsconfig.build.json`, plus `test/<module>.test.ts`.
|
|
119
106
|
|
|
120
107
|
## Versioning and compatibility
|
|
@@ -122,19 +109,17 @@ Known upstream limitation: `@deepseek-ai/dsh-llm`'s own declarations import `@de
|
|
|
122
109
|
- Semantic versioning applies to the public surface: subpaths, exported value symbols, exported types, and error/reason enums. `0.x` ranges admit patches only, so touching any of those is already breaking under the ranges consumers wrote.
|
|
123
110
|
- `deriveProjectKey`'s hash algorithm, the 8-hex slice and the trailing-segment whitespace cleaning are the untouchable part: changing one digit moves existing lessons and memories out of their buckets.
|
|
124
111
|
- Windows path normalization changed the bucket key shape. On Windows the same directory now yields `last-segment-<hash>` regardless of whether it is spelled with `\` or `/`, and a drive root falls into `default`. **Windows buckets written before normalization no longer match**; POSIX buckets are byte-for-byte unchanged and are pinned by literal assertions in `test/project-key.test.ts`.
|
|
125
|
-
- `BodyRead`'s `reason` gained `"bad-budget"`. A consumer switching exhaustively over it will get a compile error, which is the point.
|
|
126
112
|
- `Locale` is now an alias of the official `BuiltInLocaleId` instead of a hand-written union, so it tracks the host's `LOCALE_IDS`.
|
|
127
113
|
- **`errorText` now returns more text** (same name and signature). It went from a single `message` layer to the official `errorChain` semantics: a cause chain renders as `outer: inner`, an `AggregateError` appends `[e1; e2]`, an empty message falls back to `Error.name`, and a cross-realm `Error` yields its own `message` instead of `String()`'s `"Error: ..."`. **Callers that regex the old text must re-check** (a `exec` that pulls a number out of parentheses, for example).
|
|
128
114
|
- `editPathOf` returns a `kind`-discriminated `EditTarget` with only `write` / `read-view`: `"skip"` was never produced, and leaving it in the type only pushed consumers into writing unreachable branches. When no path can be read it still returns the matching arm with `path` set to `undefined`.
|
|
129
|
-
- `
|
|
130
|
-
- **`servingNonLoopback` must now come from `isNonLoopbackBindHost(webServer.host)`, not from `webServer.host === "0.0.0.0"`.** Host 0.2.1-alpha.2 changed `@deepseek-ai/dsh-host-webserver`'s `Config.host` from the two-value union `'127.0.0.1' | '0.0.0.0'` to any concrete local-interface IP literal and started rejecting wildcard bind addresses at load. The old comparison is therefore constant `false`: a plugin bound to a real interface address stops recognizing its own authority, and every LAN request comes back `403 untrusted host authority`. `TrustOptions` is unchanged, so this is not an API break — it is a fix for a derivation that silently stopped working. `test/trust.test.ts` cross-checks the new predicate against the official `isLoopbackHost`, which became a public export in the same host release.
|
|
115
|
+
- **`./lib/http` and `./lib/trust` are gone.** The plugin family's settings-card data plane moved onto the official Typert Remote, whose `/api` gateway owns authentication, the Host / DNS-rebinding / Origin checks and the body cap; the two facets kept a hand-written copy of rules the host now enforces. No consumer package imports them any more. A plugin that still wants a raw `webServer` route has to write the gate itself — that is deliberate: the published surface no longer offers a second, divergent answer to "is this request allowed".
|
|
131
116
|
|
|
132
117
|
## Quality gates
|
|
133
118
|
|
|
134
119
|
- `npm run check` = typecheck → lint → build → test (coverage thresholds 100 for lines, statements, functions and branches) → format check.
|
|
135
|
-
- `test/publish-manifest.test.ts` is the release-shape gate: every `exports` target must be inside what `files` actually ships, entries must point at build output rather than source, and `publishConfig` must not carry entry fields. It also pins **facet/`exports` parity in both directions**: every `lib/*.ts` needs a matching subpath (a missing one means that facet is not importable by any
|
|
120
|
+
- `test/publish-manifest.test.ts` is the release-shape gate: every `exports` target must be inside what `files` actually ships, entries must point at build output rather than source, and `publishConfig` must not carry entry fields. It also pins **facet/`exports` parity in both directions**: every `lib/*.ts` needs a matching subpath (a missing one means that facet is not importable by any consumer, while the build — which follows the builder's own entries array — stays green), and no dangling subpath may outlive its facet. And it pins **peer optionality**: every `peerDependencies` entry must appear in `peerDependenciesMeta`, because a peer left out of that table is a hard requirement for npm 7+ consumers while typecheck, build, tests and the parity checks all stay green. `./lib/canonicalize-region-paths` is the one facet that lives in `exports` but not in the barrel, and that subpath is load-bearing: the sibling packages' rolldown `build-*.mjs` import it (every sibling except the Python-bridge package), so removing it breaks their builds at once. One set of "internal reference" patterns scans **both planes**: **artifacts** must contain no absolute paths, home-directory references, dates, or sibling-package identifiers; **source and docs** must contain no dates and no "this round / next round" wording that only made sense during collaboration. The artifact pass cannot see comments, so the source pass is what keeps that cleanup from being a one-off.
|
|
136
121
|
- `test/build-host.test.ts` compares the bytes on disk against an in-memory build, so editing `lib/*.ts` without rebuilding goes red.
|
|
137
|
-
- `test/setup-logs.ts` (a vitest setup file) is the ledger for runtime logs: it takes over `console.*`, so the
|
|
122
|
+
- `test/setup-logs.ts` (a vitest setup file) is the ledger for runtime logs: it takes over `console.*`, so the `console.error` calls left in `lib/` neither leak into the test report (that stack trace is pure noise) nor go unclaimed — every `[shared/*]` line must match a template fragment in `test/log-templates.ts` or the run goes red. Do not substitute vitest's `silent`: it only hides, and a newly added log would still go unnoticed.
|
|
138
123
|
- `.github/workflows/ci.yml` runs the gate on push and pull request across a Node matrix and on Windows; the install matrix there additionally packs, installs into a clean directory with no flags, imports every subpath and type-checks a consumer probe.
|
|
139
124
|
|
|
140
125
|
## FAQ
|
package/README.zh-CN.md
CHANGED
|
@@ -23,7 +23,7 @@ pnpm add @jayyuen66/dsh-plugin-shared
|
|
|
23
23
|
把它声明成插件包的普通依赖,按裸包名子路径导入:
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
import {
|
|
26
|
+
import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
终端用户不需要单独安装或启用它:任一消费包都会顺带把它带进去。它缺位时的失败是 `ERR_MODULE_NOT_FOUND`(说明符指向 `@jayyuen66/dsh-plugin-shared/...`),不会静默降级。
|
|
@@ -32,8 +32,8 @@ import { readBody, sendJson } from "@jayyuen66/dsh-plugin-shared/lib/http";
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
| --- | --- |
|
|
35
|
-
| Node.js |
|
|
36
|
-
| dsh |
|
|
35
|
+
| Node.js | `>=22.12.0`——九仓发布侧共用的地板,声明同一枚 `engines.node` 的插件不会因为这个库而被拒 |
|
|
36
|
+
| dsh | `^0.2.1-rc.2`,声明为 **optional** peer(本包 import 时不需要宿主在场) |
|
|
37
37
|
| 模块形态 | 仅 ESM。`require()` 只在支持 `require(esm)` 的 Node 上可用 |
|
|
38
38
|
| Tree shaking | `"sideEffects": false` |
|
|
39
39
|
|
|
@@ -42,7 +42,6 @@ import { readBody, sendJson } from "@jayyuen66/dsh-plugin-shared/lib/http";
|
|
|
42
42
|
| 行为 | 位置 | 范围 |
|
|
43
43
|
| --- | --- | --- |
|
|
44
44
|
| 经 `realpathSync` 读一个目录项 | `lib/project-key.ts` | 只读传给 `deriveProjectKey` 的那条路径;失败(ENOENT/EACCES)退回 `path.resolve` 的结果,不外抛 |
|
|
45
|
-
| 经 `os.networkInterfaces()` 枚举本机网卡地址 | `lib/trust.ts` | **仅当**调用方传 `servingNonLoopback: true`;用来核对请求的 `Host` 是不是本机确实持有的地址。数据不出进程 |
|
|
46
45
|
| 其余一概不碰 | — | `lib/**` 里没有别的文件访问、没有任何写入、不读环境变量、不发外部请求、没有 `eval`/`new Function`/动态 import |
|
|
47
46
|
|
|
48
47
|
`lib/card-apply.ts` 按设计读写 `globalThis` 上的一枚键——这正是它能在 HMR 造成模块实例重复时仍然生效的原因。
|
|
@@ -53,32 +52,20 @@ import { readBody, sendJson } from "@jayyuen66/dsh-plugin-shared/lib/http";
|
|
|
53
52
|
|
|
54
53
|
| 子路径 | 提供什么 |
|
|
55
54
|
| --- | --- |
|
|
56
|
-
| `.` |
|
|
57
|
-
| `./lib/
|
|
58
|
-
| `./lib/
|
|
59
|
-
| `./lib/card-
|
|
55
|
+
| `.` | 下面列出的运行期切面的 barrel。`canonicalize-region-paths` 刻意不进,运行期消费方不必为它付一次导入 |
|
|
56
|
+
| `./lib/tool-events` | `scanToolEvents` → `calls`/`results` 两张表、`toolEventRowsOf`(它的单事件那一步,同一份实现;交回的记录已带归一后的 `arguments` 与 `badArguments`)、`editPathOf` 与 `EditTarget`(按 `kind` 判别,`write`/`read-view` 两支;路径取不到时 `path` 为 `undefined`);台账三个接口字段一律 `readonly`;并转出官方 `SessionEvent` 与两枚 PTC 事件类型 |
|
|
57
|
+
| `./lib/card-apply` | `claimApply` + `CardApplyCtx`:apply 幂等守卫(`globalThis` 上的**自有**布尔标记 + `ctx.effect` 卸载清理) |
|
|
58
|
+
| `./lib/card-diff` | `diffTouched`:草稿层里哪些字段真的与快照不同(按值比较,所以 `undefined`、`null` 与键缺失是同一种值) |
|
|
60
59
|
| `./lib/project-key` | `deriveProjectKey` + `ProjectKeyOptions`:cwd → `尾目录名-<sha256(norm) 前 8 位 hex>`,兜底桶 `default` |
|
|
61
60
|
| `./lib/locale` | `Locale`(官方 `BuiltInLocaleId` 的别名)、`DEFAULT_LOCALE`、`resolveLocale`、`resolveLocalePreference`、`messagesFor`、`MessagesCatalog`,加 `LOCALE_SETTINGS_NAMESPACE` 与 `LOCALE_PREFERENCE_FIELD` |
|
|
62
61
|
| `./lib/lesson-bus` | `settleLessonCall`:同步抛错与异步 rejection 归到同一个 `onFailure` 出口;函数型 thenable 也算可等待(漏判会留下未处理拒绝);`onFailure` 自己抛错时被统一收口,不会分叉成"抛给调用方"或"未处理拒绝" |
|
|
63
62
|
| `./lib/text` | `truncateEnd`(转交官方 `truncateWithoutSplittingSurrogatePair`)与 `truncateStart`(后切;官方导出面没有后切形态) |
|
|
64
|
-
| `./lib/record` | `isRecord`、`fieldOf
|
|
65
|
-
| `./lib/errors` | `errorText`:语义对齐官方 `@deepseek-ai/dsh-llm` 的 `errorChain`(cause 链、`AggregateError` 成员、空 message 回退 name、跨 realm 取自有 message、敌意 getter
|
|
63
|
+
| `./lib/record` | `isRecord`、`fieldOf`、`hasMethods`(服务面校验:值必须是 Record,**且**点名的每个方法都可调用) |
|
|
64
|
+
| `./lib/errors` | `errorText`:语义对齐官方 `@deepseek-ai/dsh-llm` 的 `errorChain`(cause 链、`AggregateError` 成员、空 message 回退 name、跨 realm 取自有 message、敌意 getter 降级),本地零依赖实现;`isSettingsConflict` 由 `code` / `name` 认出宿主的设置冲突形状,跨 realm 抛来的错误也认得 |
|
|
66
65
|
| `./lib/jsonl` | `shrinkJsonlTail`:JSONL 尾部收缩的纯决策件;触发策略与错误出口留给调用方 |
|
|
67
|
-
| `./lib/
|
|
68
|
-
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome`:把落定的子进程映射成官方作业注册表的结局 |
|
|
66
|
+
| `./lib/job-outcome` | `jobOutcomeOf` + `SettledProcess` + `JobOutcome`:把落定的子进程映射成官方作业注册表的结局;`killProc` + `KillableProcess`:收掉子进程句柄,对已回收进程抛出的 `kill()` 忽略 |
|
|
69
67
|
| `./lib/canonicalize-region-paths` | 构建期字符串件:把 rolldown 写的 `//#region <路径>` 折成与 `process.cwd()` 无关的形态。之所以只给子路径,就是为了让运行期消费方不必为它付一次导入 |
|
|
70
68
|
|
|
71
|
-
### 值得知道的安全设定
|
|
72
|
-
|
|
73
|
-
`requestTrust` 存在的原因是单看 `sec-fetch-site` 挡不住 DNS 重绑定:把自家域名解析到 `127.0.0.1` 的恶意页面,在浏览器眼里**确实**是同源,于是 `sec-fetch-site` 与 Origin 两条腿同时通过,只有 Host 那条拒得了。四处刻意选择:
|
|
74
|
-
|
|
75
|
-
1. 缺 `Host` 只在对端是回环时放行——HTTP/1.1 强制带 `Host`,能走到这一支的只有 HTTP/1.0 与裸 socket,也就是本地调用面。
|
|
76
|
-
2. `sec-fetch-site` 用白名单(`same-origin`/`none`/缺失),不是黑名单,所以「同站不同端口」不会被顺手放过。
|
|
77
|
-
3. 非回环服务面只在该地址确实属于本机某块网卡时才接受。**不放** `.local`/`.lan` 一类后缀名——mDNS 名可以被本机任意进程声称。
|
|
78
|
-
4. `guardTrust` 不肯静默 no-op:`sendJson` 在头已发时是空操作,谁把闸门挪到 `await readBody()` 之后,它就会悄悄变成放行。所以那种情形会出声。
|
|
79
|
-
|
|
80
|
-
`readBody` 按 UTF-8 **字节**计数、先做 `content-length` 预检、累积 `Buffer` 后一次解码——逐块切会把跨 chunk 边界的多字节字符变成 U+FFFD,而它仍是合法 JSON。预算不是有限非负数时判 `bad-budget`(HTTP 500)而不是当成没有限额:任何数与 `NaN` 比较都是 false,不设拦等于把整条限额关掉——而那条限额正是本模块存在的理由。
|
|
81
|
-
|
|
82
69
|
## `config/` 面
|
|
83
70
|
|
|
84
71
|
这些是给插件作者用的 lint/test 基线,不是运行期代码,它们要吃你本包自备的工具链:
|
|
@@ -97,11 +84,11 @@ sonarjs 的入口是**惰性解析**的,发生在 `definePluginConfig()` 内
|
|
|
97
84
|
|
|
98
85
|
## 依赖
|
|
99
86
|
|
|
100
|
-
`dependencies`
|
|
87
|
+
`dependencies` 是空的;所有 `@deepseek-ai/*` 只出现在 `peerDependencies` 与 `devDependencies` 里。这条由 `test/publish-manifest.test.ts` 钉住,不是风格偏好:
|
|
101
88
|
|
|
102
89
|
- **写进 `dependencies` 会让消费方的宿主瘫掉。** 消费方是 dsh 插件,被装进 profile(`nodeLinker: hoisted`、`autoInstallPeers: false`)。宿主包只要出现在 `dependencies`,pnpm 就在 profile 里把它们落成**真实目录**,与宿主进程里已有的那份并存。而 `@deepseek-ai/dsh-tools` 的 `TOOL_RUNTIME_SCHEDULER` 是普通 `Symbol`(不是 `Symbol.for`),符号身份按模块实例计算——宿主 `dsh-agent-loop` 拿自己那份的 Symbol 去读由副本挂载出来的 `ToolRuntime`,得到 `undefined`,于是**每一次原生工具调用都抛** `Cannot read properties of undefined (reading 'prepare')`,整个 profile 的工具一起失效。本包的构建、类型检查与测试在这之前全部照绿,所以判据只能落在 manifest 面。
|
|
103
|
-
- `@deepseek-ai/dsh-output-retention` 是**值导入**(`truncateEnd`
|
|
104
|
-
-
|
|
90
|
+
- `@deepseek-ai/dsh-output-retention` 是**值导入**(`truncateEnd` 用它),运行期由宿主提供,因此是唯一那条必需 peer——它在 `peerDependenciesMeta` 里明写成 `"optional": false`,不靠"没写就等于必需",因为没写对 npm 7+ 是 `ERESOLVE` 硬失败,不是一条警告。
|
|
91
|
+
- 其余那些只被发布出去的 `.d.ts` 引用,声明成 optional peer 加 dev:peer 保住类型面不静默降级——`lib/job-outcome` 的 `ShellProcess`/`JobOutcome` 曾经来自消费方根本不保证存在的包,于是在 `skipLibCheck: true` 下写错 `status` 或 `exitCode` 一条都不报,返回值类型直接塌成 `any`;dev 让本包自己的编译与测试解析得到。profile 不装 peer,所以不产生副本。
|
|
105
92
|
- 版本口径跟着宿主走:`dsh-*` 家族内一律精确钉,`cordis` 也精确钉,与宿主本体自己声明的一致,这样是复用同一份而不是装出第二份。
|
|
106
93
|
|
|
107
94
|
已知的上游限制:`@deepseek-ai/dsh-llm` 的声明文件引用了它自己没声明的 `@deepseek-ai/dsh-attachment`。它经 `dsh-session` 传递进来,所以开 `skipLibCheck: false` 的消费方会看到来自**那个包**的 `TS2307`,不是来自本包。
|
|
@@ -114,7 +101,7 @@ sonarjs 的入口是**惰性解析**的,发生在 `definePluginConfig()` 内
|
|
|
114
101
|
- `files` 是 `icon.svg`、`dist`、`config/*.json`、`README.zh-CN.md`。npm 永远附带 `package.json`、`LICENSE` 和 `README.md`;`README.zh-CN.md` 显式列出才会一起发。
|
|
115
102
|
- `build` 第一步删除 `dist/`:`files` 整目录收,切面一旦改名,上一代孤儿产物会继续被发出去。
|
|
116
103
|
- `prepare` 挂 `build`。消费方从 registry 安装时它不执行;它存在是为了让打包、发布与 workspace link 拿到的都是新鲜 `dist/`。
|
|
117
|
-
- 必须有编译产物:Node 对 `node_modules` 里的 `.ts` 直接抛 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,而发出去的包正是躺在那里被载入的。barrel 的声明文件由 `build-host.mjs` 生成,因为源码形态的 `export * from "./
|
|
104
|
+
- 必须有编译产物:Node 对 `node_modules` 里的 `.ts` 直接抛 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,而发出去的包正是躺在那里被载入的。barrel 的声明文件由 `build-host.mjs` 生成,因为源码形态的 `export * from "./record.ts"` 消费方解析不到。
|
|
118
105
|
- 新增切面要同时改三处:`exports`、`build-host.mjs` 的 `facets` 数组、`tsconfig.build.json` 的 `include`,外加 `test/<模块>.test.ts`。
|
|
119
106
|
|
|
120
107
|
## 版本与兼容
|
|
@@ -122,19 +109,17 @@ sonarjs 的入口是**惰性解析**的,发生在 `definePluginConfig()` 内
|
|
|
122
109
|
- 公共面一律按语义化版本:子路径、导出的值符号、导出的类型、错误/原因枚举。`0.x` 的区间只放行 patch 位,所以动这些就是消费方区间里的破坏性变更。
|
|
123
110
|
- `deriveProjectKey` 的哈希算法、8 位切片与尾段空白清洗是不可动的一面——动一位等于把存量教训与记忆从原桶里搬走。
|
|
124
111
|
- Windows 路径归一改变了桶键形态。Windows 上同一目录无论写成 `\` 还是 `/` 都落到同一个 `尾目录名-<哈希>` 桶,盘符根进 `default`。**归一之前写在 Windows 上的桶键不再匹配**;POSIX 的桶键逐字节不变,并由 `test/project-key.test.ts` 以字面量钉住。
|
|
125
|
-
- `BodyRead` 的 `reason` 增加了 `"bad-budget"`。对它做穷尽 `switch` 的消费方会拿到一条编译错误——这正是目的。
|
|
126
112
|
- `Locale` 现在是官方 `BuiltInLocaleId` 的别名,不再是手写的字面量联合,因此跟着宿主的 `LOCALE_IDS` 走。
|
|
127
113
|
- **`errorText` 的返回值变长了**(签名与名字不变)。它从"单层 message"扩成与官方 `errorChain` 同语义:cause 链拼成 `外: 内`、`AggregateError` 附 `[e1; e2]`、空 message 回退 `Error.name`、跨 realm 的 Error 取其自带 `message` 而不是 `String()` 的 `"Error: …"`。**依赖旧文本做正则匹配的调用方要重新核对**(例如按括号取数的那类 `exec`)。
|
|
128
114
|
- `editPathOf` 的返回类型收成按 `kind` 判别的 `EditTarget`,只保留 `write` / `read-view` 两支——`"skip"` 从来不会被产出,留在类型里只会逼调用方写永不执行的分支。路径取不到时仍返回所属那一支、`path` 为 `undefined`。
|
|
129
|
-
- `
|
|
130
|
-
- **`servingNonLoopback` 现在要由 `isNonLoopbackBindHost(webServer.host)` 取,别再写 `webServer.host === "0.0.0.0"`。** 宿主 0.2.1-alpha.2 把 `@deepseek-ai/dsh-host-webserver` 的 `Config.host` 从 `'127.0.0.1' | '0.0.0.0'` 换成任意本机接口的 IP 字面量,并在载入时拒掉通配地址。于是那句旧比较恒为 false:绑在真实网卡地址上的插件认不出自己的权威,局域网来的请求整片吃 `403 untrusted host authority`。`TrustOptions` 的签名没动,所以这不是 API 破坏,而是一条**悄悄失效**的取值口径的修复。`test/trust.test.ts` 会把新谓词与官方 `isLoopbackHost`(同一次宿主发布里才转为公开导出)逐字面对表。
|
|
115
|
+
- **`./lib/http` 与 `./lib/trust` 已删除。** 插件家族的设置卡数据面改跑官方 Typert Remote,认证、Host / DNS 重绑定 / Origin 判据与 body 上限由宿主的 `/api` 网关承担;那两条切面留着的是一份手写的、与宿主不同口径的"这个请求准不准"。没有任何消费包再 import 它们。还要自建 `webServer` 路由的插件得自己写闸门——这是刻意的:发布面不再提供第二套答案。
|
|
131
116
|
|
|
132
117
|
## 质量门
|
|
133
118
|
|
|
134
119
|
- `npm run check` = typecheck → lint → build → test(覆盖率 lines/statements/functions/branches 四阈值 100)→ fmt:check。
|
|
135
|
-
- `test/publish-manifest.test.ts` 是发布形态门:`exports` 的每个目标都必须在 `files` 真会发出去的范围内;入口必须指编译产物而不是源码;`publishConfig` 不得携带入口字段。它还钉着**切面与 `exports` 的双向 parity**:每个 `lib/*.ts` 都要有对应子路径(少一条 =
|
|
120
|
+
- `test/publish-manifest.test.ts` 是发布形态门:`exports` 的每个目标都必须在 `files` 真会发出去的范围内;入口必须指编译产物而不是源码;`publishConfig` 不得携带入口字段。它还钉着**切面与 `exports` 的双向 parity**:每个 `lib/*.ts` 都要有对应子路径(少一条 = 该切面对任何消费包都不可 import,而构建走的是 builder 自己的 entries 数组、不会报错),反过来也不许留悬空子路径。它还钉 **peer 的 optional 表态**:每一条 `peerDependencies` 都必须在 `peerDependenciesMeta` 里出现——没写进那张表的 peer 对 npm 7+ 的消费方就是硬要求,而类型检查、构建、用例与 parity 在那种状态下全部照绿。`./lib/canonicalize-region-paths` 正是"在 `exports` 里、不在 barrel 里"的那一枚,且这条子路径是承重的(除 Python 桥接包以外,各兄弟包的 rolldown `build-*.mjs` 都靠它),删它会一次性打断它们的构建。同一组"内部指涉"判据扫两个平面:**产物**不得出现绝对路径、家目录指涉、日期或兄弟包标识符;**源码与文档**不得出现日期与"这一轮/下一轮"这类只在协作当时成立的措辞——产物面那条看不见注释,所以必须单独扫源码,否则清理只是一次性动作、没有回归保护。
|
|
136
121
|
- `test/build-host.test.ts` 里的指纹用例比较磁盘产物与内存构建的字节,所以改了 `lib/*.ts` 忘了重建会直接红。
|
|
137
|
-
- `test/setup-logs.ts`(vitest setupFile)是算子日志的账本:它接管 `console.*`,于是 `lib/`
|
|
122
|
+
- `test/setup-logs.ts`(vitest setupFile)是算子日志的账本:它接管 `console.*`,于是 `lib/` 里留下的 `console.error` 既不会漏进测试报告(那条栈是纯噪点),又必须被用例认领——每条 `[shared/*]` 日志都要对得上 `test/log-templates.ts` 里的模板片段,否则当场红。别拿 vitest 的 `silent` 顶替:那只是不显示,新飘出来的日志照样没人看。
|
|
138
123
|
- `.github/workflows/ci.yml` 在 push 与 PR 上跨 Node 版本矩阵、并在 Windows 上跑同一道门;那边的安装矩阵还会打包、在干净目录里不带任何 flag 安装、逐子路径 import、并对消费方探针文件做类型检查。
|
|
139
124
|
|
|
140
125
|
## 常见问题
|
package/dist/card-apply.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e,t,n){let r=globalThis;if(r[t]===!0)return!1;r[t]=!0;try{e.effect(()=>()=>{r[t]=void 0},n)}catch(e){throw r[t]=void 0,e}return!0}export{e as claimApply};
|
|
1
|
+
function e(e,t,n){let r=globalThis;if(Object.hasOwn(r,t)&&r[t]===!0)return!1;r[t]=!0;try{e.effect(()=>()=>{r[t]=void 0},n)}catch(e){throw r[t]=void 0,e}return!0}export{e as claimApply};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,t){let n=[];for(let r of Object.keys(e))JSON.stringify(e[r]??null)!==JSON.stringify(t[r]??null)&&n.push(r);return n}export{e as diffTouched};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createRequire as e}from"node:module";import{defineConfig as t}from"oxlint";const n=`同 no-unsafe-member-access`,r={"vitest/*":{kind:`文体作用域`,measured:7,note:`用例结构类判据在非用例文件上无从计算`},"vitest/no-hooks":{kind:`上游不推荐`,measured:181,note:`禁用 hook 本身;本仓按官方 vitest 用法大量使用 before*/after*`},"vitest/no-conditional-in-test":{kind:`文体作用域`,measured:523,note:`平台/宿主版本分支是刻意的(macOS 与 Linux 的家目录形状、0.1.7 与 0.1.6 的 API 面)`},"vitest/max-expects":{kind:`上游不推荐`,measured:163,note:`给单条用例的断言条数设上限属口味,不指向缺陷`},"vitest/prefer-expect-assertions":{kind:`上游不推荐`,measured:2902,note:`要求每个 async 用例手写 expect.assertions(n),是与断言强度无关的计数仪式`},"vitest/require-test-timeout":{kind:`上游不推荐`,measured:2892,note:`要求每条用例显式 timeout;本仓用泵式假时钟,超时由 13 份 vitest 配置统一给`},"vitest/prefer-importing-vitest-globals":{kind:`实测否证`,measured:0,note:`autofix 方向是删掉显式 import 改回裸全局;13 份 vitest 配置都没开 globals:true,应用后当场 3605 处 TS2593、测试全废`},"vitest/no-importing-vitest-globals":{kind:`实测否证`,measured:133,note:`与上一条同族、方向相反,两条同时开就是把两种写法都判死`},"vitest/prefer-import-in-mock":{kind:`实测否证`,measured:6,note:"动态形态按完整模块面校验工厂返回值,而 @types/node 的 os、@types/react 都是 `export =` 形状、类型面上没有 default;实测三处 TS2769+TS2740/2741,react 那处无论怎么 spread 都补不出 default"},"vitest/prefer-lowercase-title":{kind:`上游不推荐`,measured:171,note:`只在用例文件里生效:它带 autofix,实测一轮 oxlint --fix 静默小写化了 260 条标题(含 nO_VERIFY 这种坏名)。包级 titlePrefixes 只留真正的技术名(GET/CSRF/组件名…):非技术名(工单号一类)不该借这条豁免留在标题里,而中文开头的标题天然满足判据(无大小写)⇒ 免检面能收多小收多小。实测 oxlint 前缀一命中就把整条标题免检,所以陈旧条目由配置基线自检按『免检面未被使用』点名,不靠人工巡检`},"vitest/prefer-called-times":{kind:`对侧互斥`,measured:0,note:`探针实测(同文件四种写法各跑一次):toHaveBeenCalledTimes(1) 被 prefer-called-once 判死、toHaveBeenCalledOnce() 被本条判死、expect(spy.mock.calls).toHaveLength(1) 被 prefer-to-have-been-called-times 判死,只有 n>=2 能同时满足三条 ⇒ 三条同开等于「单次调用」这条断言无法书写(实测逼得消费方改用 assert.equal(spy.mock.calls.length, 1) 绕行)。留 prefer-called-once 与 prefer-to-have-been-called-times 两条方向一致的组合,关本条`},"vitest/prefer-describe-function-title":{kind:`对侧互斥`,measured:48,note:"要求 `describe(fn)`,而 valid-title 要求字符串字面量(oxlint 1.85 的 valid-title 是 DummyRule、无任何选项);上游 recommended 收的是 valid-title,取它,26 处 `describe(fn)` 按代码改成同名字符串"},"vitest/prefer-to-be-truthy":{kind:`实测否证`,measured:117,note:`把 toStrictEqual(true) 放宽成 toTruthy(),接受任意真值是削弱断言`},"vitest/prefer-to-be-falsy":{kind:`实测否证`,measured:89,note:`同族,放宽成 toFalsy() 同样是削弱断言`},"import/no-default-export":{kind:`契约冲突`,measured:38,note:`官方 cordis 入口就是 default export(vendor/loader/src/index.ts:201 先 unwrap default),12 条正是 12 个 bundle 入口`},"import/no-named-export":{kind:`契约冲突`,measured:895,note:`13 个包与上游 deepseek-harness 一样以具名导出 + 子路径说明符对外暴露`},"import/prefer-default-export":{kind:`契约冲突`,measured:58,note:`与 no-default-export 同族、方向与 cordis 入口相反`},"import/no-nodejs-modules":{kind:`契约冲突`,measured:448,note:"host 半就跑在 node 上,`node:` 内置模块是运行环境本身而不是越界"},"import/no-named-as-default-member":{kind:`契约冲突`,measured:47,note:`对具名导出的默认对象做成员访问是本仓消费官方声明的常态`},"import/no-anonymous-default-export":{kind:`契约冲突`,measured:12,note:`官方 cordis 入口是匿名对象 default export(registry.ts:92 读 name/Config/apply),具名化等于改官方形态`},"eslint/no-duplicate-imports":{kind:`实测否证`,measured:166,note:`ESLint 官方已废弃(不区分 import type),由 import/no-duplicates 取代`},"import/group-exports":{kind:`上游不推荐`,measured:766,note:`要求单点导出,与具名导出面冲突`},"import/exports-last":{kind:`上游不推荐`,measured:528,note:`要求导出语句在文件末尾`},"import/no-relative-parent-imports":{kind:`上游不推荐`,measured:277,note:`包内 lib/ 与 test/ 的相对父级引用是既有结构`},"import/max-dependencies":{kind:`上游不推荐`,measured:25,note:`给 import 条数设上限;host 半消费官方服务面天然依赖多`},"node/no-process-env":{kind:`上游不推荐`,measured:51,note:`环境变量是本仓的输入面(DSH_HOME、profile 目录、构建期开关);eslint-plugin-n 的 recommended 也不含本条`},"eslint/no-magic-numbers":{kind:`上游不推荐`,measured:5774,note:`把 HTTP 状态码/毫秒阈值抽成常量的计数规则`},"eslint/sort-keys":{kind:`上游不推荐`,measured:4101,note:`对象键排序;成对字面量与 locale 字典的可读性会一起牺牲`},"eslint/sort-imports":{kind:`上游不推荐`,measured:675,note:`字母序 import,与 oxfmt 的分组形态冲突`},"eslint/capitalized-comments":{kind:`上游不推荐`,measured:2388,note:`本仓注释以中文为主,「首字母大写」对中文注释无从判定`},"eslint/no-ternary":{kind:`上游不推荐`,measured:1024,note:`禁三元;改写成 if/else 并不会更正确`},"eslint/no-undefined":{kind:`上游不推荐`,measured:2009,note:"禁 `undefined` 字面量,与本仓大量显式可选返回形态冲突"},"eslint/no-void":{kind:`上游不推荐`,measured:172,note:"禁 `void` 前缀,而 `void promise` 正是消解 no-floating-promises 的官方写法"},"unicorn/no-null":{kind:`上游不推荐`,measured:788,note:`禁 null;官方声明与 JSON 边界大量用 null,改成 undefined 会改变序列化形状`},"typescript/prefer-readonly-parameter-types":{kind:`上游不推荐`,measured:2433,note:`要求每个入参类型全 readonly;官方声明面(SessionEvent/ConfigForm 等)是可变的,改不动也不该改`},"oxc/no-async-await":{kind:`对侧互斥`,measured:2160,note:`禁 async/await,而同表里 promise-function-async、require-await、no-floating-promises 都在推 async`},"oxc/no-optional-chaining":{kind:`对侧互斥`,measured:1354,note:"禁 `?.`,而 typescript/prefer-optional-chain 正在要求把 && 链改成 `?.`"},"oxc/no-rest-spread-properties":{kind:`对侧互斥`,measured:550,note:`禁对象 spread,而 ES2024 目标与官方声明的投影写法都靠它`},"typescript/no-explicit-any":{kind:`文体作用域`,measured:1,note:`用例里的桩件面(mini-react 之类)需要 any 才塞得进官方签名`},"typescript/no-non-null-assertion":{kind:`文体作用域`,measured:536,note:"用例里 `!` 是「前驱条件已成立」的最短写法"},"typescript/no-unsafe-type-assertion":{kind:`文体作用域`,measured:844,note:"桩件需要 `as never` / `as unknown as` 跨官方名义类型"},"typescript/require-await":{kind:`文体作用域`,measured:467,note:`官方 async 契约的同步实现与测试替身撑不住这条;prod 侧的条数按代码修,不进豁免`},"eslint/require-await":{kind:`对侧互斥`,measured:481,note:`它与 typescript/require-await 是同一条判据的两份实现,不是两条判据:探针实测(一段无 await 的 async 函数、--type-aware)单开各自都报、同开报两次 ⇒ 2 errors / 1 defect。按 typescript-eslint 官方的 extension-rule 口径裁决:留带类型信息的扩展那条、关基线那条。覆盖面没有因此变小:全仓逐行去重实测 514 行两点同报、typescript 独有 0 行、eslint 独有只落在用例面与配置自身的中间态;而 --type-aware 实测对 .mjs 也会由扩展那条报出,所以 scripts/*.mjs 面不需要基线那条补位。用例文件由 TEST_OVERRIDES 整族豁免`},"eslint/max-statements":{kind:`文体作用域`,measured:549,note:`用例体天然长(given/when/then 展开)`},"eslint/max-lines-per-function":{kind:`文体作用域`,measured:365,note:`同上`},"eslint/max-lines":{kind:`文体作用域`,measured:54,note:`单个用例文件的长度不作为缺陷信号`},"node/no-sync":{kind:`文体作用域`,measured:299,note:`构建脚本与门禁脚本按设计同步读;生产代码的写侧由每包 syncWrites 逐项登记,不在豁免里`},"node/no-top-level-await":{kind:`文体作用域`,measured:53,note:`脚本以 node 直接执行,顶层 await 是 ESM 常态;prod 侧 0 条`},"typescript/strict-boolean-expressions":{kind:`文体作用域`,measured:0,note:`scripts/*.mjs 无类型面,隐式 any 上无从判定`},"typescript/no-implied-eval":{kind:`文体作用域`,measured:0,note:`同上:这条要类型信息才能判`},"typescript/no-unsafe-member-access":{kind:`文体作用域`,measured:0,note:`无类型标注的脚本里每个值都落进隐式 any`},"typescript/no-unsafe-call":{kind:`文体作用域`,measured:0,note:n},"typescript/no-unsafe-assignment":{kind:`文体作用域`,measured:0,note:n},"typescript/no-unsafe-return":{kind:`文体作用域`,measured:0,note:n},"typescript/no-unsafe-argument":{kind:`文体作用域`,measured:0,note:n},"typescript/require-array-sort-compare":{kind:`文体作用域`,measured:0,note:`无类型脚本里「比较器缺失」判不出来`},"unicorn/no-process-exit":{kind:`文体作用域`,measured:2,note:`CLI 脚本的 process.exit 就是它的返回方式`},"eslint/no-continue":{kind:`文体作用域`,measured:53,note:`只对 scripts/ 豁免:TS 侧实测 0 条(包代码本来就不用 continue),908 行的门禁脚本改成嵌套 if 只会更难读`},"unicorn/max-nested-calls":{kind:`文体作用域`,measured:59,note:`只对 scripts/ 豁免:TS 侧实测 0 条;CLI 脚本里链式 map/filter/join 的深度由数据形状决定,不指向缺陷`},"unicorn/no-useless-undefined":{kind:`文体作用域`,measured:0,note:`脚本侧同上调;prod 仍按配置项 checkArguments:false 生效`},"react/set-state-in-effect":{kind:`契约冲突`,measured:16,note:`卡片用 effect 同步宿主态到 store 是挂载适配器形态;Compiler 建议的 render 期推导要改写 12 份适配器的状态流`},"react/no-deriving-state-in-effects":{kind:`契约冲突`,measured:7,note:`适配器形态里「props/state 派生值在 effect 对齐宿主」是挂载动作,不是可下沉到 render 的纯派生`},"react/exhaustive-effect-dependencies":{kind:`契约冲突`,measured:8,note:`Compiler 版依赖审计对适配器的内联闭包全量追问;与 react-hooks/exhaustive-deps(保留并已修)形成双门,留后者`},"react/hooks":{kind:`契约冲突`,measured:13,note:`「同一 hook 函数跨渲染同引用」在适配器的内联闭包形态下整体不成立;收敛批次把闭包提升后再回来开`},"react/refs":{kind:`契约冲突`,measured:1,note:`挂载适配器在 render 期读 ref 对齐宿主布局;收敛批次改成挂载后读`},"react/todo":{kind:`上游不推荐`,measured:6,note:`报的是 React 编译器 BuildHIR 自己不支持 try/catch(-finally) 的上游 TODO,不是代码缺陷`},"react/rules-of-hooks":{kind:`文体作用域`,measured:3,note:`只对用例文件豁免(TEST_FILE_OVERRIDES):测试架在组件树外直接驱动卡片适配 hook(useCardOf / useSyncExternalStore 桩),调用次数顺序由桩件固定。键用表载前缀 react/(诊断文案显示 react-hooks(...) 是显示名,配置键以 --rules 的 Source 列为准)`}},i=[`typescript/no-empty-object-type`],a=[`existsSync`,`readFileSync`,`readdirSync`,`readSync`,`realpathSync`,`statSync`],o=[`eslint`,`typescript`,`unicorn`,`oxc`,`node`,`promise`,`vitest`,`import`,`react`,`jsx-a11y`],s=[`eslint-plugin-sonarjs`,`eslint-plugin-sonarjs/cjs/plugin.js`];function c(){let t=e(import.meta.url),n=[];for(let e of s)try{return t.resolve(e)}catch{n.push(e)}throw Error(`oxlint 基线要挂 eslint-plugin-sonarjs,但从本包解析不到它(试过 ${n.join(`、`)})。要么在本包 devDependencies 里装 eslint-plugin-sonarjs,要么把自己解析到的入口经 definePluginConfig({ jsPlugins: [...] }) 传进来——带了入口就不会再解析(那条提示才真正兑现)。`)}function l(e,t=c){let n=e??[],r=null;try{r=t()}catch(e){if(n.length===0)throw e}return r===null?[...n]:[r,...n.filter(e=>e!==r)]}const u={"sonarjs/no-identical-functions":`error`,"sonarjs/no-duplicated-branches":`error`,"sonarjs/no-all-duplicated-branches":`error`,"sonarjs/no-collapsible-if":`error`,"sonarjs/no-identical-conditions":`error`,"sonarjs/no-identical-expressions":`error`,"sonarjs/no-duplicate-string":`error`,"sonarjs/cognitive-complexity":`error`,"sonarjs/max-switch-cases":`error`,"sonarjs/no-inverted-boolean-check":`error`,"sonarjs/no-duplicate-in-composite":`error`,"sonarjs/duplicates-in-character-class":`error`,"sonarjs/no-useless-catch":`error`,"sonarjs/no-extra-arguments":`error`,"sonarjs/no-redundant-assignments":`error`,"sonarjs/prefer-single-boolean-return":`error`,"sonarjs/label-position":`error`},d={correctness:`error`,suspicious:`error`,style:`error`,perf:`error`,restriction:`error`,pedantic:`error`,nursery:`error`},f={typeAware:!0,typeCheck:!0,denyWarnings:!0,maxWarnings:0,reportUnusedDisableDirectives:`error`,respectEslintDisableDirectives:!1},p=[`**/node_modules/**`,`.toolchain/**`,`coverage/**`,`dist/**`,`client.js`,`host.js`],m=/* @__PURE__ */ `vitest/consistent-each-for.vitest/consistent-test-filename.vitest/consistent-test-it.vitest/consistent-vitest-vi.vitest/expect-expect.vitest/hoisted-apis-on-top.vitest/max-expects.vitest/max-nested-describe.vitest/no-alias-methods.vitest/no-commented-out-tests.vitest/no-conditional-expect.vitest/no-conditional-in-test.vitest/no-conditional-tests.vitest/no-disabled-tests.vitest/no-duplicate-hooks.vitest/no-focused-tests.vitest/no-hooks.vitest/no-identical-title.vitest/no-import-node-test.vitest/no-importing-vitest-globals.vitest/no-interpolation-in-snapshots.vitest/no-large-snapshots.vitest/no-mocks-import.vitest/no-restricted-matchers.vitest/no-restricted-vi-methods.vitest/no-standalone-expect.vitest/no-test-prefixes.vitest/no-test-return-statement.vitest/no-unneeded-async-expect-function.vitest/padding-around-after-all-blocks.vitest/padding-around-test-blocks.vitest/prefer-called-exactly-once-with.vitest/prefer-called-once.vitest/prefer-called-times.vitest/prefer-called-with.vitest/prefer-comparison-matcher.vitest/prefer-describe-function-title.vitest/prefer-each.vitest/prefer-equality-matcher.vitest/prefer-expect-assertions.vitest/prefer-expect-resolves.vitest/prefer-expect-type-of.vitest/prefer-hooks-in-order.vitest/prefer-hooks-on-top.vitest/prefer-import-in-mock.vitest/prefer-importing-vitest-globals.vitest/prefer-lowercase-title.vitest/prefer-mock-promise-shorthand.vitest/prefer-mock-return-shorthand.vitest/prefer-snapshot-hint.vitest/prefer-spy-on.vitest/prefer-strict-boolean-matchers.vitest/prefer-strict-equal.vitest/prefer-to-be.vitest/prefer-to-be-falsy.vitest/prefer-to-be-object.vitest/prefer-to-be-truthy.vitest/prefer-to-contain.vitest/prefer-to-have-been-called-times.vitest/prefer-to-have-length.vitest/prefer-todo.vitest/require-awaited-expect-poll.vitest/require-hook.vitest/require-local-test-context-for-concurrent-snapshots.vitest/require-mock-type-parameters.vitest/require-test-timeout.vitest/require-to-throw-message.vitest/require-top-level-describe.vitest/valid-describe-callback.vitest/valid-expect.vitest/valid-expect-in-promise.vitest/valid-title.vitest/warn-todo`.split(`.`),h=/* @__PURE__ */ new Set([`vitest/max-expects`,`vitest/no-conditional-in-test`,`vitest/no-hooks`,`vitest/no-importing-vitest-globals`,`vitest/prefer-called-times`,`vitest/prefer-describe-function-title`,`vitest/prefer-expect-assertions`,`vitest/prefer-import-in-mock`,`vitest/prefer-importing-vitest-globals`,`vitest/prefer-to-be-falsy`,`vitest/prefer-to-be-truthy`,`vitest/require-test-timeout`]),g={"eslint/one-var":[`error`,`never`],"eslint/func-style":[`error`,`declaration`,{allowArrowFunctions:!0,overrides:{namedExports:`ignore`}}],"eslint/id-length":[`error`,{exceptions:[`a`,`i`,`t`,`x`,`y`,`z`]}],"eslint/init-declarations":[`error`,`never`,{ignoreForLoopInit:!0}],"eslint/max-lines":[`error`,{max:2e3,skipBlankLines:!0,skipComments:!0}],"eslint/max-lines-per-function":[`error`,{max:120,skipBlankLines:!0,skipComments:!0}],"eslint/complexity":`error`,"eslint/max-params":[`error`,{max:5}],"eslint/max-statements":[`error`,50],"eslint/no-console":[`error`,{allow:[`warn`,`error`,`info`]}],"eslint/no-param-reassign":`error`,"eslint/no-underscore-dangle":[`error`,{allow:[`__memoryInsightCardApplied`,`__memoryTdaiCardApplied`,`__sessionRescueApplied`,`__wukilDevToolsCardApplied`]}],"eslint/no-magic-numbers":`off`,"eslint/sort-keys":`off`,"eslint/sort-imports":`off`,"eslint/capitalized-comments":`off`,"eslint/no-ternary":`off`,"eslint/no-undefined":`off`,"eslint/no-void":`off`,"eslint/no-duplicate-imports":`off`,"eslint/require-await":`off`,"import/no-named-export":`off`,"import/group-exports":`off`,"import/exports-last":`off`,"import/prefer-default-export":`off`,"import/no-nodejs-modules":`off`,"import/no-relative-parent-imports":`off`,"import/max-dependencies":`off`,"import/no-default-export":`off`,"import/no-anonymous-default-export":`off`,"import/no-named-as-default-member":`off`,"import/consistent-type-specifier-style":[`error`,`prefer-top-level`],"import/no-duplicates":`error`,"import/no-cycle":`error`,"import/named":`error`,"import/first":`error`,"import/newline-after-import":`error`,"node/no-process-env":`off`,"node/no-sync":[`error`,{ignores:a}],"node/no-top-level-await":`error`,"oxc/no-async-await":`off`,"oxc/no-optional-chaining":`off`,"oxc/no-rest-spread-properties":`off`,"promise/always-return":`error`,"promise/no-promise-in-callback":[`error`,{exemptDeclarations:!0}],"promise/no-return-wrap":`error`,"promise/param-names":`error`,"promise/prefer-await-to-then":`error`,"typescript/consistent-type-imports":[`error`,{disallowTypeAnnotations:!1}],"typescript/explicit-function-return-type":[`error`,{allowDirectConstAssertionInArrowFunctions:!0,allowExpressions:!0,allowHigherOrderFunctions:!0,allowIIFEs:!0,allowTypedFunctionExpressions:!0,allowedNames:[`handler`,`callback`]}],"typescript/no-explicit-any":`error`,"typescript/no-floating-promises":`error`,"typescript/no-unsafe-assignment":`error`,"typescript/no-unsafe-type-assertion":`error`,"typescript/no-non-null-assertion":`error`,"typescript/require-await":`error`,"typescript/prefer-readonly-parameter-types":`off`,"typescript/promise-function-async":[`error`,{checkArrowFunctions:!1}],"typescript/consistent-return":[`error`,{treatUndefinedAsUnspecified:!0}],"typescript/only-throw-error":`error`,"unicorn/no-array-reduce":[`error`,{allowSimpleOperations:!0}],"unicorn/no-null":`off`,"unicorn/no-useless-undefined":[`error`,{checkArguments:!1}],"react/set-state-in-effect":`off`,"react/no-deriving-state-in-effects":`off`,"react/exhaustive-effect-dependencies":`off`,"react/hooks":`off`,"react/refs":`off`,"react/todo":`off`,...Object.fromEntries(m.map(e=>[e,`off`]))},_={files:[`**/*.test.ts`,`**/*.test.tsx`,`**/test/**`,`**/__tests__/**`],rules:{"typescript/no-non-null-assertion":`off`,"typescript/no-unsafe-type-assertion":`off`,"typescript/no-explicit-any":`off`,"typescript/require-await":`off`,"eslint/max-statements":`off`,"eslint/max-lines":`off`,"eslint/max-lines-per-function":`off`,"node/no-top-level-await":`off`}},v=[`expect`,`expectTypeOf`,`assert`,`assertType`],y=/^[A-Za-z_$][\w$]*$/u,b={files:[`**/*.test.ts`,`**/*.test.tsx`,`**/__tests__/**/*.spec.ts`],rules:{...Object.fromEntries(m.filter(e=>!h.has(e)).map(e=>[e,`error`])),"vitest/expect-expect":[`error`,{assertFunctionNames:v}],"react/rules-of-hooks":`off`}};function x(e={}){let t=e.assertionHelpers??[],n=e.titlePrefixes??[],r=[...t,...n].filter(e=>!y.test(e));if(r.length>0)throw Error(`assertionHelpers / titlePrefixes 只收裸标识符,不接受通配或带点形式(前者等于把 vitest/expect-expect 掏空):${r.join(`, `)}`);let i=[.../* @__PURE__ */ new Set([...v,...t])];return{...b,rules:{...b.rules,"vitest/expect-expect":[`error`,{assertFunctionNames:i}],...n.length===0?{}:{"vitest/prefer-lowercase-title":[`error`,{allowedPrefixes:[...new Set(n)]}]}}}}const S={files:[`scripts/**`,`*.mjs`,`*.mts`],rules:{"eslint/no-continue":`off`,"unicorn/max-nested-calls":`off`,"eslint/id-length":[`error`,{exceptions:[`a`,`i`,`t`,`x`,`y`,`z`,`n`,`p`,`s`,`v`,`e`,`f`,`ok`,`fs`,`id`]}],"unicorn/no-process-exit":`off`,"unicorn/no-useless-undefined":`off`,"typescript/no-non-null-assertion":`off`,"node/no-sync":`off`,"node/no-top-level-await":`off`,"typescript/no-unsafe-member-access":`off`,"typescript/no-unsafe-call":`off`,"typescript/no-unsafe-assignment":`off`,"typescript/no-unsafe-return":`off`,"typescript/no-unsafe-argument":`off`,"typescript/strict-boolean-expressions":`off`,"typescript/no-implied-eval":`off`,"typescript/require-array-sort-compare":`off`}};function C(e){let t={...r,...e};return e=>{let n=t[e];if(n!==void 0)return n;let r=e.indexOf(`/`);return r===-1?void 0:t[`${e.slice(0,r)}/*`]}}function w(e,t,n){if(t===void 0)return;let r=[];for(let[e,i]of Object.entries(t))if((Array.isArray(i)?i[0]:i)===`off`){let t=n(e);(t===void 0||t.note.trim().length===0)&&r.push(e)}if(r.length>0)throw Error(`${e} 里这些规则被关成 off 但没有依据(OFF_JUSTIFICATIONS 查不到,或 note 为空):${r.join(`, `)}。要么在代码里修,要么给它补一条 kind + measured + note。`)}function T(e={}){let{syncWrites:n=[],extraRules:r={},extraOverrides:i=[],extraIgnorePatterns:s=[],jsPlugins:c,offReasons:m={},assertionHelpers:h=[],titlePrefixes:v=[]}=e,y=C(m),b={...g,...u,...n.length>0?{"node/no-sync":[`error`,{ignores:[.../* @__PURE__ */ new Set([...a,...n])]}]}:{},...r},T=[_,x({assertionHelpers:h,titlePrefixes:v}),S,...i];w(`rules`,b,y);for(let[e,t]of T.entries())w(`overrides[${e}]`,t.rules,y);return t({plugins:o,env:{node:!0,es2024:!0,browser:!0},options:f,categories:d,ignorePatterns:[...p,...s],jsPlugins:l(c),rules:b,overrides:T})}export{g as BASE_RULES,r as OFF_JUSTIFICATIONS,a as READONLY_SYNC_ALLOWLIST,i as RETIRED_OFFS,S as SCRIPTS_OVERRIDES,b as TEST_FILE_OVERRIDES,_ as TEST_OVERRIDES,v as VITEST_ASSERTION_DEFAULTS,T as definePluginConfig,x as makeTestFileOverrides,l as resolveJsPlugins};
|
|
1
|
+
import{createRequire as e}from"node:module";import{defineConfig as t}from"oxlint";const n=`同 no-unsafe-member-access`,r={"vitest/*":{kind:`文体作用域`,measured:7,note:`用例结构类判据在非用例文件上无从计算`},"vitest/no-hooks":{kind:`上游不推荐`,measured:181,note:`禁用 hook 本身;本仓按官方 vitest 用法大量使用 before*/after*`},"vitest/no-conditional-in-test":{kind:`文体作用域`,measured:523,note:`平台/宿主版本分支是刻意的(macOS 与 Linux 的家目录形状、0.1.7 与 0.1.6 的 API 面)`},"vitest/max-expects":{kind:`上游不推荐`,measured:163,note:`给单条用例的断言条数设上限属口味,不指向缺陷`},"vitest/prefer-expect-assertions":{kind:`上游不推荐`,measured:2902,note:`要求每个 async 用例手写 expect.assertions(n),是与断言强度无关的计数仪式`},"vitest/require-test-timeout":{kind:`上游不推荐`,measured:2892,note:`要求每条用例显式 timeout;本仓用泵式假时钟,超时由 13 份 vitest 配置统一给`},"vitest/prefer-importing-vitest-globals":{kind:`实测否证`,measured:0,note:`autofix 方向是删掉显式 import 改回裸全局;13 份 vitest 配置都没开 globals:true,应用后当场 3605 处 TS2593、测试全废`},"vitest/no-importing-vitest-globals":{kind:`实测否证`,measured:133,note:`与上一条同族、方向相反,两条同时开就是把两种写法都判死`},"vitest/prefer-import-in-mock":{kind:`实测否证`,measured:6,note:"动态形态按完整模块面校验工厂返回值,而 @types/node 的 os、@types/react 都是 `export =` 形状、类型面上没有 default;实测三处 TS2769+TS2740/2741,react 那处无论怎么 spread 都补不出 default"},"vitest/prefer-lowercase-title":{kind:`上游不推荐`,measured:171,note:`只在用例文件里生效:它带 autofix,实测一轮 oxlint --fix 静默小写化了 260 条标题(含 nO_VERIFY 这种坏名)。包级 titlePrefixes 只留真正的技术名(GET/CSRF/组件名…):非技术名(工单号一类)不该借这条豁免留在标题里,而中文开头的标题天然满足判据(无大小写)⇒ 免检面能收多小收多小。实测 oxlint 前缀一命中就把整条标题免检,所以陈旧条目由配置基线自检按『免检面未被使用』点名,不靠人工巡检`},"vitest/prefer-called-times":{kind:`对侧互斥`,measured:0,note:`探针实测(同文件四种写法各跑一次):toHaveBeenCalledTimes(1) 被 prefer-called-once 判死、toHaveBeenCalledOnce() 被本条判死、expect(spy.mock.calls).toHaveLength(1) 被 prefer-to-have-been-called-times 判死,只有 n>=2 能同时满足三条 ⇒ 三条同开等于「单次调用」这条断言无法书写(实测逼得消费方改用 assert.equal(spy.mock.calls.length, 1) 绕行)。留 prefer-called-once 与 prefer-to-have-been-called-times 两条方向一致的组合,关本条`},"vitest/prefer-describe-function-title":{kind:`对侧互斥`,measured:48,note:"要求 `describe(fn)`,而 valid-title 要求字符串字面量(oxlint 1.85 的 valid-title 是 DummyRule、无任何选项);上游 recommended 收的是 valid-title,取它,26 处 `describe(fn)` 按代码改成同名字符串"},"vitest/prefer-to-be-truthy":{kind:`实测否证`,measured:117,note:`把 toStrictEqual(true) 放宽成 toTruthy(),接受任意真值是削弱断言`},"vitest/prefer-to-be-falsy":{kind:`实测否证`,measured:89,note:`同族,放宽成 toFalsy() 同样是削弱断言`},"import/no-default-export":{kind:`契约冲突`,measured:38,note:`官方 cordis 入口就是 default export(vendor/loader/src/index.ts 的 unwrapExports 先 unwrap default),12 条正是 12 个 bundle 入口`},"import/no-named-export":{kind:`契约冲突`,measured:895,note:`13 个包与上游 deepseek-harness 一样以具名导出 + 子路径说明符对外暴露`},"import/prefer-default-export":{kind:`契约冲突`,measured:58,note:`与 no-default-export 同族、方向与 cordis 入口相反`},"import/no-nodejs-modules":{kind:`契约冲突`,measured:448,note:"host 半就跑在 node 上,`node:` 内置模块是运行环境本身而不是越界"},"import/no-named-as-default-member":{kind:`契约冲突`,measured:47,note:`对具名导出的默认对象做成员访问是本仓消费官方声明的常态`},"import/no-anonymous-default-export":{kind:`契约冲突`,measured:12,note:`官方 cordis 入口是匿名对象 default export(registry.ts 的 RegistryService.plugin() 读 name/Config,isApplicable() 判 apply),具名化等于改官方形态`},"eslint/no-duplicate-imports":{kind:`实测否证`,measured:166,note:`ESLint 官方已废弃(不区分 import type),由 import/no-duplicates 取代`},"import/group-exports":{kind:`上游不推荐`,measured:766,note:`要求单点导出,与具名导出面冲突`},"import/exports-last":{kind:`上游不推荐`,measured:528,note:`要求导出语句在文件末尾`},"import/no-relative-parent-imports":{kind:`上游不推荐`,measured:277,note:`包内 lib/ 与 test/ 的相对父级引用是既有结构`},"import/max-dependencies":{kind:`上游不推荐`,measured:25,note:`给 import 条数设上限;host 半消费官方服务面天然依赖多`},"node/no-process-env":{kind:`上游不推荐`,measured:51,note:`环境变量是本仓的输入面(DSH_HOME、profile 目录、构建期开关);eslint-plugin-n 的 recommended 也不含本条`},"eslint/no-magic-numbers":{kind:`上游不推荐`,measured:5774,note:`把 HTTP 状态码/毫秒阈值抽成常量的计数规则`},"eslint/sort-keys":{kind:`上游不推荐`,measured:4101,note:`对象键排序;成对字面量与 locale 字典的可读性会一起牺牲`},"eslint/sort-imports":{kind:`上游不推荐`,measured:675,note:`字母序 import,与 oxfmt 的分组形态冲突`},"eslint/capitalized-comments":{kind:`上游不推荐`,measured:2388,note:`本仓注释以中文为主,「首字母大写」对中文注释无从判定`},"eslint/no-ternary":{kind:`上游不推荐`,measured:1024,note:`禁三元;改写成 if/else 并不会更正确`},"eslint/no-undefined":{kind:`上游不推荐`,measured:2009,note:"禁 `undefined` 字面量,与本仓大量显式可选返回形态冲突"},"eslint/no-void":{kind:`上游不推荐`,measured:172,note:"禁 `void` 前缀,而 `void promise` 正是消解 no-floating-promises 的官方写法"},"unicorn/no-null":{kind:`上游不推荐`,measured:788,note:`禁 null;官方声明与 JSON 边界大量用 null,改成 undefined 会改变序列化形状`},"typescript/prefer-readonly-parameter-types":{kind:`上游不推荐`,measured:2433,note:`要求每个入参类型全 readonly;官方声明面(SessionEvent/ConfigForm 等)是可变的,改不动也不该改`},"oxc/no-async-await":{kind:`对侧互斥`,measured:2160,note:`禁 async/await,而同表里 promise-function-async、require-await、no-floating-promises 都在推 async`},"oxc/no-optional-chaining":{kind:`对侧互斥`,measured:1354,note:"禁 `?.`,而 typescript/prefer-optional-chain 正在要求把 && 链改成 `?.`"},"oxc/no-rest-spread-properties":{kind:`对侧互斥`,measured:550,note:`禁对象 spread,而 ES2024 目标与官方声明的投影写法都靠它`},"typescript/no-explicit-any":{kind:`文体作用域`,measured:1,note:`用例里的桩件面(mini-react 之类)需要 any 才塞得进官方签名`},"typescript/no-non-null-assertion":{kind:`文体作用域`,measured:536,note:"用例里 `!` 是「前驱条件已成立」的最短写法"},"typescript/no-unsafe-type-assertion":{kind:`文体作用域`,measured:844,note:"桩件需要 `as never` / `as unknown as` 跨官方名义类型"},"typescript/require-await":{kind:`文体作用域`,measured:467,note:`官方 async 契约的同步实现与测试替身撑不住这条;prod 侧的条数按代码修,不进豁免`},"eslint/require-await":{kind:`对侧互斥`,measured:481,note:`它与 typescript/require-await 是同一条判据的两份实现,不是两条判据:探针实测(一段无 await 的 async 函数、--type-aware)单开各自都报、同开报两次 ⇒ 2 errors / 1 defect。按 typescript-eslint 官方的 extension-rule 口径裁决:留带类型信息的扩展那条、关基线那条。覆盖面没有因此变小:全仓逐行去重实测 514 行两点同报、typescript 独有 0 行、eslint 独有只落在用例面与配置自身的中间态;而 --type-aware 实测对 .mjs 也会由扩展那条报出,所以 scripts/*.mjs 面不需要基线那条补位。用例文件由 TEST_OVERRIDES 整族豁免`},"eslint/max-statements":{kind:`文体作用域`,measured:549,note:`用例体天然长(given/when/then 展开)`},"eslint/max-lines-per-function":{kind:`文体作用域`,measured:365,note:`同上`},"eslint/max-lines":{kind:`文体作用域`,measured:54,note:`单个用例文件的长度不作为缺陷信号`},"node/no-sync":{kind:`文体作用域`,measured:299,note:`构建脚本与门禁脚本按设计同步读;生产代码的写侧由每包 syncWrites 逐项登记,不在豁免里`},"node/no-top-level-await":{kind:`文体作用域`,measured:53,note:`脚本以 node 直接执行,顶层 await 是 ESM 常态;prod 侧 0 条`},"typescript/strict-boolean-expressions":{kind:`文体作用域`,measured:0,note:`scripts/*.mjs 无类型面,隐式 any 上无从判定`},"typescript/no-implied-eval":{kind:`文体作用域`,measured:0,note:`同上:这条要类型信息才能判`},"typescript/no-unsafe-member-access":{kind:`文体作用域`,measured:0,note:`无类型标注的脚本里每个值都落进隐式 any`},"typescript/no-unsafe-call":{kind:`文体作用域`,measured:0,note:n},"typescript/no-unsafe-assignment":{kind:`文体作用域`,measured:0,note:n},"typescript/no-unsafe-return":{kind:`文体作用域`,measured:0,note:n},"typescript/no-unsafe-argument":{kind:`文体作用域`,measured:0,note:n},"typescript/require-array-sort-compare":{kind:`文体作用域`,measured:0,note:`无类型脚本里「比较器缺失」判不出来`},"unicorn/no-process-exit":{kind:`文体作用域`,measured:2,note:`CLI 脚本的 process.exit 就是它的返回方式`},"eslint/no-continue":{kind:`文体作用域`,measured:53,note:`只对 scripts/ 豁免:TS 侧实测 0 条(包代码本来就不用 continue),908 行的门禁脚本改成嵌套 if 只会更难读`},"unicorn/max-nested-calls":{kind:`文体作用域`,measured:59,note:`只对 scripts/ 豁免:TS 侧实测 0 条;CLI 脚本里链式 map/filter/join 的深度由数据形状决定,不指向缺陷`},"unicorn/no-useless-undefined":{kind:`文体作用域`,measured:0,note:`脚本侧同上调;prod 仍按配置项 checkArguments:false 生效`},"react/set-state-in-effect":{kind:`契约冲突`,measured:16,note:`卡片用 effect 同步宿主态到 store 是挂载适配器形态;Compiler 建议的 render 期推导要改写 12 份适配器的状态流`},"react/no-deriving-state-in-effects":{kind:`契约冲突`,measured:7,note:`适配器形态里「props/state 派生值在 effect 对齐宿主」是挂载动作,不是可下沉到 render 的纯派生`},"react/exhaustive-effect-dependencies":{kind:`契约冲突`,measured:8,note:`Compiler 版依赖审计对适配器的内联闭包全量追问;与 react-hooks/exhaustive-deps(保留并已修)形成双门,留后者`},"react/hooks":{kind:`契约冲突`,measured:13,note:`「同一 hook 函数跨渲染同引用」在适配器的内联闭包形态下整体不成立;收敛批次把闭包提升后再回来开`},"react/refs":{kind:`契约冲突`,measured:1,note:`挂载适配器在 render 期读 ref 对齐宿主布局;收敛批次改成挂载后读`},"react/todo":{kind:`上游不推荐`,measured:6,note:`报的是 React 编译器 BuildHIR 自己不支持 try/catch(-finally) 的上游 TODO,不是代码缺陷`},"react/rules-of-hooks":{kind:`文体作用域`,measured:3,note:`只对用例文件豁免(TEST_FILE_OVERRIDES):测试架在组件树外直接驱动卡片适配 hook(useCardOf / useSyncExternalStore 桩),调用次数顺序由桩件固定。键用表载前缀 react/(诊断文案显示 react-hooks(...) 是显示名,配置键以 --rules 的 Source 列为准)`}},i=[`typescript/no-empty-object-type`],a=[`existsSync`,`readFileSync`,`readdirSync`,`readSync`,`realpathSync`,`statSync`],o=[`eslint`,`typescript`,`unicorn`,`oxc`,`node`,`promise`,`vitest`,`import`,`react`,`jsx-a11y`],s=[`eslint-plugin-sonarjs`,`eslint-plugin-sonarjs/cjs/plugin.js`];function c(){let t=e(import.meta.url),n=[];for(let e of s)try{return t.resolve(e)}catch{n.push(e)}throw Error(`oxlint 基线要挂 eslint-plugin-sonarjs,但从本包解析不到它(试过 ${n.join(`、`)})。要么在本包 devDependencies 里装 eslint-plugin-sonarjs,要么把自己解析到的入口经 definePluginConfig({ jsPlugins: [...] }) 传进来——带了入口就不会再解析(那条提示才真正兑现)。`)}function l(e,t=c){let n=e??[],r=null;try{r=t()}catch(e){if(n.length===0)throw e}return r===null?[...n]:[r,...n.filter(e=>e!==r)]}const u={"sonarjs/no-identical-functions":`error`,"sonarjs/no-duplicated-branches":`error`,"sonarjs/no-all-duplicated-branches":`error`,"sonarjs/no-collapsible-if":`error`,"sonarjs/no-identical-conditions":`error`,"sonarjs/no-identical-expressions":`error`,"sonarjs/no-duplicate-string":`error`,"sonarjs/cognitive-complexity":`error`,"sonarjs/max-switch-cases":`error`,"sonarjs/no-inverted-boolean-check":`error`,"sonarjs/no-duplicate-in-composite":`error`,"sonarjs/duplicates-in-character-class":`error`,"sonarjs/no-useless-catch":`error`,"sonarjs/no-extra-arguments":`error`,"sonarjs/no-redundant-assignments":`error`,"sonarjs/prefer-single-boolean-return":`error`,"sonarjs/label-position":`error`},d={correctness:`error`,suspicious:`error`,style:`error`,perf:`error`,restriction:`error`,pedantic:`error`,nursery:`error`},f={typeAware:!0,typeCheck:!0,denyWarnings:!0,maxWarnings:0,reportUnusedDisableDirectives:`error`,respectEslintDisableDirectives:!1},p=[`**/node_modules/**`,`.toolchain/**`,`coverage/**`,`dist/**`,`client.js`,`host.js`],m=/* @__PURE__ */ `vitest/consistent-each-for.vitest/consistent-test-filename.vitest/consistent-test-it.vitest/consistent-vitest-vi.vitest/expect-expect.vitest/hoisted-apis-on-top.vitest/max-expects.vitest/max-nested-describe.vitest/no-alias-methods.vitest/no-commented-out-tests.vitest/no-conditional-expect.vitest/no-conditional-in-test.vitest/no-conditional-tests.vitest/no-disabled-tests.vitest/no-duplicate-hooks.vitest/no-focused-tests.vitest/no-hooks.vitest/no-identical-title.vitest/no-import-node-test.vitest/no-importing-vitest-globals.vitest/no-interpolation-in-snapshots.vitest/no-large-snapshots.vitest/no-mocks-import.vitest/no-restricted-matchers.vitest/no-restricted-vi-methods.vitest/no-standalone-expect.vitest/no-test-prefixes.vitest/no-test-return-statement.vitest/no-unneeded-async-expect-function.vitest/padding-around-after-all-blocks.vitest/padding-around-test-blocks.vitest/prefer-called-exactly-once-with.vitest/prefer-called-once.vitest/prefer-called-times.vitest/prefer-called-with.vitest/prefer-comparison-matcher.vitest/prefer-describe-function-title.vitest/prefer-each.vitest/prefer-equality-matcher.vitest/prefer-expect-assertions.vitest/prefer-expect-resolves.vitest/prefer-expect-type-of.vitest/prefer-hooks-in-order.vitest/prefer-hooks-on-top.vitest/prefer-import-in-mock.vitest/prefer-importing-vitest-globals.vitest/prefer-lowercase-title.vitest/prefer-mock-promise-shorthand.vitest/prefer-mock-return-shorthand.vitest/prefer-snapshot-hint.vitest/prefer-spy-on.vitest/prefer-strict-boolean-matchers.vitest/prefer-strict-equal.vitest/prefer-to-be.vitest/prefer-to-be-falsy.vitest/prefer-to-be-object.vitest/prefer-to-be-truthy.vitest/prefer-to-contain.vitest/prefer-to-have-been-called-times.vitest/prefer-to-have-length.vitest/prefer-todo.vitest/require-awaited-expect-poll.vitest/require-hook.vitest/require-local-test-context-for-concurrent-snapshots.vitest/require-mock-type-parameters.vitest/require-test-timeout.vitest/require-to-throw-message.vitest/require-top-level-describe.vitest/valid-describe-callback.vitest/valid-expect.vitest/valid-expect-in-promise.vitest/valid-title.vitest/warn-todo`.split(`.`),h=/* @__PURE__ */ new Set([`vitest/max-expects`,`vitest/no-conditional-in-test`,`vitest/no-hooks`,`vitest/no-importing-vitest-globals`,`vitest/prefer-called-times`,`vitest/prefer-describe-function-title`,`vitest/prefer-expect-assertions`,`vitest/prefer-import-in-mock`,`vitest/prefer-importing-vitest-globals`,`vitest/prefer-to-be-falsy`,`vitest/prefer-to-be-truthy`,`vitest/require-test-timeout`]),g={"eslint/one-var":[`error`,`never`],"eslint/func-style":[`error`,`declaration`,{allowArrowFunctions:!0,overrides:{namedExports:`ignore`}}],"eslint/id-length":[`error`,{exceptions:[`a`,`i`,`t`,`x`,`y`,`z`]}],"eslint/init-declarations":[`error`,`never`,{ignoreForLoopInit:!0}],"eslint/max-lines":[`error`,{max:2e3,skipBlankLines:!0,skipComments:!0}],"eslint/max-lines-per-function":[`error`,{max:120,skipBlankLines:!0,skipComments:!0}],"eslint/complexity":`error`,"eslint/max-params":[`error`,{max:5}],"eslint/max-statements":[`error`,50],"eslint/no-console":[`error`,{allow:[`warn`,`error`,`info`]}],"eslint/no-warning-comments":[`error`,{terms:[`todo`,`fixme`,`xxx`,`hack`],location:`start`}],"eslint/no-param-reassign":`error`,"eslint/no-underscore-dangle":[`error`,{allow:[`__memoryInsightCardApplied`,`__memoryTdaiCardApplied`,`__sessionRescueApplied`,`__wukilDevToolsCardApplied`]}],"eslint/no-magic-numbers":`off`,"eslint/sort-keys":`off`,"eslint/sort-imports":`off`,"eslint/capitalized-comments":`off`,"eslint/no-ternary":`off`,"eslint/no-undefined":`off`,"eslint/no-void":`off`,"eslint/no-duplicate-imports":`off`,"eslint/require-await":`off`,"import/no-named-export":`off`,"import/group-exports":`off`,"import/exports-last":`off`,"import/prefer-default-export":`off`,"import/no-nodejs-modules":`off`,"import/no-relative-parent-imports":`off`,"import/max-dependencies":`off`,"import/no-default-export":`off`,"import/no-anonymous-default-export":`off`,"import/no-named-as-default-member":`off`,"import/consistent-type-specifier-style":[`error`,`prefer-top-level`],"import/no-duplicates":`error`,"import/no-cycle":`error`,"import/named":`error`,"import/first":`error`,"import/newline-after-import":`error`,"node/no-process-env":`off`,"node/no-sync":[`error`,{ignores:a}],"node/no-top-level-await":`error`,"oxc/no-async-await":`off`,"oxc/no-optional-chaining":`off`,"oxc/no-rest-spread-properties":`off`,"promise/always-return":`error`,"promise/no-promise-in-callback":[`error`,{exemptDeclarations:!0}],"promise/no-return-wrap":`error`,"promise/param-names":`error`,"promise/prefer-await-to-then":`error`,"typescript/consistent-type-imports":[`error`,{disallowTypeAnnotations:!1}],"typescript/explicit-function-return-type":[`error`,{allowDirectConstAssertionInArrowFunctions:!0,allowExpressions:!0,allowHigherOrderFunctions:!0,allowIIFEs:!0,allowTypedFunctionExpressions:!0,allowedNames:[`handler`,`callback`]}],"typescript/no-explicit-any":`error`,"typescript/no-floating-promises":`error`,"typescript/no-unsafe-assignment":`error`,"typescript/no-unsafe-type-assertion":`error`,"typescript/no-non-null-assertion":`error`,"typescript/require-await":`error`,"typescript/prefer-readonly-parameter-types":`off`,"typescript/promise-function-async":[`error`,{checkArrowFunctions:!1}],"typescript/consistent-return":[`error`,{treatUndefinedAsUnspecified:!0}],"typescript/only-throw-error":`error`,"unicorn/no-array-reduce":[`error`,{allowSimpleOperations:!0}],"unicorn/no-null":`off`,"unicorn/no-useless-undefined":[`error`,{checkArguments:!1}],"react/set-state-in-effect":`off`,"react/no-deriving-state-in-effects":`off`,"react/exhaustive-effect-dependencies":`off`,"react/hooks":`off`,"react/refs":`off`,"react/todo":`off`,...Object.fromEntries(m.map(e=>[e,`off`]))},_={files:[`**/*.test.ts`,`**/*.test.tsx`,`**/test/**`,`**/__tests__/**`],rules:{"typescript/no-non-null-assertion":`off`,"typescript/no-unsafe-type-assertion":`off`,"typescript/no-explicit-any":`off`,"typescript/require-await":`off`,"eslint/max-statements":`off`,"eslint/max-lines":`off`,"eslint/max-lines-per-function":`off`,"node/no-top-level-await":`off`}},v=[`expect`,`expectTypeOf`,`assert`,`assertType`],y=/^[A-Za-z_$][\w$]*$/u,b={files:[`**/*.test.ts`,`**/*.test.tsx`,`**/__tests__/**/*.spec.ts`],rules:{...Object.fromEntries(m.filter(e=>!h.has(e)).map(e=>[e,`error`])),"vitest/expect-expect":[`error`,{assertFunctionNames:v}],"react/rules-of-hooks":`off`}};function x(e={}){let t=e.assertionHelpers??[],n=e.titlePrefixes??[],r=[...t,...n].filter(e=>!y.test(e));if(r.length>0)throw Error(`assertionHelpers / titlePrefixes 只收裸标识符,不接受通配或带点形式(前者等于把 vitest/expect-expect 掏空):${r.join(`, `)}`);let i=[.../* @__PURE__ */ new Set([...v,...t])];return{...b,rules:{...b.rules,"vitest/expect-expect":[`error`,{assertFunctionNames:i}],...n.length===0?{}:{"vitest/prefer-lowercase-title":[`error`,{allowedPrefixes:[...new Set(n)]}]}}}}const S={files:[`scripts/**`,`*.mjs`,`*.mts`],rules:{"eslint/no-continue":`off`,"unicorn/max-nested-calls":`off`,"eslint/id-length":[`error`,{exceptions:[`a`,`i`,`t`,`x`,`y`,`z`,`n`,`p`,`s`,`v`,`e`,`f`,`ok`,`fs`,`id`]}],"unicorn/no-process-exit":`off`,"unicorn/no-useless-undefined":`off`,"typescript/no-non-null-assertion":`off`,"node/no-sync":`off`,"node/no-top-level-await":`off`,"typescript/no-unsafe-member-access":`off`,"typescript/no-unsafe-call":`off`,"typescript/no-unsafe-assignment":`off`,"typescript/no-unsafe-return":`off`,"typescript/no-unsafe-argument":`off`,"typescript/strict-boolean-expressions":`off`,"typescript/no-implied-eval":`off`,"typescript/require-array-sort-compare":`off`}};function C(e){let t={...r,...e};return e=>{let n=t[e];if(n!==void 0)return n;let r=e.indexOf(`/`);return r===-1?void 0:t[`${e.slice(0,r)}/*`]}}function w(e,t,n){if(t===void 0)return;let r=[];for(let[e,i]of Object.entries(t))if((Array.isArray(i)?i[0]:i)===`off`){let t=n(e);(t===void 0||t.note.trim().length===0)&&r.push(e)}if(r.length>0)throw Error(`${e} 里这些规则被关成 off 但没有依据(OFF_JUSTIFICATIONS 查不到,或 note 为空):${r.join(`, `)}。要么在代码里修,要么给它补一条 kind + measured + note。`)}function T(e={}){let{syncWrites:n=[],extraRules:r={},extraOverrides:i=[],extraIgnorePatterns:s=[],jsPlugins:c,offReasons:m={},assertionHelpers:h=[],titlePrefixes:v=[]}=e,y=C(m),b={...g,...u,...n.length>0?{"node/no-sync":[`error`,{ignores:[.../* @__PURE__ */ new Set([...a,...n])]}]}:{},...r},T=[_,x({assertionHelpers:h,titlePrefixes:v}),S,...i];w(`rules`,b,y);for(let[e,t]of T.entries())w(`overrides[${e}]`,t.rules,y);return t({plugins:o,env:{node:!0,es2024:!0,browser:!0},options:f,categories:d,ignorePatterns:[...p,...s],jsPlugins:l(c),rules:b,overrides:T})}export{g as BASE_RULES,r as OFF_JUSTIFICATIONS,a as READONLY_SYNC_ALLOWLIST,i as RETIRED_OFFS,S as SCRIPTS_OVERRIDES,b as TEST_FILE_OVERRIDES,_ as TEST_OVERRIDES,v as VITEST_ASSERTION_DEFAULTS,T as definePluginConfig,x as makeTestFileOverrides,l as resolveJsPlugins};
|
package/dist/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
import{fieldOf as e}from"./record.js";function t(e){let t=Object.getOwnPropertyDescriptor(e,`message`);return t!==void 0&&`value`in t&&typeof t.value==`string`?t.value:void 0}function n(e,r){if(r.has(e))return`<circular cause>`;r.add(e);try{if(!(e instanceof Error)){if(typeof e==`object`&&e){let n=t(e);if(n!==void 0)return n}return String(e)}let i=e.message===``?e.name:e.message,a=e instanceof AggregateError&&e.errors.length>0?` [${e.errors.map(e=>n(e,r)).join(`; `)}]`:``,o=e.cause===void 0||e.cause===null?``:n(e.cause,r);return`${i}${a}${o===``||o===i?``:`: ${o}`}`}catch{return`<unrenderable value>`}finally{r.delete(e)}}function r(e){return n(e,/* @__PURE__ */ new Set)}function i(t){return e(t,`code`)===`SETTINGS_CONFLICT`||e(t,`name`)===`SettingsConflictError`}export{r as errorText,i as isSettingsConflict};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by build-host.mjs — 源形态的 `./x.ts` 说明符进 d.ts 后无法解析,故重写。
|
|
2
|
-
export * from "./types/http.js";
|
|
3
2
|
export * from "./types/tool-events.js";
|
|
4
3
|
export * from "./types/card-apply.js";
|
|
4
|
+
export * from "./types/card-diff.js";
|
|
5
5
|
export * from "./types/project-key.js";
|
|
6
6
|
export * from "./types/locale.js";
|
|
7
7
|
export * from "./types/lesson-bus.js";
|
|
@@ -10,4 +10,3 @@ export * from "./types/record.js";
|
|
|
10
10
|
export * from "./types/errors.js";
|
|
11
11
|
export * from "./types/jsonl.js";
|
|
12
12
|
export * from "./types/job-outcome.js";
|
|
13
|
-
export * from "./types/trust.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{fieldOf as e,hasMethods as t,isRecord as n}from"./record.js";import{editPathOf as r,scanToolEvents as i,toolEventRowsOf as a}from"./tool-events.js";import{claimApply as o}from"./card-apply.js";import{diffTouched as s}from"./card-diff.js";import{deriveProjectKey as c}from"./project-key.js";import{DEFAULT_LOCALE as l,LOCALE_PREFERENCE_FIELD as u,LOCALE_SETTINGS_NAMESPACE as d,messagesFor as f,resolveLocale as p,resolveLocalePreference as m}from"./locale.js";import{settleLessonCall as h}from"./lesson-bus.js";import{truncateEnd as g,truncateStart as _}from"./text.js";import{errorText as v,isSettingsConflict as y}from"./errors.js";import{shrinkJsonlTail as b}from"./jsonl.js";import{jobOutcomeOf as x,killProc as S}from"./job-outcome.js";export{l as DEFAULT_LOCALE,u as LOCALE_PREFERENCE_FIELD,d as LOCALE_SETTINGS_NAMESPACE,o as claimApply,c as deriveProjectKey,s as diffTouched,r as editPathOf,v as errorText,e as fieldOf,t as hasMethods,n as isRecord,y as isSettingsConflict,x as jobOutcomeOf,S as killProc,f as messagesFor,p as resolveLocale,m as resolveLocalePreference,i as scanToolEvents,h as settleLessonCall,b as shrinkJsonlTail,a as toolEventRowsOf,g as truncateEnd,_ as truncateStart};
|
package/dist/job-outcome.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){return e.status===`killed`?{status:`killed`,detail:e.signal===null?`killed before exit`:`signal: ${e.signal}`}:{status:`completed`,detail:`exit code: ${String(e.exitCode??0)}`}}export{e as jobOutcomeOf};
|
|
1
|
+
function e(e){return e.status===`killed`?{status:`killed`,detail:e.signal===null?`killed before exit`:`signal: ${e.signal}`}:{status:`completed`,detail:`exit code: ${String(e.exitCode??0)}`}}function t(e){try{e.kill()}catch{}}export{e as jobOutcomeOf,t as killProc};
|
package/dist/record.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t,n){return e(t)?t[n]:void 0}export{
|
|
1
|
+
function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t,n){if(!e(t))return!1;for(let e of n)if(typeof t[e]!=`function`)return!1;return!0}function n(t,n){return e(t)?t[n]:void 0}export{n as fieldOf,t as hasMethods,e as isRecord};
|
package/dist/tool-events.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isRecord as e}from"./record.js";function t(t){return e(t)&&t.isError===!0}function n(e){return e===!0}function r(t){return e(t.data)}function i(e){return typeof e==`string`&&e.length>0?e:void 0}function a(e){return typeof e==`string`&&e.length>0?e:void 0}function o(t){if(typeof t==`string`)try{let n=JSON.parse(t);return e(n)?n:{}}catch{return{}}return e(t)?t:{}}function s(e){if(typeof e==`string`)try{return JSON.parse(e),!1}catch{return!0}return!1}function c(e,t){let n;if(!r(e))return n;if(e.type===`tool/call`){let r=a(e.data.name);r!==void 0&&(n={name:r,callId:i(e.data.callId),arguments:o(e.data.arguments),badArguments:s(e.data.arguments),seq:t})}else if(e.type===`tool/ptc-dispatch-start`){let r=i(e.data.subCallId),c=a(e.data.name);r!==void 0&&c!==void 0&&(n={name:c,callId:r,arguments:o(e.data.arguments),badArguments:s(e.data.arguments),seq:t})}return n}function l(a,o){let s;if(!r(a))return s;if(a.type===`tool/result`){let n=a.data.message,r=e(n)?n.source:void 0;s={callId:e(r)?i(r.callId):void 0,isError:t(n),seq:o}}else if(a.type===`tool/ptc-dispatch`){let e=i(a.data.subCallId);e!==void 0&&(s={callId:e,isError:n(a.data.isError),seq:o})}return s}function u(e,t){return{call:c(e,t),result:l(e,t)}}function d(t){let n=[],r=[];for(let[i,a]of t.entries())if(e(a)){let{call:e,result:t}=u(a,i);e!==void 0&&n.push(e),t!==void 0&&r.push(t)}return{calls:n,results:r}}function f(e,t){let n=e[t];return typeof n==`string`?n:void 0}function p(e){let t=e.arguments;return e.name===`str_replace_editor`?t.command===`view`?{kind:`read-view`,path:f(t,`path`)}:{kind:`write`,path:f(t,`path`)}:{kind:`write`,path:f(t,`file_path`)}}export{p as editPathOf,
|
|
1
|
+
import{isRecord as e}from"./record.js";function t(t){return e(t)&&t.isError===!0}function n(e){return e===!0}function r(t){return e(t.data)}function i(e){return typeof e==`string`&&e.length>0?e:void 0}function a(e){return typeof e==`string`&&e.length>0?e:void 0}function o(t){if(typeof t==`string`)try{let n=JSON.parse(t);return e(n)?n:{}}catch{return{}}return e(t)?t:{}}function s(e){if(typeof e==`string`)try{return JSON.parse(e),!1}catch{return!0}return!1}function c(e,t){let n;if(!r(e))return n;if(e.type===`tool/call`){let r=a(e.data.name);r!==void 0&&(n={name:r,callId:i(e.data.callId),arguments:o(e.data.arguments),badArguments:s(e.data.arguments),seq:t})}else if(e.type===`tool/ptc-dispatch-start`){let r=i(e.data.subCallId),c=a(e.data.name);r!==void 0&&c!==void 0&&(n={name:c,callId:r,arguments:o(e.data.arguments),badArguments:s(e.data.arguments),seq:t})}return n}function l(a,o){let s;if(!r(a))return s;if(a.type===`tool/result`){let n=a.data.message,r=e(n)?n.source:void 0;s={callId:e(r)?i(r.callId):void 0,isError:t(n),seq:o}}else if(a.type===`tool/ptc-dispatch`){let e=i(a.data.subCallId);e!==void 0&&(s={callId:e,isError:n(a.data.isError),seq:o})}return s}function u(e,t){return{call:c(e,t),result:l(e,t)}}function d(t){let n=[],r=[];for(let[i,a]of t.entries())if(e(a)){let{call:e,result:t}=u(a,i);e!==void 0&&n.push(e),t!==void 0&&r.push(t)}return{calls:n,results:r}}function f(e,t){let n=e[t];return typeof n==`string`?n:void 0}function p(e){let t=e.arguments;return e.name===`str_replace_editor`?t.command===`view`?{kind:`read-view`,path:f(t,`path`)}:{kind:`write`,path:f(t,`path`)}:{kind:`write`,path:f(t,`file_path`)}}export{p as editPathOf,d as scanToolEvents,u as toolEventRowsOf};
|
|
@@ -5,9 +5,9 @@ import type { Context } from "@deepseek-ai/cordis";
|
|
|
5
5
|
* `@deepseek-ai/cordis/lib/types/fiber.d.ts`:`interface Context extends
|
|
6
6
|
* Pick<Fiber, 'effect'>`,同步 `Disposable` 与可 await 的 `AsyncDisposable` 两条重载),
|
|
7
7
|
* 而不是本地重述一条 `(factory: () => (() => void) | undefined, label?) => void`。
|
|
8
|
-
* ⚠ 为什么这条要紧:各卡片的 `ClientCtx.effect`
|
|
8
|
+
* ⚠ 为什么这条要紧:各卡片的 `ClientCtx.effect` 绑的就是官方类型,而本地那份把 factory 的
|
|
9
9
|
* 值域写**宽**了(官方 `SyncEffect` 不收 `undefined`)。函数属性位按 contravariance 检查,
|
|
10
|
-
* 于是「官方 ctx
|
|
10
|
+
* 于是「官方 ctx」反而赋不进本投影——各卡片包会各自撞出一个
|
|
11
11
|
* `TS2345 … Type 'undefined' is not assignable to type 'SyncEffect<any>'`,
|
|
12
12
|
* 并在每个消费包里用一个类型断言去过桥。守卫的 ctx 面归本模块所有,就在这里一次修好。
|
|
13
13
|
*/
|
|
@@ -17,6 +17,10 @@ export interface CardApplyCtx {
|
|
|
17
17
|
/**
|
|
18
18
|
* apply 幂等守卫:已 claim 返回 false(调用方直接 return),否则占位并
|
|
19
19
|
* 挂 ctx.effect 清理——HMR 重载时 flag 复位,再次 apply 可重新声明。
|
|
20
|
+
*
|
|
21
|
+
* ⚠ 标记是**进程级**的(同一枚 flag 在全页面只有一份),不是 per-ctx:同一张卡被第二个
|
|
22
|
+
* ctx apply 时会被这一枚挡掉。要允许多 ctx 共存就得按 ctx 记账(WeakSet 一类),
|
|
23
|
+
* 别改成这里"顺手放过第二次"——那会重新打开 HMR 重复装载这条路。
|
|
20
24
|
* @param ctx - apply 的 ctx(需要 effect)。
|
|
21
25
|
* @param flag - globalThis 上的布尔标记键名(如 "__xxxCardApplied")。
|
|
22
26
|
* @param label - effect 标签(日志/生命周期可观测)。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* touched 层与快照的差异字段(值语义比较;undefined、null 与缺失三者等价)。
|
|
3
|
+
*
|
|
4
|
+
* 用 `JSON.stringify` 而不是 `===` 比较:设置项的值是表单里的标量与数组,引用相等会把
|
|
5
|
+
* 「用户重排了同内容的数组」判成改动,也会把两份等值数组判成没改。
|
|
6
|
+
* @param touched 草稿层里被碰过的字段
|
|
7
|
+
* @param value 官方快照里的当前值
|
|
8
|
+
* @returns 真正发生改动的字段名,顺序随 `touched` 的键序
|
|
9
|
+
*/
|
|
10
|
+
export declare function diffTouched(touched: Record<string, unknown>, value: Record<string, unknown>): string[];
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -7,3 +7,12 @@
|
|
|
7
7
|
* @returns 可直接进界面通知或日志的文本。
|
|
8
8
|
*/
|
|
9
9
|
export declare function errorText(error: unknown): string;
|
|
10
|
+
/**
|
|
11
|
+
* 是不是「设置冲突」那一类失败(宿主在两个界面同时改同一份设置时抛的形)。
|
|
12
|
+
*
|
|
13
|
+
* 认两枚痕迹而不是 `instanceof`:跨 realm 抛过来的错误类不是同一个构造函数,而
|
|
14
|
+
* `code`/`name` 是宿主两边都写的那两个值。经 `fieldOf` 读,免得在敌意形状上抛。
|
|
15
|
+
* @param error 捕获到的值
|
|
16
|
+
* @returns 该失败是否应当按「设置已被别处改动」处理
|
|
17
|
+
*/
|
|
18
|
+
export declare function isSettingsConflict(error: unknown): boolean;
|
|
@@ -13,3 +13,19 @@ export type SettledProcess = Pick<ShellProcess, "status" | "exitCode" | "signal"
|
|
|
13
13
|
* @returns 交给 {@link JobRegistry} 生产者的结局:killed / completed,理由在 detail
|
|
14
14
|
*/
|
|
15
15
|
export declare function jobOutcomeOf(proc: SettledProcess): JobOutcome;
|
|
16
|
+
/**
|
|
17
|
+
* 只用得到的那一个成员。**不按 `ShellProcess` 整型收**:本函数丢弃 `kill()` 的返回值,
|
|
18
|
+
* 把官方的返回类型写进签名等于要求每个调用方(含测试替身)都得交回一个 boolean,
|
|
19
|
+
* 而这里真正需要的只是「有一个可以试着调的 kill」。
|
|
20
|
+
*/
|
|
21
|
+
export interface KillableProcess {
|
|
22
|
+
kill: () => unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 收掉子进程,**忽略抛错**:进程可能已自然退出或已被系统回收,此时 kill 失败不是本包的失败。
|
|
26
|
+
*
|
|
27
|
+
* 为什么不向调用方通报:调用点都在清理臂(卸载、被拒、超时)上,那里没有可行动的下一步;
|
|
28
|
+
* 把异常抛出去只会让清理链半途而废,留下更难的现场。
|
|
29
|
+
* @param proc 待收的句柄
|
|
30
|
+
*/
|
|
31
|
+
export declare function killProc(proc: KillableProcess): void;
|
package/dist/types/jsonl.d.ts
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
另一个在调用前就把非有限值与负数归一成 0。但那道归一在**消费方**,本包并不强制——
|
|
12
12
|
新增消费方时没有任何东西会提醒它。
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* 为什么不在这种预算上抛错拒收:抛错会被两个消费方各自的 catch 接走(一个上抛、一个内部吞),
|
|
15
|
+
* 等于把一条清晰的预算错误变成一次静默跳过,排查方向反而更差。这里"不设限"只是「这次不裁」,
|
|
16
|
+
* 不像读请求体那样会同时放开内存上限,所以没有必须拒收的那条失败模型。形状不同的同一种错,
|
|
17
|
+
* 各按各的失败模型处理。
|
|
18
18
|
*
|
|
19
|
-
* 两个消费方的具体调用点与配额归一位置记在 test/jsonl.test.ts
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* 两个消费方的具体调用点与配额归一位置记在 test/jsonl.test.ts 的同名用例里——这一段
|
|
20
|
+
* 不出现兄弟包标识符:声明上的 JSDoc 会被 tsc 抄进 dist/types/*.d.ts 一起发出去,
|
|
21
|
+
* 包名会让装了这个包的消费方以为存在一个并不存在的依赖(test/publish-manifest.test.ts 会红)。
|
|
22
22
|
*
|
|
23
23
|
* @param text 整份 JSONL 文本
|
|
24
24
|
* @param maxBytes 字节上限;调用方须保证是有限数
|
package/dist/types/record.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/** 对象类型守卫:非 `null` 的 `object` 且非数组(`Record` 索引签名投影的基础)。 */
|
|
2
2
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* 服务面校验:对象存在且**声明的每个方法**都可调用。
|
|
5
|
+
*
|
|
6
|
+
* 只做 `typeof value["shell"] === "object"` 的话,守卫声称的是「有这个服务」而实际证明的只是
|
|
7
|
+
* 「有个对象」——运行到一半才炸。逐个点名方法,守卫才名副其实。
|
|
8
|
+
* @param value 待验证的服务候选(来自 `ctx.get(...)` 的 unknown)
|
|
9
|
+
* @param methods 该服务面必须可调用的方法名
|
|
10
|
+
* @returns 是否为可用的服务面
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasMethods(value: unknown, methods: readonly string[]): boolean;
|
|
3
13
|
/**
|
|
4
14
|
* 从 `unknown` 投影单个字段;非 `Record` → `undefined`。
|
|
5
15
|
*
|
|
@@ -7,7 +17,7 @@ export declare function isRecord(value: unknown): value is Record<string, unknow
|
|
|
7
17
|
* 方法照样取得到。消费方正靠这一条做**能力探测**——cordis 服务对象的方法挂在原型上、不是
|
|
8
18
|
* 自有键,形如 `typeof fieldOf(settings, "describe") === "function"`、
|
|
9
19
|
* `hasMethods(fieldOf(value, "timer"), ["timeout"])`、`typeof fieldOf(session,
|
|
10
|
-
* "snapshotEvents") === "function"
|
|
20
|
+
* "snapshotEvents") === "function"`(消费包的调用点记在 test/record.test.ts)。
|
|
11
21
|
* 改成 `Object.hasOwn` 会让上面几处**一律判否**,那些包随即认不出合法的 cordis 服务——
|
|
12
22
|
* 而这类误判的表现是「能力探测静默返回 false」,不是报错,极难往这里归因。
|
|
13
23
|
*
|
|
@@ -48,19 +48,6 @@ export interface ToolResultRecord {
|
|
|
48
48
|
readonly isError: boolean;
|
|
49
49
|
readonly seq: number;
|
|
50
50
|
}
|
|
51
|
-
/** 解析 tool/call 的 arguments:string JSON 文本或已解析对象;非法/缺失 → 空对象。
|
|
52
|
-
*
|
|
53
|
-
* **这一对原语是公开面,不是残留。** 自建折叠单元的消费方(`toolEventRowsOf` 的使用者)
|
|
54
|
-
* 必须自己再读一遍 arguments 字段;若它们只能走 `callRecordOf` 的成片结果,就无法在
|
|
55
|
-
* "自己那一层"复现同一套判据——而本模块存在的理由正是让字段读取只有一份。 */
|
|
56
|
-
export declare function parseToolArguments(raw: unknown): Record<string, unknown>;
|
|
57
|
-
/**
|
|
58
|
-
* arguments 是否「无法参与记账」:仅当字符串 JSON **语法非法**(JSON.parse 抛错)。
|
|
59
|
-
* 合法 JSON 非对象(`"42"`)、undefined/数字/对象等一律不标——既有消费方对 JSON.parse
|
|
60
|
-
* 抛错判"不记账",但对"解析成功却非对象"仍记账(路径留空)。本标记必须精确复用那一个
|
|
61
|
-
* 语义点,不能把"非对象"也划进坏参:多划一刀就等于替消费方改了门禁的松紧。
|
|
62
|
-
*/
|
|
63
|
-
export declare function toolArgumentsBad(raw: unknown): boolean;
|
|
64
51
|
/** 一条事件产出的记账行(call 与 result 互斥;非记账事件两位都是 undefined)。 */
|
|
65
52
|
export interface ToolEventRows {
|
|
66
53
|
readonly call: ToolCallRecord | undefined;
|
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jayyuen66/dsh-plugin-shared",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "dsh 自研插件的共享工具库:tool 事件流记账扫描 +
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "dsh 自研插件的共享工具库:tool 事件流记账扫描 + 卡片 apply/差异骨架 + 官方作业结局映射 + 项目键派生 + 代理对安全截断。作为库包被 import,不由用户启用",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cordis",
|
|
7
|
-
"csrf",
|
|
8
7
|
"dsh",
|
|
9
8
|
"plugin",
|
|
10
|
-
"tool-events"
|
|
11
|
-
"webserver"
|
|
9
|
+
"tool-events"
|
|
12
10
|
],
|
|
13
11
|
"homepage": "https://github.com/JayYuen666/dsh-plugin-shared#readme",
|
|
14
12
|
"bugs": {
|
|
@@ -35,10 +33,6 @@
|
|
|
35
33
|
"types": "./dist/index.d.ts",
|
|
36
34
|
"default": "./dist/index.js"
|
|
37
35
|
},
|
|
38
|
-
"./lib/http": {
|
|
39
|
-
"types": "./dist/types/http.d.ts",
|
|
40
|
-
"default": "./dist/http.js"
|
|
41
|
-
},
|
|
42
36
|
"./lib/tool-events": {
|
|
43
37
|
"types": "./dist/types/tool-events.d.ts",
|
|
44
38
|
"default": "./dist/tool-events.js"
|
|
@@ -47,6 +41,10 @@
|
|
|
47
41
|
"types": "./dist/types/card-apply.d.ts",
|
|
48
42
|
"default": "./dist/card-apply.js"
|
|
49
43
|
},
|
|
44
|
+
"./lib/card-diff": {
|
|
45
|
+
"types": "./dist/types/card-diff.d.ts",
|
|
46
|
+
"default": "./dist/card-diff.js"
|
|
47
|
+
},
|
|
50
48
|
"./lib/project-key": {
|
|
51
49
|
"types": "./dist/types/project-key.d.ts",
|
|
52
50
|
"default": "./dist/project-key.js"
|
|
@@ -75,10 +73,6 @@
|
|
|
75
73
|
"types": "./dist/types/jsonl.d.ts",
|
|
76
74
|
"default": "./dist/jsonl.js"
|
|
77
75
|
},
|
|
78
|
-
"./lib/trust": {
|
|
79
|
-
"types": "./dist/types/trust.d.ts",
|
|
80
|
-
"default": "./dist/trust.js"
|
|
81
|
-
},
|
|
82
76
|
"./lib/job-outcome": {
|
|
83
77
|
"types": "./dist/types/job-outcome.d.ts",
|
|
84
78
|
"default": "./dist/job-outcome.js"
|
|
@@ -104,37 +98,36 @@
|
|
|
104
98
|
"registry": "https://registry.npmjs.org"
|
|
105
99
|
},
|
|
106
100
|
"devDependencies": {
|
|
107
|
-
"@deepseek-ai/cordis": "4.0.5
|
|
108
|
-
"@deepseek-ai/dsh-brand": "0.2.1-
|
|
109
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-
|
|
110
|
-
"@deepseek-ai/dsh-
|
|
111
|
-
"@deepseek-ai/dsh-
|
|
112
|
-
"@deepseek-ai/dsh-
|
|
113
|
-
"@deepseek-ai/dsh-
|
|
114
|
-
"@deepseek-ai/dsh-
|
|
115
|
-
"@deepseek-ai/dsh-
|
|
116
|
-
"@deepseek-ai/dsh-
|
|
117
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
|
|
101
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
102
|
+
"@deepseek-ai/dsh-brand": "0.2.1-rc.2",
|
|
103
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-rc.2",
|
|
105
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
106
|
+
"@deepseek-ai/dsh-output-retention": "0.2.1-rc.2",
|
|
107
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.2",
|
|
108
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.2",
|
|
109
|
+
"@deepseek-ai/dsh-shell": "0.2.1-rc.2",
|
|
110
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2",
|
|
118
111
|
"@types/node": "26.6.5",
|
|
119
|
-
"@vitest/coverage-v8": "5.0.
|
|
112
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
120
113
|
"eslint-plugin-sonarjs": "4.2.2",
|
|
121
|
-
"oxfmt": "0.
|
|
122
|
-
"oxlint": "1.
|
|
114
|
+
"oxfmt": "0.73.0",
|
|
115
|
+
"oxlint": "1.88.0",
|
|
123
116
|
"oxlint-tsgolint": "7.0.2004",
|
|
124
|
-
"rolldown": "1.2.
|
|
117
|
+
"rolldown": "1.2.14",
|
|
125
118
|
"typescript": "7.0.2",
|
|
126
119
|
"vite": "8.3.4",
|
|
127
|
-
"vitest": "5.0.
|
|
120
|
+
"vitest": "5.0.5"
|
|
128
121
|
},
|
|
129
122
|
"peerDependencies": {
|
|
130
|
-
"@deepseek-ai/cordis": "4.0.5
|
|
131
|
-
"@deepseek-ai/dsh": "^0.2.1-
|
|
132
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-
|
|
133
|
-
"@deepseek-ai/dsh-jobs": "0.2.1-
|
|
134
|
-
"@deepseek-ai/dsh-output-retention": "0.2.1-
|
|
135
|
-
"@deepseek-ai/dsh-session": "0.2.1-
|
|
136
|
-
"@deepseek-ai/dsh-shell": "0.2.1-
|
|
137
|
-
"@deepseek-ai/dsh-tools": "0.2.1-
|
|
123
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
124
|
+
"@deepseek-ai/dsh": "^0.2.1-rc.2",
|
|
125
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
|
|
126
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-rc.2",
|
|
127
|
+
"@deepseek-ai/dsh-output-retention": "0.2.1-rc.2",
|
|
128
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.2",
|
|
129
|
+
"@deepseek-ai/dsh-shell": "0.2.1-rc.2",
|
|
130
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2"
|
|
138
131
|
},
|
|
139
132
|
"peerDependenciesMeta": {
|
|
140
133
|
"@deepseek-ai/cordis": {
|
|
@@ -149,6 +142,9 @@
|
|
|
149
142
|
"@deepseek-ai/dsh-jobs": {
|
|
150
143
|
"optional": true
|
|
151
144
|
},
|
|
145
|
+
"@deepseek-ai/dsh-output-retention": {
|
|
146
|
+
"optional": false
|
|
147
|
+
},
|
|
152
148
|
"@deepseek-ai/dsh-session": {
|
|
153
149
|
"optional": true
|
|
154
150
|
},
|
|
@@ -161,8 +157,8 @@
|
|
|
161
157
|
},
|
|
162
158
|
"icon": "icon.svg",
|
|
163
159
|
"engines": {
|
|
164
|
-
"dsh": "
|
|
165
|
-
"node": "
|
|
160
|
+
"dsh": "^0.2.1-rc.2",
|
|
161
|
+
"node": ">=22.12.0"
|
|
166
162
|
},
|
|
167
163
|
"scripts": {
|
|
168
164
|
"prebuild": "node build-clean.mjs",
|
package/dist/http.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
function e(e,t,n){if(e.headersSent||e.writableEnded)return;let r=JSON.stringify(n);e.writeHead(t,{"content-type":`application/json; charset=utf-8`,"cache-control":`no-store`}),e.end(r)}function t(e,t){let n=e.headers?.[t.toLowerCase()],r=Array.isArray(n)?n[0]:n;return typeof r==`string`?r:``}function n(e){let n=t(e,`sec-fetch-site`);return n!==``&&n!==`same-origin`&&n!==`none`}function r(e,t){try{return new URL(e.url??``,`http://localhost`).searchParams.get(t)}catch{return null}}function i(e,n,r){let i=t(e,r);return i!==``&&i===n}const a={"too-large":{status:413,error:`request body too large`},aborted:{status:400,error:`request body unreadable`},"bad-budget":{status:500,error:`request body limit misconfigured`}};function o(e){return Buffer.isBuffer(e)?e:Buffer.from(String(e),`utf8`)}async function s(e,n){if(!Number.isFinite(n)||n<0)return{ok:!1,reason:`bad-budget`};let r=Number(t(e,`content-length`));if(Number.isFinite(r)&&r>n)return{ok:!1,reason:`too-large`};let i=[],a=0;try{for await(let t of e){let e=o(t);if(a+=e.length,a>n)return{ok:!1,reason:`too-large`};i.push(e)}}catch{return{ok:!1,reason:`aborted`}}return{ok:!0,text:Buffer.concat(i).toString(`utf8`)}}const c=`cross-origin request rejected`;async function l(t,r,o){if(n(t))return e(r,403,{ok:!1,error:c}),null;if(o.csrf!==void 0&&!i(t,o.csrf.token,o.csrf.headerName))return e(r,403,{ok:!1,error:`invalid csrf token`}),null;let l=await s(t,o.maxBytes);if(l.ok)return l.text;let u=a[l.reason];return e(r,u.status,{ok:!1,error:u.error}),null}export{c as CROSS_ORIGIN_TEXT,i as checkCsrf,l as guardBody,n as isCrossOrigin,r as queryParam,s as readBody,e as sendJson};
|
package/dist/trust.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{CROSS_ORIGIN_TEXT as e,sendJson as t}from"./http.js";import n from"node:os";import{isIPv4 as r,isIPv6 as i}from"node:net";const a=/* @__PURE__ */ new Set([`localhost`,`[::1]`]),o=/* @__PURE__ */ new Set([`same-origin`,`none`]),s=`[::ffff:7f`;function c(e){let t=new URL(`http://[${e.split(`%`,1).join(``)}]`).hostname;return t===`[::1]`||t.startsWith(s)}function l(e){return e===void 0?!1:r(e)?e.split(`.`,1).join(``)!==`127`:i(e)&&!c(e)}const u=`bad-origin`;function d(e,t){let n=e.headers?.[t];return n===void 0?{kind:`absent`}:Array.isArray(n)?n.length===1?{kind:`single`,value:n.join(``)}:{kind:`duplicate`}:{kind:`single`,value:n}}const f=/^[A-Za-z0-9._\-[\]:%]+$/u;function p(e){if(!f.test(e))return null;try{return new URL(`http://${e}`)}catch{return null}}function m(e){let t=e.toLowerCase();if(a.has(t)||t.startsWith(s))return!0;let n=t.split(`.`);return n.length===4&&n[0]===`127`&&n.every(e=>/^\d{1,3}$/u.test(e)&&Number(e)<=255)}function h(){let e=[];for(let t of Object.values(n.networkInterfaces()))for(let{family:n,address:r}of t??[])e.push(n===`IPv6`?`[${r}]`:r);return e}function g(e,t){return m(e)?!0:t.servingNonLoopback===!0&&(t.localInterfaceAddresses??h()).includes(e)}function _(e){let t=d(e,`sec-fetch-site`);return t.kind===`duplicate`?!1:t.kind===`absent`||o.has(t.value)}function v(e){let t=e.socket?.remoteAddress;if(typeof t!=`string`||t===``)return null;let n=t.replaceAll(/^\[|\]$/gu,``).replace(/^::ffff:/iu,``);return m(n)||n===`::1`}function y(e,t){let n=d(e,`host`);if(n.kind===`duplicate`)return`rejected`;if(n.kind===`absent`)return v(e)===!1?`rejected`:null;let r=p(n.value);return r===null||!g(r.hostname,t)?`rejected`:r}function b(e,t={}){let n=y(e,t);if(n===`rejected`)return`untrusted-host`;if(!_(e))return`cross-site`;let r=d(e,`origin`);if(r.kind===`duplicate`)return u;if(r.kind===`absent`)return`trusted`;let i=r.value;if(i===`null`||n===null)return u;let a=p(i.replace(/^[a-z][a-z0-9+.-]*:\/\//iu,``));return a===null?u:a.host===n.host?`trusted`:u}function x(t){return t===`untrusted-host`?`untrusted host authority`:e}function S(e,n,r={}){let i=b(e,r);return i===`trusted`?!0:n.headersSent||n.writableEnded?(console.error(`[shared/trust] rejecting after headers were sent:`,i),!1):(t(n,403,{ok:!1,error:x(i)}),!1)}export{S as guardTrust,l as isNonLoopbackBindHost,b as requestTrust,x as trustRejectionText};
|
package/dist/types/http.d.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
/**
|
|
3
|
-
* header 读取 helper 接受的请求形状:真 `IncomingMessage` 或只带部分字段的合成片段。
|
|
4
|
-
*
|
|
5
|
-
* Node 把 `headers` / `socket` 都标成必存在,照抄那个形状会让类型面比现实更乐观——运行时
|
|
6
|
-
* 不得不写 `?.` 自卫,而那条自卫又会被类型感知规则判成冗余。所以这里如实声明可缺字段。
|
|
7
|
-
* 仅覆盖**读头**所需的面:要消费请求体的函数(`readBody` / `guardBody`)仍收
|
|
8
|
-
* `IncomingMessage`,因为 `PartialRequest` 不保证可异步迭代。
|
|
9
|
-
*/
|
|
10
|
-
export interface PartialRequest {
|
|
11
|
-
readonly headers?: IncomingHttpHeaders;
|
|
12
|
-
readonly socket?: {
|
|
13
|
-
readonly remoteAddress?: string | undefined;
|
|
14
|
-
} | null;
|
|
15
|
-
readonly url?: string | undefined;
|
|
16
|
-
}
|
|
17
|
-
/** 只读头的 helper 接受的请求:真 `IncomingMessage` 或合成片段。 */
|
|
18
|
-
export type HttpRequest = IncomingMessage | PartialRequest;
|
|
19
|
-
/** 写 JSON 响应(no-store:端点都是运行时状态,禁缓存防陈旧)。
|
|
20
|
-
* 已发头/已结束则静默跳过:同一响应被写两次会抛 ERR_HTTP_HEADERS_SENT,
|
|
21
|
-
* 而端点多在 async 回调里,抛错变成 unhandled rejection 拖垮共享的 web host。
|
|
22
|
-
*
|
|
23
|
-
* **序列化先于写头**:循环引用与 BigInt 会让 JSON.stringify 抛错。若把它排在 writeHead
|
|
24
|
-
* 之后,头已发出而 end() 永不执行——响应就此挂住,客户端一直等到超时,调用方也再也
|
|
25
|
-
* 改不了状态码。先算出 body 就把这一支变回一个普通的同步异常。 */
|
|
26
|
-
export declare function sendJson(res: ServerResponse, status: number, payload: unknown): void;
|
|
27
|
-
/**
|
|
28
|
-
* CORS 同源纵深防御:浏览器请求带 sec-fetch-site,非 same-origin/none 即拒绝。
|
|
29
|
-
* 空头(curl/CLI)不拦——浏览器侧伪造不了同源判定,CLI 是本地可信调用面。
|
|
30
|
-
*/
|
|
31
|
-
export declare function isCrossOrigin(req: HttpRequest): boolean;
|
|
32
|
-
/** 从 req.url 读 query 参数(URLSearchParams 语义:+ 解码空格、尊重 ; 分隔等)。 */
|
|
33
|
-
export declare function queryParam(req: HttpRequest, name: string): string | null;
|
|
34
|
-
/** CSRF 校验:header 值非空且等于下发 token(sec-fetch-site 只防浏览器,可伪造)。 */
|
|
35
|
-
export declare function checkCsrf(req: HttpRequest, token: string, headerName: string): boolean;
|
|
36
|
-
/**
|
|
37
|
-
* readBody 的失败原因:`too-large` 超限(413),`aborted` 流读取中断/坏流(400),
|
|
38
|
-
* `bad-budget` 是**调用方传错了 maxBytes**(负数、`NaN`、`Infinity`)——它不是请求的错,
|
|
39
|
-
* 而是端点自己的限额配置坏了,故单列一档而不是伪装成 413。
|
|
40
|
-
*/
|
|
41
|
-
export type BodyRead = {
|
|
42
|
-
readonly ok: true;
|
|
43
|
-
readonly text: string;
|
|
44
|
-
} | {
|
|
45
|
-
readonly ok: false;
|
|
46
|
-
readonly reason: "too-large" | "aborted" | "bad-budget";
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* 读 POST body(**按 UTF-8 字节**计上限;先查 content-length 再收流)。
|
|
50
|
-
*
|
|
51
|
-
* 两个必须一次做对的点:
|
|
52
|
-
* 1. 逐块 `String(chunk)` 会把跨 chunk 边界的多字节字符(中文/emoji)解码成
|
|
53
|
-
* U+FFFD 且仍是合法 JSON —— 静默写坏用户数据。故累积 Buffer 后一次性解码。
|
|
54
|
-
* 2. 只按解码后字符数设限,恶意 body 可在触顶前已占满内存;content-length
|
|
55
|
-
* 预检让明显超限的请求根本不开始累积。
|
|
56
|
-
* 超限/坏流都立即停止读取(for-await 提前 return 会 release 迭代器并销毁流)。
|
|
57
|
-
*
|
|
58
|
-
* `maxBytes` 必须是有限非负数。负数、`NaN`、`Infinity` 一律拒读而不是"当作没有限额":
|
|
59
|
-
* 任何数与 `NaN` 比较都是 false,所以 `bytes > NaN` 永不成立——一条坏预算会退化成
|
|
60
|
-
* 完全不设限,把本模块存在的理由(防超长输出占满内存)整个绕掉。
|
|
61
|
-
* 上限为 `0` 是合法配置,语义是"只收空 body"。
|
|
62
|
-
*/
|
|
63
|
-
export declare function readBody(req: IncomingMessage, maxBytes: number): Promise<BodyRead>;
|
|
64
|
-
/**
|
|
65
|
-
* 浏览器来源不合法时的统一文案。导出是因为 `trust.ts` 的浏览器腿要给同一句话:
|
|
66
|
-
* 两处各写一份字面量,改一边就会出现"同一拒绝、两种文案",而消费方会照文案做断言。
|
|
67
|
-
*/
|
|
68
|
-
export declare const CROSS_ORIGIN_TEXT = "cross-origin request rejected";
|
|
69
|
-
/**
|
|
70
|
-
* 一次性校验组合(通用 POST 端点样板):跨域拒绝 → CSRF 拒绝 → 读 body →
|
|
71
|
-
* 413(超限)/400(流中断)/500(限额配置坏了)。全过返回 body 字符串。
|
|
72
|
-
* 缺 CSRF 规范的端点(GET、或自持 token 的端点)**省略 `csrf` 字段**即可——本函数没有
|
|
73
|
-
* `skipCsrf` 参数,旧注释写过,那是在描述一个并不存在的开关。
|
|
74
|
-
*/
|
|
75
|
-
export declare function guardBody(req: IncomingMessage, res: ServerResponse, opts: {
|
|
76
|
-
maxBytes: number;
|
|
77
|
-
csrf?: {
|
|
78
|
-
token: string;
|
|
79
|
-
headerName: string;
|
|
80
|
-
};
|
|
81
|
-
}): Promise<string | null>;
|
package/dist/types/trust.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { ServerResponse } from "node:http";
|
|
2
|
-
import type { HttpRequest } from "./http.ts";
|
|
3
|
-
/** 信任判据的结论。除 `trusted` 外都该被拒。 */
|
|
4
|
-
export type TrustVerdict = "trusted" | "untrusted-host" | "cross-site" | "bad-origin";
|
|
5
|
-
/** `requestTrust` / `guardTrust` 的可选入参。 */
|
|
6
|
-
export interface TrustOptions {
|
|
7
|
-
/**
|
|
8
|
-
* 宿主是否绑在非回环地址上——由 `isNonLoopbackBindHost(webServer.host)` 取,**不要**再自己比
|
|
9
|
-
* 字面量。缺省按 false(保守)。
|
|
10
|
-
*/
|
|
11
|
-
servingNonLoopback?: boolean;
|
|
12
|
-
/** 注入本机地址面(测试用;缺省取 `os.networkInterfaces()` 的实际值)。 */
|
|
13
|
-
localInterfaceAddresses?: readonly string[];
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* 绑定地址的字面量谓词:`webServer.host` → {@link TrustOptions.servingNonLoopback}。
|
|
17
|
-
*
|
|
18
|
-
* 为什么现在要由本包出这个谓词:`@deepseek-ai/dsh-host-webserver` 的 `Config.host` 在宿主
|
|
19
|
-
* 0.2.1-alpha.2 从 `'127.0.0.1' | '0.0.0.0'` 换成**任意本机接口 IP 字面量**,并把通配地址
|
|
20
|
-
* (`0.0.0.0`、`::` 及其映射形)**在载入时拒掉**。于是消费方沿用至今的那行
|
|
21
|
-
* `webServer.host === "0.0.0.0"` 恒为 false——非回环服务面换了形态(一枚真的网卡地址),
|
|
22
|
-
* 判据却还盯着那枚再也绑不上的字面量,局域网访问会被本模块整片拒成 403。
|
|
23
|
-
*
|
|
24
|
-
* 口径与官方 `isLoopbackHost` 对齐(台架见 `test/trust.test.ts`)。一处**刻意**分歧:非字面量
|
|
25
|
-
* (`localhost`、带方括号的 `[::1]`)本函数按 false,官方那侧 `!isLoopbackHost` 得 true。
|
|
26
|
-
* 宿主自身的 config 校验已经拒掉非字面量,那种值到不了这里;万一到了,false 收紧而非放宽
|
|
27
|
-
* 可放行的 Host 权威——这个方向上宁可错杀。
|
|
28
|
-
*
|
|
29
|
-
* @param host - `webServer.host` 的绑定值;读不到(缺字段、空串、非字面量)按只绑回环处理
|
|
30
|
-
* @returns 绑的是非回环接口地址时为 true
|
|
31
|
-
*/
|
|
32
|
-
export declare function isNonLoopbackBindHost(host: string | undefined): boolean;
|
|
33
|
-
/**
|
|
34
|
-
* 一次判据:Host 权威 → sec-fetch-site(白名单)→ Origin 逐字比对。
|
|
35
|
-
* 纯函数、不写响应;写响应的样板在 `guardTrust`。
|
|
36
|
-
*/
|
|
37
|
-
export declare function requestTrust(req: HttpRequest, opts?: TrustOptions): TrustVerdict;
|
|
38
|
-
/** 结论对应的响应文案(cross-site 与 bad-origin 同一句,两者都是"不是本站的浏览器请求")。 */
|
|
39
|
-
export declare function trustRejectionText(verdict: TrustVerdict): string;
|
|
40
|
-
/**
|
|
41
|
-
* handler 体的**第一条语句**该放的东西:非 trusted 即写一次 403 JSON 并返回 false。
|
|
42
|
-
* 返回 false 时调用方必须立即 return,不得继续读 body 或产生副作用。
|
|
43
|
-
*/
|
|
44
|
-
export declare function guardTrust(req: HttpRequest, res: ServerResponse, opts?: TrustOptions): boolean;
|