kc-cli 0.4.0__py3-none-any.whl

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 (65) hide show
  1. kc/__init__.py +5 -0
  2. kc/__main__.py +11 -0
  3. kc/artifacts/__init__.py +1 -0
  4. kc/artifacts/diff.py +76 -0
  5. kc/artifacts/frontmatter.py +26 -0
  6. kc/artifacts/markdown.py +116 -0
  7. kc/atomic_write.py +33 -0
  8. kc/cli.py +284 -0
  9. kc/commands/__init__.py +1 -0
  10. kc/commands/artifact.py +1190 -0
  11. kc/commands/citation.py +231 -0
  12. kc/commands/common.py +346 -0
  13. kc/commands/conformance.py +293 -0
  14. kc/commands/context.py +190 -0
  15. kc/commands/doctor.py +81 -0
  16. kc/commands/eval.py +133 -0
  17. kc/commands/export.py +97 -0
  18. kc/commands/guide.py +571 -0
  19. kc/commands/index.py +54 -0
  20. kc/commands/init.py +207 -0
  21. kc/commands/lint.py +238 -0
  22. kc/commands/source.py +464 -0
  23. kc/commands/status.py +52 -0
  24. kc/commands/task.py +260 -0
  25. kc/config.py +127 -0
  26. kc/embedding_models/potion-base-8M/README.md +97 -0
  27. kc/embedding_models/potion-base-8M/config.json +13 -0
  28. kc/embedding_models/potion-base-8M/model.safetensors +0 -0
  29. kc/embedding_models/potion-base-8M/modules.json +14 -0
  30. kc/embedding_models/potion-base-8M/tokenizer.json +1 -0
  31. kc/errors.py +141 -0
  32. kc/fingerprints.py +35 -0
  33. kc/ids.py +23 -0
  34. kc/locks.py +65 -0
  35. kc/models/__init__.py +17 -0
  36. kc/models/artifact.py +34 -0
  37. kc/models/citation.py +60 -0
  38. kc/models/context.py +23 -0
  39. kc/models/eval.py +21 -0
  40. kc/models/plan.py +37 -0
  41. kc/models/source.py +37 -0
  42. kc/models/source_range.py +29 -0
  43. kc/models/source_revision.py +19 -0
  44. kc/models/task.py +35 -0
  45. kc/output.py +838 -0
  46. kc/paths.py +126 -0
  47. kc/provenance/__init__.py +1 -0
  48. kc/provenance/citations.py +296 -0
  49. kc/search/__init__.py +1 -0
  50. kc/search/extract.py +268 -0
  51. kc/search/fts.py +284 -0
  52. kc/search/semantic.py +346 -0
  53. kc/store/__init__.py +1 -0
  54. kc/store/jsonl.py +55 -0
  55. kc/store/sqlite.py +444 -0
  56. kc/store/transaction.py +67 -0
  57. kc/templates/agents/skills/kc/SKILL.md +282 -0
  58. kc/templates/agents/skills/kc/agents/openai.yaml +5 -0
  59. kc/templates/agents/skills/kc/scripts/resolve_query_citations.py +134 -0
  60. kc/workspace.py +98 -0
  61. kc_cli-0.4.0.dist-info/METADATA +522 -0
  62. kc_cli-0.4.0.dist-info/RECORD +65 -0
  63. kc_cli-0.4.0.dist-info/WHEEL +4 -0
  64. kc_cli-0.4.0.dist-info/entry_points.txt +2 -0
  65. kc_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
