whykit 0.3.1__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 (84) hide show
  1. whykit/__init__.py +21 -0
  2. whykit/adopt.py +776 -0
  3. whykit/backlinks.py +106 -0
  4. whykit/check.py +320 -0
  5. whykit/ci_formats.py +283 -0
  6. whykit/cli.py +1719 -0
  7. whykit/completion.py +193 -0
  8. whykit/config.py +233 -0
  9. whykit/console.py +217 -0
  10. whykit/context.py +204 -0
  11. whykit/contract.py +210 -0
  12. whykit/contract_schemas/backlinks-report.schema.json +61 -0
  13. whykit/contract_schemas/context-bundle.schema.json +27 -0
  14. whykit/contract_schemas/context-pack.schema.json +29 -0
  15. whykit/contract_schemas/impact-report.schema.json +122 -0
  16. whykit/contract_schemas/query-result.schema.json +105 -0
  17. whykit/contract_schemas/status-report.schema.json +176 -0
  18. whykit/contract_schemas/trace-report.schema.json +211 -0
  19. whykit/diff.py +823 -0
  20. whykit/evidence.py +178 -0
  21. whykit/explorer_index.py +554 -0
  22. whykit/graph.py +318 -0
  23. whykit/immutability.py +624 -0
  24. whykit/impact.py +272 -0
  25. whykit/init_layout.py +205 -0
  26. whykit/io.py +563 -0
  27. whykit/lint.py +2190 -0
  28. whykit/lsp.py +1241 -0
  29. whykit/markdown_code.py +107 -0
  30. whykit/mcp_server.py +2289 -0
  31. whykit/messages.py +46 -0
  32. whykit/pack.py +294 -0
  33. whykit/parse_cache.py +752 -0
  34. whykit/placeholders.py +84 -0
  35. whykit/promote.py +651 -0
  36. whykit/py.typed +0 -0
  37. whykit/query.py +174 -0
  38. whykit/review.py +527 -0
  39. whykit/rule_policy.py +1063 -0
  40. whykit/rules.py +274 -0
  41. whykit/scaffold.py +784 -0
  42. whykit/sensitivity.py +95 -0
  43. whykit/snapshot.py +319 -0
  44. whykit/status.py +167 -0
  45. whykit/tables.py +120 -0
  46. whykit/template/00-context/README.md +17 -0
  47. whykit/template/00-context/company.md +52 -0
  48. whykit/template/00-context/evidence-register.md +34 -0
  49. whykit/template/00-context/goals.md +38 -0
  50. whykit/template/00-context/review-log.md +22 -0
  51. whykit/template/00-context/terminology.md +34 -0
  52. whykit/template/01-strategy/README.md +7 -0
  53. whykit/template/02-discoverability/README.md +10 -0
  54. whykit/template/03-website/README.md +7 -0
  55. whykit/template/04-automation/README.md +10 -0
  56. whykit/template/05-operations/README.md +19 -0
  57. whykit/template/06-decisions/README.md +11 -0
  58. whykit/template/06-decisions/decision-log.md +38 -0
  59. whykit/template/07-research/README.md +10 -0
  60. whykit/template/AGENTS.md +163 -0
  61. whykit/template/Home.md +57 -0
  62. whykit/template/INTEROP.md +117 -0
  63. whykit/template/OBSIDIAN.md +92 -0
  64. whykit/template/README.md +51 -0
  65. whykit/template/assets/README.md +6 -0
  66. whykit/template/reports/README.md +7 -0
  67. whykit/template/templates/README.md +7 -0
  68. whykit/template/templates/automation-spec-template.md +70 -0
  69. whykit/template/templates/decision-record-template.md +65 -0
  70. whykit/template/templates/document-template.md +55 -0
  71. whykit/template/templates/experiment-template.md +62 -0
  72. whykit/template/templates/meeting-note-template.md +47 -0
  73. whykit/template/templates/page-brief-template.md +63 -0
  74. whykit/template/templates/research-note-template.md +53 -0
  75. whykit/template/templates/source-ingestion-template.md +43 -0
  76. whykit/template/whykit.toml +39 -0
  77. whykit/trace.py +272 -0
  78. whykit/vault_index.py +190 -0
  79. whykit-0.3.1.dist-info/METADATA +162 -0
  80. whykit-0.3.1.dist-info/RECORD +84 -0
  81. whykit-0.3.1.dist-info/WHEEL +4 -0
  82. whykit-0.3.1.dist-info/entry_points.txt +3 -0
  83. whykit-0.3.1.dist-info/licenses/LICENSE +201 -0
  84. whykit-0.3.1.dist-info/licenses/NOTICE +13 -0
