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/errors.py ADDED
@@ -0,0 +1,141 @@
1
+ """Typed errors and exit-code contract for kc."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ EXIT_OK = 0
9
+ EXIT_USAGE = 2
10
+ EXIT_VALIDATION = 10
11
+ EXIT_NOT_FOUND = 11
12
+ EXIT_ALREADY_EXISTS = 12
13
+ EXIT_CONFLICT = 13
14
+ EXIT_PROVENANCE = 20
15
+ EXIT_INDEX = 30
16
+ EXIT_RETRIEVAL_MODEL = 31
17
+ EXIT_WAITING = 40
18
+ EXIT_IO = 50
19
+ EXIT_LOCK = 60
20
+ EXIT_PERSISTENCE = 70
21
+ EXIT_UNSUPPORTED = 80
22
+ EXIT_INTERNAL = 90
23
+
24
+
25
+ ERROR_EXIT_MAP: dict[str, int] = {
26
+ "KC_USAGE_ERROR": EXIT_USAGE,
27
+ "KC_CONFIG_NOT_FOUND": EXIT_NOT_FOUND,
28
+ "KC_CONFIG_INVALID": EXIT_VALIDATION,
29
+ "KC_SOURCE_NOT_FOUND": EXIT_NOT_FOUND,
30
+ "KC_SOURCE_ALREADY_REGISTERED": EXIT_ALREADY_EXISTS,
31
+ "KC_SOURCE_STALE": EXIT_PROVENANCE,
32
+ "KC_SOURCE_UNSUPPORTED_MEDIA_TYPE": EXIT_UNSUPPORTED,
33
+ "KC_RANGE_NOT_FOUND": EXIT_NOT_FOUND,
34
+ "KC_ARTIFACT_NOT_FOUND": EXIT_NOT_FOUND,
35
+ "KC_ARTIFACT_SCHEMA_INVALID": EXIT_VALIDATION,
36
+ "KC_ARTIFACT_STATUS_INVALID": EXIT_VALIDATION,
37
+ "KC_CITATION_INVALID_TOKEN": EXIT_PROVENANCE,
38
+ "KC_CITATION_SOURCE_MISSING": EXIT_PROVENANCE,
39
+ "KC_CITATION_RANGE_MISSING": EXIT_PROVENANCE,
40
+ "KC_CITATION_STALE_SOURCE": EXIT_PROVENANCE,
41
+ "KC_VALIDATION_MISSING_CITATION": EXIT_VALIDATION,
42
+ "KC_VALIDATION_TODO_IN_ACTIVE_ARTIFACT": EXIT_VALIDATION,
43
+ "KC_VALIDATION_INVALID_ARGUMENT": EXIT_VALIDATION,
44
+ "KC_PLAN_PRECONDITION_FAILED": EXIT_CONFLICT,
45
+ "KC_CONFORMANCE_FAILED": EXIT_VALIDATION,
46
+ "KC_APPLY_REQUIRES_YES": EXIT_VALIDATION,
47
+ "KC_APPLY_NOT_VALIDATED": EXIT_VALIDATION,
48
+ "KC_LOCK_HELD": EXIT_LOCK,
49
+ "KC_INDEX_BUILD_FAILED": EXIT_INDEX,
50
+ "KC_RETRIEVAL_MODEL_UNAVAILABLE": EXIT_RETRIEVAL_MODEL,
51
+ "KC_UNSUPPORTED_FEATURE": EXIT_UNSUPPORTED,
52
+ "KC_PATH_OUTSIDE_REPO": EXIT_VALIDATION,
53
+ "KC_FILE_NOT_FOUND": EXIT_NOT_FOUND,
54
+ "KC_FILE_EXISTS": EXIT_ALREADY_EXISTS,
55
+ "KC_JSON_INVALID": EXIT_VALIDATION,
56
+ "KC_TASK_NOT_FOUND": EXIT_NOT_FOUND,
57
+ "KC_TASK_NOT_WAITING": EXIT_CONFLICT,
58
+ "KC_EVENT_INVALID": EXIT_VALIDATION,
59
+ "KC_INTERNAL_ERROR": EXIT_INTERNAL,
60
+ }
61
+
62
+
63
+ ERROR_CATEGORIES: dict[str, str] = {
64
+ "CONFIG": "configuration",
65
+ "USAGE": "usage",
66
+ "SOURCE": "source",
67
+ "RANGE": "source_range",
68
+ "ARTIFACT": "artifact",
69
+ "CITATION": "provenance",
70
+ "VALIDATION": "validation",
71
+ "PLAN": "plan",
72
+ "CONFORMANCE": "validation",
73
+ "APPLY": "apply",
74
+ "LOCK": "concurrency",
75
+ "INDEX": "index",
76
+ "RETRIEVAL": "retrieval",
77
+ "PATH": "validation",
78
+ "FILE": "io",
79
+ "JSON": "validation",
80
+ "TASK": "task",
81
+ "EVENT": "task",
82
+ "UNSUPPORTED": "unsupported",
83
+ "INTERNAL": "internal",
84
+ }
85
+
86
+
87
+ def exit_code_for(code: str) -> int:
88
+ return ERROR_EXIT_MAP.get(code, EXIT_INTERNAL)
89
+
90
+
91
+ def category_for(code: str) -> str:
92
+ parts = code.split("_")
93
+ if len(parts) >= 2:
94
+ return ERROR_CATEGORIES.get(parts[1], "internal")
95
+ return "internal"
96
+
97
+
98
+ @dataclass
99
+ class KcError(Exception):
100
+ """Domain error surfaced through the kc.result.v1 envelope."""
101
+
102
+ code: str
103
+ message: str
104
+ details: dict[str, Any] = field(default_factory=dict)
105
+ category: str | None = None
106
+ exit_code: int | None = None
107
+ retryable: bool = False
108
+ suggested_action: str | None = None
109
+
110
+ def __post_init__(self) -> None:
111
+ super().__init__(self.message)
112
+ if self.category is None:
113
+ self.category = category_for(self.code)
114
+ if self.exit_code is None:
115
+ self.exit_code = exit_code_for(self.code)
116
+ if self.suggested_action is None:
117
+ if self.retryable:
118
+ self.suggested_action = "retry"
119
+ elif self.exit_code in {EXIT_USAGE, EXIT_VALIDATION, EXIT_NOT_FOUND, EXIT_ALREADY_EXISTS}:
120
+ self.suggested_action = "fix_input"
121
+ elif self.exit_code == EXIT_LOCK:
122
+ self.suggested_action = "inspect lock with kc doctor locks or retry later"
123
+ else:
124
+ self.suggested_action = "escalate"
125
+
126
+ def to_message(self) -> dict[str, Any]:
127
+ return {
128
+ "code": self.code,
129
+ "category": self.category,
130
+ "message": self.message,
131
+ "exit_code": self.exit_code,
132
+ "retryable": self.retryable,
133
+ "suggested_action": self.suggested_action,
134
+ "details": self.details,
135
+ }
136
+
137
+
138
+ def validation_error(
139
+ message: str, *, code: str = "KC_ARTIFACT_SCHEMA_INVALID", **details: Any
140
+ ) -> KcError:
141
+ return KcError(code=code, message=message, details=details)
kc/fingerprints.py ADDED
@@ -0,0 +1,35 @@
1
+ """File and text fingerprint helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from pathlib import Path
7
+
8
+
9
+ def sha256_bytes(data: bytes) -> str:
10
+ return f"sha256:{hashlib.sha256(data).hexdigest()}"
11
+
12
+
13
+ def normalize_text(text: str) -> str:
14
+ return text.replace("\r\n", "\n").replace("\r", "\n")
15
+
16
+
17
+ def raw_fingerprint(path: Path) -> str:
18
+ h = hashlib.sha256()
19
+ with path.open("rb") as f:
20
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
21
+ h.update(chunk)
22
+ return f"sha256:{h.hexdigest()}"
23
+
24
+
25
+ def normalized_fingerprint(path: Path) -> str:
26
+ text = normalize_text(path.read_text(encoding="utf-8-sig"))
27
+ return sha256_bytes(text.encode("utf-8"))
28
+
29
+
30
+ def text_hash(text: str) -> str:
31
+ return sha256_bytes(normalize_text(text).encode("utf-8"))
32
+
33
+
34
+ def fingerprint_text(text: str) -> str:
35
+ return text_hash(text)
kc/ids.py ADDED
@@ -0,0 +1,23 @@
1
+ """Deterministic-looking public IDs with kc prefixes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import uuid
7
+
8
+ try:
9
+ from ulid import ULID
10
+ except Exception: # pragma: no cover - fallback for unusual environments
11
+ ULID = None # type: ignore[assignment]
12
+
13
+
14
+ def new_id(prefix: str) -> str:
15
+ if ULID is not None:
16
+ return f"{prefix}_{ULID()}"
17
+ return f"{prefix}_{uuid.uuid4().hex}"
18
+
19
+
20
+ def stable_id(prefix: str, *parts: str) -> str:
21
+ material = "\x1f".join(parts)
22
+ digest = hashlib.sha256(material.encode("utf-8")).hexdigest()[:26].upper()
23
+ return f"{prefix}_{digest}"
kc/locks.py ADDED
@@ -0,0 +1,65 @@
1
+ """Simple visible lock files for write commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import socket
8
+ from contextlib import suppress
9
+ from dataclasses import dataclass
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from kc.errors import KcError
15
+ from kc.ids import new_id
16
+ from kc.output import state
17
+
18
+
19
+ @dataclass
20
+ class FileLock:
21
+ locks_dir: Path
22
+ name: str
23
+ command: str
24
+ target: str
25
+
26
+ def __post_init__(self) -> None:
27
+ self.locks_dir.mkdir(parents=True, exist_ok=True)
28
+ self.path = self.locks_dir / f"{self.name}.lock"
29
+ self.acquired = False
30
+
31
+ def __enter__(self) -> FileLock:
32
+ metadata = {
33
+ "schema_version": "kc.lock.v1",
34
+ "lock_id": new_id("lock"),
35
+ "created_at": datetime.now(UTC).isoformat(),
36
+ "pid": os.getpid(),
37
+ "hostname": socket.gethostname(),
38
+ "command": self.command,
39
+ "request_id": state.request_id,
40
+ "target": self.target,
41
+ }
42
+ try:
43
+ fd = os.open(str(self.path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
44
+ except FileExistsError as exc:
45
+ holder: dict[str, Any] = {}
46
+ try:
47
+ holder = json.loads(self.path.read_text(encoding="utf-8"))
48
+ except Exception:
49
+ holder = {"lock_file": str(self.path)}
50
+ raise KcError(
51
+ code="KC_LOCK_HELD",
52
+ message=f"Lock is held: {self.path}",
53
+ details={"lock_file": str(self.path), "holder": holder},
54
+ retryable=True,
55
+ ) from exc
56
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
57
+ json.dump(metadata, f, indent=2)
58
+ f.write("\n")
59
+ self.acquired = True
60
+ return self
61
+
62
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
63
+ if self.acquired:
64
+ with suppress(FileNotFoundError):
65
+ self.path.unlink()
kc/models/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from kc.models.artifact import ArtifactRecord
2
+ from kc.models.citation import CitationEdgeRecord
3
+ from kc.models.plan import PlanRecord
4
+ from kc.models.source import Authority, SourceRecord
5
+ from kc.models.source_range import Locator, SourceRangeRecord
6
+ from kc.models.task import TaskRecord
7
+
8
+ __all__ = [
9
+ "ArtifactRecord",
10
+ "Authority",
11
+ "CitationEdgeRecord",
12
+ "Locator",
13
+ "PlanRecord",
14
+ "SourceRangeRecord",
15
+ "SourceRecord",
16
+ "TaskRecord",
17
+ ]
kc/models/artifact.py ADDED
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SourceRef(BaseModel):
9
+ source_id: str
10
+ range_ids: list[str] = Field(default_factory=list)
11
+
12
+
13
+ class ArtifactRecord(BaseModel):
14
+ schema_version: Literal["kc.artifact.v1"] = "kc.artifact.v1"
15
+ artifact_id: str
16
+ path: str
17
+ artifact_type: Literal[
18
+ "knowledge_page",
19
+ "glossary",
20
+ "decision_note",
21
+ "source_index",
22
+ "log_entry",
23
+ "eval_pack",
24
+ ] = "knowledge_page"
25
+ title: str
26
+ status: Literal["draft", "active", "deprecated", "superseded"] = "draft"
27
+ domain: list[str] = Field(default_factory=list)
28
+ fingerprint: str
29
+ created_at: str
30
+ updated_at: str
31
+ last_validated_at: str | None = None
32
+ validation_status: Literal["passed", "failed", "unknown"] = "unknown"
33
+ source_refs: list[SourceRef] = Field(default_factory=list)
34
+ metadata: dict[str, Any] = Field(default_factory=dict)
kc/models/citation.py ADDED
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from kc.models.source_range import Locator
8
+
9
+
10
+ class ArtifactLocator(BaseModel):
11
+ kind: Literal["line_range"] = "line_range"
12
+ start_line: int
13
+ end_line: int
14
+
15
+
16
+ class CitationEdgeRecord(BaseModel):
17
+ schema_version: Literal["kc.citation_edge.v1"] = "kc.citation_edge.v1"
18
+ edge_id: str
19
+ artifact_id: str | None = None
20
+ artifact_path: str
21
+ artifact_locator: ArtifactLocator
22
+ citation_token: str
23
+ source_id: str
24
+ range_id: str | None = None
25
+ source_fingerprint_at_validation: str | None = None
26
+ validated_at: str
27
+ status: Literal[
28
+ "valid",
29
+ "missing_source",
30
+ "missing_range",
31
+ "stale_source",
32
+ "locator_mismatch",
33
+ "invalid_token",
34
+ ] = "valid"
35
+ metadata: dict[str, Any] = Field(default_factory=dict)
36
+
37
+
38
+ class ParsedCitation(BaseModel):
39
+ token: str
40
+ source_id: str
41
+ range_id: str | None = None
42
+ token_version: Literal["v1", "v2"] = "v1"
43
+ kind: Literal["line_range", "json_pointer", "csv_row_range"]
44
+ line: int
45
+ start_line: int | None = None
46
+ end_line: int | None = None
47
+ pointer: str | None = None
48
+ start_row: int | None = None
49
+ end_row: int | None = None
50
+
51
+ @property
52
+ def locator(self) -> Locator:
53
+ return Locator(
54
+ kind=self.kind,
55
+ start_line=self.start_line,
56
+ end_line=self.end_line,
57
+ pointer=self.pointer,
58
+ start_row=self.start_row,
59
+ end_row=self.end_row,
60
+ )
kc/models/context.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ContextPackRecord(BaseModel):
9
+ schema_version: Literal["kc.context_pack.v1"] = "kc.context_pack.v1"
10
+ context_id: str
11
+ created_at: str
12
+ ask: str
13
+ shape: str = "knowledge_page"
14
+ target: str | None = None
15
+ grounding_policy: str = "required"
16
+ workspace: dict[str, Any] = Field(default_factory=dict)
17
+ candidate_ranges: list[dict[str, Any]] = Field(default_factory=list)
18
+ existing_artifacts: list[dict[str, Any]] = Field(default_factory=list)
19
+ citation_policy: dict[str, Any] = Field(default_factory=dict)
20
+ artifact_policy: dict[str, Any] = Field(default_factory=dict)
21
+ agent_instructions: list[str] = Field(default_factory=list)
22
+ next_commands: list[str] = Field(default_factory=list)
23
+ validation: dict[str, Any] = Field(default_factory=dict)
kc/models/eval.py ADDED
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class EvalCase(BaseModel):
9
+ id: str
10
+ query: str
11
+ domain: str | None = None
12
+ limit: int = 10
13
+ expected_source_ids: list[str] = Field(default_factory=list)
14
+ expected_range_ids: list[str] = Field(default_factory=list)
15
+ must_include_citation_tokens: list[str] = Field(default_factory=list)
16
+ min_recall_at_k: float = 1.0
17
+
18
+
19
+ class EvalPack(BaseModel):
20
+ schema_version: Literal["kc.eval_pack.v1"] = "kc.eval_pack.v1"
21
+ cases: list[EvalCase] = Field(default_factory=list)
kc/models/plan.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class PlanOperation(BaseModel):
9
+ op_id: str
10
+ kind: str
11
+ path: str
12
+ before_fingerprint: str | None = None
13
+ after_fingerprint: str | None = None
14
+ risk: Literal["low", "medium", "high"] = "medium"
15
+ diff_path: str | None = None
16
+ requires_yes: bool = True
17
+ details: dict[str, Any] = Field(default_factory=dict)
18
+
19
+
20
+ class PlanCondition(BaseModel):
21
+ kind: str
22
+ path: str | None = None
23
+ expected: str | None = None
24
+
25
+
26
+ class PlanRecord(BaseModel):
27
+ schema_version: Literal["kc.plan.v1"] = "kc.plan.v1"
28
+ plan_id: str
29
+ created_at: str
30
+ command: str
31
+ mode: Literal["dry_run", "apply"] = "dry_run"
32
+ idempotency_key: str | None = None
33
+ operations: list[PlanOperation] = Field(default_factory=list)
34
+ preconditions: list[PlanCondition] = Field(default_factory=list)
35
+ postconditions: list[PlanCondition] = Field(default_factory=list)
36
+ risk_flags: list[str] = Field(default_factory=list)
37
+ metadata: dict[str, Any] = Field(default_factory=dict)
kc/models/source.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Authority(BaseModel):
9
+ level: Literal["unknown", "informal", "team-approved", "enterprise-approved", "regulatory"] = (
10
+ "unknown"
11
+ )
12
+ owner: str | None = None
13
+ review_date: str | None = None
14
+ notes: str = "Do not infer authority from file location."
15
+
16
+
17
+ class SourceRecord(BaseModel):
18
+ schema_version: Literal["kc.source.v1"] = "kc.source.v1"
19
+ source_id: str
20
+ uri: str
21
+ display_name: str
22
+ media_type: str = "text/plain"
23
+ fingerprint: str
24
+ raw_fingerprint: str | None = None
25
+ normalized_fingerprint: str | None = None
26
+ fingerprint_algorithm: str = "sha256-normalized-v1"
27
+ registered_at: str
28
+ registered_by: str = "agent-or-human"
29
+ status: Literal["active", "stale", "superseded", "missing", "excluded"] = "active"
30
+ immutability: Literal["fingerprinted", "external", "copied"] = "fingerprinted"
31
+ domain: list[str] = Field(default_factory=list)
32
+ authority: Authority = Field(default_factory=Authority)
33
+ metadata: dict[str, Any] = Field(default_factory=dict)
34
+ canonical_source_key: str | None = None
35
+ current_revision_id: str | None = None
36
+ first_registered_at: str | None = None
37
+ last_refreshed_at: str | None = None
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class Locator(BaseModel):
9
+ kind: Literal["line_range", "json_pointer", "csv_row_range", "page_text_range"] = "line_range"
10
+ start_line: int | None = None
11
+ end_line: int | None = None
12
+ pointer: str | None = None
13
+ start_row: int | None = None
14
+ end_row: int | None = None
15
+
16
+
17
+ class SourceRangeRecord(BaseModel):
18
+ schema_version: Literal["kc.source_range.v1"] = "kc.source_range.v1"
19
+ range_id: str
20
+ source_id: str
21
+ revision_id: str | None = None
22
+ source_fingerprint: str
23
+ locator: Locator
24
+ text_hash: str
25
+ excerpt: str
26
+ tokens_estimate: int = 0
27
+ extracted_at: str
28
+ status: Literal["active", "superseded"] = "active"
29
+ metadata: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SourceRevisionRecord(BaseModel):
9
+ schema_version: Literal["kc.source_revision.v1"] = "kc.source_revision.v1"
10
+ revision_id: str
11
+ source_id: str
12
+ uri: str
13
+ raw_fingerprint: str
14
+ normalized_fingerprint: str
15
+ media_type: str
16
+ extracted_at: str
17
+ status: Literal["active", "superseded"] = "active"
18
+ previous_revision_id: str | None = None
19
+ metadata: dict[str, Any] = Field(default_factory=dict)
kc/models/task.py ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class TaskRecord(BaseModel):
9
+ schema_version: Literal["kc.task.v1"] = "kc.task.v1"
10
+ task_id: str
11
+ goal: str
12
+ status: Literal[
13
+ "created",
14
+ "awaiting_agent",
15
+ "awaiting_validation",
16
+ "awaiting_apply",
17
+ "completed",
18
+ "blocked",
19
+ "cancelled",
20
+ "failed",
21
+ ] = "awaiting_agent"
22
+ context_pack_id: str | None = None
23
+ context_pack_path: str | None = None
24
+ created_at: str
25
+ updated_at: str
26
+ shape: str = "knowledge_page"
27
+ domain: list[str] = Field(default_factory=list)
28
+ candidate_sources: list[str] = Field(default_factory=list)
29
+ candidate_ranges: list[str] = Field(default_factory=list)
30
+ target_artifacts: list[str] = Field(default_factory=list)
31
+ agent_instructions: list[str] = Field(default_factory=list)
32
+ next_commands: list[str] = Field(default_factory=list)
33
+ expected_event_name: str | None = None
34
+ expected_event_schema: dict[str, Any] | None = None
35
+ events: list[dict[str, Any]] = Field(default_factory=list)