mixdog 0.9.147 → 0.9.149
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/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// patch.mjs; matching/fuzz semantics mirror the native engine and are
|
|
4
4
|
// unchanged.
|
|
5
5
|
|
|
6
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
6
7
|
import { classifyEntry, stripDiffPrefix } from './paths.mjs';
|
|
7
8
|
import { normalizeOutputPath } from '../builtin.mjs';
|
|
9
|
+
import { detectReadEncodingFromBuffer } from '../builtin/snapshot-helpers.mjs';
|
|
8
10
|
|
|
9
11
|
function collectUnifiedOldLines(hunk) {
|
|
10
12
|
const oldLines = [];
|
|
@@ -39,6 +41,19 @@ export function collectUnifiedOps(hunk) {
|
|
|
39
41
|
return ops;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/** collectUnifiedOps plus each op's line text (terminator inheritance needs it). */
|
|
45
|
+
export function collectUnifiedOpEntries(hunk) {
|
|
46
|
+
const ops = [];
|
|
47
|
+
for (const raw of hunk?.lines || []) {
|
|
48
|
+
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
49
|
+
const tag = raw[0];
|
|
50
|
+
if (tag === ' ') ops.push({ op: 'context', line: raw.slice(1) });
|
|
51
|
+
else if (tag === '-') ops.push({ op: 'delete', line: raw.slice(1) });
|
|
52
|
+
else if (tag === '+') ops.push({ op: 'add', line: raw.slice(1) });
|
|
53
|
+
}
|
|
54
|
+
return ops;
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
// New-side lines of a unified hunk (context + additions), plus whether the
|
|
43
58
|
// hunk marks the NEW content as lacking a terminating newline
|
|
44
59
|
// (`` directly after a ' ' / '+' line).
|
|
@@ -186,6 +201,145 @@ export function decodeValidUtf8OrNull(buf) {
|
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
203
|
|
|
204
|
+
// --- Patch-target codec ------------------------------------------------------
|
|
205
|
+
// Every route that rewrites a WHOLE file from decoded text (V4A rename, the JS
|
|
206
|
+
// unified route) must round-trip the file's encoding. `buf.toString('utf8')`
|
|
207
|
+
// replaced each invalid byte with U+FFFD and read UTF-16 as NUL-laden garbage,
|
|
208
|
+
// so the rewrite transcoded the file irreversibly. The BOM decides the codec;
|
|
209
|
+
// anything that is neither valid UTF-8 nor BOM-marked UTF-16 is refused.
|
|
210
|
+
// Codec of a patch target. A BOM is authoritative; a BOM-LESS file is
|
|
211
|
+
// classified by NUL-byte parity, which is what separates UTF-16 text from
|
|
212
|
+
// UTF-8 text. `certain:false` means the bytes cannot be attributed to one
|
|
213
|
+
// codec — the caller must refuse rather than rewrite them. Routing by BOM
|
|
214
|
+
// alone sent BOM-less UTF-16 to the UTF-8 engine, which produced mixed
|
|
215
|
+
// UTF-16/UTF-8 bytes.
|
|
216
|
+
// Minimum evidence for a BOM-less UTF-16 verdict: 16 code units. Below that a
|
|
217
|
+
// NUL is just as likely to be a stray byte inside UTF-8 text (`61 00`), and a
|
|
218
|
+
// wrong verdict DESTROYS the file, so short input is undecidable by
|
|
219
|
+
// definition. `buf` must be the WHOLE file: a bounded prefix cannot rule out a
|
|
220
|
+
// parity flip further in, so a prefix-only caller passes `partial:true` and
|
|
221
|
+
// gets "undecidable" instead of a guess.
|
|
222
|
+
const UTF16_MIN_EVIDENCE_BYTES = 32;
|
|
223
|
+
const UTF16_MIN_NUL_RATIO = 0.5;
|
|
224
|
+
|
|
225
|
+
export function detectPatchTargetCodec(buf, { partial = false } = {}) {
|
|
226
|
+
const bytes = Buffer.isBuffer(buf) ? buf : Buffer.from(buf || []);
|
|
227
|
+
const undecidable = { encoding: null, bomLen: 0, certain: false };
|
|
228
|
+
if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) {
|
|
229
|
+
return { encoding: 'utf16le', bomLen: 2, certain: true };
|
|
230
|
+
}
|
|
231
|
+
if (bytes.length >= 2 && bytes[0] === 0xFE && bytes[1] === 0xFF) {
|
|
232
|
+
return { encoding: 'utf16be', bomLen: 2, certain: true };
|
|
233
|
+
}
|
|
234
|
+
if (bytes.length >= 3 && bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) {
|
|
235
|
+
return { encoding: 'utf8', bomLen: 3, certain: true };
|
|
236
|
+
}
|
|
237
|
+
let nulEven = 0;
|
|
238
|
+
let nulOdd = 0;
|
|
239
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
240
|
+
if (bytes[i] !== 0) continue;
|
|
241
|
+
if (i % 2 === 0) nulEven += 1;
|
|
242
|
+
else nulOdd += 1;
|
|
243
|
+
}
|
|
244
|
+
if (partial) return undecidable;
|
|
245
|
+
if (nulEven === 0 && nulOdd === 0) return { encoding: 'utf8', bomLen: 0, certain: true };
|
|
246
|
+
if (bytes.length < UTF16_MIN_EVIDENCE_BYTES || bytes.length % 2 !== 0) return undecidable;
|
|
247
|
+
const evenSlots = Math.ceil(bytes.length / 2);
|
|
248
|
+
const oddSlots = Math.floor(bytes.length / 2);
|
|
249
|
+
if (nulEven === 0 && nulOdd / oddSlots >= UTF16_MIN_NUL_RATIO) {
|
|
250
|
+
return { encoding: 'utf16le', bomLen: 0, certain: true };
|
|
251
|
+
}
|
|
252
|
+
if (nulOdd === 0 && nulEven / evenSlots >= UTF16_MIN_NUL_RATIO) {
|
|
253
|
+
return { encoding: 'utf16be', bomLen: 0, certain: true };
|
|
254
|
+
}
|
|
255
|
+
return undecidable;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function decodePatchTargetBuffer(buf, displayPath = '') {
|
|
259
|
+
const enc = detectPatchTargetCodec(buf);
|
|
260
|
+
if (!enc.certain) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`apply_patch: ${normalizeOutputPath(displayPath)} has NUL bytes without a decidable text encoding — `
|
|
263
|
+
+ 'refusing to rewrite it, because any codec choice would corrupt the file.',
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
let text;
|
|
267
|
+
if (enc.encoding === 'utf16le' || enc.encoding === 'utf16be') {
|
|
268
|
+
const body = buf.subarray(enc.bomLen);
|
|
269
|
+
// An odd trailing byte cannot be decoded and would be DROPPED on re-encode:
|
|
270
|
+
// silent data loss. Refuse the file exactly like invalid UTF-8.
|
|
271
|
+
if (body.length % 2 !== 0) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`apply_patch: ${normalizeOutputPath(displayPath)} is malformed UTF-16 (odd payload length) — `
|
|
274
|
+
+ 'refusing to rewrite it, because the dangling byte cannot be preserved.',
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
text = enc.encoding === 'utf16le'
|
|
278
|
+
? body.toString('utf16le')
|
|
279
|
+
// Node has no 'utf16be': swap pairs into LE and decode.
|
|
280
|
+
: Buffer.from(body).swap16().toString('utf16le');
|
|
281
|
+
} else {
|
|
282
|
+
text = decodeValidUtf8OrNull(buf.subarray(enc.bomLen));
|
|
283
|
+
}
|
|
284
|
+
if (typeof text !== 'string') {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`apply_patch: ${normalizeOutputPath(displayPath)} is neither valid UTF-8 nor BOM-marked UTF-16 — `
|
|
287
|
+
+ 'refusing to rewrite it, because the edit would transcode every byte.',
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return { text, enc };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Encoding gate for a target that is about to be rewritten. Returns the
|
|
294
|
+
// refusal message, or null when the bytes round-trip. Both engines use it: the
|
|
295
|
+
// native one speaks UTF-8 only, so invalid UTF-8 must be refused there exactly
|
|
296
|
+
// as the JS writer refuses it, instead of being rewritten byte-lossily.
|
|
297
|
+
export function patchTargetEncodingError(fullPath, displayPath = fullPath) {
|
|
298
|
+
let buf;
|
|
299
|
+
try {
|
|
300
|
+
buf = readFileSync(fullPath);
|
|
301
|
+
} catch {
|
|
302
|
+
return null; // absent/unreadable — the engine reports it in its own words
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
decodePatchTargetBuffer(buf, displayPath);
|
|
306
|
+
return null;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
const message = err?.message || String(err);
|
|
309
|
+
return /^apply_patch:/.test(message) ? message : null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Routing sniff: ANY UTF-16 target (BOM or not) must take the codec-preserving
|
|
314
|
+
// JS writer instead of the UTF-8 native engine. Undecidable files stay on the
|
|
315
|
+
// native route, where the encoding gate refuses them without writing.
|
|
316
|
+
export function patchTargetUsesUtf16(fullPath) {
|
|
317
|
+
try {
|
|
318
|
+
// WHOLE file: a prefix cannot rule out a parity flip further in, and this
|
|
319
|
+
// decides whether the byte-preserving writer is used.
|
|
320
|
+
const codec = detectPatchTargetCodec(readFileSync(fullPath));
|
|
321
|
+
return codec.certain && codec.encoding !== 'utf8';
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Re-encode a rewritten file into the codec it was read with (BOM included).
|
|
328
|
+
// Plain UTF-8 stays a string so atomicWrite keeps its existing fast path.
|
|
329
|
+
export function encodePatchTargetContent(text, enc) {
|
|
330
|
+
if (!enc || (enc.encoding === 'utf8' && !enc.bomLen)) return text;
|
|
331
|
+
const body = enc.encoding === 'utf16le'
|
|
332
|
+
? Buffer.from(text, 'utf16le')
|
|
333
|
+
: (enc.encoding === 'utf16be'
|
|
334
|
+
? Buffer.from(text, 'utf16le').swap16()
|
|
335
|
+
: Buffer.from(text, 'utf8'));
|
|
336
|
+
if (!enc.bomLen) return body;
|
|
337
|
+
const bom = enc.encoding === 'utf16le'
|
|
338
|
+
? Buffer.from([0xFF, 0xFE])
|
|
339
|
+
: (enc.encoding === 'utf16be' ? Buffer.from([0xFE, 0xFF]) : Buffer.from([0xEF, 0xBB, 0xBF]));
|
|
340
|
+
return Buffer.concat([bom, body]);
|
|
341
|
+
}
|
|
342
|
+
|
|
189
343
|
export function unifiedOldLinesMatchAt(sourceLines, oldLines, startIdx, fuzz, band) {
|
|
190
344
|
if (startIdx < 0 || startIdx + oldLines.length > sourceLines.length) return null;
|
|
191
345
|
let fuzzUsed = 0;
|
|
@@ -344,25 +498,212 @@ export function normalizeTypographic(s) {
|
|
|
344
498
|
.replace(/[\u00A0\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000]/g, ' ');
|
|
345
499
|
}
|
|
346
500
|
|
|
501
|
+
// Splits into line CONTENT plus the exact terminator that followed each line
|
|
502
|
+
// (`lines.terminators`). Line content is identical to the historical
|
|
503
|
+
// CRLF-folding split — a lone CR is only a terminator in a CR-dominant file —
|
|
504
|
+
// but recording the real bytes is what lets a rewrite put every untouched
|
|
505
|
+
// line back with its ORIGINAL terminator instead of the file's dominant one.
|
|
347
506
|
export function splitTextLinesForPatch(text) {
|
|
348
507
|
const raw = String(text ?? '');
|
|
349
508
|
const eol = detectDominantEol(raw);
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
509
|
+
const splitLoneCr = eol === '\r';
|
|
510
|
+
const lines = [];
|
|
511
|
+
const terminators = [];
|
|
512
|
+
let start = 0;
|
|
513
|
+
let i = 0;
|
|
514
|
+
while (i < raw.length) {
|
|
515
|
+
const ch = raw[i];
|
|
516
|
+
if (ch === '\n') {
|
|
517
|
+
lines.push(raw.slice(start, i));
|
|
518
|
+
terminators.push('\n');
|
|
519
|
+
i += 1;
|
|
520
|
+
start = i;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (ch === '\r') {
|
|
524
|
+
if (raw[i + 1] === '\n') {
|
|
525
|
+
lines.push(raw.slice(start, i));
|
|
526
|
+
terminators.push('\r\n');
|
|
527
|
+
i += 2;
|
|
528
|
+
start = i;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (splitLoneCr) {
|
|
532
|
+
lines.push(raw.slice(start, i));
|
|
533
|
+
terminators.push('\r');
|
|
534
|
+
i += 1;
|
|
535
|
+
start = i;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
i += 1;
|
|
356
540
|
}
|
|
357
|
-
const lines = body.split('\n');
|
|
358
541
|
let hasFinalNewline = true;
|
|
359
|
-
if (
|
|
360
|
-
|
|
542
|
+
if (start < raw.length) {
|
|
543
|
+
lines.push(raw.slice(start));
|
|
544
|
+
terminators.push('');
|
|
545
|
+
hasFinalNewline = false;
|
|
546
|
+
}
|
|
361
547
|
lines.hasFinalNewline = hasFinalNewline;
|
|
362
548
|
lines.eol = eol;
|
|
549
|
+
lines.terminators = terminators;
|
|
363
550
|
return lines;
|
|
364
551
|
}
|
|
365
552
|
|
|
553
|
+
export function cloneTextLinesForPatch(sourceLines, eol) {
|
|
554
|
+
const lines = [...(sourceLines || [])];
|
|
555
|
+
lines.hasFinalNewline = sourceLines?.hasFinalNewline !== false;
|
|
556
|
+
lines.eol = eol || sourceLines?.eol || '\n';
|
|
557
|
+
lines.terminators = Array.isArray(sourceLines?.terminators)
|
|
558
|
+
? [...sourceLines.terminators]
|
|
559
|
+
: null;
|
|
560
|
+
return lines;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// The ONLY mutation entry point for a patch line array. Replaced lines hand
|
|
564
|
+
// their own terminator to the new line at the same offset (a 1:1 replacement
|
|
565
|
+
// is byte-identical outside the changed text), extra inserted lines adopt the
|
|
566
|
+
// local convention, and every line the patch did not touch keeps its bytes.
|
|
567
|
+
// Op-wise terminators for one hunk's output lines. Derived from the hunk's OWN
|
|
568
|
+
// ops — never from content similarity, which cannot describe interior context,
|
|
569
|
+
// several change runs, or a moved line:
|
|
570
|
+
// context → keeps its source line's terminator verbatim
|
|
571
|
+
// delete → contributes its terminator to the current run
|
|
572
|
+
// add → takes the terminator of the delete it replaces inside that run,
|
|
573
|
+
// else the local convention.
|
|
574
|
+
export function terminatorsForUnifiedOps(ops, oldTerminators, fallbackEol = '\n') {
|
|
575
|
+
const olds = Array.isArray(oldTerminators) ? oldTerminators : [];
|
|
576
|
+
const entries = (ops || []).map((entry) => (typeof entry === 'string'
|
|
577
|
+
? { op: entry, line: undefined }
|
|
578
|
+
: { op: entry?.op, line: entry?.line }));
|
|
579
|
+
// Pre-pass: EVERY delete of the hunk with the terminator of the source line
|
|
580
|
+
// it consumes, tagged with its run. Building the pool as we walk only saw
|
|
581
|
+
// deletes already passed, so a BACKWARD move (the add precedes its delete)
|
|
582
|
+
// could not claim its own line and lost its terminator.
|
|
583
|
+
const pool = [];
|
|
584
|
+
{
|
|
585
|
+
let cursor = 0;
|
|
586
|
+
let run = 0;
|
|
587
|
+
for (const entry of entries) {
|
|
588
|
+
if (entry.op === 'context') { cursor += 1; run += 1; }
|
|
589
|
+
else if (entry.op === 'delete') {
|
|
590
|
+
pool.push({ line: entry.line, terminator: olds[cursor], used: false, run });
|
|
591
|
+
cursor += 1;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const out = [];
|
|
596
|
+
let oldCursor = 0;
|
|
597
|
+
let currentRun = 0;
|
|
598
|
+
let lastClaimed = -1; // pool index claimed by the previous add
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
const { op, line } = entry;
|
|
601
|
+
if (op === 'context') {
|
|
602
|
+
const terminator = olds[oldCursor];
|
|
603
|
+
oldCursor += 1;
|
|
604
|
+
currentRun += 1;
|
|
605
|
+
out.push(terminator !== undefined ? terminator : fallbackEol);
|
|
606
|
+
} else if (op === 'delete') {
|
|
607
|
+
oldCursor += 1;
|
|
608
|
+
} else if (op === 'add') {
|
|
609
|
+
// 1) identity AFTER the previously claimed delete (order-preserving, so
|
|
610
|
+
// duplicates are consumed in sequence instead of first-fit),
|
|
611
|
+
// 2) identity ANYWHERE in the hunk (a line moved forwards or backwards
|
|
612
|
+
// across context keeps its own terminator),
|
|
613
|
+
// 3) the next unclaimed delete OF THIS RUN (the line it replaces).
|
|
614
|
+
let index = -1;
|
|
615
|
+
if (line !== undefined) {
|
|
616
|
+
index = pool.findIndex((slot, i) => !slot.used && slot.line === line && i > lastClaimed);
|
|
617
|
+
if (index < 0) index = pool.findIndex((slot) => !slot.used && slot.line === line);
|
|
618
|
+
}
|
|
619
|
+
if (index < 0) {
|
|
620
|
+
index = pool.findIndex((slot) => !slot.used && slot.run === currentRun);
|
|
621
|
+
}
|
|
622
|
+
let inherited;
|
|
623
|
+
if (index >= 0) {
|
|
624
|
+
pool[index].used = true;
|
|
625
|
+
lastClaimed = index;
|
|
626
|
+
inherited = pool[index].terminator;
|
|
627
|
+
}
|
|
628
|
+
out.push(inherited !== undefined ? inherited : fallbackEol);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Local convention for a replaced window: its own last terminator, else its neighbours'. */
|
|
635
|
+
export function localTerminatorForWindow(lines, start, oldLen) {
|
|
636
|
+
const terms = lines?.terminators;
|
|
637
|
+
if (!Array.isArray(terms)) return lines?.eol || '\n';
|
|
638
|
+
const replaced = terms.slice(start, start + oldLen);
|
|
639
|
+
return (replaced.length ? replaced[replaced.length - 1] : '')
|
|
640
|
+
|| terms[start - 1]
|
|
641
|
+
|| terms[start]
|
|
642
|
+
|| lines.eol
|
|
643
|
+
|| '\n';
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function spliceTextLinesForPatch(lines, start, oldLen, newLines, newTerminators = null) {
|
|
647
|
+
const terms = lines?.terminators;
|
|
648
|
+
if (Array.isArray(terms)) {
|
|
649
|
+
const replacedTerms = terms.slice(start, start + oldLen);
|
|
650
|
+
const replacedLines = lines.slice(start, start + oldLen);
|
|
651
|
+
const local = localTerminatorForWindow(lines, start, oldLen);
|
|
652
|
+
const explicit = Array.isArray(newTerminators) && newTerminators.length === newLines.length
|
|
653
|
+
? newTerminators
|
|
654
|
+
: null;
|
|
655
|
+
const nextTerms = newLines.map((line, k) => {
|
|
656
|
+
if (explicit) return explicit[k] !== undefined ? explicit[k] : local;
|
|
657
|
+
// No op information (a caller that cannot describe its edit): keep a
|
|
658
|
+
// terminator ONLY for an output line that is byte-identical to the
|
|
659
|
+
// source line at the same offset; everything else takes the local
|
|
660
|
+
// convention. Never guess across offsets.
|
|
661
|
+
return (replacedLines[k] === line && replacedTerms[k] !== undefined)
|
|
662
|
+
? replacedTerms[k]
|
|
663
|
+
: local;
|
|
664
|
+
});
|
|
665
|
+
terms.splice(start, oldLen, ...nextTerms);
|
|
666
|
+
}
|
|
667
|
+
lines.splice(start, oldLen, ...newLines);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Explicit end-of-file intent (`` in either
|
|
671
|
+
// direction). The terminator array is the state of record, so the intent is
|
|
672
|
+
// written INTO it; `hasFinalNewline` is kept in sync for matching code.
|
|
673
|
+
export function setFinalNewlineForPatch(lines, hasNewline) {
|
|
674
|
+
if (!lines) return;
|
|
675
|
+
lines.hasFinalNewline = hasNewline;
|
|
676
|
+
const terms = lines.terminators;
|
|
677
|
+
if (!Array.isArray(terms) || terms.length === 0) return;
|
|
678
|
+
const last = terms.length - 1;
|
|
679
|
+
if (!hasNewline) terms[last] = '';
|
|
680
|
+
else if (!terms[last]) terms[last] = terms[last - 1] || lines.eol || '\n';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function joinTextLinesForPatch(lines) {
|
|
684
|
+
const arr = lines || [];
|
|
685
|
+
const eol = arr.eol || '\n';
|
|
686
|
+
const terms = Array.isArray(arr.terminators) && arr.terminators.length === arr.length
|
|
687
|
+
? arr.terminators
|
|
688
|
+
: null;
|
|
689
|
+
if (!terms) {
|
|
690
|
+
const body = arr.join(eol);
|
|
691
|
+
return arr.hasFinalNewline !== false ? `${body}${eol}` : body;
|
|
692
|
+
}
|
|
693
|
+
// Terminators are authoritative. The file-level `hasFinalNewline` flag must
|
|
694
|
+
// NOT be re-applied here: after a tail deletion the surviving last line is a
|
|
695
|
+
// line the patch never touched, and forcing the old "no final newline" state
|
|
696
|
+
// onto it removed a terminator that belonged to untouched bytes.
|
|
697
|
+
let out = '';
|
|
698
|
+
for (let i = 0; i < arr.length; i++) {
|
|
699
|
+
const isLast = i === arr.length - 1;
|
|
700
|
+
let terminator = terms[i];
|
|
701
|
+
if (!terminator && !isLast) terminator = terms[i - 1] || eol;
|
|
702
|
+
out += `${arr[i]}${terminator}`;
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
|
|
366
707
|
export function detectDominantEol(text) {
|
|
367
708
|
const raw = String(text ?? '');
|
|
368
709
|
if (raw.includes('\r') && !raw.includes('\n')) return '\r';
|