opencode-swarm 7.114.9 → 7.115.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.
@@ -7,6 +7,17 @@
7
7
  * Pure TypeScript, no shell/git/external binaries, standard node:fs sync I/O.
8
8
  */
9
9
  import type { ToolDefinition } from '@opencode-ai/plugin/tool';
10
+ import { loadPluginConfigWithMeta } from '../config';
11
+ /**
12
+ * DI seam for hermetic config-load substitution in tests (AGENTS.md invariant 7).
13
+ * Mirrors the pattern at `src/tools/context-status.ts:81-88`. Tests override
14
+ * `_internals.loadPluginConfigWithMeta` and restore it in `afterEach` instead
15
+ * of writing real config files or using `mock.module` (which leaks across
16
+ * test files in Bun's shared runner).
17
+ */
18
+ export declare const _internals: {
19
+ loadPluginConfigWithMeta: typeof loadPluginConfigWithMeta;
20
+ };
10
21
  /** Per-file error detail in the structured output. */
11
22
  export interface ApplyPatchFileError {
12
23
  hunkIndex: number;
@@ -45,6 +56,16 @@ export interface ApplyPatchArgs {
45
56
  allowCreates?: boolean;
46
57
  allowDeletes?: boolean;
47
58
  }
59
+ /**
60
+ * Opt-in fuzzy-matching options threaded from `execute` → `processFileDiff`
61
+ * → `applyHunks`. Both flags default false (exact-match-only, B3 decision).
62
+ */
63
+ export interface ApplyHunksOptions {
64
+ /** Enable fuzzy fallback (strategies 1-8) on exact-match failure. */
65
+ fuzzyMatch: boolean;
66
+ /** Additionally enable strategy 9 (context_aware). Only effective with fuzzyMatch. */
67
+ fuzzyMatchContextAware: boolean;
68
+ }
48
69
  /**
49
70
  * Swarm unified-diff patch tool (formerly registered as apply_patch).
50
71
  * Renamed to swarm_apply_patch so it no longer shadows the native opencode
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Fuzzy text-matching engine — faithful TypeScript port of hermes-agent's
3
+ * `tools/fuzzy_match.py` (issue #1718).
4
+ *
5
+ * Implements a 9-strategy matching chain that finds and replaces text,
6
+ * tolerating the whitespace, indentation, escape-sequence, and Unicode
7
+ * drift common in LLM-generated patches. Integrated into `apply-patch`
8
+ * as an **opt-in fallback** (default off) gated by the `apply_patch.fuzzy_match`
9
+ * and `apply_patch.fuzzy_match_context_aware` config flags.
10
+ *
11
+ * Porting conventions:
12
+ * - Operates on **UTF-16 code units** consistently (`s[i]`, `s.length`).
13
+ * Matches `src/utils/sequence-matcher.ts`. Round-trip safe.
14
+ * - Per-line stripping uses JS `String.prototype.trim()`, which strips the
15
+ * Unicode WhiteSpace + LineTerminator set. Coincides with Python
16
+ * `str.strip()` for the ASCII/BMP content in the test suite; minor
17
+ * divergence on exotic whitespace categories is acceptable.
18
+ * - Strategy 9 (`contextAware`) is opt-in via the `includeContextAware`
19
+ * option to `fuzzyFindAndReplace`. It is the loosest, most-false-positive-
20
+ * prone strategy and is separately gated in apply-patch by
21
+ * `apply_patch.fuzzy_match_context_aware`.
22
+ */
23
+ /** A `[start, end)` character span in the content string. */
24
+ type Span = [number, number];
25
+ /** Result of {@link fuzzyFindAndReplace}. */
26
+ export interface FuzzyResult {
27
+ /** Modified content on success; original content on failure. */
28
+ content: string;
29
+ /** Number of replacements made (0 on failure). */
30
+ matchCount: number;
31
+ /** Name of the strategy that matched, or `null` on failure. */
32
+ strategy: string | null;
33
+ /** `null` on success; an error description on failure. */
34
+ error: string | null;
35
+ }
36
+ /** Options for {@link fuzzyFindAndReplace}. */
37
+ export interface FuzzyOptions {
38
+ /**
39
+ * When true (default at the utility level), the `context_aware` strategy
40
+ * (9) is included in the chain. Strategy 9 is the loosest/most-false-
41
+ * positive-prone strategy (requires 50% of lines to reach 0.80 per-line
42
+ * similarity) and has quadratic cost, so it is bounded by an internal
43
+ * cell-count cap.
44
+ *
45
+ * **Default divergence (intentional):** the utility defaults this to
46
+ * `true` so the byte-faithful ported hermes test suite passes unmodified
47
+ * (strategy 9 is a legitimate part of the matching chain and some test
48
+ * cases depend on it). The `apply-patch` integration — the only consumer
49
+ * today — explicitly passes `false` unless the separate
50
+ * `apply_patch.fuzzy_match_context_aware` config flag is set, honoring
51
+ * issue #1718's Non-Goal "do not port strategy 9 as default-on" at the
52
+ * integration layer. New callers should pass `false` unless they
53
+ * specifically want strategy 9.
54
+ */
55
+ includeContextAware?: boolean;
56
+ }
57
+ /**
58
+ * Maps Unicode typographic characters to their ASCII equivalents.
59
+ * Some replacements EXPAND a single code point into multiple ASCII chars
60
+ * (em-dash → "--", ellipsis → "..."); the position-remap helpers handle the
61
+ * resulting offset divergence.
62
+ */
63
+ export declare const UNICODE_MAP: Record<string, string>;
64
+ /** Normalize Unicode typographic characters to ASCII equivalents. */
65
+ export declare function unicodeNormalize(text: string): string;
66
+ /**
67
+ * Find and replace text using a chain of increasingly fuzzy strategies.
68
+ *
69
+ * Strategies are tried in order; the first that yields matches wins. On a
70
+ * unique match (or when `replaceAll` is true), the replacement is applied.
71
+ * On failure, returns the original content with an error description.
72
+ *
73
+ * Guards (ported verbatim from hermes):
74
+ * - Ambiguity: `>1 match && !replaceAll` → fail with a helpful message.
75
+ * - Escape-drift: `\'`/`\"` present in both old+new but absent from the
76
+ * matched file region → block (transport serialization artifact).
77
+ * - Selective unescape: `\t`/`\r` in new_string → real bytes only when the
78
+ * matched file region contains the corresponding control char. `\n` excluded.
79
+ * - Unicode preservation: under strategy 7, unchanged spans keep the file's
80
+ * original Unicode characters rather than the ASCII-normalized equivalents.
81
+ */
82
+ export declare function fuzzyFindAndReplace(content: string, oldString: string, newString: string, replaceAll?: boolean, options?: FuzzyOptions): FuzzyResult;
83
+ /** Strategy 1: exact string match, non-overlapping. */
84
+ export declare function strategyExact(content: string, pattern: string): Span[];
85
+ /** Strategy 2: per-line `.trim()` + block equality. */
86
+ export declare function strategyLineTrimmed(content: string, pattern: string): Span[];
87
+ /** Strategy 3: collapse `[ \t]+` → single space, preserve newlines. */
88
+ export declare function strategyWhitespaceNormalized(content: string, pattern: string): Span[];
89
+ /** Strategy 4: strip all leading whitespace per line (lstrip). */
90
+ export declare function strategyIndentationFlexible(content: string, pattern: string): Span[];
91
+ /** Strategy 5: unescape `\n`/`\t`/`\r` literals → bytes, then exact match. */
92
+ export declare function strategyEscapeNormalized(content: string, pattern: string): Span[];
93
+ /** Strategy 6: trim only first and last lines, sliding window. */
94
+ export declare function strategyTrimmedBoundary(content: string, pattern: string): Span[];
95
+ /** Strategy 7: Unicode normalization (smart quotes, em/en dash, ellipsis, NBSP). */
96
+ export declare function strategyUnicodeNormalized(content: string, pattern: string): Span[];
97
+ /** Strategy 8: anchor on first+last lines, similarity for the middle. */
98
+ export declare function strategyBlockAnchor(content: string, pattern: string): Span[];
99
+ export declare function strategyContextAware(content: string, pattern: string): Span[];
100
+ /**
101
+ * Find lines in `content` most similar to `oldString` for "did you mean?" feedback.
102
+ *
103
+ * Returns a formatted string showing the closest matching lines with context
104
+ * and line numbers, or `''` if no useful match is found.
105
+ */
106
+ export declare function findClosestLines(oldString: string, content: string, contextLines?: number, maxResults?: number): string;
107
+ /**
108
+ * Return a "Did you mean..." snippet for plain no-match errors.
109
+ *
110
+ * Gated so the hint only fires for actual "old_string not found" failures.
111
+ * Ambiguous-match, escape-drift, and identical-strings errors all have
112
+ * `matchCount === 0` but a did-you-mean snippet would be misleading.
113
+ */
114
+ export declare function formatNoMatchHint(error: string | null, matchCount: number, oldString: string, content: string): string;
115
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Faithful TypeScript port of CPython's `difflib.SequenceMatcher`.
3
+ *
4
+ * Implements the Ratcliff/Obershelp pattern-matching algorithm with the
5
+ * "popular element" (autojunk) guard, so that `.ratio()` produces values
6
+ * identical to CPython 3.x for the BMP character content used by the
7
+ * fuzzy-match strategies. The thresholds in `src/utils/fuzzy-match.ts`
8
+ * (0.50 / 0.70 / 0.80) were tuned against CPython's exact `2*M/T` ratio
9
+ * formula, so substituting a Levenshtein-based library would silently
10
+ * break those thresholds.
11
+ *
12
+ * Porting notes:
13
+ * - Operates on **UTF-16 code units** (`s[i]`, `s.length`, `s.charCodeAt(i)`)
14
+ * consistently throughout. CPython operates on Unicode code points; for
15
+ * ASCII/BMP content the two coincide. Astral-plane characters (emoji)
16
+ * consume two UTF-16 units, so ratios for emoji-heavy input may differ
17
+ * marginally from CPython. This is acceptable for the fuzzy-match use
18
+ * case (source-code patches) and is round-trip-safe — no index mixing,
19
+ * no file corruption.
20
+ * - `autojunk` defaults to `true`, matching CPython. When `b.length >= 200`
21
+ * and an element appears more than 1% of the time in `b`, it is treated
22
+ * as "popular" and excluded as an anchor candidate in `find_longest_match`.
23
+ * This is essential for ratio fidelity on real file sections.
24
+ */
25
+ export interface Match {
26
+ /** Start index in sequence `a`. */
27
+ a: number;
28
+ /** Start index in sequence `b`. */
29
+ b: number;
30
+ /** Length of the matching block. */
31
+ size: number;
32
+ }
33
+ export type OpcodeTag = 'equal' | 'replace' | 'delete' | 'insert';
34
+ export interface Opcode {
35
+ tag: OpcodeTag;
36
+ i1: number;
37
+ i2: number;
38
+ j1: number;
39
+ j2: number;
40
+ }
41
+ /** A predicate that marks a character as "junk" (never a sync point). */
42
+ export type IsJunk = ((ch: string) => boolean) | null;
43
+ /**
44
+ * Difflib-compatible sequence matcher. Construct with two strings, then
45
+ * call `.ratio()`, `.get_matching_blocks()`, or `.get_opcodes()`.
46
+ */
47
+ export declare class SequenceMatcher {
48
+ private a;
49
+ private b;
50
+ private isJunk;
51
+ private autoJunk;
52
+ private b2j;
53
+ private popularSet;
54
+ private matchingBlocks;
55
+ constructor(isJunk: IsJunk, a: string, b: string, autoJunk?: boolean);
56
+ /** Allow changing the input sequences (mirrors CPython `set_seqs`/`set_seq2`). */
57
+ setSeqs(a: string, b: string): void;
58
+ /** Precompute the `b` index map and the popular-element set. */
59
+ private chainB;
60
+ /**
61
+ * Find the longest matching block in `a[alo:ahi]` and `b[blo:bhi]`.
62
+ *
63
+ * Returns `{ a, b, size }` where `size` is maximal; ties are broken by
64
+ * smallest `a`, then smallest `b` (CPython canonical tiebreak).
65
+ *
66
+ * A "match" means `a[i] === b[j]` and the run extends as far as possible.
67
+ * Elements in the popular set are only used as anchor candidates when no
68
+ * non-popular match exists (CPython behavior: popular elements are
69
+ * excluded from `b2j`, so they only match opportunistically via the
70
+ * extending run after a non-popular anchor).
71
+ */
72
+ findLongestMatch(alo: number, ahi: number, blo: number, bhi: number): Match;
73
+ /** Recursively compute the list of matching blocks (descending order). */
74
+ getMatchingBlocks(): Match[];
75
+ /**
76
+ * Return a float in [0, 1]: `2*M / T`, where `M` is the total matched
77
+ * characters and `T` is the sum of the two sequence lengths.
78
+ * Returns `1.0` when both sequences are empty (matches CPython).
79
+ */
80
+ ratio(): number;
81
+ /** Compute the opcodes describing how to turn `a` into `b`. */
82
+ getOpcodes(): Opcode[];
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.114.9",
3
+ "version": "7.115.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",