whykit/context.py ADDED
@@ -0,0 +1,204 @@
1
+ """Build bounded, evidence-aware context packs for people and AI agents."""
2
+ from __future__ import annotations
3
+
4
+ from .io import consistent_read
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ from dataclasses import asdict
10
+ from pathlib import Path
11
+
12
+ from .contract import emit_error, vault_not_found
13
+ from .impact import analyze_impact
14
+ from .lint import (
15
+ Note,
16
+ path_cache,
17
+ DECISION_ID_RE,
18
+ EVIDENCE_ID_RE,
19
+ evidence_register,
20
+ find_vault_root,
21
+ is_vault_root,
22
+ lint,
23
+ rel,
24
+ )
25
+ from .vault_index import VaultIndex
26
+ from .console import emit_machine
27
+
28
+
29
+ def _resolve_note(root: Path, target: str, vault: VaultIndex):
30
+ if DECISION_ID_RE.fullmatch(target):
31
+ matches = [
32
+ note for note in vault.notes
33
+ if str(note.front.get("decision_id") or "").strip() == target
34
+ ]
35
+ return (matches[0], False) if len(matches) == 1 else (None, len(matches) > 1)
36
+ path, ambiguous = vault.resolve_link(target)
37
+ if path is None:
38
+ return None, ambiguous
39
+ return vault.note_for(path), ambiguous
40
+
41
+
42
+ def _evidence_details(root: Path, ids: list[str]) -> list[dict]:
43
+ active, retired, _ = evidence_register(root)
44
+ out = []
45
+ for evidence_id in sorted(set(ids)):
46
+ row = active.get(evidence_id) or retired.get(evidence_id)
47
+ out.append({
48
+ "id": evidence_id,
49
+ "state": "active" if evidence_id in active else "retired" if evidence_id in retired else "missing",
50
+ "record": row,
51
+ })
52
+ return out
53
+
54
+
55
+ FRONT_MATTER_RE = re.compile(r"\A\ufeff?---[ \t]*\r?\n.*?^---[ \t]*\r?$\n?", re.S | re.M)
56
+ SUMMARY_KEYS = ("summary", "description")
57
+
58
+
59
+ def _budgeted(note: Note, max_chars: int) -> tuple[str, bool]:
60
+ """The most useful *max_chars* of a note that does not fit whole.
61
+
62
+ Front matter goes first, because the pack already carries it as
63
+ structured fields (``record``): a 200-character budget spent on
64
+ ``aliases:`` and ``tags:`` would say nothing about the note. What is left
65
+ is the ``summary`` (or ``description``) front-matter value when the note
66
+ has one, then the body from its first line.
67
+ """
68
+ match = FRONT_MATTER_RE.match(note.text)
69
+ if not match:
70
+ return note.text[:max_chars], False
71
+ body = note.text[match.end():].lstrip("\r\n")
72
+ summary = next(
73
+ (str(note.front.get(key)).strip() for key in SUMMARY_KEYS if str(note.front.get(key) or "").strip()),
74
+ "",
75
+ )
76
+ if summary and summary not in body:
77
+ body = f"{summary}\n\n{body}"
78
+ return body[:max_chars], True
79
+
80
+
81
+ @consistent_read
82
+ @path_cache()
83
+ def build_context(
84
+ root: Path,
85
+ target: str,
86
+ *,
87
+ max_chars: int = 20_000,
88
+ include_body: bool = True,
89
+ vault: VaultIndex | None = None,
90
+ ) -> dict:
91
+ if max_chars < 0:
92
+ raise ValueError("max_chars must be >= 0")
93
+ vault_index = vault or VaultIndex.load(root)
94
+ impact = analyze_impact(root, target, vault=vault_index)
95
+ if not impact.get("exists"):
96
+ return {
97
+ "contract_version": 1,
98
+ "target": target,
99
+ "exists": False,
100
+ "ambiguous": bool(impact.get("ambiguous")),
101
+ "kind": impact.get("kind"),
102
+ }
103
+
104
+ if EVIDENCE_ID_RE.fullmatch(target):
105
+ return {
106
+ "contract_version": 1,
107
+ "target": target,
108
+ "exists": True,
109
+ "kind": "evidence",
110
+ "evidence": _evidence_details(root, [target])[0],
111
+ "references": impact.get("references", []),
112
+ "replacement": impact.get("replacement"),
113
+ }
114
+
115
+ note, ambiguous = _resolve_note(root, target, vault_index)
116
+ if note is None:
117
+ return {
118
+ "contract_version": 1,
119
+ "target": target,
120
+ "exists": False,
121
+ "ambiguous": ambiguous,
122
+ "kind": impact.get("kind", "document"),
123
+ }
124
+
125
+ content = note.text
126
+ truncated = False
127
+ front_matter_omitted = False
128
+ if include_body and len(content) > max_chars:
129
+ content, front_matter_omitted = _budgeted(note, max_chars)
130
+ truncated = True
131
+ if not include_body:
132
+ content = ""
133
+
134
+ source_ids = list(note.cited_evidence)
135
+ note_path = rel(root, note.path)
136
+ _, scoped_findings = lint(root, [note_path], orphans=False, secrets=False, vault=vault_index)
137
+ scoped_findings = [item for item in scoped_findings if item.path == note_path]
138
+ return {
139
+ "contract_version": 1,
140
+ "target": target,
141
+ "exists": True,
142
+ "kind": impact.get("kind", "document"),
143
+ "record": impact.get("record"),
144
+ "content": content,
145
+ "content_truncated": truncated,
146
+ **({"front_matter_omitted": True} if front_matter_omitted else {}),
147
+ "content_chars": len(note.text),
148
+ "evidence": _evidence_details(root, source_ids),
149
+ "incoming": impact.get("incoming", []),
150
+ "outgoing": impact.get("outgoing", []),
151
+ "supersedes": impact.get("supersedes"),
152
+ "superseded_by": impact.get("superseded_by", []),
153
+ "findings": [asdict(item) for item in scoped_findings],
154
+ }
155
+
156
+
157
+ def _human(report: dict) -> None:
158
+ if not report.get("exists"):
159
+ print(f"{report['target']}: {'ambiguous' if report.get('ambiguous') else 'not found'}")
160
+ return
161
+ print(f"Context for {report['target']} [{report['kind']}]")
162
+ if report["kind"] == "evidence":
163
+ evidence = report["evidence"]
164
+ print(f" state {evidence['state']}")
165
+ print(f" references {len(report.get('references', []))}")
166
+ if evidence.get("record"):
167
+ row = evidence["record"]
168
+ print(f" source {row.get('source') or '—'}")
169
+ print(f" location {row.get('location') or '—'}")
170
+ return
171
+ record = report.get("record") or {}
172
+ print(f" document {record.get('path', '—')}")
173
+ print(f" owner {record.get('owner') or '—'}")
174
+ print(f" status {record.get('status') or '—'}")
175
+ print(f" evidence {len(report.get('evidence', []))}")
176
+ print(f" backlinks {len(report.get('incoming', []))}")
177
+ print(f" outgoing {len(report.get('outgoing', []))}")
178
+ print(f" findings {len(report.get('findings', []))}")
179
+ if report.get("content"):
180
+ print("\n--- document ---")
181
+ print(report["content"])
182
+ if report.get("content_truncated"):
183
+ print("\n[content truncated]")
184
+
185
+
186
+ def main(argv: list[str] | None = None) -> int:
187
+ parser = argparse.ArgumentParser(prog="whykit context", description="Build a bounded evidence-aware context pack for one WhyKit target.")
188
+ parser.add_argument("target", help="E-NNN, D-NNN, path, stem or alias")
189
+ parser.add_argument("--root", help="vault root (default: nearest vault)")
190
+ parser.add_argument("--max-chars", type=int, default=20_000)
191
+ parser.add_argument("--no-body", action="store_true")
192
+ parser.add_argument("--json", action="store_true")
193
+ args = parser.parse_args(argv)
194
+ if args.max_chars < 0:
195
+ return emit_error("invalid_argument", "--max-chars must be >= 0", json_mode=args.json)
196
+ root = Path(args.root).expanduser().resolve() if args.root else find_vault_root()
197
+ if root is None or not is_vault_root(root):
198
+ return vault_not_found(args.root, json_mode=args.json)
199
+ report = build_context(root, args.target, max_chars=args.max_chars, include_body=not args.no_body)
200
+ if args.json:
201
+ emit_machine(json.dumps(report, ensure_ascii=False, indent=2))
202
+ else:
203
+ _human(report)
204
+ return 0 if report.get("exists") else 1
whykit/contract.py ADDED
@@ -0,0 +1,210 @@
1
+ """The machine contract shared by every command that writes JSON to stdout.
2
+
3
+ Success payloads carry ``contract_version``. Failures that stop a command from
4
+ producing its report are written as one error object instead::
5
+
6
+ {"contract_version": 1, "error": {"code": "...", "message": "...", "hint": "..."}}
7
+
8
+ ``code`` is stable and listed in :data:`ERROR_CODES`; ``message`` and ``hint``
9
+ are for people and may be reworded. Exit codes do not change: the error object
10
+ is an additional channel, and the same human-readable text still goes to
11
+ stderr, so scripts that already read stderr keep working.
12
+
13
+ See ``docs/automation.md`` for the full policy and ``schemas/`` for the shapes.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ from typing import Any, TextIO
20
+
21
+ from .console import emit_machine
22
+ from .messages import no_vault
23
+
24
+ CONTRACT_VERSION = 1
25
+
26
+ # Stable error codes. Adding a code is a compatible change; renaming or removing
27
+ # one is a breaking change and needs a CHANGELOG entry, exactly like a lint rule
28
+ # code. Keep this table, schemas/error.schema.json and docs/automation.md in
29
+ # step; tests/test_machine_contract.py enforces it.
30
+ ERROR_CODES: dict[str, str] = {
31
+ "usage": "The command line could not be parsed, or options conflict.",
32
+ "invalid_argument": "An option or argument value is malformed or out of range (a date, a number, an ID, a snapshot file).",
33
+ "invalid_target": "The named source or destination exists but cannot be used by this command.",
34
+ "vault_not_found": "No WhyKit vault at --root, or at or above the working directory.",
35
+ "invalid_config": "whykit.toml could not be read or failed validation.",
36
+ "not_found": "The vault record named as the command's target does not exist.",
37
+ "unsafe_path": "A path was refused because it escapes the vault or passes through a symlink.",
38
+ "target_exists": "The command refused to overwrite existing content.",
39
+ "operation_rejected": "The vault refused the requested change, e.g. an already retired ID or a reference to an unknown record.",
40
+ "missing_dependency": "A required external tool or bundled file is missing (git, the vault template).",
41
+ "git_error": "Git could not answer, e.g. an unknown ref or a directory that is not a repository.",
42
+ "vault_invalid": "The vault is broken in a way that blocks this command: lint errors, or a missing register or log.",
43
+ "io_error": "The filesystem refused a read or write.",
44
+ "interrupted": "The command was interrupted.",
45
+ "internal_error": "WhyKit itself failed: an unexpected exception, which is a bug to report.",
46
+ }
47
+
48
+ # Where the internal_error hint sends people. Kept here so the CLI and the docs
49
+ # test agree on one address.
50
+ ISSUES_URL = "https://github.com/CometWeb-io/whykit/issues"
51
+
52
+ # Each code has exactly one exit code. 1 means the command ran and the answer is
53
+ # a problem with the vault (a missing target, a broken vault); 2 means it could
54
+ # not run as asked; 70 means WhyKit crashed. Everything not listed here exits 2.
55
+ ERROR_EXIT_CODES: dict[str, int] = {code: 2 for code in ERROR_CODES} | {
56
+ "not_found": 1,
57
+ "vault_invalid": 1,
58
+ "interrupted": 130,
59
+ # EX_SOFTWARE from sysexits.h: distinct from 1 and 2, so a CI job can tell
60
+ # "WhyKit broke" from "the vault is wrong" and "the job is misconfigured".
61
+ "internal_error": 70,
62
+ }
63
+
64
+
65
+ class TargetNotFound(ValueError):
66
+ """The record a command was asked to act on does not exist.
67
+
68
+ A ``ValueError`` so callers that already catch ``ValueError`` keep working;
69
+ the CLI reports it as ``not_found`` with exit code 1, matching read commands
70
+ that report a missing target as ``"exists": false`` with exit code 1.
71
+ """
72
+
73
+
74
+ # The JSON Schema in ``schemas/`` that each command's JSON stdout follows.
75
+ # Every command that accepts ``--json`` (or always writes JSON) is listed; a
76
+ # test walks the argument parser to keep this table complete.
77
+ OUTPUT_SCHEMAS: dict[str, str] = {
78
+ "init": "init-result.schema.json",
79
+ "recover": "recovery-result.schema.json",
80
+ "lint": "lint-report.schema.json",
81
+ "new decision": "record-create.schema.json",
82
+ "new evidence": "record-create.schema.json",
83
+ "new note": "record-create.schema.json",
84
+ "status": "status-report.schema.json",
85
+ "graph": "graph.schema.json",
86
+ "backlinks": "backlinks-report.schema.json",
87
+ "impact": "impact-report.schema.json",
88
+ "trace": "trace-report.schema.json",
89
+ "query": "query-result.schema.json",
90
+ "context": "context-pack.schema.json",
91
+ "pack": "context-bundle.schema.json",
92
+ "review list": "review-queue.schema.json",
93
+ "review record": "review-record-result.schema.json",
94
+ "review approve": "decision-approval-result.schema.json",
95
+ "snapshot": "snapshot.schema.json",
96
+ "verify-snapshot": "snapshot-verify.schema.json",
97
+ "check": "check-report.schema.json",
98
+ "policy": "policy.schema.json",
99
+ "evidence list": "evidence-list.schema.json",
100
+ "evidence retire": "evidence-retire-result.schema.json",
101
+ "adopt": "adopt-report.schema.json",
102
+ "history": "history-report.schema.json",
103
+ "diff": "diff-report.schema.json",
104
+ "rules": "rule-catalog.schema.json",
105
+ "rules <code>": "rule-detail.schema.json",
106
+ "doctor": "doctor-report.schema.json",
107
+ "explorer-index": "explorer-index.schema.json",
108
+ "explorer-index --publication-preview": "publication-preview.schema.json",
109
+ }
110
+ ERROR_SCHEMA = "error.schema.json"
111
+
112
+ # Non-default machine formats that are not the JSON contract itself (so they
113
+ # carry no top-level ``contract_version``) but still have a published schema.
114
+ # ``--format github`` is GitHub's workflow-command text and has none.
115
+ FORMAT_SCHEMAS: dict[str, str] = {
116
+ "lint --format sarif": "lint-sarif.schema.json",
117
+ }
118
+
119
+ _HINT_PREFIX = "hint: "
120
+
121
+
122
+ def _split_hint(text: str) -> tuple[str, str | None]:
123
+ """Separate trailing ``hint: ...`` lines from a human error message."""
124
+ lines = text.rstrip("\n").split("\n")
125
+ hints: list[str] = []
126
+ while lines and lines[-1].startswith(_HINT_PREFIX):
127
+ hints.insert(0, lines.pop()[len(_HINT_PREFIX):])
128
+ message = "\n".join(lines).strip()
129
+ return message, ("\n".join(hints) if hints else None)
130
+
131
+
132
+ def error_payload(code: str, message: str, hint: str | None = None) -> dict[str, Any]:
133
+ """Build the error object. ``code`` must be one of :data:`ERROR_CODES`."""
134
+ if code not in ERROR_CODES:
135
+ raise ValueError(f"unregistered error code: {code}")
136
+ return {
137
+ "contract_version": CONTRACT_VERSION,
138
+ "error": {"code": code, "message": message, "hint": hint},
139
+ }
140
+
141
+
142
+ def emit_error(
143
+ code: str,
144
+ text: str,
145
+ *,
146
+ json_mode: bool,
147
+ hint: str | None = None,
148
+ json_hint: str | None = None,
149
+ stdout: TextIO | None = None,
150
+ stderr: TextIO | None = None,
151
+ ) -> int:
152
+ """Report a failure and return its exit code (see :data:`ERROR_EXIT_CODES`).
153
+
154
+ ``text`` is printed to stderr exactly as before, so human output does not
155
+ change. ``hint``, when given, is printed verbatim on the next line; when it
156
+ is omitted, trailing ``hint: ...`` lines of ``text`` become the JSON hint.
157
+ ``json_hint`` sets the JSON hint without adding a line to the human output.
158
+ With ``json_mode`` the error object is also written to stdout.
159
+ """
160
+ if code not in ERROR_CODES:
161
+ raise ValueError(f"unregistered error code: {code}")
162
+ out = stdout or sys.stdout
163
+ err = stderr or sys.stderr
164
+ print(text, file=err)
165
+ if hint is not None:
166
+ print(hint, file=err)
167
+ if json_mode:
168
+ message, embedded_hint = _split_hint(text)
169
+ if json_hint is None and hint is not None:
170
+ json_hint = hint[len(_HINT_PREFIX):] if hint.startswith(_HINT_PREFIX) else hint
171
+ if json_hint is None:
172
+ json_hint = embedded_hint
173
+ # Same UTF-8-safe path as every success payload (cp1252 consoles included).
174
+ emit_machine(json.dumps(error_payload(code, message, json_hint), ensure_ascii=False, indent=2), file=out)
175
+ return ERROR_EXIT_CODES[code]
176
+
177
+
178
+ def describe_os_error(exc: OSError) -> str:
179
+ """One readable line for a filesystem refusal (no ``[Errno 13]`` prefix)."""
180
+ reason = exc.strerror or exc.__class__.__name__
181
+ if exc.filename is not None and exc.filename2 is not None:
182
+ return f"{reason}: {exc.filename} -> {exc.filename2}"
183
+ if exc.filename is not None:
184
+ return f"{reason}: {exc.filename}"
185
+ return str(exc) or reason
186
+
187
+
188
+ def vault_not_found(explicit: str | None, *, json_mode: bool) -> int:
189
+ """The shared "no vault here" failure."""
190
+ return emit_error("vault_not_found", no_vault(explicit), json_mode=json_mode)
191
+
192
+
193
+ def argv_wants_json(argv: list[str]) -> bool:
194
+ """Best-effort: did this command line ask for JSON on stdout?
195
+
196
+ Used only where the parsed namespace is not available yet, i.e. when the
197
+ command line itself cannot be parsed. ``--json`` may be abbreviated, as
198
+ argparse allows.
199
+ """
200
+ tokens = list(argv)
201
+ for index, token in enumerate(tokens):
202
+ if token == "--":
203
+ break
204
+ if len(token) >= 3 and token.startswith("--j") and "--json".startswith(token):
205
+ return True
206
+ if token == "--format=json":
207
+ return True
208
+ if token == "--format" and index + 1 < len(tokens) and tokens[index + 1] == "json":
209
+ return True
210
+ return "explorer-index" in tokens[:1]
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cometweb.io/schemas/whykit/backlinks-report.schema.json",
4
+ "title": "WhyKit backlinks report",
5
+ "description": "Output of `whykit backlinks --json`. A target that does not exist is a report with `exists: false` (exit 1), not an error object.",
6
+ "type": "object",
7
+ "required": [
8
+ "contract_version",
9
+ "target",
10
+ "id",
11
+ "kind",
12
+ "exists",
13
+ "backlinks",
14
+ "count"
15
+ ],
16
+ "properties": {
17
+ "contract_version": {
18
+ "const": 1
19
+ },
20
+ "target": {
21
+ "type": "string"
22
+ },
23
+ "id": {
24
+ "type": "string"
25
+ },
26
+ "kind": {
27
+ "type": "string"
28
+ },
29
+ "exists": {
30
+ "type": "boolean"
31
+ },
32
+ "backlinks": {
33
+ "type": "array",
34
+ "items": {
35
+ "type": "object",
36
+ "required": [
37
+ "from",
38
+ "type",
39
+ "to"
40
+ ],
41
+ "properties": {
42
+ "from": {
43
+ "type": "string"
44
+ },
45
+ "type": {
46
+ "type": "string"
47
+ },
48
+ "to": {
49
+ "type": "string"
50
+ }
51
+ },
52
+ "additionalProperties": true
53
+ }
54
+ },
55
+ "count": {
56
+ "type": "integer",
57
+ "minimum": 0
58
+ }
59
+ },
60
+ "additionalProperties": true
61
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cometweb.io/schemas/whykit/context-bundle.schema.json",
4
+ "title": "WhyKit multi-record context bundle",
5
+ "type": "object",
6
+ "required": ["format", "contract_version", "resolved", "missing", "contexts", "evidence", "budget"],
7
+ "properties": {
8
+ "format": {"const": "whykit.context-bundle/v1"},
9
+ "contract_version": {"const": 1},
10
+ "query": {"type": ["string", "null"]},
11
+ "canonical_only": {"type": "boolean"},
12
+ "agent": {"enum": ["generic", "cursor", "claude", "codex"]},
13
+ "preamble": {"type": "string"},
14
+ "content_trust": {"const": "untrusted_data"},
15
+ "requested_targets": {"type": "array", "items": {"type": "string"}},
16
+ "selected": {"type": "array", "items": {"type": "string"}},
17
+ "resolved": {"type": "integer", "minimum": 0},
18
+ "missing": {"type": "array", "items": {"type": "object"}},
19
+ "contexts": {"type": "array", "items": {"type": "object"}},
20
+ "evidence": {"type": "array", "items": {"type": "object"}},
21
+ "budget": {
22
+ "type": "object",
23
+ "required": ["max_docs", "max_chars", "used_chars", "remaining_chars", "exhausted"]
24
+ }
25
+ },
26
+ "additionalProperties": false
27
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cometweb.io/schemas/whykit/context-pack.schema.json",
4
+ "title": "WhyKit bounded context pack",
5
+ "type": "object",
6
+ "required": ["contract_version", "target", "exists"],
7
+ "properties": {
8
+ "contract_version": {"const": 1},
9
+ "target": {"type": "string"},
10
+ "exists": {"type": "boolean"},
11
+ "kind": {"type": "string"},
12
+ "path": {"type": ["string", "null"]},
13
+ "content": {"type": "string"},
14
+ "content_truncated": {"type": "boolean"},
15
+ "front_matter_omitted": {
16
+ "const": true,
17
+ "description": "Present when the body did not fit max_chars: the front matter was left out first (its fields are in `record`), so the content starts with the summary or the body."
18
+ },
19
+ "evidence": {
20
+ "description": "Evidence rows cited by a document or decision; for an E-NNN target, that entry's own register row.",
21
+ "anyOf": [{"type": "array", "items": {"type": "object"}}, {"type": "object"}]
22
+ },
23
+ "incoming": {"type": "array", "items": {"type": "object"}},
24
+ "outgoing": {"type": "array", "items": {"type": "object"}},
25
+ "supersession": {"type": "object"},
26
+ "findings": {"type": "array", "items": {"type": "object"}}
27
+ },
28
+ "additionalProperties": true
29
+ }
@@ -0,0 +1,122 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cometweb.io/schemas/whykit/impact-report.schema.json",
4
+ "title": "WhyKit impact report",
5
+ "description": "Output of `whykit impact --json`. The fields after `exists` depend on `kind`; a missing target is a report with `exists: false`.",
6
+ "type": "object",
7
+ "required": [
8
+ "contract_version",
9
+ "target",
10
+ "kind",
11
+ "exists",
12
+ "reference_count"
13
+ ],
14
+ "properties": {
15
+ "contract_version": {
16
+ "const": 1
17
+ },
18
+ "target": {
19
+ "type": "string"
20
+ },
21
+ "kind": {
22
+ "enum": [
23
+ "evidence",
24
+ "decision",
25
+ "document"
26
+ ]
27
+ },
28
+ "exists": {
29
+ "type": "boolean"
30
+ },
31
+ "state": {
32
+ "enum": [
33
+ "active",
34
+ "retired",
35
+ "missing"
36
+ ]
37
+ },
38
+ "ambiguous": {
39
+ "type": "boolean"
40
+ },
41
+ "record": {
42
+ "type": [
43
+ "object",
44
+ "null"
45
+ ]
46
+ },
47
+ "replacement": {
48
+ "type": [
49
+ "string",
50
+ "null"
51
+ ]
52
+ },
53
+ "references": {
54
+ "type": "array",
55
+ "items": {
56
+ "type": "object",
57
+ "required": [
58
+ "path"
59
+ ],
60
+ "properties": {
61
+ "path": {
62
+ "type": "string"
63
+ },
64
+ "title": {
65
+ "type": "string"
66
+ },
67
+ "type": {
68
+ "type": "string"
69
+ },
70
+ "status": {
71
+ "type": "string"
72
+ },
73
+ "owner": {
74
+ "type": "string"
75
+ },
76
+ "sensitivity": {
77
+ "type": "string"
78
+ },
79
+ "source_of_truth": {
80
+ "type": "boolean"
81
+ }
82
+ },
83
+ "additionalProperties": true
84
+ }
85
+ },
86
+ "incoming": {
87
+ "type": "array",
88
+ "items": {
89
+ "type": "object"
90
+ }
91
+ },
92
+ "outgoing": {
93
+ "type": "array",
94
+ "items": {
95
+ "type": "object"
96
+ }
97
+ },
98
+ "evidence": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "string"
102
+ }
103
+ },
104
+ "supersedes": {
105
+ "type": [
106
+ "string",
107
+ "null"
108
+ ]
109
+ },
110
+ "superseded_by": {
111
+ "type": "array",
112
+ "items": {
113
+ "type": "object"
114
+ }
115
+ },
116
+ "reference_count": {
117
+ "type": "integer",
118
+ "minimum": 0
119
+ }
120
+ },
121
+ "additionalProperties": true
122
+ }