handoff-mcp-server 0.26.0 → 0.27.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 (72) hide show
  1. package/README.md +31 -4
  2. package/bin/handoff-mcp.js +56 -13
  3. package/bin/resolve-binary.js +122 -0
  4. package/package.json +14 -9
  5. package/Cargo.lock +0 -686
  6. package/Cargo.toml +0 -30
  7. package/scripts/cargo-env.sh +0 -29
  8. package/scripts/handoff-memory-hook.py +0 -208
  9. package/scripts/install-local.sh +0 -109
  10. package/scripts/postinstall.js +0 -50
  11. package/scripts/sync-plugin-skills.sh +0 -35
  12. package/scripts/sync-plugin-version.sh +0 -85
  13. package/scripts/sync-workflow-inline.sh +0 -138
  14. package/src/cli.rs +0 -551
  15. package/src/context/injection.rs +0 -276
  16. package/src/context/mod.rs +0 -129
  17. package/src/lib.rs +0 -5
  18. package/src/main.rs +0 -157
  19. package/src/mcp/handlers/assignees.rs +0 -254
  20. package/src/mcp/handlers/auto_schedule.rs +0 -489
  21. package/src/mcp/handlers/bulk_update.rs +0 -155
  22. package/src/mcp/handlers/calendar.rs +0 -196
  23. package/src/mcp/handlers/capacity.rs +0 -318
  24. package/src/mcp/handlers/check_criterion.rs +0 -70
  25. package/src/mcp/handlers/config.rs +0 -402
  26. package/src/mcp/handlers/config_crud.rs +0 -183
  27. package/src/mcp/handlers/dashboard.rs +0 -214
  28. package/src/mcp/handlers/docs.rs +0 -2288
  29. package/src/mcp/handlers/docs_query.rs +0 -1335
  30. package/src/mcp/handlers/fork_session.rs +0 -91
  31. package/src/mcp/handlers/get_session.rs +0 -48
  32. package/src/mcp/handlers/get_task.rs +0 -53
  33. package/src/mcp/handlers/import_context.rs +0 -470
  34. package/src/mcp/handlers/init.rs +0 -28
  35. package/src/mcp/handlers/list_sessions.rs +0 -187
  36. package/src/mcp/handlers/list_tasks.rs +0 -308
  37. package/src/mcp/handlers/load_context.rs +0 -361
  38. package/src/mcp/handlers/log_time.rs +0 -67
  39. package/src/mcp/handlers/memory.rs +0 -961
  40. package/src/mcp/handlers/merge_sessions.rs +0 -103
  41. package/src/mcp/handlers/metrics.rs +0 -196
  42. package/src/mcp/handlers/milestones.rs +0 -102
  43. package/src/mcp/handlers/mod.rs +0 -140
  44. package/src/mcp/handlers/refer.rs +0 -307
  45. package/src/mcp/handlers/referrals.rs +0 -74
  46. package/src/mcp/handlers/save_context.rs +0 -354
  47. package/src/mcp/handlers/task_checklist.rs +0 -507
  48. package/src/mcp/handlers/timer.rs +0 -529
  49. package/src/mcp/handlers/update_session.rs +0 -197
  50. package/src/mcp/handlers/update_task.rs +0 -452
  51. package/src/mcp/mod.rs +0 -6
  52. package/src/mcp/protocol.rs +0 -41
  53. package/src/mcp/resources.rs +0 -57
  54. package/src/mcp/router.rs +0 -154
  55. package/src/mcp/tools.rs +0 -1522
  56. package/src/mcp/types.rs +0 -108
  57. package/src/setup.rs +0 -1212
  58. package/src/storage/config.rs +0 -578
  59. package/src/storage/docs/frontmatter.rs +0 -509
  60. package/src/storage/docs/mod.rs +0 -835
  61. package/src/storage/docs/model.rs +0 -708
  62. package/src/storage/docs/reassemble.rs +0 -167
  63. package/src/storage/docs/split.rs +0 -377
  64. package/src/storage/git.rs +0 -47
  65. package/src/storage/memory/injected.rs +0 -340
  66. package/src/storage/memory/mod.rs +0 -236
  67. package/src/storage/memory/model.rs +0 -127
  68. package/src/storage/mod.rs +0 -96
  69. package/src/storage/referrals.rs +0 -248
  70. package/src/storage/sessions.rs +0 -859
  71. package/src/storage/tasks.rs +0 -957
  72. package/templates/claude-md-section.md +0 -12
