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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """kc — deterministic knowledge compiler harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.4.0"
kc/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from kc.cli import app
4
+
5
+
6
+ def main() -> None:
7
+ app()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1 @@
1
+ """Artifact parsing, validation, and diff helpers."""
kc/artifacts/diff.py ADDED
@@ -0,0 +1,76 @@
1
+ """Structured artifact diff planning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+
9
+ from kc.fingerprints import raw_fingerprint
10
+ from kc.ids import new_id
11
+ from kc.models.plan import PlanCondition, PlanOperation, PlanRecord
12
+
13
+
14
+ def build_artifact_plan(
15
+ path: Path,
16
+ *,
17
+ registered_fingerprint: str | None,
18
+ baseline_path: Path | None = None,
19
+ mode: str = "dry_run",
20
+ idempotency_key: str | None = None,
21
+ ) -> tuple[PlanRecord, str, dict[str, str | None]]:
22
+ after = raw_fingerprint(path) if path.exists() else None
23
+ before = registered_fingerprint
24
+ baseline: dict[str, str | None] = {"kind": "unavailable", "path": None, "fingerprint": before}
25
+ old_lines: list[str] = []
26
+ if baseline_path is not None and baseline_path.exists():
27
+ old_lines = baseline_path.read_text(encoding="utf-8-sig").splitlines(keepends=True)
28
+ baseline = {
29
+ "kind": "last_applied_snapshot",
30
+ "path": baseline_path.as_posix(),
31
+ "fingerprint": raw_fingerprint(baseline_path),
32
+ }
33
+ new_lines = (
34
+ path.read_text(encoding="utf-8-sig").splitlines(keepends=True) if path.exists() else []
35
+ )
36
+ if before is None:
37
+ risk_flags = ["new_artifact"]
38
+ old_label = "/dev/null"
39
+ else:
40
+ risk_flags = ["updates_existing_artifact"] if before != after else []
41
+ old_label = baseline_path.as_posix() if baseline_path is not None and baseline_path.exists() else f"{path.as_posix()}@registry"
42
+ diff_text = "".join(
43
+ difflib.unified_diff(
44
+ old_lines,
45
+ new_lines,
46
+ fromfile=old_label,
47
+ tofile=path.as_posix(),
48
+ )
49
+ )
50
+ risk = "medium" if risk_flags else "low"
51
+ plan = PlanRecord(
52
+ plan_id=new_id("plan"),
53
+ created_at=datetime.now(UTC).isoformat(),
54
+ command="artifact.apply",
55
+ mode=mode, # type: ignore[arg-type]
56
+ idempotency_key=idempotency_key,
57
+ operations=[
58
+ PlanOperation(
59
+ op_id="op_01",
60
+ kind="register_artifact",
61
+ path=path.as_posix(),
62
+ before_fingerprint=before,
63
+ after_fingerprint=after,
64
+ risk=risk, # type: ignore[arg-type]
65
+ requires_yes=True,
66
+ )
67
+ ],
68
+ preconditions=[
69
+ PlanCondition(kind="file_exists", path=path.as_posix(), expected="true"),
70
+ ],
71
+ postconditions=[
72
+ PlanCondition(kind="artifact_validates", path=path.as_posix()),
73
+ ],
74
+ risk_flags=risk_flags,
75
+ )
76
+ return plan, diff_text, baseline
@@ -0,0 +1,26 @@
1
+ """Markdown frontmatter parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+
10
+ def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
11
+ if not text.startswith("---\n") and not text.startswith("---\r\n"):
12
+ return {}, text
13
+ normalized = text.replace("\r\n", "\n")
14
+ parts = normalized.split("---\n", 2)
15
+ if len(parts) < 3:
16
+ return {}, text
17
+ raw = parts[1]
18
+ body = parts[2]
19
+ data = yaml.safe_load(raw) or {}
20
+ if not isinstance(data, dict):
21
+ data = {}
22
+ return data, body
23
+
24
+
25
+ def dump_frontmatter(data: dict[str, Any], body: str) -> str:
26
+ return "---\n" + yaml.safe_dump(data, sort_keys=False) + "---\n" + body.lstrip("\n")
@@ -0,0 +1,116 @@
1
+ """Markdown artifact helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from kc.artifacts.frontmatter import parse_frontmatter
9
+ from kc.provenance.citations import has_citation_or_marker
10
+
11
+
12
+ def read_markdown_artifact(path: Path) -> tuple[dict[str, Any], str, str]:
13
+ text = path.read_text(encoding="utf-8-sig")
14
+ frontmatter, body = parse_frontmatter(text)
15
+ return frontmatter, body, text
16
+
17
+
18
+ def markdown_body_line_offset(text: str) -> int:
19
+ normalized = text.replace("\r\n", "\n")
20
+ if not normalized.startswith("---\n"):
21
+ return 0
22
+ parts = normalized.split("---\n", 2)
23
+ if len(parts) < 3:
24
+ return 0
25
+ return ("---\n" + parts[1] + "---\n").count("\n")
26
+
27
+
28
+ def required_section_names(body: str) -> set[str]:
29
+ headings = set()
30
+ for line in body.splitlines():
31
+ if line.startswith("## "):
32
+ headings.add(line[3:].strip().lower())
33
+ return headings
34
+
35
+
36
+ def citation_coverage_issues(
37
+ body: str,
38
+ *,
39
+ status: str,
40
+ requires_citations: bool,
41
+ allow_uncited: bool,
42
+ line_offset: int = 0,
43
+ ) -> list[dict[str, Any]]:
44
+ issues: list[dict[str, Any]] = []
45
+ if not requires_citations:
46
+ return issues
47
+
48
+ in_code = False
49
+ paragraph: list[tuple[int, str]] = []
50
+
51
+ def flush() -> None:
52
+ nonlocal paragraph
53
+ if not paragraph:
54
+ return
55
+ text = " ".join(line for _line_no, line in paragraph).strip()
56
+ first_line = paragraph[0][0]
57
+ paragraph = []
58
+ if not text:
59
+ return
60
+ if text.startswith("#"):
61
+ return
62
+ if text.startswith("|") and text.endswith("|"):
63
+ return
64
+ if has_citation_or_marker(text):
65
+ if "[kc:uncited]" in text and not allow_uncited:
66
+ issues.append(
67
+ {
68
+ "code": "KC_VALIDATION_MISSING_CITATION",
69
+ "message": "[kc:uncited] is not allowed without --allow-uncited.",
70
+ "line": first_line,
71
+ }
72
+ )
73
+ if "[kc:todo]" in text and status != "draft":
74
+ issues.append(
75
+ {
76
+ "code": "KC_VALIDATION_TODO_IN_ACTIVE_ARTIFACT",
77
+ "message": "[kc:todo] is allowed only for draft artifacts.",
78
+ "line": first_line,
79
+ }
80
+ )
81
+ return
82
+ issues.append(
83
+ {
84
+ "code": "KC_VALIDATION_MISSING_CITATION",
85
+ "message": "Paragraph requires at least one citation token or explicit kc marker.",
86
+ "line": first_line,
87
+ }
88
+ )
89
+
90
+ for line_no, raw_line in enumerate(body.splitlines(), start=1 + line_offset):
91
+ line = raw_line.strip()
92
+ if line.startswith("```"):
93
+ flush()
94
+ in_code = not in_code
95
+ continue
96
+ if in_code:
97
+ continue
98
+ if not line:
99
+ flush()
100
+ continue
101
+ if line.startswith("#"):
102
+ flush()
103
+ continue
104
+ paragraph.append((line_no, line))
105
+ flush()
106
+ return issues
107
+
108
+
109
+ def markdown_title(frontmatter: dict[str, Any], body: str, fallback: str) -> str:
110
+ title = frontmatter.get("title")
111
+ if isinstance(title, str) and title.strip():
112
+ return title.strip()
113
+ for line in body.splitlines():
114
+ if line.startswith("# "):
115
+ return line[2:].strip()
116
+ return fallback
kc/atomic_write.py ADDED
@@ -0,0 +1,33 @@
1
+ """Atomic file writing helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+
11
+ def atomic_write_bytes(target: Path, data: bytes) -> None:
12
+ target.parent.mkdir(parents=True, exist_ok=True)
13
+ fd, tmp_name = tempfile.mkstemp(dir=target.parent, prefix=".kc_tmp_", suffix=target.suffix)
14
+ tmp_path = Path(tmp_name)
15
+ try:
16
+ with os.fdopen(fd, "wb") as f:
17
+ f.write(data)
18
+ f.flush()
19
+ os.fsync(f.fileno())
20
+ os.replace(tmp_path, target)
21
+ except Exception:
22
+ if tmp_path.exists():
23
+ tmp_path.unlink()
24
+ raise
25
+
26
+
27
+ def atomic_write_text(target: Path, text: str) -> None:
28
+ atomic_write_bytes(target, text.encode("utf-8"))
29
+
30
+
31
+ def copy_snapshot(src: Path, dest: Path) -> None:
32
+ dest.parent.mkdir(parents=True, exist_ok=True)
33
+ shutil.copy2(src, dest)
kc/cli.py ADDED
@@ -0,0 +1,284 @@
1
+ """Typer CLI application for kc."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Annotated, Any
7
+
8
+ import click
9
+ import typer
10
+ from typer.core import TyperGroup
11
+
12
+ from kc.errors import KcError
13
+ from kc.output import emit_error, init_request, is_interactive, is_llm_mode, state
14
+
15
+
16
+ def _value_after(args: list[str], option: str) -> str | None:
17
+ for idx, item in enumerate(args):
18
+ if item == option and idx + 1 < len(args):
19
+ return args[idx + 1]
20
+ if item.startswith(f"{option}="):
21
+ return item.split("=", 1)[1]
22
+ return None
23
+
24
+
25
+ def _initialize_error_state(args: list[str]) -> None:
26
+ if not state.request_id:
27
+ init_request(_value_after(args, "--request-id"))
28
+ requested_format = _value_after(args, "--format") or _value_after(args, "-f")
29
+ state.format = requested_format if requested_format in {"json", "table", "markdown"} else "json"
30
+ if is_llm_mode():
31
+ state.format = "json"
32
+ state.root_override = _value_after(args, "--root")
33
+ state.data_dir = _value_after(args, "--data-dir")
34
+ state.state_dir = _value_after(args, "--state-dir")
35
+ state.quiet = True
36
+
37
+
38
+ def _command_id_from_args(args: list[str]) -> str:
39
+ value_opts = {"--format", "-f", "--root", "--data-dir", "--state-dir", "--request-id"}
40
+ top_level = {
41
+ "guide",
42
+ "conformance",
43
+ "init",
44
+ "status",
45
+ "lint",
46
+ "export",
47
+ "source",
48
+ "index",
49
+ "context",
50
+ "artifact",
51
+ "citation",
52
+ "task",
53
+ "eval",
54
+ "doctor",
55
+ }
56
+ tokens: list[str] = []
57
+ index = 0
58
+ while index < len(args):
59
+ item = args[index]
60
+ if item in value_opts:
61
+ index += 2
62
+ continue
63
+ if any(item.startswith(f"{opt}=") for opt in value_opts):
64
+ index += 1
65
+ continue
66
+ if item.startswith("-"):
67
+ index += 1
68
+ continue
69
+ tokens.append(item)
70
+ if len(tokens) == 2:
71
+ break
72
+ index += 1
73
+ if not tokens or tokens[0] not in top_level:
74
+ return "kc"
75
+ if tokens[0] in {"source", "index", "context", "artifact", "citation", "task", "eval"} and len(tokens) > 1:
76
+ return f"{tokens[0]}.{tokens[1]}"
77
+ if tokens[0] == "doctor" and len(tokens) > 1:
78
+ return f"doctor.{tokens[1]}"
79
+ return tokens[0]
80
+
81
+
82
+ class FlexibleGroup(TyperGroup):
83
+ """Allow root global options before or after the first subcommand."""
84
+
85
+ _VALUE_OPTS = frozenset(("--format", "-f", "--root", "--data-dir", "--state-dir", "--request-id"))
86
+ _FLAG_OPTS = frozenset(("--quiet", "-q", "--no-input", "--version", "-V"))
87
+ _VALUE_PREFIXES = ("--format=", "--root=", "--data-dir=", "--state-dir=", "--request-id=")
88
+
89
+ def main(
90
+ self,
91
+ args: list[str] | None = None,
92
+ prog_name: str | None = None,
93
+ complete_var: str | None = None,
94
+ standalone_mode: bool = True,
95
+ **extra: Any,
96
+ ) -> object:
97
+ raw_args = list(sys.argv[1:] if args is None else args)
98
+ try:
99
+ result = super().main(
100
+ args=raw_args,
101
+ prog_name=prog_name,
102
+ complete_var=complete_var,
103
+ standalone_mode=False,
104
+ **extra,
105
+ )
106
+ if standalone_mode:
107
+ raise SystemExit(result if isinstance(result, int) else 0)
108
+ return result
109
+ except click.UsageError as exc:
110
+ if not standalone_mode:
111
+ raise
112
+ _initialize_error_state(raw_args)
113
+ emit_error(
114
+ _command_id_from_args(raw_args),
115
+ KcError(
116
+ code="KC_USAGE_ERROR",
117
+ message=exc.format_message(),
118
+ details={"usage": exc.format_message()},
119
+ ),
120
+ )
121
+
122
+ def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
123
+ cmd_idx: int | None = None
124
+ i = 0
125
+ while i < len(args):
126
+ item = args[i]
127
+ if not item.startswith("-"):
128
+ cmd_idx = i
129
+ break
130
+ if item in self._VALUE_OPTS:
131
+ i += 2
132
+ continue
133
+ if item.startswith(self._VALUE_PREFIXES):
134
+ i += 1
135
+ continue
136
+ i += 1
137
+ if cmd_idx is None:
138
+ return super().parse_args(ctx, args)
139
+ before = list(args[:cmd_idx])
140
+ cmd_and_after = list(args[cmd_idx:])
141
+ cmd_name = cmd_and_after[0]
142
+ sub_cmd = self.commands.get(cmd_name) if self.commands else None
143
+ sub_opts: set[str] = set()
144
+ if sub_cmd:
145
+ for param in sub_cmd.params:
146
+ sub_opts.update(param.opts)
147
+ sub_opts.update(getattr(param, "secondary_opts", []))
148
+ moved: list[str] = []
149
+ kept: list[str] = [cmd_name]
150
+ i = 1
151
+ end_of_opts = False
152
+ while i < len(cmd_and_after):
153
+ item = cmd_and_after[i]
154
+ if item == "--":
155
+ end_of_opts = True
156
+ kept.append(item)
157
+ i += 1
158
+ continue
159
+ if not end_of_opts and item in self._VALUE_OPTS and item not in sub_opts:
160
+ moved.append(item)
161
+ if i + 1 < len(cmd_and_after):
162
+ i += 1
163
+ moved.append(cmd_and_after[i])
164
+ i += 1
165
+ continue
166
+ if not end_of_opts and item in self._FLAG_OPTS and item not in sub_opts:
167
+ moved.append(item)
168
+ i += 1
169
+ continue
170
+ if (
171
+ not end_of_opts
172
+ and item.startswith(self._VALUE_PREFIXES)
173
+ and not any(item.startswith(f"{opt}=") for opt in sub_opts)
174
+ ):
175
+ moved.append(item)
176
+ i += 1
177
+ continue
178
+ kept.append(item)
179
+ i += 1
180
+ return super().parse_args(ctx, before + moved + kept)
181
+
182
+
183
+ app = typer.Typer(
184
+ name="kc",
185
+ cls=FlexibleGroup,
186
+ help=(
187
+ "kc — deterministic knowledge compiler harness.\n\n"
188
+ "Agents provide the intelligence. kc provides source registration, search, "
189
+ "context preparation, citation validation, safe apply, and task state."
190
+ ),
191
+ no_args_is_help=True,
192
+ pretty_exceptions_enable=False,
193
+ context_settings={"help_option_names": ["-h", "--help"]},
194
+ )
195
+
196
+
197
+ def _version_callback(value: bool) -> None:
198
+ if value:
199
+ from kc import __version__
200
+
201
+ typer.echo(f"kc {__version__}")
202
+ raise typer.Exit()
203
+
204
+
205
+ @app.callback()
206
+ def main(
207
+ format: Annotated[
208
+ str,
209
+ typer.Option("--format", "-f", help="Output format: json, table, or markdown."),
210
+ ] = "json",
211
+ root: Annotated[
212
+ str | None, typer.Option("--root", help="Workspace root override.")
213
+ ] = None,
214
+ data_dir: Annotated[
215
+ str | None, typer.Option("--data-dir", help="Knowledge data directory override.")
216
+ ] = None,
217
+ state_dir: Annotated[str | None, typer.Option("--state-dir", help="kc state directory override.")] = None,
218
+ quiet: Annotated[
219
+ bool, typer.Option("--quiet", "-q", help="Suppress stderr diagnostics.")
220
+ ] = False,
221
+ request_id: Annotated[
222
+ str | None, typer.Option("--request-id", help="Trace request ID.")
223
+ ] = None,
224
+ no_input: Annotated[
225
+ bool, typer.Option("--no-input", help="Fail instead of prompting.")
226
+ ] = False,
227
+ version: Annotated[
228
+ bool,
229
+ typer.Option(
230
+ "--version",
231
+ "-V",
232
+ help="Show version and exit.",
233
+ callback=_version_callback,
234
+ is_eager=True,
235
+ ),
236
+ ] = False,
237
+ ) -> None:
238
+ del version
239
+ init_request(request_id)
240
+ state.root_override = root
241
+ state.data_dir = data_dir
242
+ state.state_dir = state_dir
243
+ state.workspace_root = None
244
+ state.workspace_resolution_source = None
245
+ state.no_input = no_input or is_llm_mode()
246
+ if format not in {"json", "table", "markdown"}:
247
+ from kc.errors import KcError
248
+ from kc.output import emit_error
249
+
250
+ state.format = "json"
251
+ emit_error(
252
+ "kc",
253
+ KcError(
254
+ code="KC_VALIDATION_INVALID_ARGUMENT",
255
+ message=f"Unknown output format: {format}",
256
+ details={"requested": format, "supported": ["json", "table", "markdown"]},
257
+ ),
258
+ )
259
+ state.format = "json" if is_llm_mode() else format
260
+ state.quiet = quiet or is_llm_mode() or not is_interactive()
261
+
262
+
263
+ from kc.commands import artifact, citation, context, doctor, eval, index, source, task # noqa: E402
264
+ from kc.commands import conformance as conformance_command # noqa: E402
265
+ from kc.commands import export as export_command # noqa: E402
266
+ from kc.commands import guide as guide_command # noqa: E402
267
+ from kc.commands import init as init_command # noqa: E402
268
+ from kc.commands import lint as lint_command # noqa: E402
269
+ from kc.commands import status as status_command # noqa: E402
270
+
271
+ guide_command.register(app)
272
+ conformance_command.register(app)
273
+ init_command.register(app)
274
+ status_command.register(app)
275
+ lint_command.register(app)
276
+ export_command.register(app)
277
+ app.add_typer(source.app, name="source")
278
+ app.add_typer(index.app, name="index")
279
+ app.add_typer(context.app, name="context")
280
+ app.add_typer(artifact.app, name="artifact")
281
+ app.add_typer(citation.app, name="citation")
282
+ app.add_typer(task.app, name="task")
283
+ app.add_typer(eval.app, name="eval")
284
+ app.add_typer(doctor.app, name="doctor")
@@ -0,0 +1 @@
1
+ """CLI command modules."""