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/query.py ADDED
@@ -0,0 +1,174 @@
1
+ """Metadata-aware vault queries for humans and agents."""
2
+ from __future__ import annotations
3
+
4
+ from .io import consistent_read
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from .contract import emit_error, vault_not_found
11
+ from .lint import EVIDENCE_ID_RE, find_vault_root, is_vault_root, path_cache
12
+ from .vault_index import VaultIndex
13
+ from .console import emit_machine
14
+
15
+
16
+ def _as_list(value: object) -> list[str]:
17
+ if value is None or value == "":
18
+ return []
19
+ if isinstance(value, list):
20
+ return [str(item).strip() for item in value if str(item).strip()]
21
+ return [str(value).strip()]
22
+
23
+
24
+ def _summary(index: VaultIndex, note) -> dict:
25
+ path = index.relative(note.path)
26
+ inline_ids = set() if path == "00-context/evidence-register.md" else set(note.cited_evidence)
27
+ source_ids = sorted(set(_as_list(note.front.get("source_ids"))) | inline_ids)
28
+ return {
29
+ "path": path,
30
+ "title": str(note.front.get("title") or note.path.stem),
31
+ "type": str(note.front.get("type") or ""),
32
+ "status": str(note.front.get("status") or ""),
33
+ "owner": str(note.front.get("owner") or ""),
34
+ "sensitivity": str(note.front.get("sensitivity") or ""),
35
+ "source_of_truth": note.front.get("source_of_truth") is True,
36
+ "decision_id": str(note.front.get("decision_id") or "") or None,
37
+ "review_by": str(note.front.get("review_by") or "") or None,
38
+ "source_ids": source_ids,
39
+ "tags": _as_list(note.front.get("tags")),
40
+ }
41
+
42
+
43
+ @consistent_read
44
+ @path_cache()
45
+ def query_vault(
46
+ root: Path,
47
+ *,
48
+ text: str | None = None,
49
+ doc_type: str | None = None,
50
+ status: str | None = None,
51
+ owner: str | None = None,
52
+ sensitivity: str | None = None,
53
+ allowed_sensitivities: set[str] | None = None,
54
+ source_id: str | None = None,
55
+ tag: str | None = None,
56
+ canonical_only: bool = False,
57
+ limit: int = 100,
58
+ vault: VaultIndex | None = None,
59
+ ) -> dict:
60
+ index = vault or VaultIndex.load(root)
61
+ needle = (text or "").casefold().strip()
62
+ owner_needle = (owner or "").casefold().strip()
63
+ tag_needle = (tag or "").casefold().strip()
64
+ matches: list[tuple[int, dict]] = []
65
+ for note in index.notes:
66
+ summary = _summary(index, note)
67
+ record_sensitivity = summary["sensitivity"] or "internal"
68
+ if allowed_sensitivities is not None and record_sensitivity not in allowed_sensitivities:
69
+ continue
70
+ if doc_type and summary["type"] != doc_type:
71
+ continue
72
+ # A template declares the type it is a starter for, but it is not a
73
+ # record of that type: `--type decision` lists decisions. Ask for
74
+ # `--status template` to see templates.
75
+ if doc_type and summary["status"] == "template" and status != "template":
76
+ continue
77
+ if status and summary["status"] != status:
78
+ continue
79
+ if sensitivity and summary["sensitivity"] != sensitivity:
80
+ continue
81
+ if canonical_only and not summary["source_of_truth"]:
82
+ continue
83
+ if source_id and source_id not in summary["source_ids"]:
84
+ continue
85
+ if owner_needle and owner_needle not in summary["owner"].casefold():
86
+ continue
87
+ if tag_needle and not any(tag_needle == value.casefold() for value in summary["tags"]):
88
+ continue
89
+
90
+ score = 0
91
+ if needle:
92
+ title = summary["title"].casefold()
93
+ path_text = summary["path"].casefold()
94
+ body = note.text.casefold()
95
+ if needle not in title and needle not in path_text and needle not in body:
96
+ continue
97
+ if needle == title:
98
+ score += 100
99
+ elif needle in title:
100
+ score += 50
101
+ if needle in path_text:
102
+ score += 20
103
+ score += min(body.count(needle), 20)
104
+ if summary["source_of_truth"]:
105
+ score += 5
106
+ if summary["status"] == "approved":
107
+ score += 2
108
+ matches.append((score, summary))
109
+
110
+ matches.sort(key=lambda item: (-item[0], item[1]["path"]))
111
+ total = len(matches)
112
+ limited = [item for _, item in matches[: max(0, limit)]]
113
+ return {
114
+ "contract_version": 1,
115
+ "query": {
116
+ "text": text,
117
+ "type": doc_type,
118
+ "status": status,
119
+ "owner": owner,
120
+ "sensitivity": sensitivity,
121
+ "allowed_sensitivities": sorted(allowed_sensitivities) if allowed_sensitivities is not None else None,
122
+ "source_id": source_id,
123
+ "tag": tag,
124
+ "canonical_only": canonical_only,
125
+ "limit": limit,
126
+ },
127
+ "total": total,
128
+ "returned": len(limited),
129
+ "results": limited,
130
+ }
131
+
132
+
133
+ def main(argv: list[str] | None = None) -> int:
134
+ parser = argparse.ArgumentParser(prog="whykit query", description="Query WhyKit notes by metadata, evidence and text.")
135
+ parser.add_argument("text", nargs="?", help="case-insensitive text to find in title, path or body")
136
+ parser.add_argument("--root", help="vault root (default: nearest vault)")
137
+ parser.add_argument("--type", dest="doc_type")
138
+ parser.add_argument("--status")
139
+ parser.add_argument("--owner")
140
+ parser.add_argument("--sensitivity")
141
+ parser.add_argument("--source", dest="source_id", help="filter to documents using E-NNN")
142
+ parser.add_argument("--tag")
143
+ parser.add_argument("--canonical-only", action="store_true")
144
+ parser.add_argument("--limit", type=int, default=100)
145
+ parser.add_argument("--json", action="store_true")
146
+ args = parser.parse_args(argv)
147
+ if args.limit < 0:
148
+ return emit_error("invalid_argument", "--limit must be >= 0", json_mode=args.json)
149
+ if args.source_id and not EVIDENCE_ID_RE.fullmatch(args.source_id):
150
+ return emit_error("invalid_argument", "--source must be an E-NNN identifier", json_mode=args.json)
151
+ root = Path(args.root).expanduser().resolve() if args.root else find_vault_root()
152
+ if root is None or not is_vault_root(root):
153
+ return vault_not_found(args.root, json_mode=args.json)
154
+ report = query_vault(
155
+ root,
156
+ text=args.text,
157
+ doc_type=args.doc_type,
158
+ status=args.status,
159
+ owner=args.owner,
160
+ sensitivity=args.sensitivity,
161
+ source_id=args.source_id,
162
+ tag=args.tag,
163
+ canonical_only=args.canonical_only,
164
+ limit=args.limit,
165
+ )
166
+ if args.json:
167
+ emit_machine(json.dumps(report, ensure_ascii=False, indent=2))
168
+ else:
169
+ for item in report["results"]:
170
+ decision = f" {item['decision_id']}" if item.get("decision_id") else ""
171
+ canonical = " canonical" if item["source_of_truth"] else ""
172
+ print(f"{item['path']} [{item['status'] or 'no-status'}/{item['type'] or 'no-type'}{decision}{canonical}] {item['title']}")
173
+ print(f"\n{report['returned']} returned / {report['total']} matched")
174
+ return 0