@chances-ai/engine 27.0.0 → 29.0.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.
Files changed (153) hide show
  1. package/dist/agents/bundled.d.ts +5 -0
  2. package/dist/agents/bundled.d.ts.map +1 -0
  3. package/dist/agents/bundled.js +66 -0
  4. package/dist/agents/bundled.js.map +1 -0
  5. package/dist/agents/index.d.ts +1 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/index.js +1 -0
  8. package/dist/agents/index.js.map +1 -1
  9. package/dist/agents/parse.d.ts +3 -0
  10. package/dist/agents/parse.d.ts.map +1 -1
  11. package/dist/agents/parse.js +17 -0
  12. package/dist/agents/parse.js.map +1 -1
  13. package/dist/agents/types.d.ts +8 -0
  14. package/dist/agents/types.d.ts.map +1 -1
  15. package/dist/ai/adapters/ai-sdk-stream.js +15 -0
  16. package/dist/ai/adapters/ai-sdk-stream.js.map +1 -1
  17. package/dist/ai/types.d.ts +12 -0
  18. package/dist/ai/types.d.ts.map +1 -1
  19. package/dist/core/coordinator-mode.d.ts +32 -0
  20. package/dist/core/coordinator-mode.d.ts.map +1 -0
  21. package/dist/core/coordinator-mode.js +98 -0
  22. package/dist/core/coordinator-mode.js.map +1 -0
  23. package/dist/core/coordinator-tools.d.ts +22 -0
  24. package/dist/core/coordinator-tools.d.ts.map +1 -0
  25. package/dist/core/coordinator-tools.js +262 -0
  26. package/dist/core/coordinator-tools.js.map +1 -0
  27. package/dist/core/engine.d.ts +50 -6
  28. package/dist/core/engine.d.ts.map +1 -1
  29. package/dist/core/engine.js +107 -17
  30. package/dist/core/engine.js.map +1 -1
  31. package/dist/core/index.d.ts +3 -1
  32. package/dist/core/index.d.ts.map +1 -1
  33. package/dist/core/index.js +3 -1
  34. package/dist/core/index.js.map +1 -1
  35. package/dist/core/task-tool.d.ts +85 -1
  36. package/dist/core/task-tool.d.ts.map +1 -1
  37. package/dist/core/task-tool.js +456 -500
  38. package/dist/core/task-tool.js.map +1 -1
  39. package/dist/hashline/apply.d.ts +9 -0
  40. package/dist/hashline/apply.d.ts.map +1 -0
  41. package/dist/hashline/apply.js +523 -0
  42. package/dist/hashline/apply.js.map +1 -0
  43. package/dist/hashline/block.d.ts +25 -0
  44. package/dist/hashline/block.d.ts.map +1 -0
  45. package/dist/hashline/block.js +71 -0
  46. package/dist/hashline/block.js.map +1 -0
  47. package/dist/hashline/format.d.ts +106 -0
  48. package/dist/hashline/format.d.ts.map +1 -0
  49. package/dist/hashline/format.js +191 -0
  50. package/dist/hashline/format.js.map +1 -0
  51. package/dist/hashline/fs.d.ts +87 -0
  52. package/dist/hashline/fs.d.ts.map +1 -0
  53. package/dist/hashline/fs.js +123 -0
  54. package/dist/hashline/fs.js.map +1 -0
  55. package/dist/hashline/index.d.ts +27 -0
  56. package/dist/hashline/index.d.ts.map +1 -0
  57. package/dist/hashline/index.js +27 -0
  58. package/dist/hashline/index.js.map +1 -0
  59. package/dist/hashline/input.d.ts +101 -0
  60. package/dist/hashline/input.d.ts.map +1 -0
  61. package/dist/hashline/input.js +378 -0
  62. package/dist/hashline/input.js.map +1 -0
  63. package/dist/hashline/messages.d.ts +87 -0
  64. package/dist/hashline/messages.d.ts.map +1 -0
  65. package/dist/hashline/messages.js +94 -0
  66. package/dist/hashline/messages.js.map +1 -0
  67. package/dist/hashline/mismatch.d.ts +45 -0
  68. package/dist/hashline/mismatch.d.ts.map +1 -0
  69. package/dist/hashline/mismatch.js +118 -0
  70. package/dist/hashline/mismatch.js.map +1 -0
  71. package/dist/hashline/normalize.d.ts +22 -0
  72. package/dist/hashline/normalize.d.ts.map +1 -0
  73. package/dist/hashline/normalize.js +31 -0
  74. package/dist/hashline/normalize.js.map +1 -0
  75. package/dist/hashline/parser.d.ts +24 -0
  76. package/dist/hashline/parser.d.ts.map +1 -0
  77. package/dist/hashline/parser.js +295 -0
  78. package/dist/hashline/parser.js.map +1 -0
  79. package/dist/hashline/patcher.d.ts +111 -0
  80. package/dist/hashline/patcher.d.ts.map +1 -0
  81. package/dist/hashline/patcher.js +332 -0
  82. package/dist/hashline/patcher.js.map +1 -0
  83. package/dist/hashline/recovery.d.ts +41 -0
  84. package/dist/hashline/recovery.d.ts.map +1 -0
  85. package/dist/hashline/recovery.js +175 -0
  86. package/dist/hashline/recovery.js.map +1 -0
  87. package/dist/hashline/snapshots.d.ts +62 -0
  88. package/dist/hashline/snapshots.d.ts.map +1 -0
  89. package/dist/hashline/snapshots.js +127 -0
  90. package/dist/hashline/snapshots.js.map +1 -0
  91. package/dist/hashline/tokenizer.d.ts +66 -0
  92. package/dist/hashline/tokenizer.d.ts.map +1 -0
  93. package/dist/hashline/tokenizer.js +408 -0
  94. package/dist/hashline/tokenizer.js.map +1 -0
  95. package/dist/hashline/types.d.ts +117 -0
  96. package/dist/hashline/types.d.ts.map +1 -0
  97. package/dist/hashline/types.js +13 -0
  98. package/dist/hashline/types.js.map +1 -0
  99. package/dist/tools/builtins/_hashline-fs.d.ts +16 -0
  100. package/dist/tools/builtins/_hashline-fs.d.ts.map +1 -0
  101. package/dist/tools/builtins/_hashline-fs.js +62 -0
  102. package/dist/tools/builtins/_hashline-fs.js.map +1 -0
  103. package/dist/tools/builtins/_image.d.ts +26 -0
  104. package/dist/tools/builtins/_image.d.ts.map +1 -0
  105. package/dist/tools/builtins/_image.js +76 -0
  106. package/dist/tools/builtins/_image.js.map +1 -0
  107. package/dist/tools/builtins/_login-shell.d.ts +17 -0
  108. package/dist/tools/builtins/_login-shell.d.ts.map +1 -0
  109. package/dist/tools/builtins/_login-shell.js +66 -0
  110. package/dist/tools/builtins/_login-shell.js.map +1 -0
  111. package/dist/tools/builtins/_notebook.d.ts +15 -0
  112. package/dist/tools/builtins/_notebook.d.ts.map +1 -0
  113. package/dist/tools/builtins/_notebook.js +81 -0
  114. package/dist/tools/builtins/_notebook.js.map +1 -0
  115. package/dist/tools/builtins/_pdf.d.ts +19 -0
  116. package/dist/tools/builtins/_pdf.d.ts.map +1 -0
  117. package/dist/tools/builtins/_pdf.js +42 -0
  118. package/dist/tools/builtins/_pdf.js.map +1 -0
  119. package/dist/tools/builtins/_shared.d.ts +11 -0
  120. package/dist/tools/builtins/_shared.d.ts.map +1 -1
  121. package/dist/tools/builtins/_shared.js +40 -1
  122. package/dist/tools/builtins/_shared.js.map +1 -1
  123. package/dist/tools/builtins/ast-edit.d.ts +18 -0
  124. package/dist/tools/builtins/ast-edit.d.ts.map +1 -0
  125. package/dist/tools/builtins/ast-edit.js +109 -0
  126. package/dist/tools/builtins/ast-edit.js.map +1 -0
  127. package/dist/tools/builtins/ast-grep.d.ts +6 -0
  128. package/dist/tools/builtins/ast-grep.d.ts.map +1 -0
  129. package/dist/tools/builtins/ast-grep.js +67 -0
  130. package/dist/tools/builtins/ast-grep.js.map +1 -0
  131. package/dist/tools/builtins/bash.d.ts.map +1 -1
  132. package/dist/tools/builtins/bash.js +13 -2
  133. package/dist/tools/builtins/bash.js.map +1 -1
  134. package/dist/tools/builtins/edit.d.ts.map +1 -1
  135. package/dist/tools/builtins/edit.js +112 -31
  136. package/dist/tools/builtins/edit.js.map +1 -1
  137. package/dist/tools/builtins/read.d.ts.map +1 -1
  138. package/dist/tools/builtins/read.js +187 -11
  139. package/dist/tools/builtins/read.js.map +1 -1
  140. package/dist/tools/builtins.d.ts.map +1 -1
  141. package/dist/tools/builtins.js +4 -0
  142. package/dist/tools/builtins.js.map +1 -1
  143. package/dist/tools/file-lock.d.ts +8 -0
  144. package/dist/tools/file-lock.d.ts.map +1 -1
  145. package/dist/tools/file-lock.js +22 -0
  146. package/dist/tools/file-lock.js.map +1 -1
  147. package/dist/tools/index.d.ts +1 -1
  148. package/dist/tools/index.d.ts.map +1 -1
  149. package/dist/tools/index.js.map +1 -1
  150. package/dist/tools/types.d.ts +26 -0
  151. package/dist/tools/types.d.ts.map +1 -1
  152. package/dist/tools/types.js.map +1 -1
  153. package/package.json +8 -3
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Storage seam for the hashline patcher. {@link Filesystem} is intentionally
3
+ * minimal — `readText`, `writeText`, `exists` — so any backing store can be
4
+ * adapted: disk, memory, S3, an LSP text-document protocol, a Git tree, a
5
+ * VFS, etc. Ported from oh-my-pi `packages/hashline/src/fs.ts`.
6
+ *
7
+ * **chances-cli change (7.9):** the Bun-based `NodeFilesystem` is dropped (it
8
+ * used `Bun.file`/`Bun.write`, unavailable under Node — serve-on-Node). The
9
+ * disk-backed implementation lives in the engine `edit` tool as `EngineToolFs`,
10
+ * which routes every write through `guardWrite` (realpath jail + protected-path
11
+ * floor) + `writeFileAtomic` + fs-scan cache invalidation. {@link
12
+ * InMemoryFilesystem} stays here for tests/sandboxes.
13
+ *
14
+ * The patcher does its own BOM stripping and LF normalization between
15
+ * {@link Filesystem.readText} and {@link Filesystem.writeText}; the FS deals
16
+ * only in raw text strings.
17
+ */
18
+ /**
19
+ * ENOENT-like error thrown by {@link Filesystem.readText} when a path is
20
+ * missing. Carrying a `code` property keeps the contract compatible with
21
+ * `node:fs` callers that already check `err.code === "ENOENT"`.
22
+ */
23
+ export class NotFoundError extends Error {
24
+ code = "ENOENT";
25
+ constructor(path, cause) {
26
+ super(`File not found: ${path}`);
27
+ this.name = "NotFoundError";
28
+ if (cause !== undefined)
29
+ this.cause = cause;
30
+ }
31
+ }
32
+ /** Type guard for {@link NotFoundError} and structurally-compatible errors. */
33
+ export function isNotFound(error) {
34
+ if (error instanceof NotFoundError)
35
+ return true;
36
+ if (error instanceof Error && error.code === "ENOENT")
37
+ return true;
38
+ return false;
39
+ }
40
+ /**
41
+ * Abstract storage backend the {@link Patcher} reads from and writes to.
42
+ * Subclass for new backends; the package ships {@link InMemoryFilesystem} for
43
+ * tests and the engine wires `EngineToolFs` for disk.
44
+ *
45
+ * Implementations work with raw text — the patcher handles BOM stripping and
46
+ * line-ending normalization itself. `readText` MUST throw {@link
47
+ * NotFoundError} (or any error for which {@link isNotFound} returns true)
48
+ * when the path doesn't exist; that's how the patcher detects a create-vs-
49
+ * update.
50
+ */
51
+ export class Filesystem {
52
+ /** Validate that `path` is writable before a prepared batch starts committing. */
53
+ async preflightWrite(_path) { }
54
+ /** Return true when the path exists and can be read. Default: probe via {@link readText}. */
55
+ async exists(path) {
56
+ try {
57
+ await this.readText(path);
58
+ return true;
59
+ }
60
+ catch (error) {
61
+ if (isNotFound(error))
62
+ return false;
63
+ throw error;
64
+ }
65
+ }
66
+ /**
67
+ * Canonical path used as a key by external caches (e.g. snapshot
68
+ * stores). The default is identity; override to return an absolute or
69
+ * otherwise canonicalised path so producers and consumers of cached
70
+ * snapshots agree on the key without each having to redo the resolution.
71
+ */
72
+ canonicalPath(path) {
73
+ return path;
74
+ }
75
+ }
76
+ /**
77
+ * In-memory {@link Filesystem}. Useful for tests, sandboxes, dry-runs, and as
78
+ * a building block for stacked adapters (e.g. an LRU layer on top).
79
+ */
80
+ export class InMemoryFilesystem extends Filesystem {
81
+ #files = new Map();
82
+ constructor(initial) {
83
+ super();
84
+ if (initial) {
85
+ for (const [path, content] of initial)
86
+ this.#files.set(path, content);
87
+ }
88
+ }
89
+ async readText(path) {
90
+ const text = this.#files.get(path);
91
+ if (text === undefined)
92
+ throw new NotFoundError(path);
93
+ return text;
94
+ }
95
+ async writeText(path, content) {
96
+ this.#files.set(path, content);
97
+ return { text: content };
98
+ }
99
+ async exists(path) {
100
+ return this.#files.has(path);
101
+ }
102
+ /** Synchronous helper for setting up fixtures without awaiting. */
103
+ set(path, content) {
104
+ this.#files.set(path, content);
105
+ }
106
+ /** Synchronous helper for inspecting state without awaiting. */
107
+ get(path) {
108
+ return this.#files.get(path);
109
+ }
110
+ /** Remove a single entry. Returns true when something was removed. */
111
+ delete(path) {
112
+ return this.#files.delete(path);
113
+ }
114
+ /** Wipe all entries. */
115
+ clear() {
116
+ this.#files.clear();
117
+ }
118
+ /** Iterate `[path, content]` pairs. */
119
+ entries() {
120
+ return this.#files.entries();
121
+ }
122
+ }
123
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/hashline/fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @chances-ai/engine hashline core — a compact, line-anchored patch language
3
+ * and applier for LLM-driven file edits. Ported from oh-my-pi
4
+ * `packages/hashline` (7.9 task 09); see the per-file headers for the
5
+ * chances-cli deltas (cross-runtime hash, exact-text integrity, FS seam).
6
+ *
7
+ * Every section binds to a whole-file content hash so stale anchors are
8
+ * rejected before they corrupt code; the patcher abstracts over the
9
+ * filesystem so the same core works on disk, in memory, or against any custom
10
+ * backend. The engine `edit` tool wires `EngineToolFs` (guardWrite + atomic) +
11
+ * a `native.blockRangeAt` block resolver + a per-session `SnapshotStore`.
12
+ */
13
+ export * from "./apply.js";
14
+ export * from "./block.js";
15
+ export * from "./format.js";
16
+ export * from "./fs.js";
17
+ export * from "./input.js";
18
+ export * from "./messages.js";
19
+ export * from "./mismatch.js";
20
+ export * from "./normalize.js";
21
+ export * from "./parser.js";
22
+ export * from "./patcher.js";
23
+ export * from "./recovery.js";
24
+ export * from "./snapshots.js";
25
+ export * from "./tokenizer.js";
26
+ export * from "./types.js";
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hashline/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @chances-ai/engine hashline core — a compact, line-anchored patch language
3
+ * and applier for LLM-driven file edits. Ported from oh-my-pi
4
+ * `packages/hashline` (7.9 task 09); see the per-file headers for the
5
+ * chances-cli deltas (cross-runtime hash, exact-text integrity, FS seam).
6
+ *
7
+ * Every section binds to a whole-file content hash so stale anchors are
8
+ * rejected before they corrupt code; the patcher abstracts over the
9
+ * filesystem so the same core works on disk, in memory, or against any custom
10
+ * backend. The engine `edit` tool wires `EngineToolFs` (guardWrite + atomic) +
11
+ * a `native.blockRangeAt` block resolver + a per-session `SnapshotStore`.
12
+ */
13
+ export * from "./apply.js";
14
+ export * from "./block.js";
15
+ export * from "./format.js";
16
+ export * from "./fs.js";
17
+ export * from "./input.js";
18
+ export * from "./messages.js";
19
+ export * from "./mismatch.js";
20
+ export * from "./normalize.js";
21
+ export * from "./parser.js";
22
+ export * from "./patcher.js";
23
+ export * from "./recovery.js";
24
+ export * from "./snapshots.js";
25
+ export * from "./tokenizer.js";
26
+ export * from "./types.js";
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hashline/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,101 @@
1
+ import type { ApplyResult, BlockResolver, Edit, SplitOptions } from "./types.js";
2
+ interface RawSection {
3
+ path: string;
4
+ fileHash?: string;
5
+ diff: string;
6
+ }
7
+ /**
8
+ * Returns true when the input contains at least one line that the tokenizer
9
+ * recognizes as a hashline op. Used by streaming previews to decide whether
10
+ * the partial input is worth treating as a hashline patch yet.
11
+ */
12
+ export declare function containsRecognizableHashlineOperations(input: string): boolean;
13
+ /**
14
+ * Snapshot of one section in a parsed {@link Patch}: a target file plus the
15
+ * lazily-parsed list of edits that should land on it. Constructed by
16
+ * {@link Patch.parse}; consumers usually iterate `patch.sections` rather
17
+ * than build these directly.
18
+ */
19
+ export declare class PatchSection {
20
+ #private;
21
+ readonly path: string;
22
+ readonly fileHash: string | undefined;
23
+ readonly diff: string;
24
+ constructor(raw: RawSection);
25
+ /**
26
+ * Parse this section's diff body. Cached: subsequent calls return the
27
+ * same `{ edits, warnings }` object so callers can safely call this from
28
+ * multiple paths (preflight, apply, diff-preview).
29
+ */
30
+ parse(): {
31
+ edits: Edit[];
32
+ warnings: readonly string[];
33
+ };
34
+ /** Parsed edits for this section. */
35
+ get edits(): readonly Edit[];
36
+ /** Warnings emitted during parsing of this section. */
37
+ get warnings(): readonly string[];
38
+ /**
39
+ * True when at least one edit anchors to concrete file content. Pure
40
+ * `insert head:` / `insert tail:` literal inserts do not count: those are
41
+ * safe to apply to files that don't yet exist.
42
+ */
43
+ get hasAnchorScopedEdit(): boolean;
44
+ /** Anchor lines touched by this section, sorted ascending and deduplicated. */
45
+ collectAnchorLines(): readonly number[];
46
+ /**
47
+ * Apply this section's edits to `text` and return the post-edit result.
48
+ * Pure: does no I/O, does not validate the section snapshot tag. The
49
+ * {@link Patcher} owns tag validation and recovery; reach for this
50
+ * method directly when you've already validated the file content and
51
+ * just want the result.
52
+ *
53
+ * `blockResolver` resolves any `replace block N:` edits against `text`; an
54
+ * unresolvable block throws (this is the final, authoritative preview path).
55
+ */
56
+ applyTo(text: string, blockResolver?: BlockResolver): ApplyResult;
57
+ /**
58
+ * Streaming-tolerant counterpart to {@link applyTo}. Uses
59
+ * {@link parsePatchStreaming} so a trailing in-flight op (no payload yet,
60
+ * or a per-token parse error mid-stream) does not throw or emit a phantom
61
+ * empty-payload edit. Intended for incremental diff previews; the writer
62
+ * path should always use {@link applyTo}.
63
+ *
64
+ * `blockResolver` resolves any `replace block N:` edits against `text`; an
65
+ * unresolvable block is silently dropped so a half-written file does not
66
+ * throw mid-stream.
67
+ */
68
+ applyPartialTo(text: string, blockResolver?: BlockResolver): ApplyResult;
69
+ }
70
+ /**
71
+ * A parsed hashline patch — zero or more {@link PatchSection}s, each rooted
72
+ * at a `¶PATH#HASH` header. Construct via {@link Patch.parse}.
73
+ *
74
+ * `Patch` is pure data: parsing is line-anchored and does not look at the
75
+ * filesystem. To apply a patch, hand it to {@link Patcher.apply}.
76
+ */
77
+ export declare class Patch {
78
+ readonly sections: readonly PatchSection[];
79
+ private constructor();
80
+ /**
81
+ * Parse `input` into a {@link Patch}. `options.cwd` resolves absolute
82
+ * paths inside headers to cwd-relative form; `options.path` provides a
83
+ * fallback when the input lacks a header but contains hashline ops
84
+ * (useful for streaming previews).
85
+ *
86
+ * Consecutive sections targeting the same path are merged into a single
87
+ * section with concatenated diff bodies. Anchors authored against the
88
+ * same file snapshot must be applied as one batch; otherwise the first
89
+ * sub-edit shifts line numbers out from under the second's anchors and
90
+ * validation fails.
91
+ */
92
+ static parse(input: string, options?: SplitOptions): Patch;
93
+ /**
94
+ * Parse `input` and return only the first section. Throws if the input
95
+ * has zero sections. Convenience for the single-section case where the
96
+ * caller already knows the patch is one hunk.
97
+ */
98
+ static parseSingle(input: string, options?: SplitOptions): PatchSection;
99
+ }
100
+ export {};
101
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/hashline/input.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAgEjF,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AA+CD;;;;GAIG;AACH,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK7E;AAyED;;;;;GAKG;AACH,qBAAa,YAAY;;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAGV,GAAG,EAAE,UAAU;IAM3B;;;;OAIG;IACH,KAAK,IAAI;QAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE;IAKvD,qCAAqC;IACrC,IAAI,KAAK,IAAI,SAAS,IAAI,EAAE,CAE3B;IAED,uDAAuD;IACvD,IAAI,QAAQ,IAAI,SAAS,MAAM,EAAE,CAEhC;IAED;;;;OAIG;IACH,IAAI,mBAAmB,IAAI,OAAO,CAOjC;IAED,+EAA+E;IAC/E,kBAAkB,IAAI,SAAS,MAAM,EAAE;IAkBvC;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,WAAW;IAYjE;;;;;;;;;;OAUG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,WAAW;CASzE;AAED;;;;;;GAMG;AACH,qBAAa,KAAK;IAChB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAE3C,OAAO;IAIP;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,KAAK;IAK9D;;;;OAIG;IACH,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,YAAY;CAM5E"}
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Top-level patch parser. Splits an authored hashline input into a list of
3
+ * {@link PatchSection}s, each rooted at a `¶PATH#HASH` header, then exposes
4
+ * a {@link Patch} class that gives lazy access to the parsed edits per
5
+ * section. Ported from oh-my-pi `packages/hashline/src/input.ts`.
6
+ *
7
+ * The splitter is purely lexical — it doesn't know whether a section's path
8
+ * actually exists. That's the patcher's job.
9
+ */
10
+ import * as path from "node:path";
11
+ import { applyEdits } from "./apply.js";
12
+ import { resolveBlockEdits } from "./block.js";
13
+ import { HL_FILE_HASH_LENGTH, HL_FILE_HASH_SEP, HL_FILE_PREFIX } from "./format.js";
14
+ import { parsePatch, parsePatchStreaming } from "./parser.js";
15
+ import { Tokenizer } from "./tokenizer.js";
16
+ /** UTF-8 BOM (U+FEFF). Built from a code point so the source stays ASCII. */
17
+ const BOM = String.fromCharCode(0xfeff);
18
+ // Pure classification — single shared tokenizer is safe.
19
+ const TOKENIZER = new Tokenizer();
20
+ function unquoteHashlinePath(pathText) {
21
+ if (pathText.length < 2)
22
+ return pathText;
23
+ const first = pathText[0];
24
+ const last = pathText[pathText.length - 1];
25
+ if ((first === '"' || first === "'") && first === last)
26
+ return pathText.slice(1, -1);
27
+ return pathText;
28
+ }
29
+ /**
30
+ * Strip apply_patch-style noise that models reflexively prepend to the
31
+ * path. Examples observed in benchmark traces:
32
+ *
33
+ * `Update File:foo.ts`, `Update:foo.ts`, `UpdateFile:foo.ts`,
34
+ * `Update/File:foo.ts`, `Update-file:foo.ts`, `Update(File):foo.ts`,
35
+ * `Update<File:foo.ts`, `Add File:foo.ts`, `Delete File:foo.ts`,
36
+ * `Move to:foo.ts`, `***foo.ts`, `***Update File:foo.ts`.
37
+ *
38
+ * We strip a leading `***` (the model duplicating the header sigil) and a
39
+ * leading `(Update|Add|Delete|Move)[<separator>]*(File|to)?[<separator>]*:`
40
+ * keyword block, case-insensitive. The remaining text is the real path.
41
+ */
42
+ const APPLY_PATCH_PATH_NOISE_RE = /^\*{0,3}\s*(?:(?:update|add|delete|move)[^A-Za-z0-9]*(?:file|to)?[^A-Za-z0-9]*:)?\s*\*{0,3}\s*/i;
43
+ function stripApplyPatchPathNoise(pathText) {
44
+ return pathText.replace(APPLY_PATCH_PATH_NOISE_RE, "");
45
+ }
46
+ /**
47
+ * Best-effort recovery for `¶`-prefixed lines the strict tokenizer
48
+ * rejects. Strips apply_patch keyword noise (`Update File:`, `Update:`,
49
+ * etc.) and an extra leading `***` (some models emit a hybrid `¶***foo.ts`
50
+ * shape), then expects `PATH(#HASH)?` with no embedded whitespace.
51
+ * Returns `null` when no clean path can be salvaged.
52
+ */
53
+ function tryParseRecoveryHeader(line, cwd) {
54
+ if (!line.startsWith(HL_FILE_PREFIX))
55
+ return null;
56
+ const body = stripApplyPatchPathNoise(line.slice(HL_FILE_PREFIX.length).trim());
57
+ if (body.length === 0)
58
+ return null;
59
+ const match = new RegExp(`^(\\S+?)(?:#([0-9A-Fa-f]{${HL_FILE_HASH_LENGTH}}))?\\s*$`).exec(body);
60
+ if (match === null)
61
+ return null;
62
+ const parsedPath = normalizeHashlinePath(match[1] ?? "", cwd);
63
+ if (parsedPath.length === 0)
64
+ return null;
65
+ return match[2] !== undefined
66
+ ? { path: parsedPath, fileHash: match[2].toUpperCase(), diff: "" }
67
+ : { path: parsedPath, diff: "" };
68
+ }
69
+ function normalizeHashlinePath(rawPath, cwd) {
70
+ const unquoted = stripApplyPatchPathNoise(unquoteHashlinePath(rawPath.trim()));
71
+ if (!cwd || !path.isAbsolute(unquoted))
72
+ return unquoted;
73
+ const relative = path.relative(path.resolve(cwd), path.resolve(unquoted));
74
+ const isWithinCwd = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
75
+ return isWithinCwd ? relative || "." : unquoted;
76
+ }
77
+ /**
78
+ * Parse a `¶PATH[#hash]` header line. Returns `null` for lines that do
79
+ * not start with `¶`. Throws the strict "Input header must be …" error
80
+ * when a `¶`-prefixed line fails the strict shape (so malformed paths
81
+ * surface immediately instead of being silently re-classified as payload).
82
+ */
83
+ function parseHashlineHeaderLine(line, cwd) {
84
+ const trimmed = line.trimEnd();
85
+ if (!trimmed.startsWith(HL_FILE_PREFIX))
86
+ return null;
87
+ const token = TOKENIZER.tokenize(trimmed);
88
+ if (token.kind !== "header") {
89
+ // Recovery: try to extract a path from the raw line after stripping
90
+ // apply_patch noise. This handles `*** Update File:foo.ts#CB5` and
91
+ // the half-dozen variants models actually emit.
92
+ const recovered = tryParseRecoveryHeader(trimmed, cwd);
93
+ if (recovered !== null)
94
+ return recovered;
95
+ throw new Error(`Input header must be ${HL_FILE_PREFIX}PATH or ${HL_FILE_PREFIX}PATH${HL_FILE_HASH_SEP}TAG with a ${HL_FILE_HASH_LENGTH}-hex content-hash tag; got ${JSON.stringify(trimmed)}.`);
96
+ }
97
+ const parsedPath = normalizeHashlinePath(token.path, cwd);
98
+ if (parsedPath.length === 0) {
99
+ throw new Error(`Input header "${HL_FILE_PREFIX}" is empty; provide a file path.`);
100
+ }
101
+ return token.fileHash !== undefined
102
+ ? { path: parsedPath, fileHash: token.fileHash, diff: "" }
103
+ : { path: parsedPath, diff: "" };
104
+ }
105
+ function stripLeadingBlankLines(input) {
106
+ const stripped = input.startsWith(BOM) ? input.slice(1) : input;
107
+ const lines = stripped.split("\n");
108
+ while (lines.length > 0) {
109
+ const head = lines[0].replace(/\r$/, "");
110
+ if (head.trim().length === 0 || TOKENIZER.tokenize(head).kind === "envelope-begin") {
111
+ lines.shift();
112
+ continue;
113
+ }
114
+ break;
115
+ }
116
+ return lines.join("\n");
117
+ }
118
+ /**
119
+ * Returns true when the input contains at least one line that the tokenizer
120
+ * recognizes as a hashline op. Used by streaming previews to decide whether
121
+ * the partial input is worth treating as a hashline patch yet.
122
+ */
123
+ export function containsRecognizableHashlineOperations(input) {
124
+ for (const line of input.split(/\r?\n/)) {
125
+ if (TOKENIZER.isOp(line))
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+ function normalizeFallbackInput(input, options) {
131
+ const stripped = input.startsWith(BOM) ? input.slice(1) : input;
132
+ const hasExplicitHeader = stripped
133
+ .split(/\r?\n/)
134
+ .some((rawLine) => parseHashlineHeaderLine(rawLine, options.cwd) !== null);
135
+ if (hasExplicitHeader)
136
+ return input;
137
+ if (!options.path || !containsRecognizableHashlineOperations(input))
138
+ return input;
139
+ const fallbackPath = normalizeHashlinePath(options.path, options.cwd);
140
+ if (fallbackPath.length === 0)
141
+ return input;
142
+ return `${HL_FILE_PREFIX}${fallbackPath}\n${input}`;
143
+ }
144
+ function splitRawSections(input, options = {}) {
145
+ const stripped = stripLeadingBlankLines(normalizeFallbackInput(input, options));
146
+ const lines = stripped.split(/\r?\n/);
147
+ const firstLine = lines[0] ?? "";
148
+ if (parseHashlineHeaderLine(firstLine, options.cwd) === null) {
149
+ // Catch unified-diff hunk-header contamination on the first line so
150
+ // the model sees a focused error.
151
+ const firstTrimmed = firstLine.trimEnd();
152
+ if (/^@@\s+[-+]?\d+,\d+\s+[-+]?\d+,\d+\s+@@/.test(firstTrimmed)) {
153
+ throw new Error("unified-diff hunk header (`@@ -N,M +N,M @@`) is not valid in hashline. " +
154
+ "File sections start with `¶path#HASH`; use `replace`, `delete`, or `insert` ops.");
155
+ }
156
+ const preview = JSON.stringify(firstLine.slice(0, 120));
157
+ throw new Error(`input must begin with "${HL_FILE_PREFIX}PATH${HL_FILE_HASH_SEP}HASH" on the first non-blank line for anchored edits; got: ${preview}. ` +
158
+ `Example: "${HL_FILE_PREFIX}src/foo.ts${HL_FILE_HASH_SEP}0A3" then edit ops.`);
159
+ }
160
+ const sections = [];
161
+ let current;
162
+ let currentLines = [];
163
+ const flush = () => {
164
+ if (!current)
165
+ return;
166
+ const hasOps = currentLines.some((line) => line.trim().length > 0);
167
+ if (hasOps)
168
+ sections.push({ ...current, diff: currentLines.join("\n") });
169
+ currentLines = [];
170
+ };
171
+ for (const line of lines) {
172
+ const trimmed = line.trimEnd();
173
+ const token = TOKENIZER.tokenize(line);
174
+ if (token.kind === "envelope-end" || token.kind === "abort")
175
+ break;
176
+ if (token.kind === "envelope-begin")
177
+ continue;
178
+ // Route every `¶`-prefixed line through parseHashlineHeaderLine so
179
+ // malformed headers still raise the strict "Input header must be …"
180
+ // diagnostic (the tokenizer alone would silently classify them as
181
+ // payload).
182
+ if (trimmed.startsWith(HL_FILE_PREFIX)) {
183
+ const header = parseHashlineHeaderLine(line, options.cwd);
184
+ if (header !== null) {
185
+ flush();
186
+ current = header;
187
+ currentLines = [];
188
+ continue;
189
+ }
190
+ }
191
+ currentLines.push(line);
192
+ }
193
+ flush();
194
+ return sections;
195
+ }
196
+ /**
197
+ * Snapshot of one section in a parsed {@link Patch}: a target file plus the
198
+ * lazily-parsed list of edits that should land on it. Constructed by
199
+ * {@link Patch.parse}; consumers usually iterate `patch.sections` rather
200
+ * than build these directly.
201
+ */
202
+ export class PatchSection {
203
+ path;
204
+ fileHash;
205
+ diff;
206
+ #parsed;
207
+ constructor(raw) {
208
+ this.path = raw.path;
209
+ this.fileHash = raw.fileHash;
210
+ this.diff = raw.diff;
211
+ }
212
+ /**
213
+ * Parse this section's diff body. Cached: subsequent calls return the
214
+ * same `{ edits, warnings }` object so callers can safely call this from
215
+ * multiple paths (preflight, apply, diff-preview).
216
+ */
217
+ parse() {
218
+ this.#parsed ??= parsePatch(this.diff);
219
+ return this.#parsed;
220
+ }
221
+ /** Parsed edits for this section. */
222
+ get edits() {
223
+ return this.parse().edits;
224
+ }
225
+ /** Warnings emitted during parsing of this section. */
226
+ get warnings() {
227
+ return this.parse().warnings;
228
+ }
229
+ /**
230
+ * True when at least one edit anchors to concrete file content. Pure
231
+ * `insert head:` / `insert tail:` literal inserts do not count: those are
232
+ * safe to apply to files that don't yet exist.
233
+ */
234
+ get hasAnchorScopedEdit() {
235
+ return this.edits.some((edit) => {
236
+ if (edit.kind === "delete")
237
+ return true;
238
+ // A `replace block N:` edit is anchored to concrete content on line N.
239
+ if (edit.kind === "block")
240
+ return true;
241
+ return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
242
+ });
243
+ }
244
+ /** Anchor lines touched by this section, sorted ascending and deduplicated. */
245
+ collectAnchorLines() {
246
+ const lines = new Set();
247
+ for (const edit of this.edits) {
248
+ if (edit.kind === "delete") {
249
+ lines.add(edit.anchor.line);
250
+ continue;
251
+ }
252
+ if (edit.kind === "block") {
253
+ lines.add(edit.anchor.line);
254
+ continue;
255
+ }
256
+ if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor") {
257
+ lines.add(edit.cursor.anchor.line);
258
+ }
259
+ }
260
+ return [...lines].sort((a, b) => a - b);
261
+ }
262
+ /**
263
+ * Apply this section's edits to `text` and return the post-edit result.
264
+ * Pure: does no I/O, does not validate the section snapshot tag. The
265
+ * {@link Patcher} owns tag validation and recovery; reach for this
266
+ * method directly when you've already validated the file content and
267
+ * just want the result.
268
+ *
269
+ * `blockResolver` resolves any `replace block N:` edits against `text`; an
270
+ * unresolvable block throws (this is the final, authoritative preview path).
271
+ */
272
+ applyTo(text, blockResolver) {
273
+ const { edits, warnings } = this.parse();
274
+ const resolved = resolveBlockEdits(edits, text, this.path, blockResolver, { onUnresolved: "throw" });
275
+ const result = applyEdits(text, resolved);
276
+ // Preserve parse warnings so consumers don't need to call `parse()`
277
+ // separately.
278
+ const merged = warnings.length === 0 ? result.warnings : [...warnings, ...(result.warnings ?? [])];
279
+ return merged && merged.length > 0
280
+ ? { ...result, warnings: merged }
281
+ : { text: result.text, firstChangedLine: result.firstChangedLine };
282
+ }
283
+ /**
284
+ * Streaming-tolerant counterpart to {@link applyTo}. Uses
285
+ * {@link parsePatchStreaming} so a trailing in-flight op (no payload yet,
286
+ * or a per-token parse error mid-stream) does not throw or emit a phantom
287
+ * empty-payload edit. Intended for incremental diff previews; the writer
288
+ * path should always use {@link applyTo}.
289
+ *
290
+ * `blockResolver` resolves any `replace block N:` edits against `text`; an
291
+ * unresolvable block is silently dropped so a half-written file does not
292
+ * throw mid-stream.
293
+ */
294
+ applyPartialTo(text, blockResolver) {
295
+ const { edits, warnings } = parsePatchStreaming(this.diff);
296
+ const resolved = resolveBlockEdits(edits, text, this.path, blockResolver, { onUnresolved: "drop" });
297
+ const result = applyEdits(text, resolved);
298
+ const merged = warnings.length === 0 ? result.warnings : [...warnings, ...(result.warnings ?? [])];
299
+ return merged && merged.length > 0
300
+ ? { ...result, warnings: merged }
301
+ : { text: result.text, firstChangedLine: result.firstChangedLine };
302
+ }
303
+ }
304
+ /**
305
+ * A parsed hashline patch — zero or more {@link PatchSection}s, each rooted
306
+ * at a `¶PATH#HASH` header. Construct via {@link Patch.parse}.
307
+ *
308
+ * `Patch` is pure data: parsing is line-anchored and does not look at the
309
+ * filesystem. To apply a patch, hand it to {@link Patcher.apply}.
310
+ */
311
+ export class Patch {
312
+ sections;
313
+ constructor(sections) {
314
+ this.sections = sections;
315
+ }
316
+ /**
317
+ * Parse `input` into a {@link Patch}. `options.cwd` resolves absolute
318
+ * paths inside headers to cwd-relative form; `options.path` provides a
319
+ * fallback when the input lacks a header but contains hashline ops
320
+ * (useful for streaming previews).
321
+ *
322
+ * Consecutive sections targeting the same path are merged into a single
323
+ * section with concatenated diff bodies. Anchors authored against the
324
+ * same file snapshot must be applied as one batch; otherwise the first
325
+ * sub-edit shifts line numbers out from under the second's anchors and
326
+ * validation fails.
327
+ */
328
+ static parse(input, options = {}) {
329
+ const raw = mergeSamePathSections(splitRawSections(input, options));
330
+ return new Patch(raw.map((section) => new PatchSection(section)));
331
+ }
332
+ /**
333
+ * Parse `input` and return only the first section. Throws if the input
334
+ * has zero sections. Convenience for the single-section case where the
335
+ * caller already knows the patch is one hunk.
336
+ */
337
+ static parseSingle(input, options = {}) {
338
+ const patch = Patch.parse(input, options);
339
+ const first = patch.sections[0];
340
+ if (!first)
341
+ throw new Error("Patch input did not produce any sections.");
342
+ return first;
343
+ }
344
+ }
345
+ /**
346
+ * Collapse consecutive or interleaved sections targeting the same path into a
347
+ * single section with concatenated diffs. Anchors authored against the same
348
+ * file snapshot must be applied as one batch; otherwise the first sub-edit
349
+ * shifts line numbers out from under the second's anchors and validation
350
+ * fails. Path order is preserved by first occurrence.
351
+ */
352
+ function mergeSamePathSections(sections) {
353
+ const byPath = new Map();
354
+ for (const section of sections) {
355
+ const existing = byPath.get(section.path);
356
+ if (existing) {
357
+ if (existing.fileHash !== undefined &&
358
+ section.fileHash !== undefined &&
359
+ existing.fileHash !== section.fileHash) {
360
+ throw new Error(`Conflicting hashline snapshot tags for ${section.path}: #${existing.fileHash} and #${section.fileHash}. Re-read the file and retry with one current header.`);
361
+ }
362
+ if (existing.fileHash === undefined && section.fileHash !== undefined)
363
+ existing.fileHash = section.fileHash;
364
+ existing.diffs.push(section.diff);
365
+ continue;
366
+ }
367
+ byPath.set(section.path, {
368
+ ...(section.fileHash !== undefined ? { fileHash: section.fileHash } : {}),
369
+ diffs: [section.diff],
370
+ });
371
+ }
372
+ return Array.from(byPath, ([sectionPath, entry]) => ({
373
+ path: sectionPath,
374
+ ...(entry.fileHash !== undefined ? { fileHash: entry.fileHash } : {}),
375
+ diff: entry.diffs.join("\n"),
376
+ }));
377
+ }
378
+ //# sourceMappingURL=input.js.map