@osovv/vv-opencode 1.5.0 → 1.7.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 (94) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +41 -10
  3. package/dist/lib/agent-tool-catalog.d.ts +144 -0
  4. package/dist/lib/agent-tool-catalog.js +2551 -0
  5. package/dist/lib/agent-tool-catalog.js.map +1 -0
  6. package/dist/lib/agent-tool-contract.d.ts +200 -0
  7. package/dist/lib/agent-tool-contract.js +407 -0
  8. package/dist/lib/agent-tool-contract.js.map +1 -0
  9. package/dist/lib/workflow-contract.d.ts +12 -0
  10. package/dist/lib/workflow-contract.js +71 -18
  11. package/dist/lib/workflow-contract.js.map +1 -1
  12. package/dist/plugins/hashline-edit/index.js +102 -97
  13. package/dist/plugins/hashline-edit/index.js.map +1 -1
  14. package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
  15. package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
  16. package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
  17. package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
  18. package/dist/plugins/hashline-edit/schemas.js +659 -0
  19. package/dist/plugins/hashline-edit/schemas.js.map +1 -0
  20. package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
  21. package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
  22. package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
  23. package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
  24. package/dist/plugins/hashline-edit/tool-description.js +20 -4
  25. package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
  26. package/dist/plugins/system-context-injection/index.js +9 -4
  27. package/dist/plugins/system-context-injection/index.js.map +1 -1
  28. package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
  29. package/dist/plugins/web-tools/fetch-service.js +30 -45
  30. package/dist/plugins/web-tools/fetch-service.js.map +1 -1
  31. package/dist/plugins/web-tools/index.js +23 -5
  32. package/dist/plugins/web-tools/index.js.map +1 -1
  33. package/dist/plugins/web-tools/schemas.d.ts +469 -0
  34. package/dist/plugins/web-tools/schemas.js +523 -0
  35. package/dist/plugins/web-tools/schemas.js.map +1 -0
  36. package/dist/plugins/web-tools/search-service.d.ts +6 -3
  37. package/dist/plugins/web-tools/search-service.js +24 -29
  38. package/dist/plugins/web-tools/search-service.js.map +1 -1
  39. package/dist/plugins/workflow/authority.d.ts +6 -0
  40. package/dist/plugins/workflow/authority.js +45 -10
  41. package/dist/plugins/workflow/authority.js.map +1 -1
  42. package/dist/plugins/workflow/authorization.js +16 -8
  43. package/dist/plugins/workflow/authorization.js.map +1 -1
  44. package/dist/plugins/workflow/checkpoints.d.ts +41 -2
  45. package/dist/plugins/workflow/checkpoints.js +176 -47
  46. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  47. package/dist/plugins/workflow/delegated.d.ts +15 -1
  48. package/dist/plugins/workflow/delegated.js +65 -35
  49. package/dist/plugins/workflow/delegated.js.map +1 -1
  50. package/dist/plugins/workflow/execution.d.ts +15 -30
  51. package/dist/plugins/workflow/execution.js +133 -33
  52. package/dist/plugins/workflow/execution.js.map +1 -1
  53. package/dist/plugins/workflow/index.js +177 -112
  54. package/dist/plugins/workflow/index.js.map +1 -1
  55. package/dist/plugins/workflow/input-validation.d.ts +48 -0
  56. package/dist/plugins/workflow/input-validation.js +536 -0
  57. package/dist/plugins/workflow/input-validation.js.map +1 -0
  58. package/dist/plugins/workflow/inspection.d.ts +60 -0
  59. package/dist/plugins/workflow/inspection.js +316 -0
  60. package/dist/plugins/workflow/inspection.js.map +1 -0
  61. package/dist/plugins/workflow/protocol.d.ts +2 -0
  62. package/dist/plugins/workflow/protocol.js +26 -4
  63. package/dist/plugins/workflow/protocol.js.map +1 -1
  64. package/dist/plugins/workflow/recovery.d.ts +2 -1
  65. package/dist/plugins/workflow/recovery.js +79 -12
  66. package/dist/plugins/workflow/recovery.js.map +1 -1
  67. package/dist/plugins/workflow/repair.d.ts +1 -1
  68. package/dist/plugins/workflow/repair.js +9 -10
  69. package/dist/plugins/workflow/repair.js.map +1 -1
  70. package/dist/plugins/workflow/results.d.ts +2566 -0
  71. package/dist/plugins/workflow/results.js +1111 -0
  72. package/dist/plugins/workflow/results.js.map +1 -0
  73. package/dist/plugins/workflow/schemas.d.ts +214 -24
  74. package/dist/plugins/workflow/schemas.js +350 -89
  75. package/dist/plugins/workflow/schemas.js.map +1 -1
  76. package/dist/plugins/workflow/state.d.ts +3 -2
  77. package/dist/plugins/workflow/state.js +15 -8
  78. package/dist/plugins/workflow/state.js.map +1 -1
  79. package/dist/plugins/workflow/system-instruction.md +17 -1
  80. package/dist/plugins/workflow/tooling.d.ts +2 -18
  81. package/dist/plugins/workflow/tooling.js +477 -460
  82. package/dist/plugins/workflow/tooling.js.map +1 -1
  83. package/dist/plugins/workflow/transactions.d.ts +13 -0
  84. package/dist/plugins/workflow/transactions.js +13 -3
  85. package/dist/plugins/workflow/transactions.js.map +1 -1
  86. package/package.json +6 -3
  87. package/schemas/vvoc/v3.json +1 -1
  88. package/templates/agents/vv-code-reviewer.md +13 -4
  89. package/templates/agents/vv-controller.md +17 -0
  90. package/templates/agents/vv-implementer.md +20 -9
  91. package/templates/agents/vv-spec-reviewer.md +13 -4
  92. package/templates/skills/vv-execute/SKILL.md +3 -3
  93. package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
  94. package/templates/skills/vv-review/SKILL.md +1 -1
