@lannguyensi/harness 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1213 -0
- package/dist/cli/adopt/derive.d.ts +16 -7
- package/dist/cli/adopt/derive.js +19 -2
- package/dist/cli/adopt/derive.js.map +1 -1
- package/dist/cli/adopt/index.js +18 -3
- package/dist/cli/adopt/index.js.map +1 -1
- package/dist/cli/apply/apply.d.ts +1 -0
- package/dist/cli/apply/apply.js +62 -6
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/apply/generate-codex-config.js +5 -6
- package/dist/cli/apply/generate-codex-config.js.map +1 -1
- package/dist/cli/apply/generate-opencode-config.d.ts +58 -0
- package/dist/cli/apply/generate-opencode-config.js +210 -0
- package/dist/cli/apply/generate-opencode-config.js.map +1 -0
- package/dist/cli/apply/generate-settings.d.ts +90 -32
- package/dist/cli/apply/generate-settings.js +133 -10
- package/dist/cli/apply/generate-settings.js.map +1 -1
- package/dist/cli/apply/index.d.ts +2 -1
- package/dist/cli/apply/index.js +2 -1
- package/dist/cli/apply/index.js.map +1 -1
- package/dist/cli/approve/stdin-report.d.ts +15 -0
- package/dist/cli/approve/stdin-report.js +1 -1
- package/dist/cli/approve/stdin-report.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +75 -0
- package/dist/cli/approve/understanding.js +150 -2
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/audit.d.ts +1 -1
- package/dist/cli/audit.js +3 -2
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/doctor/claude-mcp.d.ts +19 -1
- package/dist/cli/doctor/claude-mcp.js +36 -10
- package/dist/cli/doctor/claude-mcp.js.map +1 -1
- package/dist/cli/doctor/codex.d.ts +3 -1
- package/dist/cli/doctor/codex.js +6 -9
- package/dist/cli/doctor/codex.js.map +1 -1
- package/dist/cli/doctor/format.js +94 -8
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +23 -0
- package/dist/cli/doctor/index.js +106 -2
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/opencode.d.ts +24 -0
- package/dist/cli/doctor/opencode.js +107 -0
- package/dist/cli/doctor/opencode.js.map +1 -0
- package/dist/cli/doctor/target-checks.d.ts +11 -0
- package/dist/cli/doctor/target-checks.js +25 -0
- package/dist/cli/doctor/target-checks.js.map +1 -0
- package/dist/cli/doctor/toolchain-parity.d.ts +63 -0
- package/dist/cli/doctor/toolchain-parity.js +154 -0
- package/dist/cli/doctor/toolchain-parity.js.map +1 -0
- package/dist/cli/doctor/types.d.ts +85 -1
- package/dist/cli/doctor/types.js +1 -1
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/doctor/understanding-mode-env.d.ts +39 -0
- package/dist/cli/doctor/understanding-mode-env.js +83 -0
- package/dist/cli/doctor/understanding-mode-env.js.map +1 -0
- package/dist/cli/dry-run.js +32 -17
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/explain.d.ts +1 -1
- package/dist/cli/explain.js +1 -1
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/index.js +58 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/composer.js +36 -13
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/interactive.d.ts +7 -4
- package/dist/cli/init/interactive.js +79 -27
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +32 -1
- package/dist/cli/init/profiles.js.map +1 -1
- package/dist/cli/init/templates.d.ts +2 -1
- package/dist/cli/init/templates.js +130 -20
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/pack/approve-escape.js +69 -8
- package/dist/cli/pack/approve-escape.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +1 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-codex-pre-tool-use.js +27 -1
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate.js +1 -1
- package/dist/cli/pack/hook-post-merge-gate.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +91 -4
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.js +19 -2
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/pack/understanding-report-schema-hint.d.ts +13 -0
- package/dist/cli/pack/understanding-report-schema-hint.js +52 -3
- package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -1
- package/dist/cli/pause/index.js +1 -1
- package/dist/cli/pause/index.js.map +1 -1
- package/dist/cli/policy/intercept.d.ts +148 -0
- package/dist/cli/policy/intercept.js +379 -48
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/session-start/index.d.ts +12 -3
- package/dist/cli/session-start/index.js +67 -11
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/session-start/stale-base-check.d.ts +71 -0
- package/dist/cli/session-start/stale-base-check.js +379 -0
- package/dist/cli/session-start/stale-base-check.js.map +1 -0
- package/dist/cli/session-start/toolchain-parity.d.ts +142 -8
- package/dist/cli/session-start/toolchain-parity.js +244 -36
- package/dist/cli/session-start/toolchain-parity.js.map +1 -1
- package/dist/cli/smoke/assertions.d.ts +9 -2
- package/dist/cli/smoke/assertions.js +9 -2
- package/dist/cli/smoke/assertions.js.map +1 -1
- package/dist/cli/uninstall/index.js +18 -2
- package/dist/cli/uninstall/index.js.map +1 -1
- package/dist/cli/validate/checks.d.ts +3 -1
- package/dist/cli/validate/checks.js +298 -3
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/io/claude-mcp.d.ts +122 -1
- package/dist/io/claude-mcp.js +214 -12
- package/dist/io/claude-mcp.js.map +1 -1
- package/dist/io/duration.js.map +1 -0
- package/dist/io/expand-home.js.map +1 -0
- package/dist/io/extract.js.map +1 -0
- package/dist/io/harness-lock.js +1 -1
- package/dist/io/harness-lock.js.map +1 -1
- package/dist/{runtime → io}/ledger-record.d.ts +1 -1
- package/dist/io/ledger-record.js.map +1 -0
- package/dist/policies/index.d.ts +2 -2
- package/dist/policies/index.js +2 -2
- package/dist/policies/index.js.map +1 -1
- package/dist/policies/ledger-client.js +10 -6
- package/dist/policies/ledger-client.js.map +1 -1
- package/dist/policies/producers.js +1 -1
- package/dist/policies/producers.js.map +1 -1
- package/dist/policies/requires.js +2 -2
- package/dist/policies/requires.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.js +28 -3
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate.js +45 -9
- package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance-runtime.d.ts +139 -8
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js +246 -12
- package/dist/policy-packs/builtin/solution-acceptance-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/solution-acceptance.js +17 -3
- package/dist/policy-packs/builtin/solution-acceptance.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/active-claim.d.ts +25 -0
- package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js +89 -0
- package/dist/policy-packs/builtin/understanding-before-execution/active-claim.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +13 -0
- package/dist/policy-packs/builtin/understanding-before-execution/index.js +39 -0
- package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/ledger.d.ts +25 -0
- package/dist/policy-packs/builtin/understanding-before-execution/ledger.js +56 -0
- package/dist/policy-packs/builtin/understanding-before-execution/ledger.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.d.ts +20 -0
- package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js +79 -0
- package/dist/policy-packs/builtin/understanding-before-execution/lifecycle.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/markers.d.ts +103 -0
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js +203 -0
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +177 -0
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +309 -0
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +106 -0
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +209 -0
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +78 -0
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +185 -0
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +1 -536
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +9 -1095
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +109 -1
- package/dist/policy-packs/builtin/understanding-before-execution.js +183 -15
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/policy-packs/registry.d.ts +11 -0
- package/dist/policy-packs/registry.js +1 -1
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/policy-packs/runtime.d.ts +1 -1
- package/dist/policy-packs/runtime.js +10 -1
- package/dist/policy-packs/runtime.js.map +1 -1
- package/dist/runtime/agent-facing.js +1 -1
- package/dist/runtime/agent-facing.js.map +1 -1
- package/dist/runtime/bash-match-facts.d.ts +9 -7
- package/dist/runtime/bash-match-facts.js +9 -7
- package/dist/runtime/bash-match-facts.js.map +1 -1
- package/dist/runtime/bash-match-registry.d.ts +75 -38
- package/dist/runtime/bash-match-registry.js +279 -232
- package/dist/runtime/bash-match-registry.js.map +1 -1
- package/dist/runtime/bash-prefix-parse.d.ts +7 -0
- package/dist/runtime/bash-prefix-parse.js +205 -17
- package/dist/runtime/bash-prefix-parse.js.map +1 -1
- package/dist/runtime/command-normalize.d.ts +112 -0
- package/dist/runtime/command-normalize.js +411 -38
- package/dist/runtime/command-normalize.js.map +1 -1
- package/dist/runtime/hermetic-spawn-guard.js +6 -0
- package/dist/runtime/hermetic-spawn-guard.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/intercept.d.ts +78 -3
- package/dist/runtime/intercept.js +151 -39
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/ledger-add.js +1 -1
- package/dist/runtime/ledger-add.js.map +1 -1
- package/dist/runtime/read-only-bash.js +303 -24
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/schema/doctor.d.ts +25 -0
- package/dist/schema/doctor.js +23 -0
- package/dist/schema/doctor.js.map +1 -0
- package/dist/schema/extract.js +1 -1
- package/dist/schema/extract.js.map +1 -1
- package/dist/schema/index.d.ts +76 -0
- package/dist/schema/index.js +9 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/requires.js +2 -2
- package/dist/schema/requires.js.map +1 -1
- package/dist/schema/risk.d.ts +7 -0
- package/dist/schema/risk.js +23 -0
- package/dist/schema/risk.js.map +1 -1
- package/dist/schema/stale-base-check.d.ts +18 -0
- package/dist/schema/stale-base-check.js +37 -0
- package/dist/schema/stale-base-check.js.map +1 -0
- package/dist/schema/toolchain-parity.d.ts +3 -0
- package/dist/schema/toolchain-parity.js +13 -0
- package/dist/schema/toolchain-parity.js.map +1 -1
- package/package.json +3 -2
- package/dist/policies/duration.js.map +0 -1
- package/dist/policies/extract.js.map +0 -1
- package/dist/runtime/expand-home.js.map +0 -1
- package/dist/runtime/ledger-record.js.map +0 -1
- /package/dist/{policies → io}/duration.d.ts +0 -0
- /package/dist/{policies → io}/duration.js +0 -0
- /package/dist/{runtime → io}/expand-home.d.ts +0 -0
- /package/dist/{runtime → io}/expand-home.js +0 -0
- /package/dist/{policies → io}/extract.d.ts +0 -0
- /package/dist/{policies → io}/extract.js +0 -0
- /package/dist/{runtime → io}/ledger-record.js +0 -0
|
@@ -81,6 +81,59 @@
|
|
|
81
81
|
// on bash actually running the verb, per the same never-fold-into-a-zero
|
|
82
82
|
// discipline.
|
|
83
83
|
//
|
|
84
|
+
// UPDATE (task cf3dff51): closes the quoted-shell-boundary residual named
|
|
85
|
+
// in the "DELIBERATELY NOT SUPPORTED" list below (`VAR='a; b' git push` —
|
|
86
|
+
// task 13e55484's pinned gap). A HALT criterion, written before any
|
|
87
|
+
// implementation code, forced the closing mechanism into an ADDITIVE,
|
|
88
|
+
// fully independent third normalisation pass (`normalizeCommandQuoteAware`
|
|
89
|
+
// / `findNextBoundaryQuoteAware`, defined near `findNextBoundary` below)
|
|
90
|
+
// rather than an in-place edit of `BOUNDARY_RE` / `segmentAndCanonicalize`
|
|
91
|
+
// itself. First round of this task built and fully tested the new pass but
|
|
92
|
+
// left it UNWIRED (the wiring — a `policyMatchesEvent` fourth disjunct in
|
|
93
|
+
// `src/runtime/intercept.ts` — touched a file outside that round's
|
|
94
|
+
// assigned scope). The orchestrator EXTENDED the scope for a second round
|
|
95
|
+
// specifically to include that one wiring point (not a general re-open of
|
|
96
|
+
// scope), so `normalizeCommandQuoteAware` IS now `policyMatchesEvent`'s
|
|
97
|
+
// fourth arm, mirroring `normalizeCommandAmpAware`'s own wiring (task
|
|
98
|
+
// aabbad63) exactly — see that function's own doc comment and
|
|
99
|
+
// `src/runtime/intercept.ts`'s `policyMatchesEvent` for the wired shape.
|
|
100
|
+
// UPDATE (task f561e44c): `src/cli/dry-run.ts`'s own, independently-copied
|
|
101
|
+
// raw-OR-normalised-OR-amp-normalised matcher (the `harness dry-run` debug
|
|
102
|
+
// verb, which predicts `policyMatchesEvent`'s verdict without running it)
|
|
103
|
+
// was left out of the wiring above (named residual, not silently left
|
|
104
|
+
// implicit) and, as a result, under-predicted the quote-aware family. This
|
|
105
|
+
// follow-up task added the same fourth OR-arm to that matcher too, so
|
|
106
|
+
// `harness dry-run` and `policy intercept` now agree on this family — see
|
|
107
|
+
// that file's own comment for the fix and its parity-fixture test.
|
|
108
|
+
//
|
|
109
|
+
// THE HALT CRITERION (decided and written down before writing the first
|
|
110
|
+
// line of this pass's implementation, mirroring 13e55484's own "ONE-
|
|
111
|
+
// DIRECTIONAL GUARD" structure): quote-aware segmentation MUST be built as
|
|
112
|
+
// a NEW, independent function — never as an in-place edit of the EXISTING,
|
|
113
|
+
// already-wired primary pass (`BOUNDARY_RE` / `findNextBoundary` /
|
|
114
|
+
// `segmentAndCanonicalize`'s behaviour for `normalizeCommand`). If closing
|
|
115
|
+
// this residual is ever found to require editing that existing pass in
|
|
116
|
+
// place instead of adding a new one, HALT and fall back to documentation-
|
|
117
|
+
// only (pin the residual, as this header already does for its NOT-
|
|
118
|
+
// SUPPORTED siblings) rather than risk a two-directional regression. This
|
|
119
|
+
// is not a hypothetical: this task PROVED the trigger before writing any
|
|
120
|
+
// code, by reading `tests/runtime/command-normalize.test.ts`'s pinned
|
|
121
|
+
// "accepted over-block: a quoted assignment inside TEXT after a boundary
|
|
122
|
+
// char now matches" case — `echo "a; VAR='x y' git push"` — which matches
|
|
123
|
+
// TODAY only because `BOUNDARY_RE`'s quote-unawareness un-splits `VAR='x
|
|
124
|
+
// y' git push` into its own segment. An in-place quote-aware edit of the
|
|
125
|
+
// SAME pass would un-split it the other way (merge it back into the outer
|
|
126
|
+
// `echo "..."` segment, which is not itself a recognised invocation) and
|
|
127
|
+
// flip that already-accepted, already-WIRED pin from match to no-match —
|
|
128
|
+
// a genuine lost match under this module's own no-lost-matches discipline
|
|
129
|
+
// (see AMP_BOUNDARY_RE's own comment for why that discipline exists at
|
|
130
|
+
// all), not merely a lost false positive. Building a separate, additive
|
|
131
|
+
// pass instead means the existing primary pass — and every pin resting on
|
|
132
|
+
// it, including that exact one — stays byte-for-byte untouched; see
|
|
133
|
+
// `normalizeCommandQuoteAware`'s own comment for the differential
|
|
134
|
+
// measurement that confirms this by construction rather than by
|
|
135
|
+
// inspection alone.
|
|
136
|
+
//
|
|
84
137
|
// THE TRIGGER-MATCHING GAP THIS CLOSES: every `bash_match` policy trigger
|
|
85
138
|
// (`src/cli/init/templates.ts`, `docs/examples/full-manifest.yaml`) is a
|
|
86
139
|
// single regex tested against the UNPARSED command string, anchored on a
|
|
@@ -265,13 +318,33 @@
|
|
|
265
318
|
// agent writes and then executes.
|
|
266
319
|
// - `pushd`/`popd`, and a `cd` inside a nested subshell
|
|
267
320
|
// (`(cd X && ...)`) — mirrors `bash-prefix-parse.ts`'s own scope.
|
|
268
|
-
// -
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
321
|
+
// - CLOSED (was: a shell-boundary character INSIDE a quoted assignment
|
|
322
|
+
// VALUE — `VAR='a; b' git push`, task 13e55484's pinned residual, no
|
|
323
|
+
// longer belongs in a NOT-SUPPORTED list). Task cf3dff51: a quote-
|
|
324
|
+
// aware THIRD normalisation pass, `normalizeCommandQuoteAware`
|
|
325
|
+
// (defined near `findNextBoundary` below), built as a new, additive
|
|
326
|
+
// pass rather than an in-place edit of `BOUNDARY_RE` /
|
|
327
|
+
// `segmentAndCanonicalize` (see the module header's HALT CRITERION
|
|
328
|
+
// paragraph and that function's own comment for why), now wired as
|
|
329
|
+
// `policyMatchesEvent`'s FOURTH arm (`src/runtime/intercept.ts`,
|
|
330
|
+
// mirroring `normalizeCommandAmpAware`'s own wiring, task aabbad63).
|
|
331
|
+
// Closes `;`, `|`, `&&`, `(`, and a literal newline, each with an
|
|
332
|
+
// internal whitespace split inside the quoted value (the exact
|
|
333
|
+
// `VAR='a; b' git push` shape) — measured 12/12 at the real
|
|
334
|
+
// `runInterceptCli` hook entry point (isolated harness home,
|
|
335
|
+
// PATH-shim-verified bash execution, two gated verbs). Over-block
|
|
336
|
+
// surface: MEASURED, not assumed, against an adversarial probe —
|
|
337
|
+
// every ADDITIONAL match this pass adds beyond raw /
|
|
338
|
+
// `normalizeCommand` / `normalizeCommandAmpAware` is a TRUE positive
|
|
339
|
+
// (a real invocation bash would actually execute); no NEW over-block
|
|
340
|
+
// CLASS distinct from `consumeAssignment`'s own, already-reviewed
|
|
341
|
+
// precision limits (the "master false positive" class already pinned
|
|
342
|
+
// elsewhere in the test file) was found. See `normalizeCommandQuoteAware`'s
|
|
343
|
+
// own doc comment for the full mechanism and reasoning. CLOSED (task
|
|
344
|
+
// f561e44c): `src/cli/dry-run.ts`'s independently-copied matcher was
|
|
345
|
+
// initially not extended to try this fourth form — named residual — but
|
|
346
|
+
// that follow-up task added the same fourth OR-arm there too, see that
|
|
347
|
+
// file's own comment.
|
|
275
348
|
// - A quoted assignment VALUE containing the literal word `git`/`gh`/
|
|
276
349
|
// `npm`/`harness` as its own token (`VAR='a git b' git push`): the
|
|
277
350
|
// continuation's one-directional guard abandons rather than risk
|
|
@@ -838,6 +911,26 @@ export function segmentViewOf(command) {
|
|
|
838
911
|
* common (already-matched) path.
|
|
839
912
|
*/
|
|
840
913
|
export function normalizeCommandAmpAware(command) {
|
|
914
|
+
return normalizeWithGuards(command, (c) => segmentAndCanonicalize(c, AMP_BOUNDARY_RE, false).normalized);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Shared empty/length/throw guard clauses for a normalisation pass whose
|
|
918
|
+
* public return shape is exactly `{ normalized, truncated }` — no
|
|
919
|
+
* `targetDir`/`targetBase` (task cf3dff51 extraction: before this,
|
|
920
|
+
* `normalizeCommandAmpAware` and `normalizeCommandQuoteAware` carried
|
|
921
|
+
* byte-identical copies of this wrapper, which `npm run check:duplication`
|
|
922
|
+
* — a jscpd clone-count fitness function, see that script's own header
|
|
923
|
+
* comment for the precedent this follows — flagged as new duplication
|
|
924
|
+
* against the pinned baseline; extracted rather than raising the pin,
|
|
925
|
+
* per that script's own stated preference). `normalizeCommand` itself is
|
|
926
|
+
* NOT this shape (its return type also carries `targetDir`/`targetBase`)
|
|
927
|
+
* and keeps its own, separate guard clauses — deliberately not folded in
|
|
928
|
+
* here, since unifying THREE different return shapes behind one generic
|
|
929
|
+
* helper would obscure more than the ~10 duplicated lines it would save.
|
|
930
|
+
* `compute` is called only once `command` has passed both guards, so it
|
|
931
|
+
* never needs to re-check emptiness or the length bound itself.
|
|
932
|
+
*/
|
|
933
|
+
function normalizeWithGuards(command, compute) {
|
|
841
934
|
if (typeof command !== "string" || command.length === 0) {
|
|
842
935
|
return { normalized: typeof command === "string" ? command : "", truncated: false };
|
|
843
936
|
}
|
|
@@ -845,10 +938,7 @@ export function normalizeCommandAmpAware(command) {
|
|
|
845
938
|
return { normalized: command, truncated: true };
|
|
846
939
|
}
|
|
847
940
|
try {
|
|
848
|
-
return {
|
|
849
|
-
normalized: segmentAndCanonicalize(command, AMP_BOUNDARY_RE, false).normalized,
|
|
850
|
-
truncated: false,
|
|
851
|
-
};
|
|
941
|
+
return { normalized: compute(command), truncated: false };
|
|
852
942
|
}
|
|
853
943
|
catch {
|
|
854
944
|
return { normalized: command, truncated: false };
|
|
@@ -858,6 +948,96 @@ function normalizeCommandInner(command) {
|
|
|
858
948
|
const { normalized, targetDir, targetBase } = segmentAndCanonicalize(command, BOUNDARY_RE, false);
|
|
859
949
|
return { normalized, targetDir, targetBase, truncated: false };
|
|
860
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Quote-aware THIRD normalisation pass (task cf3dff51). Segments the
|
|
953
|
+
* command under `BOUNDARY_RE`'s own alphabet — same characters as the
|
|
954
|
+
* primary pass, no widening — but tracks single/double-quote state while
|
|
955
|
+
* scanning (`findNextBoundaryQuoteAware` below) so a boundary character
|
|
956
|
+
* sitting INSIDE a quoted assignment value is no longer treated as a real
|
|
957
|
+
* segment split. Closes the gap the module header's "DELIBERATELY NOT
|
|
958
|
+
* SUPPORTED" list and `BOUNDARY_RE`'s own comment both name: `VAR='a; b'
|
|
959
|
+
* git push` (task 13e55484's pinned residual) — `BOUNDARY_RE` used to cut
|
|
960
|
+
* the segment at the `;`, landing squarely between the two halves of one
|
|
961
|
+
* quoted value, so neither resulting piece carried a recognisable
|
|
962
|
+
* invocation and `consumeAssignment`'s own multi-token quote continuation
|
|
963
|
+
* (built for exactly this shape) never got the chance to run: it operates
|
|
964
|
+
* AFTER segmentation, on tokens WITHIN one segment, and the quoted value
|
|
965
|
+
* was never in one segment to begin with. Once quote-aware segmentation
|
|
966
|
+
* keeps the value whole, `consumeAssignment` needs no changes at all — it
|
|
967
|
+
* already reconstructs a multi-token quoted value correctly; this pass
|
|
968
|
+
* only fixes what gets handed to it as ONE segment in the first place.
|
|
969
|
+
*
|
|
970
|
+
* BUILT AS A NEW, INDEPENDENT PASS RATHER THAN AN IN-PLACE EDIT OF
|
|
971
|
+
* `BOUNDARY_RE` / `segmentAndCanonicalize` — this is the module header's
|
|
972
|
+
* HALT CRITERION, restated here at the point it governs: quote-aware
|
|
973
|
+
* segmentation can MERGE a segment the primary pass currently splits, and
|
|
974
|
+
* that merge cuts in BOTH directions — it can turn a previously-missed
|
|
975
|
+
* bypass into a match (the fix), but it can just as easily turn a
|
|
976
|
+
* previously-accepted, ALREADY-WIRED over-block into a miss (a
|
|
977
|
+
* regression). Measured, not hypothetical: `tests/runtime/command-
|
|
978
|
+
* normalize.test.ts`'s "accepted over-block: a quoted assignment inside
|
|
979
|
+
* TEXT after a boundary char now matches" pin — `echo "a; VAR='x y' git
|
|
980
|
+
* push"` — matches via `normalizeCommand` TODAY only because `BOUNDARY_RE`
|
|
981
|
+
* splits it at the `;`, landing `VAR='x y' git push` at a segment start
|
|
982
|
+
* where the assignment continuation turns it into a match. An in-place
|
|
983
|
+
* quote-aware edit of THAT pass would un-split it back into the outer
|
|
984
|
+
* `echo "..."` segment — not itself a recognised invocation — and flip
|
|
985
|
+
* that pin to a lost match, in violation of the raw-OR-normalised-OR-amp-
|
|
986
|
+
* aware-OR-(this) never-lose-a-match discipline every other pass in this
|
|
987
|
+
* module already honours. Building this as a SEPARATE pass instead means
|
|
988
|
+
* `normalizeCommand`'s own segmentation, and every test resting on it —
|
|
989
|
+
* including that exact one — is untouched by this task; see this file's
|
|
990
|
+
* own test suite for the differential proof (every case the first three
|
|
991
|
+
* arms already matched still matches, by construction, because none of
|
|
992
|
+
* those three arms' code changed at all).
|
|
993
|
+
*
|
|
994
|
+
* WIRED to `policyMatchesEvent` (`src/runtime/intercept.ts`) as its
|
|
995
|
+
* FOURTH arm — same shape as `normalizeCommandAmpAware`'s own third-arm
|
|
996
|
+
* wiring (task aabbad63): consulted only after the raw command,
|
|
997
|
+
* `normalizedCommand`, and the amp-aware pass have all missed, via a
|
|
998
|
+
* memoised thunk (`InterceptOptions.quoteNormalizedCommandThunk`,
|
|
999
|
+
* `src/cli/policy/intercept.ts`'s `quoteNormalizedCommandThunk`) so the
|
|
1000
|
+
* segmentation cost is paid at most once per event, on the common
|
|
1001
|
+
* (already-matched) path it costs nothing at all. Wiring landed in a
|
|
1002
|
+
* SECOND round of this task, after the orchestrator extended the first
|
|
1003
|
+
* round's file scope specifically for this one wiring point — the first
|
|
1004
|
+
* round shipped this function fully built and tested but genuinely
|
|
1005
|
+
* unwired (an inert security fix, called out explicitly as the wrong
|
|
1006
|
+
* shipped state in that round's own report) — `src/cli/dry-run.ts`'s own,
|
|
1007
|
+
* independently-copied matcher was deliberately NOT extended in the same
|
|
1008
|
+
* round (out of the extended scope too) and stayed a named residual until
|
|
1009
|
+
* task f561e44c closed it by adding the same fourth OR-arm there too — see
|
|
1010
|
+
* that file's own comment.
|
|
1011
|
+
*
|
|
1012
|
+
* THIS PASS'S OWN OVER-BLOCK SURFACE is measured and named in the test
|
|
1013
|
+
* file rather than assumed — and the measurement CORRECTS an earlier,
|
|
1014
|
+
* pre-measurement guess that used to sit in this comment: it does NOT
|
|
1015
|
+
* reproduce the `echo "a; VAR='x y' git push"` accepted over-block named
|
|
1016
|
+
* above. Quote-aware merging keeps that whole `echo "..."` segment
|
|
1017
|
+
* together, starting with the unrecognised head token `echo`, so this
|
|
1018
|
+
* function's output for it is byte-identical to the raw input
|
|
1019
|
+
* (`normalizeCommand`'s OWN, separate, unchanged-by-this-task match for
|
|
1020
|
+
* that same string is unaffected either way). Measured against a broader
|
|
1021
|
+
* adversarial probe, both at this function's own level and again at
|
|
1022
|
+
* `policyMatchesEvent`'s wired level (a bash-shim-verified corpus of
|
|
1023
|
+
* chained/prefixed/subshell-wrapped genuine invocations plus candidate
|
|
1024
|
+
* over-block shapes — entirely quoted `echo`/`printf` arguments that only
|
|
1025
|
+
* PRINT the spelling): every additional match this pass adds beyond raw /
|
|
1026
|
+
* `normalizeCommand` / `normalizeCommandAmpAware` is a TRUE positive (a
|
|
1027
|
+
* real invocation bash would actually execute); no NEW over-block class
|
|
1028
|
+
* distinct from `consumeAssignment`'s own, already-reviewed precision
|
|
1029
|
+
* limits was found. This function shares `canonicalizeSegment` /
|
|
1030
|
+
* `consumeAssignment` completely unchanged with the other passes — only
|
|
1031
|
+
* the segmentation boundary decision differs — which is also the
|
|
1032
|
+
* structural reason a longer (merged) segment can only REVEAL a match
|
|
1033
|
+
* those functions would already accept from a shorter view, never
|
|
1034
|
+
* fabricate a new kind of one; see the module header's cf3dff51 UPDATE
|
|
1035
|
+
* paragraph for the fuller statement of this reasoning and its
|
|
1036
|
+
* "not a formal proof" caveat.
|
|
1037
|
+
*/
|
|
1038
|
+
export function normalizeCommandQuoteAware(command) {
|
|
1039
|
+
return normalizeWithGuards(command, (c) => segmentAndCanonicalize(c, BOUNDARY_RE, false, true).normalized);
|
|
1040
|
+
}
|
|
861
1041
|
/**
|
|
862
1042
|
* The shared segmentation + canonicalisation engine behind BOTH
|
|
863
1043
|
* normalisation passes (task aabbad63). Parameterised on the boundary
|
|
@@ -898,8 +1078,21 @@ function normalizeCommandInner(command) {
|
|
|
898
1078
|
* view at all (see that function's own comment). `segments` is `null` on
|
|
899
1079
|
* the returned object when `collectSegments` is `false`, an array
|
|
900
1080
|
* (possibly empty) otherwise.
|
|
1081
|
+
*
|
|
1082
|
+
* `quoteAware` (task cf3dff51, default `false` — every PRE-EXISTING call
|
|
1083
|
+
* site is unchanged, positionally omitting this parameter): selects
|
|
1084
|
+
* `findNextBoundaryQuoteAware` instead of `findNextBoundary` as the one
|
|
1085
|
+
* thing this walk asks to find the next segment split. Everything else in
|
|
1086
|
+
* this function — `canonicalizeSegment`, the target-directory bookkeeping,
|
|
1087
|
+
* the `collectSegments` per-segment view — is IDENTICAL either way; only
|
|
1088
|
+
* WHERE a segment ends changes. Only `normalizeCommandQuoteAware` passes
|
|
1089
|
+
* `true`; see that function's own comment for why this had to be a new
|
|
1090
|
+
* parameter on the SHARED walk rather than a wholly separate function
|
|
1091
|
+
* (reuse, same reasoning `AMP_BOUNDARY_RE`'s parameterisation on
|
|
1092
|
+
* `boundaryRe` already established) and for the HALT CRITERION that ruled
|
|
1093
|
+
* out editing `findNextBoundary` itself in place instead.
|
|
901
1094
|
*/
|
|
902
|
-
function segmentAndCanonicalize(command, boundaryRe, collectSegments) {
|
|
1095
|
+
function segmentAndCanonicalize(command, boundaryRe, collectSegments, quoteAware = false) {
|
|
903
1096
|
const parts = [];
|
|
904
1097
|
const explicitTargets = new Set();
|
|
905
1098
|
let explicitTargetBase = null;
|
|
@@ -939,7 +1132,9 @@ function segmentAndCanonicalize(command, boundaryRe, collectSegments) {
|
|
|
939
1132
|
// least one boundary token's length each iteration a boundary is
|
|
940
1133
|
// found, and the loop stops once none remain.
|
|
941
1134
|
for (;;) {
|
|
942
|
-
const boundary =
|
|
1135
|
+
const boundary = quoteAware
|
|
1136
|
+
? findNextBoundaryQuoteAware(command, i, boundaryRe)
|
|
1137
|
+
: findNextBoundary(command, i, boundaryRe);
|
|
943
1138
|
const segEnd = boundary ? boundary.start : n;
|
|
944
1139
|
const segmentText = command.slice(i, segEnd);
|
|
945
1140
|
const result = canonicalizeSegment(segmentText);
|
|
@@ -1125,6 +1320,205 @@ function findNextBoundary(s, from, boundaryRe) {
|
|
|
1125
1320
|
const m = boundaryRe.exec(s);
|
|
1126
1321
|
return m === null ? null : { start: m.index, token: m[0] };
|
|
1127
1322
|
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Quote-aware sibling of `findNextBoundary` (task cf3dff51): the earliest
|
|
1325
|
+
* boundary token at or after `from` that sits OUTSIDE any open single- or
|
|
1326
|
+
* double-quoted run, under the SAME `boundaryRe` alphabet the caller
|
|
1327
|
+
* already scans with — no new boundary characters, only quote-tracking
|
|
1328
|
+
* around the existing ones. `null` when no such boundary exists (either
|
|
1329
|
+
* none remain at all, or every remaining candidate sits inside quoting
|
|
1330
|
+
* that never closes before the string ends — the same conservative "no
|
|
1331
|
+
* further split" fallback `consumeAssignment`'s own unterminated-quote
|
|
1332
|
+
* case already uses, restated here for whole-string segmentation instead
|
|
1333
|
+
* of one token).
|
|
1334
|
+
*
|
|
1335
|
+
* TWO-PHASE, NOT a per-character regex probe (task cf3dff51, latency fix
|
|
1336
|
+
* round — this replaces this function's first shipped implementation,
|
|
1337
|
+
* which called a sticky-regex `.exec()` at EVERY character position;
|
|
1338
|
+
* measured ~2x `normalizeCommand`'s own cost on a large, quote-free
|
|
1339
|
+
* command, because a JS-to-native regex-engine round trip at every single
|
|
1340
|
+
* character is far more expensive than the SAME regex engine's own
|
|
1341
|
+
* internal forward scan across many characters in one call — exactly what
|
|
1342
|
+
* `findNextBoundary` already does for the primary, non-quote-aware pass).
|
|
1343
|
+
* This version instead COMPOSES `findNextBoundary` (unchanged, still the
|
|
1344
|
+
* quote-UNAWARE fast forward scanner) with `QuoteScanner` (below): repeat
|
|
1345
|
+
* — find the next quote-unaware CANDIDATE boundary via `findNextBoundary`
|
|
1346
|
+
* (one native regex scan, however far away the candidate is), then walk
|
|
1347
|
+
* ONLY the span between the previous position and that candidate,
|
|
1348
|
+
* character-by-character, through `QuoteScanner` (below) to learn whether
|
|
1349
|
+
* a quote is open at the candidate's position; if not, the candidate IS
|
|
1350
|
+
* the real boundary — return it; if so, the candidate is inside a quote
|
|
1351
|
+
* (exactly the `VAR='a; b' git push` shape this pass exists to see
|
|
1352
|
+
* through), step through the candidate token's own characters too (never
|
|
1353
|
+
* quote-relevant — `BOUNDARY_RE`'s alphabet contains no `'`/`"`/`\` — so
|
|
1354
|
+
* this is a correctness no-op, only here so the running quote/escape
|
|
1355
|
+
* state stays byte-for-byte identical to a full character-by-character
|
|
1356
|
+
* walk) and continue the search from just past it. For a typical command
|
|
1357
|
+
* (few boundary characters, most of them outside any quote) this pays
|
|
1358
|
+
* for ONE `findNextBoundary` native scan per REAL segment plus one plain
|
|
1359
|
+
* (non-regex) character walk of the same total length `findNextBoundary`
|
|
1360
|
+
* would have scanned internally anyway — no longer one regex call per
|
|
1361
|
+
* character. `null` when no such boundary exists (either none remain at
|
|
1362
|
+
* all — `findNextBoundary` itself returns `null` — or every remaining
|
|
1363
|
+
* candidate sits inside quoting that never closes before the string ends,
|
|
1364
|
+
* the same conservative "no further split" fallback `consumeAssignment`'s
|
|
1365
|
+
* own unterminated-quote case already uses, restated here for whole-
|
|
1366
|
+
* string segmentation instead of one token).
|
|
1367
|
+
*
|
|
1368
|
+
* `QuoteScanner` (below) is the SAME quote/escape state machine
|
|
1369
|
+
* `consumeAssignment`'s own per-token loop uses (three states — `""`,
|
|
1370
|
+
* `'`, `"` — same POSIX approximation: outside quotes `\` escapes the
|
|
1371
|
+
* next char, `'...'` closes only at an UNESCAPED-by-construction `'`
|
|
1372
|
+
* (single quotes never honour backslash escapes at all), `"..."` closes
|
|
1373
|
+
* at an unescaped `"`). Extracted into a shared class (task cf3dff51 fix
|
|
1374
|
+
* round: `npm run check:duplication`'s jscpd scan flagged the two
|
|
1375
|
+
* independently-written copies of this loop as new duplication against
|
|
1376
|
+
* the pinned baseline; see that script's own header comment for why
|
|
1377
|
+
* extraction, not raising the pin, is the stated preference) rather than
|
|
1378
|
+
* duplicated a second time. Generalised from per-token to whole-string
|
|
1379
|
+
* scope by THIS function, not by the scanner itself: `consumeAssignment`
|
|
1380
|
+
* only ever sees one already-whitespace-tokenised segment at a time; this
|
|
1381
|
+
* function walks the RAW command text directly, whitespace included, so
|
|
1382
|
+
* it can tell whether a `;`/`|`/`&&`/`(`/`\n` character sits inside a
|
|
1383
|
+
* quote that a later whitespace-delimited token will re-enter (exactly
|
|
1384
|
+
* the shape `VAR='a; b' git push` needs: the space after `;` is itself
|
|
1385
|
+
* INSIDE the quoted run, not a token boundary this function cares about
|
|
1386
|
+
* at all — only the eventual UNQUOTED position matters).
|
|
1387
|
+
*
|
|
1388
|
+
* Reuses the MODULE-LEVEL `BOUNDARY_QUOTE_SCANNER` instance (`.reset()`
|
|
1389
|
+
* at the top of each call — see that constant's and `reset()`'s own
|
|
1390
|
+
* comments) instead of allocating a fresh `QuoteScanner` per call, and
|
|
1391
|
+
* calls `findNextBoundary` with the CALLER-SUPPLIED `boundaryRe` object
|
|
1392
|
+
* directly rather than a sticky clone — `findNextBoundary` already resets
|
|
1393
|
+
* `lastIndex` itself on every call (its own comment documents exactly
|
|
1394
|
+
* this discipline for the primary pass sharing the SAME `BOUNDARY_RE`
|
|
1395
|
+
* object), so composing it here needs no extra care. Called once per
|
|
1396
|
+
* segment (not once per character) by `segmentAndCanonicalize`'s loop;
|
|
1397
|
+
* the character walk itself still visits each character in the
|
|
1398
|
+
* `[from, end-of-match-or-string)` span exactly once IN TOTAL across all
|
|
1399
|
+
* calls for one command (split across the phase-1 "verify" loop and the
|
|
1400
|
+
* phase-2 "step through the false candidate" loop, never both for the
|
|
1401
|
+
* same character), keeping the whole walk O(length), same complexity
|
|
1402
|
+
* class `findNextBoundary`'s own comment establishes for the primary
|
|
1403
|
+
* pass — this rewrite changes the CONSTANT factor, not the asymptotic
|
|
1404
|
+
* class.
|
|
1405
|
+
*/
|
|
1406
|
+
function findNextBoundaryQuoteAware(s, from, boundaryRe) {
|
|
1407
|
+
BOUNDARY_QUOTE_SCANNER.reset();
|
|
1408
|
+
let pos = from;
|
|
1409
|
+
const n = s.length;
|
|
1410
|
+
for (;;) {
|
|
1411
|
+
const candidate = findNextBoundary(s, pos, boundaryRe);
|
|
1412
|
+
if (candidate === null)
|
|
1413
|
+
return null;
|
|
1414
|
+
// Phase 1: verify quote-safety by walking ONLY the span strictly
|
|
1415
|
+
// BEFORE the candidate — mirrors the original per-character scan,
|
|
1416
|
+
// which never steps through the boundary token's own characters
|
|
1417
|
+
// before deciding whether to return it.
|
|
1418
|
+
for (let i = pos; i < candidate.start; i++) {
|
|
1419
|
+
BOUNDARY_QUOTE_SCANNER.step(s[i]);
|
|
1420
|
+
}
|
|
1421
|
+
if (BOUNDARY_QUOTE_SCANNER.state === "") {
|
|
1422
|
+
return candidate;
|
|
1423
|
+
}
|
|
1424
|
+
// Phase 2: the candidate sits inside an open quote — not a real
|
|
1425
|
+
// boundary. Step through ITS OWN characters too (a correctness
|
|
1426
|
+
// no-op, see this function's own comment) so the running state stays
|
|
1427
|
+
// exactly what a full character-by-character walk would have
|
|
1428
|
+
// produced, then resume the search just past it.
|
|
1429
|
+
const tokenEnd = candidate.start + candidate.token.length;
|
|
1430
|
+
for (let i = candidate.start; i < tokenEnd; i++) {
|
|
1431
|
+
BOUNDARY_QUOTE_SCANNER.step(s[i]);
|
|
1432
|
+
}
|
|
1433
|
+
if (tokenEnd >= n)
|
|
1434
|
+
return null;
|
|
1435
|
+
pos = tokenEnd;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Mutable single-/double-quote + backslash-escape state machine (task
|
|
1440
|
+
* cf3dff51 extraction — see `findNextBoundaryQuoteAware`'s own comment for
|
|
1441
|
+
* why this was pulled out of two independently-written copies rather than
|
|
1442
|
+
* left duplicated). POSIX-approximated semantics, shared verbatim by both
|
|
1443
|
+
* consumers: outside quotes `\` escapes the next character; `'...'`
|
|
1444
|
+
* closes only at a literal `'` (single quotes never honour backslash
|
|
1445
|
+
* escapes — the `escaped` branch below is unreachable whenever
|
|
1446
|
+
* `state === "'"`, by construction: `step` checks `this.escaped` BEFORE
|
|
1447
|
+
* checking `this.state`, so an escape flagged one character earlier is
|
|
1448
|
+
* always consumed first, but nothing inside the `state === "'"` branch
|
|
1449
|
+
* itself ever SETS `escaped`, so it can never observe a `\` as anything
|
|
1450
|
+
* other than a literal character); `"..."` closes at an unescaped `"`. A
|
|
1451
|
+
* mutable class rather than a pure step function returning a new state
|
|
1452
|
+
* object: this module runs on every Bash/Edit/Write tool call (module
|
|
1453
|
+
* header) and both consumers step through EVERY character of a segment or
|
|
1454
|
+
* token, so one allocation per scan (not one per character) matters for
|
|
1455
|
+
* the same "allocation-light" reason the module header states elsewhere.
|
|
1456
|
+
*/
|
|
1457
|
+
class QuoteScanner {
|
|
1458
|
+
state = "";
|
|
1459
|
+
escaped = false;
|
|
1460
|
+
/** Advance the state machine by exactly one character. */
|
|
1461
|
+
step(c) {
|
|
1462
|
+
if (this.escaped) {
|
|
1463
|
+
this.escaped = false;
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (this.state === "'") {
|
|
1467
|
+
if (c === "'")
|
|
1468
|
+
this.state = "";
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (this.state === '"') {
|
|
1472
|
+
if (c === "\\")
|
|
1473
|
+
this.escaped = true;
|
|
1474
|
+
else if (c === '"')
|
|
1475
|
+
this.state = "";
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
// state === ""
|
|
1479
|
+
if (c === "\\")
|
|
1480
|
+
this.escaped = true;
|
|
1481
|
+
else if (c === "'" || c === '"')
|
|
1482
|
+
this.state = c;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Reset to the initial `""` / not-escaped state — behaviourally
|
|
1486
|
+
* IDENTICAL to discarding this instance and constructing a fresh one
|
|
1487
|
+
* (task cf3dff51, latency fix round: lets `findNextBoundaryQuoteAware`
|
|
1488
|
+
* reuse ONE module-level instance across every segment of a command
|
|
1489
|
+
* instead of allocating a new one per segment — see that function's own
|
|
1490
|
+
* comment for the measured latency this closes).
|
|
1491
|
+
*/
|
|
1492
|
+
reset() {
|
|
1493
|
+
this.state = "";
|
|
1494
|
+
this.escaped = false;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Clear a pending escape without consuming a character. Used only by
|
|
1498
|
+
* `consumeAssignment`, at a whitespace-tokenised TOKEN boundary: a
|
|
1499
|
+
* backslash pending at a token's end would otherwise escape the
|
|
1500
|
+
* separating whitespace itself (task b093911d's class, deliberately
|
|
1501
|
+
* NOT continued — see that function's own comment). `state` is never
|
|
1502
|
+
* touched by this — an open quote genuinely stays open across
|
|
1503
|
+
* whitespace, only a dangling escape does not.
|
|
1504
|
+
*/
|
|
1505
|
+
resetEscape() {
|
|
1506
|
+
this.escaped = false;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Module-level `QuoteScanner` instance reused by `findNextBoundaryQuoteAware`
|
|
1511
|
+
* across every segment of every command (task cf3dff51, latency fix
|
|
1512
|
+
* round) — `.reset()` at the top of each call makes reusing this single
|
|
1513
|
+
* instance behaviourally IDENTICAL to `new QuoteScanner()` per call (see
|
|
1514
|
+
* `reset()`'s own doc comment), just without the allocation. Placed here,
|
|
1515
|
+
* textually AFTER `class QuoteScanner`, because a top-level `const`
|
|
1516
|
+
* initialiser referencing a class runs in real module-evaluation order —
|
|
1517
|
+
* `findNextBoundaryQuoteAware` itself sits ABOVE this point in the file
|
|
1518
|
+
* but is a hoisted function declaration whose BODY only runs when called,
|
|
1519
|
+
* by which time this line has already executed.
|
|
1520
|
+
*/
|
|
1521
|
+
const BOUNDARY_QUOTE_SCANNER = new QuoteScanner();
|
|
1128
1522
|
/**
|
|
1129
1523
|
* Consume one leading `VAR=value` assignment starting at `idx`, returning
|
|
1130
1524
|
* the index of the first token AFTER it. Task 13e55484: the VALUE may be
|
|
@@ -1185,40 +1579,19 @@ function findNextBoundary(s, from, boundaryRe) {
|
|
|
1185
1579
|
* pinned.
|
|
1186
1580
|
*/
|
|
1187
1581
|
function consumeAssignment(tokens, idx) {
|
|
1188
|
-
|
|
1189
|
-
let escaped = false;
|
|
1582
|
+
const quote = new QuoteScanner();
|
|
1190
1583
|
for (let i = idx; i < tokens.length; i++) {
|
|
1191
1584
|
const text = tokens[i].text;
|
|
1192
1585
|
if (i > idx && (GIT_TOKEN_RE.test(text) || NON_GIT_HEAD_TOKENS.has(text))) {
|
|
1193
1586
|
return idx + 1;
|
|
1194
1587
|
}
|
|
1195
1588
|
for (let k = 0; k < text.length; k++) {
|
|
1196
|
-
|
|
1197
|
-
if (escaped) {
|
|
1198
|
-
escaped = false;
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
if (state === "'") {
|
|
1202
|
-
if (c === "'")
|
|
1203
|
-
state = "";
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
if (state === '"') {
|
|
1207
|
-
if (c === "\\")
|
|
1208
|
-
escaped = true;
|
|
1209
|
-
else if (c === '"')
|
|
1210
|
-
state = "";
|
|
1211
|
-
continue;
|
|
1212
|
-
}
|
|
1213
|
-
if (c === "\\")
|
|
1214
|
-
escaped = true;
|
|
1215
|
-
else if (c === "'" || c === '"')
|
|
1216
|
-
state = c;
|
|
1589
|
+
quote.step(text[k]);
|
|
1217
1590
|
}
|
|
1218
1591
|
// A backslash pending at a token's end would escape the separating
|
|
1219
1592
|
// whitespace itself — b093911d's class, not continued here.
|
|
1220
|
-
|
|
1221
|
-
if (state === "")
|
|
1593
|
+
quote.resetEscape();
|
|
1594
|
+
if (quote.state === "")
|
|
1222
1595
|
return i + 1;
|
|
1223
1596
|
}
|
|
1224
1597
|
// Ran out of tokens with a quote still open: unterminated. Preserve the
|