@@ -1,507 +0,0 @@
1
- //! `handoff_task_checklist` — pure-view aggregation of a task's
2
- //! `done_criteria` and its linked documents' verification matrices
3
- //! (doc-20260712-191142-602891 §3.1/§3.2, "タスク×ドキュメント連携チェックシート
4
- //! — 改訂仕様 (v2)"). Phase 1 implements `action="view"`; Phase 2 adds
5
- //! `action="generate"` (doc-20260712-191142-602891 §3.2), which turns a
6
- //! linked spec/design document's level-2 section headings into
7
- //! `done_criteria` items (hardcoded defaults, no config template — spec §3.2
8
- //! "ハードコードデフォルト (config 不要)").
9
- //!
10
- //! `view` writes nothing back to disk: it is a computed view over existing
11
- //! `TaskData.task_links` (`link_type == "doc"`) and each linked document's
12
- //! `DocMetadata.verification` matrix. `generate` also reads only
13
- //! `DocMetadata.sections` (never writes to the document); it writes to the
14
- //! task's `done_criteria` only in `append`/`replace` mode (never in the
15
- //! default `preview` mode).
16
-
17
- use anyhow::Result;
18
- use serde_json::{json, Value};
19
-
20
- use super::resolve_project_dir;
21
- use crate::storage::docs::{
22
- batch_resolve_docs, find_doc_by_id, read_doc, DocMetadata, SectionIndex, VerificationItem,
23
- };
24
- use crate::storage::ensure_handoff_exists;
25
- use crate::storage::tasks::{
26
- find_task_dir_by_id, read_modify_write_task, read_task, suggest_task_id, DoneCriterion,
27
- TaskData,
28
- };
29
-
30
- /// `handoff_task_checklist` entry point: dispatches on `action`
31
- /// (`"view"` default, or `"generate"`).
32
- pub fn handle(arguments: &Value) -> Result<String> {
33
- let project_dir = resolve_project_dir(arguments)?;
34
- let handoff = ensure_handoff_exists(&project_dir)?;
35
- let tasks_dir = handoff.join("tasks");
36
-
37
- let task_id = arguments
38
- .get("task_id")
39
- .and_then(|v| v.as_str())
40
- .ok_or_else(|| anyhow::anyhow!("'task_id' is required"))?;
41
- let action = arguments
42
- .get("action")
43
- .and_then(|v| v.as_str())
44
- .unwrap_or("view");
45
-
46
- match action {
47
- "view" => handle_view(arguments, &handoff, &tasks_dir, task_id),
48
- "generate" => handle_generate(arguments, &handoff, &tasks_dir, task_id),
49
- other => anyhow::bail!("Unknown action '{other}'; expected 'view' or 'generate'."),
50
- }
51
- }
52
-
53
- fn handle_view(
54
- _arguments: &Value,
55
- handoff: &std::path::Path,
56
- tasks_dir: &std::path::Path,
57
- task_id: &str,
58
- ) -> Result<String> {
59
- let task_dir = find_task_dir_by_id(tasks_dir, task_id)?
60
- .ok_or_else(|| anyhow::anyhow!("{}", suggest_task_id(tasks_dir, task_id)))?;
61
- let (data, _status) = read_task(&task_dir)?
62
- .ok_or_else(|| anyhow::anyhow!("Task file not found in {}", task_dir.display()))?;
63
-
64
- let doc_links: Vec<_> = data
65
- .links()
66
- .into_iter()
67
- .filter(|l| l.link_type == "doc")
68
- .collect();
69
-
70
- if doc_links.is_empty() {
71
- return Ok(to_json(&json!({
72
- "task_id": data.id,
73
- "title": data.title,
74
- "no_linked_docs": true,
75
- })));
76
- }
77
-
78
- let docs = batch_resolve_docs(handoff, &doc_links)?;
79
-
80
- let done_criteria = done_criteria_json(&data);
81
- let documents: Vec<Value> = docs.iter().map(doc_coverage_json).collect();
82
- let overall = overall_progress_json(&docs);
83
- let combined_readiness = combined_readiness_json(&data, &docs);
84
- let suggested_actions = suggested_actions_json(&data, &docs);
85
-
86
- Ok(to_json(&json!({
87
- "task_id": data.id,
88
- "title": data.title,
89
- "no_linked_docs": false,
90
- "done_criteria": done_criteria,
91
- "verification_coverage": {
92
- "documents": documents,
93
- "overall": overall,
94
- },
95
- "combined_readiness": combined_readiness,
96
- "suggested_actions": suggested_actions,
97
- })))
98
- }
99
-
100
- /// Resolve a document by either its file-naming `slug` or its stable `id`
101
- /// (mirrors the private `resolve_doc` helper in `docs.rs`, duplicated here
102
- /// rather than made `pub` there to avoid growing that module's public
103
- /// surface for a single two-line lookup used by only one other handler).
104
- fn resolve_doc_by_slug_or_id(
105
- handoff: &std::path::Path,
106
- slug_or_id: &str,
107
- ) -> Result<Option<DocMetadata>> {
108
- if let Some(doc) = read_doc(handoff, slug_or_id)? {
109
- return Ok(Some(doc));
110
- }
111
- find_doc_by_id(handoff, slug_or_id)
112
- }
113
-
114
- /// Only `level == 2` sections are eligible for generation (spec §3.2
115
- /// "ハードコードデフォルトルール: level 2 の見出しのみ対象"). `skip_seqs` is the
116
- /// fully-resolved exclusion set built by the caller (`handle_generate`'s
117
- /// `skipped_seqs`), which always includes seq=0 (the preamble) by default
118
- /// plus any caller-supplied `skip_seqs` param values.
119
- fn eligible_sections<'a>(
120
- sections: &'a [SectionIndex],
121
- skip_seqs: &[usize],
122
- ) -> Vec<&'a SectionIndex> {
123
- sections
124
- .iter()
125
- .filter(|s| s.level == 2 && !skip_seqs.contains(&s.seq))
126
- .collect()
127
- }
128
-
129
- /// Fixed checklist items appended alongside the generated per-section
130
- /// criteria, keyed by `doc_type` (spec §3.2 "ハードコードデフォルト").
131
- fn fixed_items_for_doc_type(doc_type: &str) -> Vec<&'static str> {
132
- match doc_type {
133
- "spec" => vec![
134
- "仕様書の全セクションがカバーされていることを確認",
135
- "仕様変更があれば doc_save で更新済み",
136
- ],
137
- "design" => vec!["設計と実装の乖離がないことを確認"],
138
- _ => vec![],
139
- }
140
- }
141
-
142
- fn handle_generate(
143
- arguments: &Value,
144
- handoff: &std::path::Path,
145
- tasks_dir: &std::path::Path,
146
- task_id: &str,
147
- ) -> Result<String> {
148
- let task_dir = find_task_dir_by_id(tasks_dir, task_id)?
149
- .ok_or_else(|| anyhow::anyhow!("{}", suggest_task_id(tasks_dir, task_id)))?;
150
- let (data, _status) = read_task(&task_dir)?
151
- .ok_or_else(|| anyhow::anyhow!("Task file not found in {}", task_dir.display()))?;
152
-
153
- let doc = match arguments.get("doc_id").and_then(|v| v.as_str()) {
154
- Some(doc_id) => resolve_doc_by_slug_or_id(handoff, doc_id)?
155
- .ok_or_else(|| anyhow::anyhow!("Document not found: {doc_id}"))?,
156
- None => {
157
- let doc_links: Vec<_> = data
158
- .links()
159
- .into_iter()
160
- .filter(|l| l.link_type == "doc")
161
- .collect();
162
- let docs = batch_resolve_docs(handoff, &doc_links)?;
163
- docs.into_iter()
164
- .find(|d| d.doc_type == "spec" || d.doc_type == "design")
165
- .ok_or_else(|| {
166
- anyhow::anyhow!(
167
- "No 'doc_id' given and task {task_id} has no linked document with doc_type 'spec' or 'design'; pass 'doc_id' explicitly."
168
- )
169
- })?
170
- }
171
- };
172
-
173
- let mode = arguments
174
- .get("mode")
175
- .and_then(|v| v.as_str())
176
- .unwrap_or("preview");
177
- if !["preview", "append", "replace"].contains(&mode) {
178
- anyhow::bail!("Unknown mode '{mode}'; expected 'preview', 'append', or 'replace'.");
179
- }
180
-
181
- let caller_skip_seqs: Vec<usize> = arguments
182
- .get("skip_seqs")
183
- .and_then(|v| v.as_array())
184
- .map(|arr| {
185
- arr.iter()
186
- .filter_map(|v| v.as_u64())
187
- .map(|n| n as usize)
188
- .collect()
189
- })
190
- .unwrap_or_default();
191
- let mut skipped_seqs: Vec<usize> = std::iter::once(0).chain(caller_skip_seqs).collect();
192
- skipped_seqs.sort_unstable();
193
- skipped_seqs.dedup();
194
-
195
- let generated_criteria: Vec<Value> = eligible_sections(&doc.sections, &skipped_seqs)
196
- .into_iter()
197
- .map(|s| {
198
- json!({
199
- "item": format!("[{}§{}] {}", doc.doc_type, s.seq, s.heading),
200
- "fragment_seq": s.seq,
201
- })
202
- })
203
- .collect();
204
- let fixed_items = fixed_items_for_doc_type(&doc.doc_type);
205
-
206
- let applied = match mode {
207
- "preview" => false,
208
- "append" => {
209
- apply_generated_criteria(&task_dir, &generated_criteria, false)?;
210
- true
211
- }
212
- "replace" => {
213
- apply_generated_criteria(&task_dir, &generated_criteria, true)?;
214
- true
215
- }
216
- _ => unreachable!("mode already validated above"),
217
- };
218
-
219
- Ok(to_json(&json!({
220
- "task_id": data.id,
221
- "generated_criteria": generated_criteria,
222
- "applied": applied,
223
- "skipped_seqs": skipped_seqs,
224
- "fixed_items": fixed_items,
225
- })))
226
- }
227
-
228
- /// Writes `generated_criteria` into the task's `done_criteria`: appends when
229
- /// `replace` is `false`, overwrites entirely when `true`. Uses
230
- /// `read_modify_write_task` for optimistic-concurrency safety (mirrors
231
- /// `log_time.rs`), since this is a write path shared with other tools that
232
- /// mutate the same task file (e.g. `handoff_check_criterion`).
233
- fn apply_generated_criteria(
234
- task_dir: &std::path::Path,
235
- generated_criteria: &[Value],
236
- replace: bool,
237
- ) -> Result<()> {
238
- let new_items: Vec<DoneCriterion> = generated_criteria
239
- .iter()
240
- .map(|c| DoneCriterion {
241
- item: c["item"].as_str().unwrap_or_default().to_string(),
242
- checked: false,
243
- })
244
- .collect();
245
-
246
- read_modify_write_task(task_dir, |data, status| {
247
- if replace {
248
- data.done_criteria = new_items.clone();
249
- } else {
250
- data.done_criteria.extend(new_items.clone());
251
- }
252
- data.updated_at = Some(chrono::Utc::now().to_rfc3339());
253
- Ok(status.to_string())
254
- })
255
- }
256
-
257
- fn done_criteria_json(data: &TaskData) -> Value {
258
- let items: Vec<Value> = data
259
- .done_criteria
260
- .iter()
261
- .enumerate()
262
- .map(|(index, c)| {
263
- json!({
264
- "index": index,
265
- "item": c.item,
266
- "checked": c.checked,
267
- })
268
- })
269
- .collect();
270
- let checked = data.done_criteria.iter().filter(|c| c.checked).count();
271
- let total = data.done_criteria.len();
272
- let percentage = if total == 0 {
273
- 0.0
274
- } else {
275
- checked as f64 / total as f64 * 100.0
276
- };
277
- json!({
278
- "items": items,
279
- "progress": { "checked": checked, "total": total, "percentage": percentage },
280
- })
281
- }
282
-
283
- /// Priority order (highest first): stale > skipped > verified > implemented
284
- /// > in_progress > untouched (doc-20260712-191142-602891 §3.1 table).
285
- fn visual_state(doc: &DocMetadata, item: &VerificationItem) -> &'static str {
286
- if item_is_stale(doc, item) {
287
- return "stale";
288
- }
289
- match item.status.as_str() {
290
- "skipped" => "skipped",
291
- "verified" => "verified",
292
- "pending" => {
293
- let has_impl = !item.impl_refs.is_empty();
294
- let has_test = !item.test_refs.is_empty();
295
- if has_impl && has_test {
296
- "implemented"
297
- } else if has_impl {
298
- "in_progress"
299
- } else {
300
- "untouched"
301
- }
302
- }
303
- _ => "untouched",
304
- }
305
- }
306
-
307
- /// An item is stale when it was verified at a content_hash that no longer
308
- /// matches its section's current content_hash. Mirrors
309
- /// `crate::mcp::handlers::docs::item_is_stale` (not reused directly since
310
- /// that helper is private to `docs.rs`; duplicated here rather than exposed
311
- /// publicly to avoid growing that already-1492-line file's public surface
312
- /// for a single one-line predicate).
313
- fn item_is_stale(doc: &DocMetadata, item: &VerificationItem) -> bool {
314
- let Some(hash_at_verify) = &item.content_hash_at_verify else {
315
- return false;
316
- };
317
- let Some(fragment_seq) = item.fragment_seq else {
318
- // Freeform items (v2, fragment_seq=None) are never stale: they are
319
- // not tied to any section's content_hash.
320
- return false;
321
- };
322
- match doc.sections.iter().find(|s| s.seq == fragment_seq) {
323
- Some(section) => &section.content_hash != hash_at_verify,
324
- None => true,
325
- }
326
- }
327
-
328
- fn doc_coverage_json(doc: &DocMetadata) -> Value {
329
- let empty_items: Vec<VerificationItem> = Vec::new();
330
- let items = doc
331
- .verification
332
- .as_ref()
333
- .map(|v| &v.items)
334
- .unwrap_or(&empty_items);
335
-
336
- let items_json: Vec<Value> = items
337
- .iter()
338
- .map(|i| {
339
- json!({
340
- "fragment_seq": i.fragment_seq,
341
- "heading": i.heading,
342
- "status": i.status,
343
- "stale": item_is_stale(doc, i),
344
- "visual_state": visual_state(doc, i),
345
- "impl_refs": i.impl_refs,
346
- "test_refs": i.test_refs,
347
- })
348
- })
349
- .collect();
350
-
351
- let verified = items.iter().filter(|i| i.status == "verified").count();
352
- let pending = items.iter().filter(|i| i.status == "pending").count();
353
- let skipped = items.iter().filter(|i| i.status == "skipped").count();
354
- let stale = items.iter().filter(|i| item_is_stale(doc, i)).count();
355
- let total = items.len();
356
- let percentage = if total == 0 {
357
- 0.0
358
- } else {
359
- (verified + skipped) as f64 / total as f64 * 100.0
360
- };
361
-
362
- json!({
363
- "doc_id": doc.id,
364
- "slug": doc.slug,
365
- "title": doc.title,
366
- "doc_type": doc.doc_type,
367
- "items": items_json,
368
- "progress": {
369
- "verified": verified,
370
- "pending": pending,
371
- "skipped": skipped,
372
- "stale": stale,
373
- "total": total,
374
- "percentage": percentage,
375
- },
376
- })
377
- }
378
-
379
- fn overall_progress_json(docs: &[DocMetadata]) -> Value {
380
- let mut verified = 0;
381
- let mut pending = 0;
382
- let mut stale = 0;
383
- let mut total = 0;
384
- for doc in docs {
385
- if let Some(v) = &doc.verification {
386
- verified += v.items.iter().filter(|i| i.status == "verified").count();
387
- pending += v.items.iter().filter(|i| i.status == "pending").count();
388
- stale += v.items.iter().filter(|i| item_is_stale(doc, i)).count();
389
- total += v.items.len();
390
- }
391
- }
392
- let percentage = if total == 0 {
393
- 0.0
394
- } else {
395
- verified as f64 / total as f64 * 100.0
396
- };
397
- json!({ "verified": verified, "pending": pending, "stale": stale, "total": total, "percentage": percentage })
398
- }
399
-
400
- /// Typed blockers (doc-20260712-191142-602891 §3.1 M7: "blockers は typed
401
- /// objects"): one entry per unchecked done_criteria item
402
- /// (`{type:"criteria", index, item}`), one per non-verified/non-skipped
403
- /// verification item (`{type:"verification", doc_id, doc_slug, fragment_seq,
404
- /// heading}`), and one per linked doc that has no verification matrix at all
405
- /// (`{type:"verification_missing", doc_id, doc_slug}` — mirrors
406
- /// `handle_doc_verify_status`'s hard error on the same condition, so a doc
407
- /// that never had `action="generate"` run cannot silently count as ready).
408
- fn combined_readiness_json(data: &TaskData, docs: &[DocMetadata]) -> Value {
409
- let mut blockers = Vec::new();
410
-
411
- for (index, c) in data.done_criteria.iter().enumerate() {
412
- if !c.checked {
413
- blockers.push(json!({ "type": "criteria", "index": index, "item": c.item }));
414
- }
415
- }
416
-
417
- for doc in docs {
418
- let Some(v) = &doc.verification else {
419
- blockers.push(json!({
420
- "type": "verification_missing",
421
- "doc_id": doc.id,
422
- "doc_slug": doc.slug,
423
- }));
424
- continue;
425
- };
426
- for item in &v.items {
427
- let resolved = item.status == "verified" || item.status == "skipped";
428
- if !resolved || item_is_stale(doc, item) {
429
- blockers.push(json!({
430
- "type": "verification",
431
- "doc_id": doc.id,
432
- "doc_slug": doc.slug,
433
- "fragment_seq": item.fragment_seq,
434
- "heading": item.heading,
435
- }));
436
- }
437
- }
438
- }
439
-
440
- let done_criteria_met =
441
- !data.done_criteria.is_empty() && data.done_criteria.iter().all(|c| c.checked);
442
- let verification_complete = docs.iter().all(|d| match &d.verification {
443
- None => false,
444
- Some(v) => v
445
- .items
446
- .iter()
447
- .all(|i| (i.status == "verified" || i.status == "skipped") && !item_is_stale(d, i)),
448
- });
449
- let ready = done_criteria_met && verification_complete;
450
-
451
- json!({
452
- "done_criteria_met": done_criteria_met,
453
- "verification_complete": verification_complete,
454
- "ready": ready,
455
- "blockers": blockers,
456
- })
457
- }
458
-
459
- /// Advisory next-action hints (doc-20260712-191142-602891 §3.1 / §2.3: no
460
- /// auto-sync, `suggested_actions` presents next steps for the caller to run
461
- /// itself). One suggestion per unresolved verification item, one per doc
462
- /// with no verification matrix yet, and one per unchecked done_criteria
463
- /// item, each naming the concrete tool call to make.
464
- fn suggested_actions_json(data: &TaskData, docs: &[DocMetadata]) -> Vec<String> {
465
- let mut actions = Vec::new();
466
-
467
- for doc in docs {
468
- let Some(v) = &doc.verification else {
469
- actions.push(format!(
470
- "handoff_doc_verify(doc_id=\"{}\", action=\"generate\") — \"{}\" の検証マトリクスがまだ存在しない",
471
- doc.id, doc.title
472
- ));
473
- continue;
474
- };
475
- for item in &v.items {
476
- let resolved = item.status == "verified" || item.status == "skipped";
477
- if !resolved || item_is_stale(doc, item) {
478
- let action = match item.fragment_seq {
479
- Some(seq) => format!(
480
- "handoff_doc_verify(doc_id=\"{}\", action=\"check\", fragment_seq={}) — \"{}\" のレビュー完了時",
481
- doc.id, seq, item.heading
482
- ),
483
- None => format!(
484
- "handoff_doc_verify(doc_id=\"{}\", action=\"check\", fragment_seq=null, label=\"{}\") — \"{}\" のレビュー完了時 (フリーフォーム項目)",
485
- doc.id, item.label.as_deref().unwrap_or(&item.heading), item.heading
486
- ),
487
- };
488
- actions.push(action);
489
- }
490
- }
491
- }
492
-
493
- for (index, c) in data.done_criteria.iter().enumerate() {
494
- if !c.checked {
495
- actions.push(format!(
496
- "handoff_check_criterion(task_id=\"{}\", criterion_index={}) — \"{}\" 完了時",
497
- data.id, index, c.item
498
- ));
499
- }
500
- }
501
-
502
- actions
503
- }
504
-
505
- fn to_json(v: &Value) -> String {
506
- serde_json::to_string_pretty(v).unwrap_or_else(|_| v.to_string())
507
- }