# Conflicts: # .agents/notes/rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.i18n.yaml
15 KiB
Agent Note: Dependency swaps rejected by the 2026-07 NIH audit
Status: rejected — every swap below fails the net-simplification bar on evidence; recorded so the survey is not re-run from scratch
English | 中文
Problem
A repository-wide "Not Invented Here" audit (2026-07-26, ten parallel surveys covering every package group, scripts/, native/, vendor/ edges, python/, test infrastructure, and CI) asked of each hand-rolled surface: would a maintained external package or Node builtin delete it with a net win under the dependency policy? The positive findings became their own proposed notes. The negative verdicts carry equal value — each names a plausible-looking swap whose hand-rolled shape is load-bearing — but would otherwise live only in a PR body. This note freezes them.
Proposal
Adopt the following dependency swaps. Rejected — per-item evidence below; a future proposal for any item must beat its recorded reason, not just re-cite the policy.
Protocol and parsing:
vscode-jsonrpcfor LSP base-protocol framing/correlation (lsp-local): the swappable core is ~255 of ~1,800 src lines; the package cannot express the configuredmaxMessageBytesincoming-size bound (restoring it means rebuilding the deleted framing), inverts the cancel-grace teardown semantics (raceAbortrejects immediately then tears down; vscode-jsonrpc keeps the promise pending), errors on pre-header stdout banners real servers emit, and is CJS in an ESM-everywhere repo. The LSP seam note assigns JSON-RPC ownership todsh-lsp-local; this audit is the explicit on-record weighing of the dependency it lacked.vscode-languageserver-typesfor lsp-local's wire-type subset: ~80 type lines and ~45 guard lines, but upstream guards differ in both directions (accepturi: undefinedthe repo must reject; requiretargetRangethe repo tolerates absent), and the initialize-result shapes live invscode-languageserver-protocol, draggingvscode-jsonrpcin as a runtime dep — ~1 MB for 80 spec-exact lines.json-rpc-2.0fordsh-jsonrpc: deletable correlation/dispatch is real (~100–130 lines) but the NDJSON wire must stay bit-identical for the hand-rolled Python SDK client, the package is single-maintainer, and the GUI RPC note already treats this package as a frozen narrow surface.vscode-jsonrpcis a worse fit still (Content-Length framing, cancellation vocabulary the protocol lacks).jsonrpcclientfor the Python SDK client: v4 builds/parses messages only — ~20 lines — while the 500 lines that matter (subprocess lifecycle, threaded reader, id correlation, bidirectional server-role responses) stay; the library is in low-maintenance mode.eventsource-parserfor apiproxy'sreadSse: only ~15 lines of framing are deletable, both wire ends are in-repo so spec conformance is moot, and it would add a dep to a browser-safe package. (Contrast with the llm-deepseek proposal, where a real provider sits across the wire.)
Retry, timers, async:
p-retry/exponential-backoffforllm-retry: wrong execution model — the plugin is a decision-returning waterfall listener and the agent loop owns re-execution from the durable log; there is no function to re-invoke, which is those libraries' entire API. ProviderRetry-Afteroverride, budget from prior-failure codes, durablellm/retryevents, and HMR-quiescent abort are all uncovered. Bounded-recovery note already rejected SDK-owned retries.p-timeout/AbortSignal.timeoutfordsh-timeout: the builtin cannot be disarmed early and carries a genericTimeoutError, not the capability-codedTimeoutReasonthat distinguishes nested deadlines;idleWatchdog's per-demand rearm has no equivalent. Timeout-library note owns the design.p-limit/p-queuefor the agent-loop tool-call pool: pool bookkeeping is ~25 lines; the substance (model-ordered commits, mid-group reclassification, exclusive barriers, abort-drain with synthetic durable results) is not a concurrency-limiter shape.p-queue/async-mutexfor per-key promise-chain serializers (fs-local,storage-domain): 8–14-line serializers; the packages are strictly larger than the code they would delete.events.once+AbortSignal.timeoutfor subagent-subprocessexitsWithin:events.oncerejects iferrorfires first, but the hand-roll deliberately ignoreserror(captured separately by the spawn-failure path); the swap changes teardown-race behavior in exactly the code whose semantics are teardown races.
Data and validation:
- Ajv for the tools JSON Schema validator: the schema-DSL note explicitly rejected accepting a larger schema language; the validator also does realm-intrinsic prototype checks Ajv does not.
structuredClonefor sessionsnapshotJsonValue/isJsonValue: it is a validator + detacher enforcing the lossless-JSON boundary with single-read-per-getter and cross-realm intrinsic checks;structuredCloneaccepts Map/Date/-0 and enforces nothing. Same for the deliberately dependency-freecode-runtime-workermirror hardened against a model-mutated realm.fast-deep-equalfor session surfaceisDeepEqualJsonandsafe-stable-stringifyfor repeat-tool-guard canonicalization: both swaps work mechanically but each trades ~17–20 commented, tested lines for the first external runtime dependency of a core package — negative net at this size.- zod/valibot for durable-event strict decoders (goal fold, tool-ralph, session): exact-key fail-loud decoders at durable boundaries with event-specific messages; a second schema library beside repo-standard schemastery is a policy change, not a deletion.
gpt-tokenizer/tiktoken for token-meter: the replay-token-meter note explicitly rejected tokenizer backends; a GPT BPE is also the wrong tokenizer for DeepSeek models, and ~350 of the package's lines are replay-fold bookkeeping no tokenizer covers.partial-jsonfor streamed tool-call arguments: nothing to replace — arguments stay raw JSON strings end-to-end by documented contract;JSON.parseruns only on complete payloads.
Filesystem, subprocess, terminal:
write-file-atomicfor fs-local/storage-json atomic writes: the packages lack the private 0700 staging dir, Win32 DACL copy/ReplaceFileW, AbortSignal support, and parent-dir fsync — each the point of the hand-roll. The koffi Win32 bindings themselves are justified by the Windows durable-publish note.fzstd/native zstd packages for JSONL frame scanning:node:zlib's builtin zstd already does the compression (zstd note, which explicitly rejected an external native dependency); the remainingscanZstdFrameslocates RFC 8878 frame boundaries without decompressing for torn-tail repair, which no package exposes.picomatch/tinyglobby/ignorefor fs search: no glob engine exists — both discovery tools shell out to ripgrep per the bash-backed discovery note.istextorbinary/chardetfor text detection: the hand-roll is a ~15-line NUL-sample plus fatalTextDecoder; heuristic packages are larger and would change which files the model can read (model-visibleFS_NOT_TEXTdrift).shell-quotefor POSIX single-quoting: two 1-line quoting helpers with exhaustive tests versus a maintenance-mode package with a CVE history and different escaping output — a safety boundary is the wrong place to save one line.strip-ansifor pty sanitization: the pty sanitizer is a streaming state machine with split-sequence carry across chunks and OSC133;Dprompt-marker extraction (the shell-readiness signal); stateless strippers replace ~20 inner lines while all state machinery stays.stripVTControlCharactersalso demonstrably leaks unterminated-OSC payloads the session-title normalizer must strip (anti-spoofing).pidtree/ps-treefor the pty process inspector: bare PID trees; the code needs start-time identity against PID reuse plus/procstdin-wait detection no package does.execafor the subagent-subprocess dispose ladder:forceKillAfterDelaycovers SIGTERM→SIGKILL but not the stdin-EOF-first cooperative tier or the reject-if-no-exit-edge contract; adopting it here rewrites spawn sites while keeping the ladder. (Test-infrastructure spawn plumbing is different — see the execa Agent Note.)tree-killfor acp-snapshot teardown and lsp process kill: the lines are drain-ordering/error-propagation, not tree traversal; lsp/bash already use detached process groups + taskkill.- node-pty everywhere for the TUI test driver: Windows-TUI note explicitly rejected node-pty-on-every-host; it is already the Windows leg.
Servers and HTTP:
mswfor llm-mock-server: the server exists to fault the wire — socket destroy, mid-SSE disconnect, stall, pre-listen refusal — for real HTTP adapters and subprocesses; in-process interception can express none of that. Wire-fault-server note owns the design.hono/sirvfor host/webserver: the core is a disposer-based dynamic route registry (registrations-are-effects contract, HMR unregistration) plus index-HTML transform taps; hono routers are add-only, and static middleware cannot serve the transformed index. ~244 lines total, genuinely small.@mozilla/readability/iconv-litefor web-fetch-local: the provider returns raw HTML; charset handling is already the builtinTextDecoder; MIME parsing is ~11 lines; redirect following is same-origin security policy.
SQLite and storage:
better-sqlite3for the three SQLite backends: all use builtinnode:sqlite, intentional twice over — it gates the Node engine floor and works inside the single-file executable where a native addon would complicate packaging. No hand-rolled migrations or busy-retry loops exist.
Repo tooling:
wireitforrun-gates.ts: could express theneeds:graph, but allowFailure observational legs and mode-specific concurrency caps have no equivalent, caching must be defensively disabled for a correctness gate runner, and every CI workflow invocation would restructure. The parallel-gates note accepts a custom scheduler as the cost; keep is defensible.@arethetypeswrong/cliforverify-node-next-types: attw is per-package (100+ invocations vs one fast whole-workspace compile) and does not check the repo-specific explicit-.ts-specifier invariant, so the scan half stays regardless. Recorded as considered; keep the script.syncpack/manypkgforcheck-workspace-constraints.ts: they cover ~20 lines of range alignment; the load-bearing 200+ lines (computedfileslists, cordis peer=dev pairing, hierarchy shape) are repo policy no generic engine expresses.remark-validate-linksforverify-md-links.ts: the gate rides the repo's shared mdast toolchain; adopting remark-cli adds a second markdown stack to delete one small file.prebuildify/node-gyp-buildfor the landlock launcher packaging: inapplicable — those load.nodeaddons via dlopen; the launcher ships a standalone exec'd static binary, and per-platformoptionalDependenciesis the ecosystem convention for binaries.- Replacing the Landlock launcher itself with
@landstrip/landstrip: fails the security-invariant test — the launcher is a ~300-line reviewable C file with byte-pinned provenance that already migrated away from a Rust dependency; a single-maintainer LGPL Rust binary set is a larger audit surface with weaker provenance. (The unbuilt Windows rung was weighed separately and also rejected — landstrip is not battle-tested.) hatch-nodejs-versionfor Python release versioning: roughly LOC-neutral (a custom metadata hook replaces the regex), inverts the recorded decision that the dev sentinel never determines a release version, and puts a single-maintainer build plugin in the release supply chain.- YAML consolidation (
js-yamlvsyaml): the repo carries both parsers, with the!!jstag defined four times on js-yaml (vendored include, app-boot, apps/cli,scripts/verify-cordis-config.ts) and twice onyaml(sdk-telemetry'sScalarTag, sdk-helper's comment-preserving Document editing). The direction is forced — js-yaml cannot replaceyaml(sdk-helper needs the Document API) — but migrating the js-yaml sites cannot retire the library either (the vendored include pins it) and would put two parsers in charge of one dialect that must agree exactly, against the personal-config note's deliberate load-only-copy parity. Deletable: ~20–25 lines of duplicate tag definitions and two@types/js-yamlentries. The consolidation moment is a future include sync, not now.
Alternatives considered
- Record nothing and let the PR body carry the verdicts. Rejected: PR bodies are not part of the maintained record, and the whole point of surveying is that the next audit starts from these verdicts instead of re-deriving them.
- One rejected note per item. Rejected: ~30 files of ceremony for verdicts that share one evidence standard and one fate; per-item notes are warranted only if an item is re-proposed with new evidence.
- Fold each verdict into the implemented note that owns the seam. Partially done — where an owning note already rejected the alternative (retry, token-meter, schema DSL, zstd, sandbox, node-pty), this note cites rather than duplicates it. The remaining items have no owning note, which is why they are recorded here.