kc/output.py ADDED
@@ -0,0 +1,838 @@
1
+ """kc.result.v1 envelope and output mode handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import time
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ import orjson
13
+ from pydantic import BaseModel
14
+
15
+ from kc.errors import KcError
16
+ from kc.ids import new_id
17
+
18
+ SCHEMA_VERSION = "kc.result.v1"
19
+
20
+
21
+ @dataclass
22
+ class RuntimeState:
23
+ format: str = "json"
24
+ quiet: bool = False
25
+ root_override: str | None = None
26
+ data_dir: str | None = None
27
+ state_dir: str | None = None
28
+ workspace_root: str | None = None
29
+ workspace_resolution_source: str | None = None
30
+ request_id: str = ""
31
+ no_input: bool = False
32
+ start_time: float = 0.0
33
+
34
+
35
+ state = RuntimeState()
36
+
37
+
38
+ def is_llm_mode() -> bool:
39
+ return os.environ.get("LLM", "").lower() == "true"
40
+
41
+
42
+ def is_interactive() -> bool:
43
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
44
+
45
+
46
+ def init_request(request_id: str | None = None) -> None:
47
+ state.request_id = request_id or new_id("req")
48
+ state.start_time = time.monotonic()
49
+
50
+
51
+ def duration_ms() -> int:
52
+ if state.start_time == 0.0:
53
+ return 0
54
+ return int((time.monotonic() - state.start_time) * 1000)
55
+
56
+
57
+ def to_data(value: Any) -> Any:
58
+ if isinstance(value, BaseModel):
59
+ return value.model_dump(mode="json")
60
+ if isinstance(value, list):
61
+ return [to_data(v) for v in value]
62
+ if isinstance(value, tuple):
63
+ return [to_data(v) for v in value]
64
+ if isinstance(value, dict):
65
+ return {str(k): to_data(v) for k, v in value.items()}
66
+ return value
67
+
68
+
69
+ def warning(code: str, message: str, details: dict[str, Any] | None = None) -> dict[str, Any]:
70
+ return {"code": code, "message": message, "details": details or {}}
71
+
72
+
73
+ def envelope(
74
+ command: str,
75
+ result: Any,
76
+ *,
77
+ target: dict[str, Any] | None = None,
78
+ ok: bool = True,
79
+ warnings: list[dict[str, Any]] | None = None,
80
+ errors: list[dict[str, Any]] | None = None,
81
+ metrics: dict[str, Any] | None = None,
82
+ ) -> dict[str, Any]:
83
+ metric_payload = {"duration_ms": duration_ms()}
84
+ if metrics:
85
+ metric_payload.update(metrics)
86
+ target_payload = dict(target or {})
87
+ if state.workspace_root and "workspace_root" not in target_payload:
88
+ target_payload["workspace_root"] = state.workspace_root
89
+ return {
90
+ "schema_version": SCHEMA_VERSION,
91
+ "request_id": state.request_id,
92
+ "ok": ok,
93
+ "command": command,
94
+ "target": target_payload,
95
+ "result": to_data(result),
96
+ "warnings": warnings or [],
97
+ "errors": errors or [],
98
+ "metrics": metric_payload,
99
+ }
100
+
101
+
102
+ def dumps(payload: dict[str, Any]) -> str:
103
+ return orjson.dumps(to_data(payload), option=orjson.OPT_INDENT_2).decode()
104
+
105
+
106
+ Summary = dict[str, Any]
107
+ SummaryRenderer = Callable[[dict[str, Any]], Summary]
108
+ HUMAN_RENDERERS: dict[str, SummaryRenderer] = {}
109
+
110
+
111
+ def _renderer(command: str) -> Callable[[SummaryRenderer], SummaryRenderer]:
112
+ def _register(func: SummaryRenderer) -> SummaryRenderer:
113
+ HUMAN_RENDERERS[command] = func
114
+ return func
115
+
116
+ return _register
117
+
118
+
119
+ def _value(value: Any) -> str:
120
+ if value is None:
121
+ return ""
122
+ if isinstance(value, bool):
123
+ return "true" if value else "false"
124
+ if isinstance(value, int | float):
125
+ return str(value)
126
+ if isinstance(value, list):
127
+ if not value:
128
+ return "0"
129
+ if all(not isinstance(item, dict | list | tuple) for item in value) and len(value) <= 5:
130
+ return ", ".join(_value(item) for item in value)
131
+ return f"{len(value)} items"
132
+ if isinstance(value, dict):
133
+ return f"{len(value)} fields"
134
+ return str(value)
135
+
136
+
137
+ def _count(value: Any) -> int:
138
+ return len(value) if isinstance(value, list | dict | tuple | set) else 0
139
+
140
+
141
+ def _summary(title: str, pairs: list[tuple[str, Any]], rows: list[dict[str, Any]] | None = None) -> Summary:
142
+ return {"title": title, "pairs": pairs, "rows": rows or []}
143
+
144
+
145
+ def _plan_id(result: dict[str, Any]) -> str:
146
+ plan = result.get("plan")
147
+ return str(plan.get("plan_id", "")) if isinstance(plan, dict) else ""
148
+
149
+
150
+ def _artifact_path(result: dict[str, Any]) -> str:
151
+ artifact = result.get("artifact")
152
+ if isinstance(artifact, dict):
153
+ return str(artifact.get("path", ""))
154
+ return str(result.get("path", ""))
155
+
156
+
157
+ def _locator_text(row: dict[str, Any]) -> str:
158
+ locator = row.get("locator")
159
+ if not isinstance(locator, dict):
160
+ return ""
161
+ if locator.get("kind") == "json_pointer":
162
+ return str(locator.get("pointer", ""))
163
+ if locator.get("kind") == "csv_row_range":
164
+ return f"R{locator.get('start_row')}-R{locator.get('end_row')}"
165
+ start = locator.get("start_line")
166
+ end = locator.get("end_line")
167
+ return f"L{start}-L{end}" if start is not None and end is not None else ""
168
+
169
+
170
+ def _first_results(result: dict[str, Any], *, limit: int = 8) -> list[dict[str, Any]]:
171
+ rows = result.get("results")
172
+ return list(rows[:limit]) if isinstance(rows, list) else []
173
+
174
+
175
+ def _render_pairs_table(pairs: list[tuple[str, Any]]) -> list[str]:
176
+ if not pairs:
177
+ return []
178
+ width = max(len(label) for label, _value_item in pairs)
179
+ return [f"{label.ljust(width)} {_value(value)}" for label, value in pairs]
180
+
181
+
182
+ def _render_rows_table(rows: list[dict[str, Any]]) -> list[str]:
183
+ if not rows:
184
+ return []
185
+ headers = list(rows[0])
186
+ widths = {
187
+ header: max(len(header), *(len(_value(row.get(header))) for row in rows))
188
+ for header in headers
189
+ }
190
+ output = [" ".join(header.ljust(widths[header]) for header in headers)]
191
+ output.append(" ".join("-" * widths[header] for header in headers))
192
+ output.extend(
193
+ " ".join(_value(row.get(header)).ljust(widths[header]) for header in headers)
194
+ for row in rows
195
+ )
196
+ return output
197
+
198
+
199
+ def _markdown_table(headers: list[str], rows: list[list[Any]]) -> list[str]:
200
+ if not rows:
201
+ return []
202
+ output = [
203
+ "| " + " | ".join(headers) + " |",
204
+ "| " + " | ".join("---" for _header in headers) + " |",
205
+ ]
206
+ for row in rows:
207
+ output.append("| " + " | ".join(_value(item).replace("\n", " ") for item in row) + " |")
208
+ return output
209
+
210
+
211
+ def _warning_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
212
+ warnings = payload.get("warnings")
213
+ if not isinstance(warnings, list):
214
+ return []
215
+ return [
216
+ {"code": warning.get("code", ""), "message": warning.get("message", "")}
217
+ for warning in warnings
218
+ if isinstance(warning, dict)
219
+ ]
220
+
221
+
222
+ def _render_success_table(payload: dict[str, Any]) -> str:
223
+ renderer = HUMAN_RENDERERS.get(str(payload.get("command")))
224
+ summary = renderer(payload) if renderer else _generic_summary(payload)
225
+ lines = [str(summary["title"])]
226
+ lines.extend(_render_pairs_table(list(summary.get("pairs", []))))
227
+ rows = list(summary.get("rows", []))
228
+ if rows:
229
+ lines.append("")
230
+ lines.extend(_render_rows_table(rows))
231
+ warnings = _warning_rows(payload)
232
+ if warnings:
233
+ lines.append("")
234
+ lines.append("Warnings")
235
+ lines.extend(_render_rows_table(warnings))
236
+ return "\n".join(lines)
237
+
238
+
239
+ def _render_success_markdown(payload: dict[str, Any]) -> str:
240
+ renderer = HUMAN_RENDERERS.get(str(payload.get("command")))
241
+ summary = renderer(payload) if renderer else _generic_summary(payload)
242
+ lines = [f"# {summary['title']}", ""]
243
+ pair_rows = [[label, value] for label, value in list(summary.get("pairs", []))]
244
+ lines.extend(_markdown_table(["Field", "Value"], pair_rows))
245
+ rows = list(summary.get("rows", []))
246
+ if rows:
247
+ headers = list(rows[0])
248
+ lines.extend(["", "## Results", ""])
249
+ lines.extend(_markdown_table(headers, [[row.get(header) for header in headers] for row in rows]))
250
+ warnings = _warning_rows(payload)
251
+ if warnings:
252
+ lines.extend(["", "## Warnings", ""])
253
+ lines.extend(
254
+ _markdown_table(["Code", "Message"], [[row["code"], row["message"]] for row in warnings])
255
+ )
256
+ return "\n".join(lines)
257
+
258
+
259
+ def _render_error_table(payload: dict[str, Any]) -> str:
260
+ raw_errors = payload.get("errors")
261
+ errors = raw_errors if isinstance(raw_errors, list) else []
262
+ rows = [
263
+ {
264
+ "code": error.get("code", ""),
265
+ "message": error.get("message", ""),
266
+ "exit_code": error.get("exit_code", ""),
267
+ "suggested_action": error.get("suggested_action", ""),
268
+ }
269
+ for error in errors
270
+ if isinstance(error, dict)
271
+ ]
272
+ lines = [f"Error: {payload.get('command', 'kc')}"]
273
+ lines.extend(_render_rows_table(rows))
274
+ return "\n".join(lines)
275
+
276
+
277
+ def _render_error_markdown(payload: dict[str, Any]) -> str:
278
+ raw_errors = payload.get("errors")
279
+ errors = raw_errors if isinstance(raw_errors, list) else []
280
+ rows = [
281
+ [
282
+ error.get("code", ""),
283
+ error.get("message", ""),
284
+ error.get("exit_code", ""),
285
+ error.get("suggested_action", ""),
286
+ ]
287
+ for error in errors
288
+ if isinstance(error, dict)
289
+ ]
290
+ lines = [f"# Error: {payload.get('command', 'kc')}", ""]
291
+ lines.extend(_markdown_table(["Code", "Message", "Exit Code", "Suggested Action"], rows))
292
+ return "\n".join(lines)
293
+
294
+
295
+ def render_human(payload: dict[str, Any]) -> str:
296
+ if state.format == "markdown":
297
+ return _render_success_markdown(payload) if payload.get("ok") else _render_error_markdown(payload)
298
+ return _render_success_table(payload) if payload.get("ok") else _render_error_table(payload)
299
+
300
+
301
+ def _generic_summary(payload: dict[str, Any]) -> Summary:
302
+ result = payload.get("result")
303
+ pairs: list[tuple[str, Any]] = [("command", payload.get("command")), ("ok", payload.get("ok"))]
304
+ if isinstance(result, dict):
305
+ pairs.extend((key, value) for key, value in result.items() if not isinstance(value, list | dict))
306
+ return _summary(str(payload.get("command", "kc")), pairs)
307
+
308
+
309
+ @_renderer("guide")
310
+ def _guide_summary(payload: dict[str, Any]) -> Summary:
311
+ raw_result = payload.get("result")
312
+ result: dict[str, Any] = raw_result if isinstance(raw_result, dict) else {}
313
+ raw_commands = result.get("commands")
314
+ commands: dict[str, Any] = raw_commands if isinstance(raw_commands, dict) else {}
315
+ raw_target = payload.get("target")
316
+ target: dict[str, Any] = raw_target if isinstance(raw_target, dict) else {}
317
+ rows = [
318
+ {
319
+ "command": command,
320
+ "mutates": data.get("mutates"),
321
+ "confirmation": data.get("confirmation"),
322
+ }
323
+ for command, data in list(commands.items())[:12]
324
+ if isinstance(data, dict)
325
+ ]
326
+ return _summary(
327
+ "guide",
328
+ [
329
+ ("section", target.get("section")),
330
+ ("commands", len(commands)),
331
+ ("schema_version", result.get("schema_version")),
332
+ ],
333
+ rows,
334
+ )
335
+
336
+
337
+ @_renderer("conformance")
338
+ def _conformance_summary(payload: dict[str, Any]) -> Summary:
339
+ result = payload["result"]
340
+ summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
341
+ rows = [
342
+ {
343
+ "check": check.get("check_id"),
344
+ "passed": check.get("passed"),
345
+ "message": check.get("message"),
346
+ }
347
+ for check in result.get("checks", [])
348
+ if isinstance(check, dict)
349
+ ]
350
+ return _summary(
351
+ "conformance",
352
+ [
353
+ ("profile", result.get("profile")),
354
+ ("valid", result.get("valid")),
355
+ ("total", summary.get("total")),
356
+ ("passed", summary.get("passed")),
357
+ ("failed", summary.get("failed")),
358
+ ],
359
+ rows,
360
+ )
361
+
362
+
363
+ @_renderer("init")
364
+ def _init_summary(payload: dict[str, Any]) -> Summary:
365
+ result = payload["result"]
366
+ return _summary(
367
+ "init",
368
+ [
369
+ ("dry_run", result.get("dry_run")),
370
+ ("profile", result.get("profile")),
371
+ ("created", _count(result.get("created"))),
372
+ ("updated", _count(result.get("updated"))),
373
+ ("planned", _count(result.get("planned"))),
374
+ ("noop", _count(result.get("noop"))),
375
+ ],
376
+ )
377
+
378
+
379
+ @_renderer("status")
380
+ def _status_summary(payload: dict[str, Any]) -> Summary:
381
+ result = payload["result"]
382
+ workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
383
+ counts = result.get("counts") if isinstance(result.get("counts"), dict) else {}
384
+ rows = [{"next_command": command} for command in result.get("next_commands", [])]
385
+ return _summary(
386
+ "status",
387
+ [
388
+ ("initialized", result.get("initialized")),
389
+ ("workspace_root", workspace.get("root")),
390
+ ("resolution_source", workspace.get("resolution_source")),
391
+ ("sources", counts.get("sources")),
392
+ ("ranges", counts.get("ranges")),
393
+ ("artifacts", counts.get("artifacts")),
394
+ ],
395
+ rows,
396
+ )
397
+
398
+
399
+ @_renderer("source.add")
400
+ def _source_add_summary(payload: dict[str, Any]) -> Summary:
401
+ result = payload["result"]
402
+ return _summary(
403
+ "source.add",
404
+ [
405
+ ("dry_run", result.get("dry_run")),
406
+ ("source_id", result.get("source_id")),
407
+ ("uri", result.get("uri")),
408
+ ("media_type", result.get("media_type")),
409
+ ("ranges_extracted", result.get("ranges_extracted")),
410
+ ("copied", result.get("copied")),
411
+ ],
412
+ )
413
+
414
+
415
+ @_renderer("source.inspect")
416
+ def _source_inspect_summary(payload: dict[str, Any]) -> Summary:
417
+ result = payload["result"]
418
+ source = result.get("source") if isinstance(result.get("source"), dict) else {}
419
+ rows = []
420
+ ranges = result.get("ranges")
421
+ if isinstance(ranges, list):
422
+ rows = [
423
+ {
424
+ "range_id": row.get("range_id"),
425
+ "locator": _locator_text(row),
426
+ "excerpt": str(row.get("excerpt", ""))[:80],
427
+ }
428
+ for row in ranges[:8]
429
+ if isinstance(row, dict)
430
+ ]
431
+ return _summary(
432
+ "source.inspect",
433
+ [
434
+ ("source_id", source.get("source_id")),
435
+ ("uri", source.get("uri")),
436
+ ("stale", result.get("stale")),
437
+ ("ranges", _count(ranges)),
438
+ ],
439
+ rows,
440
+ )
441
+
442
+
443
+ @_renderer("source.refresh")
444
+ def _source_refresh_summary(payload: dict[str, Any]) -> Summary:
445
+ result = payload["result"]
446
+ return _summary(
447
+ "source.refresh",
448
+ [
449
+ ("dry_run", result.get("dry_run")),
450
+ ("source_id", result.get("source_id")),
451
+ ("ranges_removed", result.get("ranges_removed")),
452
+ ("ranges_extracted", result.get("ranges_extracted")),
453
+ ("impacted_artifacts", _count(result.get("impacted_artifacts"))),
454
+ ("semantic_index_rebuilt", result.get("semantic_index_rebuilt")),
455
+ ],
456
+ )
457
+
458
+
459
+ @_renderer("source.search")
460
+ def _source_search_summary(payload: dict[str, Any]) -> Summary:
461
+ result = payload["result"]
462
+ rows = [
463
+ {
464
+ "rank": item.get("scores", {}).get("hybrid_rank"),
465
+ "source_id": item.get("source_id"),
466
+ "locator": _locator_text(item),
467
+ "citation": item.get("citation_token"),
468
+ }
469
+ for item in _first_results(result)
470
+ if isinstance(item, dict)
471
+ ]
472
+ return _summary(
473
+ "source.search",
474
+ [("query", result.get("query")), ("mode", result.get("mode")), ("total", result.get("total"))],
475
+ rows,
476
+ )
477
+
478
+
479
+ @_renderer("index.build")
480
+ def _index_build_summary(payload: dict[str, Any]) -> Summary:
481
+ result = payload["result"]
482
+ semantic = result.get("semantic")
483
+ enabled = semantic.get("enabled") if isinstance(semantic, dict) else result.get("semantic")
484
+ return _summary(
485
+ "index.build",
486
+ [
487
+ ("dry_run", result.get("dry_run")),
488
+ ("clean", result.get("clean")),
489
+ ("sources", result.get("sources")),
490
+ ("ranges", result.get("ranges")),
491
+ ("semantic", enabled),
492
+ ("db_path", result.get("db_path")),
493
+ ],
494
+ )
495
+
496
+
497
+ @_renderer("context.prepare")
498
+ def _context_prepare_summary(payload: dict[str, Any]) -> Summary:
499
+ result = payload["result"]
500
+ rows = [
501
+ {
502
+ "source_id": item.get("source_id"),
503
+ "locator": _locator_text(item),
504
+ "citation": item.get("citation_token"),
505
+ }
506
+ for item in list(result.get("candidate_ranges", []))[:8]
507
+ if isinstance(item, dict)
508
+ ]
509
+ return _summary(
510
+ "context.prepare",
511
+ [
512
+ ("query", result.get("search_query")),
513
+ ("mode", result.get("mode")),
514
+ ("candidate_ranges", _count(result.get("candidate_ranges"))),
515
+ ("existing_artifacts", _count(result.get("existing_artifacts"))),
516
+ ("grounding_policy", result.get("grounding_policy")),
517
+ ],
518
+ rows,
519
+ )
520
+
521
+
522
+ @_renderer("artifact.new")
523
+ def _artifact_new_summary(payload: dict[str, Any]) -> Summary:
524
+ result = payload["result"]
525
+ return _summary(
526
+ "artifact.new",
527
+ [
528
+ ("dry_run", result.get("dry_run")),
529
+ ("artifact_id", result.get("artifact_id")),
530
+ ("path", result.get("path")),
531
+ ("bytes", result.get("bytes")),
532
+ ],
533
+ )
534
+
535
+
536
+ @_renderer("artifact.validate")
537
+ def _artifact_validate_summary(payload: dict[str, Any]) -> Summary:
538
+ result = payload["result"]
539
+ return _summary(
540
+ "artifact.validate",
541
+ [
542
+ ("valid", result.get("valid")),
543
+ ("path", result.get("path")),
544
+ ("fingerprint", result.get("fingerprint")),
545
+ ("citations", _count(result.get("citation_edges"))),
546
+ ("errors", _count(result.get("errors"))),
547
+ ],
548
+ )
549
+
550
+
551
+ @_renderer("artifact.diff")
552
+ def _artifact_diff_summary(payload: dict[str, Any]) -> Summary:
553
+ result = payload["result"]
554
+ plan = result.get("plan") if isinstance(result.get("plan"), dict) else {}
555
+ return _summary(
556
+ "artifact.diff",
557
+ [
558
+ ("plan_id", plan.get("plan_id")),
559
+ ("operations", _count(plan.get("operations"))),
560
+ ("risk_flags", _count(result.get("risk_flags"))),
561
+ ],
562
+ )
563
+
564
+
565
+ @_renderer("artifact.apply")
566
+ def _artifact_apply_summary(payload: dict[str, Any]) -> Summary:
567
+ result = payload["result"]
568
+ return _summary(
569
+ "artifact.apply",
570
+ [
571
+ ("dry_run", result.get("dry_run")),
572
+ ("applied", result.get("applied")),
573
+ ("noop", result.get("noop")),
574
+ ("plan_id", _plan_id(result)),
575
+ ("artifact", _artifact_path(result)),
576
+ ("citation_edges", result.get("citation_edges")),
577
+ ],
578
+ )
579
+
580
+
581
+ @_renderer("citation.check")
582
+ def _citation_check_summary(payload: dict[str, Any]) -> Summary:
583
+ result = payload["result"]
584
+ rows = [
585
+ {"path": item.get("path"), "valid": item.get("valid"), "citations": item.get("citations")}
586
+ for item in list(result.get("files", []))[:8]
587
+ if isinstance(item, dict)
588
+ ]
589
+ return _summary(
590
+ "citation.check",
591
+ [("valid", result.get("valid")), ("files", _count(result.get("files"))), ("problems", _count(result.get("problems")))],
592
+ rows,
593
+ )
594
+
595
+
596
+ @_renderer("citation.rewrite")
597
+ def _citation_rewrite_summary(payload: dict[str, Any]) -> Summary:
598
+ result = payload["result"]
599
+ return _summary(
600
+ "citation.rewrite",
601
+ [
602
+ ("dry_run", result.get("dry_run")),
603
+ ("path", result.get("path")),
604
+ ("rewritten", result.get("rewritten")),
605
+ ("unresolved", result.get("unresolved")),
606
+ ],
607
+ )
608
+
609
+
610
+ @_renderer("citation.repair")
611
+ def _citation_repair_summary(payload: dict[str, Any]) -> Summary:
612
+ result = payload["result"]
613
+ return _summary(
614
+ "citation.repair",
615
+ [
616
+ ("dry_run", result.get("dry_run")),
617
+ ("path", result.get("path")),
618
+ ("applied", result.get("applied")),
619
+ ("unresolved", result.get("unresolved")),
620
+ ],
621
+ )
622
+
623
+
624
+ @_renderer("lint")
625
+ def _lint_summary(payload: dict[str, Any]) -> Summary:
626
+ result = payload["result"]
627
+ rows = [{"next_command": command} for command in result.get("next_commands", [])]
628
+ return _summary(
629
+ "lint",
630
+ [
631
+ ("valid", result.get("valid")),
632
+ ("checks", result.get("checks")),
633
+ ("sources", result.get("sources")),
634
+ ("artifacts", result.get("artifacts")),
635
+ ("issues", _count(result.get("issues"))),
636
+ ],
637
+ rows,
638
+ )
639
+
640
+
641
+ @_renderer("export")
642
+ def _export_summary(payload: dict[str, Any]) -> Summary:
643
+ result = payload["result"]
644
+ return _summary(
645
+ "export",
646
+ [("format", result.get("format")), ("bytes", result.get("bytes")), ("out", result.get("out"))],
647
+ )
648
+
649
+
650
+ @_renderer("task.start")
651
+ def _task_start_summary(payload: dict[str, Any]) -> Summary:
652
+ result = payload["result"]
653
+ task = result.get("task") if isinstance(result.get("task"), dict) else {}
654
+ return _summary(
655
+ "task.start",
656
+ [
657
+ ("task_id", task.get("task_id")),
658
+ ("status", task.get("status")),
659
+ ("candidate_ranges", _count(task.get("candidate_ranges"))),
660
+ ("resume_command", result.get("resume_command")),
661
+ ],
662
+ )
663
+
664
+
665
+ @_renderer("task.status")
666
+ def _task_status_summary(payload: dict[str, Any]) -> Summary:
667
+ result = payload["result"]
668
+ rows = [{"next_command": command} for command in result.get("next_commands", [])]
669
+ return _summary(
670
+ "task.status",
671
+ [("task_id", result.get("task_id")), ("status", result.get("status")), ("updated_at", result.get("updated_at"))],
672
+ rows,
673
+ )
674
+
675
+
676
+ @_renderer("task.inspect")
677
+ def _task_inspect_summary(payload: dict[str, Any]) -> Summary:
678
+ result = payload["result"]
679
+ task = result.get("task") if isinstance(result.get("task"), dict) else {}
680
+ return _summary(
681
+ "task.inspect",
682
+ [
683
+ ("task_id", task.get("task_id")),
684
+ ("status", task.get("status")),
685
+ ("goal", task.get("goal")),
686
+ ("events", _count(task.get("events"))),
687
+ ],
688
+ )
689
+
690
+
691
+ @_renderer("task.next")
692
+ def _task_next_summary(payload: dict[str, Any]) -> Summary:
693
+ result = payload["result"]
694
+ rows = [{"next_command": command} for command in result.get("next_commands", [])]
695
+ return _summary(
696
+ "task.next",
697
+ [
698
+ ("task_id", result.get("task_id")),
699
+ ("status", result.get("status")),
700
+ ("expected_event_name", result.get("expected_event_name")),
701
+ ],
702
+ rows,
703
+ )
704
+
705
+
706
+ @_renderer("task.resume")
707
+ def _task_resume_summary(payload: dict[str, Any]) -> Summary:
708
+ result = payload["result"]
709
+ task = result.get("task") if isinstance(result.get("task"), dict) else {}
710
+ return _summary(
711
+ "task.resume",
712
+ [
713
+ ("task_id", task.get("task_id")),
714
+ ("status", task.get("status")),
715
+ ("events", _count(task.get("events"))),
716
+ ],
717
+ )
718
+
719
+
720
+ @_renderer("eval.run")
721
+ def _eval_run_summary(payload: dict[str, Any]) -> Summary:
722
+ result = payload["result"]
723
+ return _summary(
724
+ "eval.run",
725
+ [("pack", result.get("pack")), ("total", result.get("total")), ("passed", result.get("passed"))],
726
+ )
727
+
728
+
729
+ @_renderer("doctor")
730
+ def _doctor_summary(payload: dict[str, Any]) -> Summary:
731
+ result = payload["result"]
732
+ workspace = result.get("workspace_resolution") if isinstance(result.get("workspace_resolution"), dict) else {}
733
+ index = result.get("index") if isinstance(result.get("index"), dict) else {}
734
+ raw_last_build = index.get("last_build")
735
+ last_build: dict[str, Any] = raw_last_build if isinstance(raw_last_build, dict) else {}
736
+ semantic = result.get("semantic") if isinstance(result.get("semantic"), dict) else {}
737
+ raw_index_metadata = semantic.get("index_metadata")
738
+ index_metadata: dict[str, Any] = raw_index_metadata if isinstance(raw_index_metadata, dict) else {}
739
+ return _summary(
740
+ "doctor",
741
+ [
742
+ ("workspace_root", workspace.get("root")),
743
+ ("workspace_source", workspace.get("source")),
744
+ ("config_exists", result.get("config_exists")),
745
+ ("data_dir_exists", result.get("data_dir_exists")),
746
+ ("state_dir_exists", result.get("state_dir_exists")),
747
+ ("sqlite_exists", result.get("sqlite_exists")),
748
+ ("locks", result.get("locks")),
749
+ ("index_stale", index.get("stale")),
750
+ ("index_sources", last_build.get("sources")),
751
+ ("index_ranges", last_build.get("ranges")),
752
+ ("semantic_model_available", semantic.get("model_available")),
753
+ ("semantic_metadata_match", semantic.get("metadata_match")),
754
+ ("semantic_vectors", semantic.get("vector_count")),
755
+ ("semantic_index_ranges", index_metadata.get("ranges")),
756
+ ("semantic_missing_vectors", semantic.get("missing_vectors")),
757
+ ("semantic_stale_vectors", semantic.get("stale_vectors")),
758
+ ("semantic_unavailable_reason", semantic.get("unavailable_reason")),
759
+ ],
760
+ )
761
+
762
+
763
+ @_renderer("doctor.locks")
764
+ def _doctor_locks_summary(payload: dict[str, Any]) -> Summary:
765
+ result = payload["result"]
766
+ rows = [
767
+ {"path": item.get("path"), "metadata": _count(item.get("metadata"))}
768
+ for item in list(result.get("locks", []))[:8]
769
+ if isinstance(item, dict)
770
+ ]
771
+ return _summary(
772
+ "doctor.locks",
773
+ [
774
+ ("clear_stale", result.get("clear_stale")),
775
+ ("dry_run", result.get("dry_run")),
776
+ ("locks", _count(result.get("locks"))),
777
+ ("cleared", _count(result.get("cleared"))),
778
+ ],
779
+ rows,
780
+ )
781
+
782
+
783
+ def emit(payload: dict[str, Any], *, exit_code: int = 0) -> None:
784
+ rendered = dumps(payload) if state.format == "json" else render_human(payload)
785
+ sys.stdout.write(rendered + "\n")
786
+ raise SystemExit(exit_code)
787
+
788
+
789
+ def emit_success(
790
+ command: str,
791
+ result: Any,
792
+ *,
793
+ target: dict[str, Any] | None = None,
794
+ warnings: list[dict[str, Any]] | None = None,
795
+ metrics: dict[str, Any] | None = None,
796
+ exit_code: int = 0,
797
+ ) -> None:
798
+ emit(
799
+ envelope(
800
+ command,
801
+ result,
802
+ target=target,
803
+ warnings=warnings,
804
+ metrics=metrics,
805
+ ),
806
+ exit_code=exit_code,
807
+ )
808
+
809
+
810
+ def emit_error(command: str, error: KcError, *, target: dict[str, Any] | None = None) -> None:
811
+ emit(
812
+ envelope(
813
+ command,
814
+ None,
815
+ target=target,
816
+ ok=False,
817
+ errors=[error.to_message()],
818
+ ),
819
+ exit_code=error.exit_code or 90,
820
+ )
821
+
822
+
823
+ def emit_unexpected(command: str, exc: BaseException) -> None:
824
+ emit_error(
825
+ command,
826
+ KcError(
827
+ code="KC_INTERNAL_ERROR",
828
+ message=f"Internal error: {exc}",
829
+ details={"exception_type": type(exc).__name__},
830
+ ),
831
+ )
832
+
833
+
834
+ def progress(message: str) -> None:
835
+ if state.quiet:
836
+ return
837
+ sys.stderr.write(message.rstrip() + "\n")
838
+ sys.stderr.flush()