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.
- kc/__init__.py +5 -0
- kc/__main__.py +11 -0
- kc/artifacts/__init__.py +1 -0
- kc/artifacts/diff.py +76 -0
- kc/artifacts/frontmatter.py +26 -0
- kc/artifacts/markdown.py +116 -0
- kc/atomic_write.py +33 -0
- kc/cli.py +284 -0
- kc/commands/__init__.py +1 -0
- kc/commands/artifact.py +1190 -0
- kc/commands/citation.py +231 -0
- kc/commands/common.py +346 -0
- kc/commands/conformance.py +293 -0
- kc/commands/context.py +190 -0
- kc/commands/doctor.py +81 -0
- kc/commands/eval.py +133 -0
- kc/commands/export.py +97 -0
- kc/commands/guide.py +571 -0
- kc/commands/index.py +54 -0
- kc/commands/init.py +207 -0
- kc/commands/lint.py +238 -0
- kc/commands/source.py +464 -0
- kc/commands/status.py +52 -0
- kc/commands/task.py +260 -0
- kc/config.py +127 -0
- kc/embedding_models/potion-base-8M/README.md +97 -0
- kc/embedding_models/potion-base-8M/config.json +13 -0
- kc/embedding_models/potion-base-8M/model.safetensors +0 -0
- kc/embedding_models/potion-base-8M/modules.json +14 -0
- kc/embedding_models/potion-base-8M/tokenizer.json +1 -0
- kc/errors.py +141 -0
- kc/fingerprints.py +35 -0
- kc/ids.py +23 -0
- kc/locks.py +65 -0
- kc/models/__init__.py +17 -0
- kc/models/artifact.py +34 -0
- kc/models/citation.py +60 -0
- kc/models/context.py +23 -0
- kc/models/eval.py +21 -0
- kc/models/plan.py +37 -0
- kc/models/source.py +37 -0
- kc/models/source_range.py +29 -0
- kc/models/source_revision.py +19 -0
- kc/models/task.py +35 -0
- kc/output.py +838 -0
- kc/paths.py +126 -0
- kc/provenance/__init__.py +1 -0
- kc/provenance/citations.py +296 -0
- kc/search/__init__.py +1 -0
- kc/search/extract.py +268 -0
- kc/search/fts.py +284 -0
- kc/search/semantic.py +346 -0
- kc/store/__init__.py +1 -0
- kc/store/jsonl.py +55 -0
- kc/store/sqlite.py +444 -0
- kc/store/transaction.py +67 -0
- kc/templates/agents/skills/kc/SKILL.md +282 -0
- kc/templates/agents/skills/kc/agents/openai.yaml +5 -0
- kc/templates/agents/skills/kc/scripts/resolve_query_citations.py +134 -0
- kc/workspace.py +98 -0
- kc_cli-0.4.0.dist-info/METADATA +522 -0
- kc_cli-0.4.0.dist-info/RECORD +65 -0
- kc_cli-0.4.0.dist-info/WHEEL +4 -0
- kc_cli-0.4.0.dist-info/entry_points.txt +2 -0
- 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()
|