@@ -0,0 +1,659 @@
1
+ // FILE: src/plugins/hashline-edit/schemas.ts
2
+ // VERSION: 1.0.0
3
+ // START_MODULE_CONTRACT
4
+ // PURPOSE: Single source of the two vvoc-owned edit tool contracts: registered raw argument maps, closed nested edit/command structures, strict runtime schemas with args-only branch validation, model-facing descriptions, operation examples, and concrete metadata/result producer schemas.
5
+ // SCOPE: Pure contract declarations and args-only validation for hashline_edit and str_replace_editor: structural parsing (unknown keys, enums, types), operation/command allowed/required field matrices, delete/rename/edits branch rules, anchor-conflict detection, and view_range exact-shape rules. No filesystem, session cache, editor execution, plugin registration, or anchor-versus-current-file validation; the normalizer and editor keep literal-content, three-part-anchor, and current-file authority.
6
+ // DEPENDS: [@opencode-ai/plugin, zod (types), src/lib/agent-tool-contract.ts, src/plugins/hashline-edit/routing.ts, src/plugins/hashline-edit/tool-description.ts]
7
+ // LINKS: [M-PLUGIN-HASHLINE-EDIT, M-AGENT-TOOL-CONTRACT]
8
+ // ROLE: RUNTIME
9
+ // MAP_MODE: EXPORTS
10
+ // END_MODULE_CONTRACT
11
+ //
12
+ // START_MODULE_MAP
13
+ // HASHLINE_EDIT_TOOL_ID - Registered tool id for the hash-anchored editor.
14
+ // STR_REPLACE_EDITOR_TOOL_ID - Registered tool id for the dsh str_replace_editor.
15
+ // HASHLINE_EDIT_OPS - Canonical hash-anchored operation vocabulary.
16
+ // HashlineToolOp - Union of hash-anchored operation identifiers.
17
+ // RawHashlineEdit - Loose tool-facing edit input accepted by the normalizer.
18
+ // STR_REPLACE_EDITOR_COMMANDS - Canonical str_replace_editor command vocabulary.
19
+ // StrReplaceEditorCommand - Union of str_replace_editor command identifiers.
20
+ // HashlineEditToolArgs - Schema-derived hashline_edit argument shape.
21
+ // RawHashlineEditEntry - Schema-derived shape of one raw hash-anchored edit entry.
22
+ // StrReplaceEditorToolArgs - Schema-derived str_replace_editor argument shape.
23
+ // StrReplaceEditorResult - Str editor ok/error result shape (producer/test contract).
24
+ // hashlineEditEntrySchema - Closed single-edit shape shared with the normalizer.
25
+ // hashlineEditArgs - Registered raw argument map for hashline_edit.
26
+ // strReplaceEditorArgs - Registered raw argument map for str_replace_editor.
27
+ // hashlineEditContract - Owned contract for hashline_edit.
28
+ // strReplaceEditorContract - Owned contract for str_replace_editor.
29
+ // editToolContracts - Owned contracts for both edit tools (definition adapter input).
30
+ // HashlineEditValidation - Successful parsed hashline args or bounded issues.
31
+ // StrReplaceEditorValidation - Successful parsed str editor args or bounded issues.
32
+ // validateRawHashlineEditEntry - Closed single-edit shape validation reused by the direct normalizer.
33
+ // validateHashlineEditToolInput - Structural plus branch validation for hashline_edit.
34
+ // validateStrReplaceEditorToolInput - Structural plus command validation for str_replace_editor.
35
+ // hashlineEditFilediffSchema - Concrete filediff metadata envelope schema.
36
+ // hashlineEditMetadataSchema - Concrete hashline success metadata schema (no blanket records).
37
+ // strReplaceEditorMetadataSchema - Concrete str editor success metadata schema.
38
+ // strReplaceEditorResultSchema - Closed ok/error result schema for producer checks.
39
+ // END_MODULE_MAP
40
+ //
41
+ // START_CHANGE_SUMMARY
42
+ // LAST_CHANGE: [C-AGENT-TOOL-CONTRACTS T-005 - Initial single-source edit-tool contracts plus a correction cycle: non-empty path checks for filePath/rename/path, representable insert_line min(0) and view_range length(2) bounds, and a shared closed single-edit shape validation for the direct normalizer.]
43
+ // END_CHANGE_SUMMARY
44
+ import { tool } from "@opencode-ai/plugin";
45
+ import { MAX_CONTRACT_ISSUES, MAX_ISSUE_MESSAGE_CHARS, MAX_ISSUE_PATH_CHARS, MAX_ISSUE_VALUE_CHARS, defineOwnedToolContract, formatIssuePath, strictObject, toContractIssues, } from "../../lib/agent-tool-contract.js";
46
+ import { EDIT_MODES } from "./routing.js";
47
+ import { HASHLINE_EDIT_DESCRIPTION, STR_REPLACE_EDITOR_DESCRIPTION } from "./tool-description.js";
48
+ const schema = tool.schema;
49
+ // START_BLOCK_TOOL_IDS
50
+ /** Registered tool id for the hash-anchored editor. */
51
+ export const HASHLINE_EDIT_TOOL_ID = "hashline_edit";
52
+ /** Registered tool id for the dsh str_replace_editor. */
53
+ export const STR_REPLACE_EDITOR_TOOL_ID = "str_replace_editor";
54
+ /** Canonical hash-anchored operation vocabulary. */
55
+ export const HASHLINE_EDIT_OPS = ["replace", "replace_range", "append", "prepend"];
56
+ /** Canonical str_replace_editor command vocabulary. */
57
+ export const STR_REPLACE_EDITOR_COMMANDS = ["view", "create", "str_replace", "insert"];
58
+ // END_BLOCK_TOOL_IDS
59
+ // START_BLOCK_ARG_MAPS
60
+ const hashlineEditEntryShape = {
61
+ op: schema
62
+ .enum(HASHLINE_EDIT_OPS)
63
+ .describe("Operation: replace one line, replace a pos..end range, append, or prepend."),
64
+ pos: schema
65
+ .string()
66
+ .optional()
67
+ .describe("Primary anchor in LINE#HASH#ANCHOR three-part format. Required for replace/replace_range; optional for append/prepend (no anchor inserts at the file boundary)."),
68
+ end: schema
69
+ .string()
70
+ .optional()
71
+ .describe("Optional inclusive range end anchor. With end, replace covers pos..end; required for replace_range. For append/prepend it is only a fallback when pos is omitted."),
72
+ lines: schema
73
+ .union([schema.array(schema.string()), schema.string(), schema.null()])
74
+ .describe('Replacement or inserted lines as plain text content. null or [] deletes for replace/replace_range; [""] is rejected as ambiguous.'),
75
+ };
76
+ /** Closed single-edit shape shared by registration, args validation, and the direct normalizer. */
77
+ export const hashlineEditEntrySchema = strictObject(hashlineEditEntryShape);
78
+ /** Registered raw argument map for hashline_edit (single source of the tool surface). */
79
+ export const hashlineEditArgs = {
80
+ filePath: schema
81
+ .string()
82
+ .describe("Absolute path to the file to edit (non-empty; spaces inside the name are preserved)"),
83
+ delete: schema.boolean().optional().describe("Delete the file instead of editing it"),
84
+ rename: schema
85
+ .string()
86
+ .optional()
87
+ .describe("Rename the file after edits are applied (non-empty when provided)"),
88
+ edits: schema
89
+ .array(hashlineEditEntrySchema)
90
+ .describe("Hash-anchored edit operations to apply to the file"),
91
+ };
92
+ const hashlineEditArgsObject = strictObject(hashlineEditArgs);
93
+ /** Registered raw argument map for str_replace_editor (single source of the tool surface). */
94
+ export const strReplaceEditorArgs = {
95
+ command: schema
96
+ .enum(STR_REPLACE_EDITOR_COMMANDS)
97
+ .describe("The command to run: view, create, str_replace, or insert"),
98
+ path: schema.string().describe("Absolute path to file or directory"),
99
+ file_text: schema
100
+ .string()
101
+ .optional()
102
+ .describe("create only: content of the new file; an explicitly empty string is allowed"),
103
+ old_str: schema
104
+ .string()
105
+ .optional()
106
+ .describe("str_replace only: the exact, non-empty text to replace (whitespace is significant)"),
107
+ new_str: schema
108
+ .string()
109
+ .optional()
110
+ .describe("str_replace replacement text (omitted defaults to empty deletion; explicit empty is valid); required for insert"),
111
+ insert_line: schema
112
+ .number()
113
+ .int()
114
+ .min(0)
115
+ .optional()
116
+ .describe("insert only: integer line index >= 0; new_str is inserted AFTER this line (the file-dependent upper bound is enforced by the editor)"),
117
+ view_range: schema
118
+ .array(schema.number().int())
119
+ .length(2)
120
+ .optional()
121
+ .describe("view only: exact [start, end] line range. The length is fixed at 2; start >= 1 and end is -1 (end of file) or >= start are runtime-checked."),
122
+ };
123
+ const strReplaceEditorArgsObject = strictObject(strReplaceEditorArgs);
124
+ // END_BLOCK_ARG_MAPS
125
+ // START_BLOCK_RESULT_SCHEMAS
126
+ /** Concrete filediff metadata envelope published after a successful hashline edit. */
127
+ export const hashlineEditFilediffSchema = strictObject({
128
+ file: schema.string(),
129
+ path: schema.string(),
130
+ filePath: schema.string(),
131
+ before: schema.string(),
132
+ after: schema.string(),
133
+ });
134
+ /** Concrete success metadata envelope for hashline_edit (known fields, no blanket records). */
135
+ export const hashlineEditMetadataSchema = strictObject({
136
+ filePath: schema.string(),
137
+ path: schema.string(),
138
+ file: schema.string(),
139
+ noopEdits: schema.number().int(),
140
+ deduplicatedEdits: schema.number().int(),
141
+ firstChangedLine: schema.number().int().optional(),
142
+ editMode: schema.enum(EDIT_MODES),
143
+ providerID: schema.string().optional(),
144
+ modelID: schema.string().optional(),
145
+ filediff: hashlineEditFilediffSchema,
146
+ });
147
+ /** Concrete success metadata envelope for str_replace_editor. */
148
+ export const strReplaceEditorMetadataSchema = strictObject({
149
+ filePath: schema.string(),
150
+ path: schema.string(),
151
+ file: schema.string(),
152
+ editMode: schema.enum(EDIT_MODES),
153
+ providerID: schema.string().optional(),
154
+ modelID: schema.string().optional(),
155
+ });
156
+ /**
157
+ * Closed ok/error result schema for str_replace_editor producer/test checks.
158
+ * This is never applied as a throw-after-side-effect execute wrapper.
159
+ */
160
+ export const strReplaceEditorResultSchema = schema.union([
161
+ strictObject({ ok: schema.literal(true), output: schema.string() }),
162
+ strictObject({ ok: schema.literal(false), error: schema.string() }),
163
+ ]);
164
+ // END_BLOCK_RESULT_SCHEMAS
165
+ // START_BLOCK_OPERATION_EXAMPLES
166
+ const hashlineEditExamples = [
167
+ {
168
+ operation: "replace",
169
+ label: "single-line replace at pos",
170
+ expect: "accept",
171
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "replace", pos: "2#VK#ZZ", lines: ["x"] }] },
172
+ },
173
+ {
174
+ operation: "replace",
175
+ label: "replace with end is an inclusive range",
176
+ expect: "accept",
177
+ input: {
178
+ filePath: "/tmp/a.ts",
179
+ edits: [{ op: "replace", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
180
+ },
181
+ },
182
+ {
183
+ operation: "replace_range",
184
+ label: "replace_range alias requires both anchors",
185
+ expect: "accept",
186
+ input: {
187
+ filePath: "/tmp/a.ts",
188
+ edits: [{ op: "replace_range", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
189
+ },
190
+ },
191
+ {
192
+ operation: "replace_range",
193
+ label: "deletion via lines null",
194
+ expect: "accept",
195
+ input: {
196
+ filePath: "/tmp/a.ts",
197
+ edits: [{ op: "replace_range", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: null }],
198
+ },
199
+ },
200
+ {
201
+ operation: "append",
202
+ label: "boundary append without an anchor creates a missing file",
203
+ expect: "accept",
204
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "append", lines: ["x"] }] },
205
+ },
206
+ {
207
+ operation: "prepend",
208
+ label: "end-anchor fallback when pos is omitted",
209
+ expect: "accept",
210
+ input: { filePath: "/tmp/a.ts", edits: [{ op: "prepend", end: "3#MB#ZZ", lines: ["x"] }] },
211
+ },
212
+ {
213
+ operation: "delete",
214
+ label: "delete mode with empty edits",
215
+ expect: "accept",
216
+ input: { filePath: "/tmp/a.ts", delete: true, edits: [] },
217
+ },
218
+ {
219
+ operation: "delete",
220
+ label: "delete and rename conflict",
221
+ expect: "reject",
222
+ input: { filePath: "/tmp/a.ts", delete: true, rename: "/tmp/b.ts", edits: [] },
223
+ },
224
+ {
225
+ operation: "delete",
226
+ label: "delete with non-empty edits",
227
+ expect: "reject",
228
+ input: {
229
+ filePath: "/tmp/a.ts",
230
+ delete: true,
231
+ edits: [{ op: "replace", pos: "2#VK#ZZ", lines: ["x"] }],
232
+ },
233
+ },
234
+ {
235
+ operation: "append",
236
+ label: "conflicting insert anchors",
237
+ expect: "reject",
238
+ input: {
239
+ filePath: "/tmp/a.ts",
240
+ edits: [{ op: "append", pos: "2#VK#ZZ", end: "3#MB#ZZ", lines: ["x"] }],
241
+ },
242
+ },
243
+ {
244
+ operation: "replace_range",
245
+ label: "replace_range missing end",
246
+ expect: "reject",
247
+ input: {
248
+ filePath: "/tmp/a.ts",
249
+ edits: [{ op: "replace_range", pos: "2#VK#ZZ", lines: ["x"] }],
250
+ },
251
+ },
252
+ {
253
+ operation: "replace",
254
+ label: "non-empty edits required when not deleting",
255
+ expect: "reject",
256
+ input: { filePath: "/tmp/a.ts", edits: [] },
257
+ },
258
+ ];
259
+ const strReplaceEditorExamples = [
260
+ {
261
+ operation: "view",
262
+ label: "view with an end-of-file range",
263
+ expect: "accept",
264
+ input: { command: "view", path: "/tmp/a.ts", view_range: [2, -1] },
265
+ },
266
+ {
267
+ operation: "create",
268
+ label: "create with empty file_text",
269
+ expect: "accept",
270
+ input: { command: "create", path: "/tmp/a.ts", file_text: "" },
271
+ },
272
+ {
273
+ operation: "str_replace",
274
+ label: "str_replace with explicit empty new_str (deletion)",
275
+ expect: "accept",
276
+ input: { command: "str_replace", path: "/tmp/a.ts", old_str: "x", new_str: "" },
277
+ },
278
+ {
279
+ operation: "str_replace",
280
+ label: "str_replace with omitted new_str defaults to deletion",
281
+ expect: "accept",
282
+ input: { command: "str_replace", path: "/tmp/a.ts", old_str: "x" },
283
+ },
284
+ {
285
+ operation: "insert",
286
+ label: "insert requires an integer insert_line and new_str",
287
+ expect: "accept",
288
+ input: { command: "insert", path: "/tmp/a.ts", insert_line: 0, new_str: "" },
289
+ },
290
+ {
291
+ operation: "view",
292
+ label: "unsupported field for the selected command",
293
+ expect: "reject",
294
+ input: { command: "view", path: "/tmp/a.ts", old_str: "x" },
295
+ },
296
+ {
297
+ operation: "str_replace",
298
+ label: "old_str must be non-empty",
299
+ expect: "reject",
300
+ input: { command: "str_replace", path: "/tmp/a.ts", old_str: "", new_str: "x" },
301
+ },
302
+ {
303
+ operation: "insert",
304
+ label: "insert requires insert_line",
305
+ expect: "reject",
306
+ input: { command: "insert", path: "/tmp/a.ts", new_str: "x" },
307
+ },
308
+ {
309
+ operation: "view",
310
+ label: "view_range must be exactly two integers",
311
+ expect: "reject",
312
+ input: { command: "view", path: "/tmp/a.ts", view_range: [1] },
313
+ },
314
+ {
315
+ operation: "view",
316
+ label: "view_range end must be -1 or >= start",
317
+ expect: "reject",
318
+ input: { command: "view", path: "/tmp/a.ts", view_range: [3, 2] },
319
+ },
320
+ ];
321
+ // END_BLOCK_OPERATION_EXAMPLES
322
+ // START_BLOCK_CONTRACTS
323
+ /** Owned contract for hashline_edit. */
324
+ export const hashlineEditContract = defineOwnedToolContract({
325
+ toolId: HASHLINE_EDIT_TOOL_ID,
326
+ description: HASHLINE_EDIT_DESCRIPTION,
327
+ registeredArgs: hashlineEditArgs,
328
+ examples: hashlineEditExamples,
329
+ });
330
+ /** Owned contract for str_replace_editor. */
331
+ export const strReplaceEditorContract = defineOwnedToolContract({
332
+ toolId: STR_REPLACE_EDITOR_TOOL_ID,
333
+ description: STR_REPLACE_EDITOR_DESCRIPTION,
334
+ registeredArgs: strReplaceEditorArgs,
335
+ examples: strReplaceEditorExamples,
336
+ });
337
+ /** Owned contracts for both edit tools (input to the owned definition/pre-execute adapters). */
338
+ export const editToolContracts = [
339
+ hashlineEditContract,
340
+ strReplaceEditorContract,
341
+ ];
342
+ // END_BLOCK_CONTRACTS
343
+ // START_BLOCK_ISSUE_HELPERS
344
+ function truncateChars(value, max) {
345
+ if (value.length <= max)
346
+ return value;
347
+ return `${value.slice(0, Math.max(0, max - 1))}…`;
348
+ }
349
+ function boundedIssue(issue) {
350
+ return {
351
+ code: issue.code,
352
+ path: truncateChars(issue.path, MAX_ISSUE_PATH_CHARS),
353
+ message: truncateChars(issue.message, MAX_ISSUE_MESSAGE_CHARS),
354
+ ...(issue.expected !== undefined
355
+ ? { expected: truncateChars(issue.expected, MAX_ISSUE_MESSAGE_CHARS) }
356
+ : {}),
357
+ ...(issue.received !== undefined
358
+ ? { received: truncateChars(issue.received, MAX_ISSUE_VALUE_CHARS) }
359
+ : {}),
360
+ };
361
+ }
362
+ function pushIssue(issues, issue) {
363
+ if (issues.length >= MAX_CONTRACT_ISSUES)
364
+ return;
365
+ issues.push(boundedIssue(issue));
366
+ }
367
+ /** Trim an anchor exactly like the normalizer does before comparing references. */
368
+ function normalizeAnchor(value) {
369
+ if (typeof value !== "string")
370
+ return undefined;
371
+ const trimmed = value.trim();
372
+ return trimmed === "" ? undefined : trimmed;
373
+ }
374
+ function validateHashlineEditBranches(issues, data) {
375
+ if (data.filePath.trim().length === 0) {
376
+ pushIssue(issues, {
377
+ code: "missing_value",
378
+ path: "filePath",
379
+ message: "filePath must be a non-empty path",
380
+ expected: "a non-empty file path (spaces inside the name are preserved)",
381
+ });
382
+ }
383
+ if (data.rename !== undefined && data.rename.trim().length === 0) {
384
+ pushIssue(issues, {
385
+ code: "missing_value",
386
+ path: "rename",
387
+ message: "rename must be a non-empty path when provided",
388
+ expected: "a non-empty destination path, or omit rename",
389
+ });
390
+ }
391
+ if (data.delete === true && data.rename !== undefined) {
392
+ pushIssue(issues, {
393
+ code: "invalid_value",
394
+ path: "rename",
395
+ message: "delete and rename cannot be used together",
396
+ expected: "either delete:true or rename, not both",
397
+ });
398
+ }
399
+ if (data.delete === true && data.edits.length > 0) {
400
+ pushIssue(issues, {
401
+ code: "invalid_value",
402
+ path: "edits",
403
+ message: "delete mode requires edits to be an empty array",
404
+ expected: "[]",
405
+ });
406
+ }
407
+ if (data.delete !== true && data.edits.length === 0) {
408
+ pushIssue(issues, {
409
+ code: "missing_value",
410
+ path: "edits",
411
+ message: "edits parameter must be a non-empty array",
412
+ expected: "at least one edit operation",
413
+ });
414
+ }
415
+ data.edits.forEach((edit, index) => {
416
+ if (edit.pos !== undefined && edit.pos.trim() === "") {
417
+ pushIssue(issues, {
418
+ code: "invalid_value",
419
+ path: formatIssuePath(["edits", index, "pos"]),
420
+ message: "pos was provided but is blank",
421
+ expected: "a full three-part anchor, or omit pos",
422
+ });
423
+ }
424
+ if (edit.end !== undefined && edit.end.trim() === "") {
425
+ pushIssue(issues, {
426
+ code: "invalid_value",
427
+ path: formatIssuePath(["edits", index, "end"]),
428
+ message: "end was provided but is blank",
429
+ expected: "a full three-part anchor, or omit end",
430
+ });
431
+ }
432
+ const pos = normalizeAnchor(edit.pos);
433
+ const end = normalizeAnchor(edit.end);
434
+ if (edit.op === "replace" && pos === undefined) {
435
+ pushIssue(issues, {
436
+ code: "missing_value",
437
+ path: formatIssuePath(["edits", index, "pos"]),
438
+ message: "replace requires a pos anchor",
439
+ expected: "a three-part LINE#HASH#ANCHOR reference",
440
+ });
441
+ return;
442
+ }
443
+ if (edit.op === "replace_range") {
444
+ if (pos === undefined) {
445
+ pushIssue(issues, {
446
+ code: "missing_value",
447
+ path: formatIssuePath(["edits", index, "pos"]),
448
+ message: "replace_range requires both pos and end anchors",
449
+ expected: "a three-part pos anchor",
450
+ });
451
+ }
452
+ if (end === undefined) {
453
+ pushIssue(issues, {
454
+ code: "missing_value",
455
+ path: formatIssuePath(["edits", index, "end"]),
456
+ message: "replace_range requires both pos and end anchors",
457
+ expected: "a three-part end anchor",
458
+ });
459
+ }
460
+ return;
461
+ }
462
+ if ((edit.op === "append" || edit.op === "prepend") &&
463
+ pos !== undefined &&
464
+ end !== undefined &&
465
+ pos !== end) {
466
+ pushIssue(issues, {
467
+ code: "invalid_value",
468
+ path: formatIssuePath(["edits", index, "end"]),
469
+ message: "pos and end anchors reference different lines for an insert operation",
470
+ expected: "one anchor, or identical pos and end",
471
+ });
472
+ }
473
+ });
474
+ }
475
+ /**
476
+ * Closed single-edit shape validation reused by the direct normalizer.
477
+ * Rejects unknown properties, malformed provided optional types, a provided
478
+ * non-canonical operation name, and (through the shared schema) missing
479
+ * op/lines. Blank `pos`/`end` are valid strings here and are rejected by the
480
+ * normalizer's anchor step so blank-versus-absent stays distinguishable.
481
+ */
482
+ export function validateRawHashlineEditEntry(raw, index) {
483
+ const issues = [];
484
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
485
+ const op = raw.op;
486
+ if (typeof op === "string" && !HASHLINE_EDIT_OPS.includes(op)) {
487
+ pushIssue(issues, {
488
+ code: "invalid_value",
489
+ path: formatIssuePath(["edits", index, "op"]),
490
+ message: `unsupported op "${op}". Use replace, replace_range, append, or prepend.`,
491
+ expected: HASHLINE_EDIT_OPS.join(" | "),
492
+ });
493
+ return { ok: false, issues };
494
+ }
495
+ }
496
+ const parsed = hashlineEditEntrySchema.safeParse(raw);
497
+ if (parsed.success) {
498
+ return { ok: true, data: parsed.data };
499
+ }
500
+ const prefix = formatIssuePath(["edits", index]);
501
+ for (const issue of toContractIssues(parsed.error)) {
502
+ pushIssue(issues, {
503
+ ...issue,
504
+ path: issue.path === "(root)" ? prefix : `${prefix}.${issue.path}`,
505
+ });
506
+ }
507
+ return { ok: false, issues };
508
+ }
509
+ /**
510
+ * Strict structural plus args-only branch validation for hashline_edit.
511
+ * Unknown keys, enums, and nested shapes fail first; then non-empty paths,
512
+ * delete/rename/edits combinations, and per-operation anchor requirements are
513
+ * enforced. Current-file anchor validation, literal payload rejection, and
514
+ * creation remain in the normalizer/executor and stay authoritative.
515
+ */
516
+ export function validateHashlineEditToolInput(raw) {
517
+ const parsed = hashlineEditContract.safeParse(raw);
518
+ if (!parsed.success) {
519
+ return { ok: false, issues: parsed.issues };
520
+ }
521
+ const issues = [];
522
+ validateHashlineEditBranches(issues, parsed.data);
523
+ if (issues.length > 0) {
524
+ return { ok: false, issues };
525
+ }
526
+ return { ok: true, data: parsed.data };
527
+ }
528
+ const STR_REPLACE_EDITOR_COMMAND_FIELDS = {
529
+ view: ["command", "path", "view_range"],
530
+ create: ["command", "path", "file_text"],
531
+ str_replace: ["command", "path", "old_str", "new_str"],
532
+ insert: ["command", "path", "new_str", "insert_line"],
533
+ };
534
+ function validateViewRangeShape(issues, viewRange) {
535
+ if (viewRange.length !== 2 || !viewRange.every((value) => Number.isInteger(value))) {
536
+ pushIssue(issues, {
537
+ code: "invalid_value",
538
+ path: "view_range",
539
+ message: "view_range must be a list of exactly two integers",
540
+ expected: "[start, end] with integer start and end",
541
+ });
542
+ return;
543
+ }
544
+ const start = viewRange[0];
545
+ const end = viewRange[1];
546
+ if (start < 1) {
547
+ pushIssue(issues, {
548
+ code: "too_small",
549
+ path: "view_range",
550
+ message: `view_range start ${start} must be >= 1`,
551
+ expected: "start >= 1",
552
+ });
553
+ }
554
+ if (end !== -1 && end < start) {
555
+ pushIssue(issues, {
556
+ code: "invalid_value",
557
+ path: "view_range",
558
+ message: `view_range end ${end} must be -1 or >= start ${start}`,
559
+ expected: "end = -1 or end >= start",
560
+ });
561
+ }
562
+ }
563
+ /**
564
+ * Strict structural plus command-specific validation for str_replace_editor.
565
+ * Unknown keys, enums, and types fail first; then recognized fields that the
566
+ * selected command does not consume are rejected rather than dropped, the
567
+ * command's required fields are enforced, and view_range is required to be an
568
+ * exact two-integer range. Path existence, directory checks, the file-length
569
+ * insert bound, exact-match behavior, and freshness checks remain in the editor.
570
+ */
571
+ export function validateStrReplaceEditorToolInput(raw) {
572
+ const parsed = strReplaceEditorContract.safeParse(raw);
573
+ if (!parsed.success) {
574
+ return { ok: false, issues: parsed.issues };
575
+ }
576
+ const data = parsed.data;
577
+ const issues = [];
578
+ const allowed = new Set(STR_REPLACE_EDITOR_COMMAND_FIELDS[data.command]);
579
+ for (const key of Object.keys(data)) {
580
+ const value = data[key];
581
+ if (value === undefined || allowed.has(key))
582
+ continue;
583
+ pushIssue(issues, {
584
+ code: "invalid_value",
585
+ path: key,
586
+ message: `${key} is not consumed by command ${data.command}`,
587
+ expected: `only ${[...allowed].join(", ")}`,
588
+ });
589
+ }
590
+ if (data.path.trim().length === 0) {
591
+ pushIssue(issues, {
592
+ code: "missing_value",
593
+ path: "path",
594
+ message: "path must be a non-empty path",
595
+ expected: "a non-empty file or directory path (spaces inside the name are preserved)",
596
+ });
597
+ }
598
+ if (data.command === "create" && data.file_text === undefined) {
599
+ pushIssue(issues, {
600
+ code: "missing_value",
601
+ path: "file_text",
602
+ message: "create requires file_text (an explicitly empty string is allowed)",
603
+ expected: 'a string, including ""',
604
+ });
605
+ }
606
+ if (data.command === "str_replace") {
607
+ if (data.old_str === undefined) {
608
+ pushIssue(issues, {
609
+ code: "missing_value",
610
+ path: "old_str",
611
+ message: "str_replace requires old_str",
612
+ expected: "the exact non-empty text to replace",
613
+ });
614
+ }
615
+ else if (data.old_str.length === 0) {
616
+ pushIssue(issues, {
617
+ code: "invalid_value",
618
+ path: "old_str",
619
+ message: "old_str must be a non-empty exact string",
620
+ expected: "a non-empty literal string",
621
+ });
622
+ }
623
+ }
624
+ if (data.command === "insert") {
625
+ if (data.new_str === undefined) {
626
+ pushIssue(issues, {
627
+ code: "missing_value",
628
+ path: "new_str",
629
+ message: "insert requires new_str (an explicitly empty string is allowed)",
630
+ expected: 'a string, including ""',
631
+ });
632
+ }
633
+ if (data.insert_line === undefined) {
634
+ pushIssue(issues, {
635
+ code: "missing_value",
636
+ path: "insert_line",
637
+ message: "insert requires an integer insert_line >= 0",
638
+ expected: "an integer >= 0",
639
+ });
640
+ }
641
+ else if (!Number.isInteger(data.insert_line) || data.insert_line < 0) {
642
+ pushIssue(issues, {
643
+ code: "too_small",
644
+ path: "insert_line",
645
+ message: "insert_line must be an integer >= 0",
646
+ expected: "an integer >= 0",
647
+ });
648
+ }
649
+ }
650
+ if (data.view_range !== undefined) {
651
+ validateViewRangeShape(issues, data.view_range);
652
+ }
653
+ if (issues.length > 0) {
654
+ return { ok: false, issues };
655
+ }
656
+ return { ok: true, data };
657
+ }
658
+ // END_BLOCK_STR_REPLACE_VALIDATION
659
+ //# sourceMappingURL=schemas.js.map