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.
- whykit/__init__.py +21 -0
- whykit/adopt.py +776 -0
- whykit/backlinks.py +106 -0
- whykit/check.py +320 -0
- whykit/ci_formats.py +283 -0
- whykit/cli.py +1719 -0
- whykit/completion.py +193 -0
- whykit/config.py +233 -0
- whykit/console.py +217 -0
- whykit/context.py +204 -0
- whykit/contract.py +210 -0
- whykit/contract_schemas/backlinks-report.schema.json +61 -0
- whykit/contract_schemas/context-bundle.schema.json +27 -0
- whykit/contract_schemas/context-pack.schema.json +29 -0
- whykit/contract_schemas/impact-report.schema.json +122 -0
- whykit/contract_schemas/query-result.schema.json +105 -0
- whykit/contract_schemas/status-report.schema.json +176 -0
- whykit/contract_schemas/trace-report.schema.json +211 -0
- whykit/diff.py +823 -0
- whykit/evidence.py +178 -0
- whykit/explorer_index.py +554 -0
- whykit/graph.py +318 -0
- whykit/immutability.py +624 -0
- whykit/impact.py +272 -0
- whykit/init_layout.py +205 -0
- whykit/io.py +563 -0
- whykit/lint.py +2190 -0
- whykit/lsp.py +1241 -0
- whykit/markdown_code.py +107 -0
- whykit/mcp_server.py +2289 -0
- whykit/messages.py +46 -0
- whykit/pack.py +294 -0
- whykit/parse_cache.py +752 -0
- whykit/placeholders.py +84 -0
- whykit/promote.py +651 -0
- whykit/py.typed +0 -0
- whykit/query.py +174 -0
- whykit/review.py +527 -0
- whykit/rule_policy.py +1063 -0
- whykit/rules.py +274 -0
- whykit/scaffold.py +784 -0
- whykit/sensitivity.py +95 -0
- whykit/snapshot.py +319 -0
- whykit/status.py +167 -0
- whykit/tables.py +120 -0
- whykit/template/00-context/README.md +17 -0
- whykit/template/00-context/company.md +52 -0
- whykit/template/00-context/evidence-register.md +34 -0
- whykit/template/00-context/goals.md +38 -0
- whykit/template/00-context/review-log.md +22 -0
- whykit/template/00-context/terminology.md +34 -0
- whykit/template/01-strategy/README.md +7 -0
- whykit/template/02-discoverability/README.md +10 -0
- whykit/template/03-website/README.md +7 -0
- whykit/template/04-automation/README.md +10 -0
- whykit/template/05-operations/README.md +19 -0
- whykit/template/06-decisions/README.md +11 -0
- whykit/template/06-decisions/decision-log.md +38 -0
- whykit/template/07-research/README.md +10 -0
- whykit/template/AGENTS.md +163 -0
- whykit/template/Home.md +57 -0
- whykit/template/INTEROP.md +117 -0
- whykit/template/OBSIDIAN.md +92 -0
- whykit/template/README.md +51 -0
- whykit/template/assets/README.md +6 -0
- whykit/template/reports/README.md +7 -0
- whykit/template/templates/README.md +7 -0
- whykit/template/templates/automation-spec-template.md +70 -0
- whykit/template/templates/decision-record-template.md +65 -0
- whykit/template/templates/document-template.md +55 -0
- whykit/template/templates/experiment-template.md +62 -0
- whykit/template/templates/meeting-note-template.md +47 -0
- whykit/template/templates/page-brief-template.md +63 -0
- whykit/template/templates/research-note-template.md +53 -0
- whykit/template/templates/source-ingestion-template.md +43 -0
- whykit/template/whykit.toml +39 -0
- whykit/trace.py +272 -0
- whykit/vault_index.py +190 -0
- whykit-0.3.1.dist-info/METADATA +162 -0
- whykit-0.3.1.dist-info/RECORD +84 -0
- whykit-0.3.1.dist-info/WHEEL +4 -0
- whykit-0.3.1.dist-info/entry_points.txt +3 -0
- whykit-0.3.1.dist-info/licenses/LICENSE +201 -0
- 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
|