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/evidence.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Evidence lifecycle operations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .contract import TargetNotFound, describe_os_error, emit_error, vault_not_found
|
|
10
|
+
from .io import consistent_read, atomic_write_text, safe_vault_target, vault_mutation_lock
|
|
11
|
+
from .impact import analyze_impact
|
|
12
|
+
from .lint import HISTORICAL_STATUSES, EVIDENCE_ID_RE, _split_table_row, evidence_register, find_vault_root, is_vault_root
|
|
13
|
+
from .scaffold import _frontmatter_replace, _table_cell, _with_evidence_sensitivity, _register_inherited_label
|
|
14
|
+
from .tables import evidence_table_bounds
|
|
15
|
+
from .console import emit_machine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@consistent_read
|
|
19
|
+
def list_evidence(root: Path, *, state: str = "all") -> dict:
|
|
20
|
+
active, retired, _ = evidence_register(root)
|
|
21
|
+
items: list[dict] = []
|
|
22
|
+
if state in {"all", "active"}:
|
|
23
|
+
for evidence_id, row in active.items():
|
|
24
|
+
items.append({"id": evidence_id, "state": "active", **row})
|
|
25
|
+
if state in {"all", "retired"}:
|
|
26
|
+
for evidence_id, row in retired.items():
|
|
27
|
+
items.append({"id": evidence_id, "state": "retired", **row})
|
|
28
|
+
items.sort(key=lambda item: item["id"])
|
|
29
|
+
return {"contract_version": 1, "state": state, "count": len(items), "evidence": items}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def retire_evidence(
|
|
33
|
+
root: Path,
|
|
34
|
+
evidence_id: str,
|
|
35
|
+
*,
|
|
36
|
+
reason: str,
|
|
37
|
+
replaced_by: str | None = None,
|
|
38
|
+
today: dt.date | None = None,
|
|
39
|
+
) -> dict:
|
|
40
|
+
today = today or dt.date.today()
|
|
41
|
+
if not EVIDENCE_ID_RE.fullmatch(evidence_id):
|
|
42
|
+
raise ValueError("evidence ID must be E-NNN")
|
|
43
|
+
reason = reason.strip()
|
|
44
|
+
if not reason:
|
|
45
|
+
raise ValueError("--why must explain why the evidence is being retired")
|
|
46
|
+
|
|
47
|
+
with vault_mutation_lock(root):
|
|
48
|
+
active, retired, occurrences = evidence_register(root)
|
|
49
|
+
if evidence_id in retired:
|
|
50
|
+
raise ValueError(f"{evidence_id} is already retired")
|
|
51
|
+
row = active.get(evidence_id)
|
|
52
|
+
if row is None:
|
|
53
|
+
raise TargetNotFound(f"unknown active evidence: {evidence_id}")
|
|
54
|
+
if sum(key == evidence_id for key, _ in occurrences) != 1:
|
|
55
|
+
raise ValueError("evidence ID must identify exactly one populated row")
|
|
56
|
+
if replaced_by:
|
|
57
|
+
if not EVIDENCE_ID_RE.fullmatch(replaced_by):
|
|
58
|
+
raise ValueError("--replaced-by must be an E-NNN identifier")
|
|
59
|
+
if replaced_by == evidence_id:
|
|
60
|
+
raise ValueError("evidence cannot replace itself")
|
|
61
|
+
if replaced_by not in active:
|
|
62
|
+
raise ValueError(f"replacement evidence must exist and be active: {replaced_by}")
|
|
63
|
+
|
|
64
|
+
impact = analyze_impact(root, evidence_id)
|
|
65
|
+
|
|
66
|
+
path = safe_vault_target(root, "00-context/evidence-register.md", create_parents=False)
|
|
67
|
+
original = path.read_text(encoding="utf-8")
|
|
68
|
+
inherited = _register_inherited_label(path, original)
|
|
69
|
+
if "sensitivity" in row:
|
|
70
|
+
original = _with_evidence_sensitivity(original, "retired", inherited)
|
|
71
|
+
lines = original.splitlines()
|
|
72
|
+
active_header, active_end, _ = evidence_table_bounds(lines, "active")
|
|
73
|
+
active_start = active_header + 2
|
|
74
|
+
row_idx = None
|
|
75
|
+
source = str(row.get("source") or "")
|
|
76
|
+
for idx in range(active_start, active_end):
|
|
77
|
+
cells = _split_table_row(lines[idx])
|
|
78
|
+
if cells and cells[0] == evidence_id:
|
|
79
|
+
row_idx = idx
|
|
80
|
+
if len(cells) > 1:
|
|
81
|
+
source = cells[1]
|
|
82
|
+
break
|
|
83
|
+
if row_idx is None:
|
|
84
|
+
raise ValueError(f"could not locate active row for {evidence_id}")
|
|
85
|
+
|
|
86
|
+
del lines[row_idx]
|
|
87
|
+
# Recompute retired bounds after deletion because line indexes shifted.
|
|
88
|
+
_, retired_end, labeled = evidence_table_bounds(lines, "retired")
|
|
89
|
+
values: tuple[str, ...] = (
|
|
90
|
+
evidence_id,
|
|
91
|
+
source,
|
|
92
|
+
today.isoformat(),
|
|
93
|
+
reason,
|
|
94
|
+
replaced_by or "—",
|
|
95
|
+
)
|
|
96
|
+
if labeled:
|
|
97
|
+
values += (row.get("sensitivity", inherited),)
|
|
98
|
+
retired_row = "| " + " | ".join(_table_cell(value) for value in values) + " |"
|
|
99
|
+
lines.insert(retired_end, retired_row)
|
|
100
|
+
updated = "\n".join(lines) + ("\n" if original.endswith("\n") else "")
|
|
101
|
+
updated = _frontmatter_replace(updated, "last_updated", today.isoformat())
|
|
102
|
+
atomic_write_text(path, updated)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"contract_version": 1,
|
|
106
|
+
"id": evidence_id,
|
|
107
|
+
"state": "retired",
|
|
108
|
+
"retired_on": today.isoformat(),
|
|
109
|
+
"why": reason,
|
|
110
|
+
"replaced_by": replaced_by,
|
|
111
|
+
"references": impact.get("reference_count", 0),
|
|
112
|
+
# Superseded and archived records cite what was believed at the
|
|
113
|
+
# time; only the remainder can still need a new citation.
|
|
114
|
+
"current_references": sum(
|
|
115
|
+
1 for item in impact.get("references", [])
|
|
116
|
+
if str(item.get("status") or "") not in HISTORICAL_STATUSES
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main(argv: list[str] | None = None) -> int:
|
|
122
|
+
parser = argparse.ArgumentParser(prog="whykit evidence", description="Inspect and manage evidence lifecycle.")
|
|
123
|
+
parser.add_argument("--root", help="vault root (default: nearest vault)")
|
|
124
|
+
sub = parser.add_subparsers(dest="evidence_command", required=True)
|
|
125
|
+
|
|
126
|
+
listing = sub.add_parser("list", help="list active/retired evidence")
|
|
127
|
+
listing.add_argument("--state", choices=("all", "active", "retired"), default="all")
|
|
128
|
+
listing.add_argument("--json", action="store_true")
|
|
129
|
+
|
|
130
|
+
retire = sub.add_parser("retire", help="move active evidence to the retired ledger")
|
|
131
|
+
retire.add_argument("id")
|
|
132
|
+
retire.add_argument("--why", required=True, dest="reason")
|
|
133
|
+
retire.add_argument("--replaced-by")
|
|
134
|
+
retire.add_argument("--today")
|
|
135
|
+
retire.add_argument("--json", action="store_true")
|
|
136
|
+
|
|
137
|
+
args = parser.parse_args(argv)
|
|
138
|
+
root = Path(args.root).expanduser().resolve() if args.root else find_vault_root()
|
|
139
|
+
if root is None or not is_vault_root(root):
|
|
140
|
+
return vault_not_found(args.root, json_mode=args.json)
|
|
141
|
+
|
|
142
|
+
if args.evidence_command == "list":
|
|
143
|
+
payload = list_evidence(root, state=args.state)
|
|
144
|
+
if args.json:
|
|
145
|
+
emit_machine(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
146
|
+
else:
|
|
147
|
+
for item in payload["evidence"]:
|
|
148
|
+
replacement = f" -> {item.get('replaced_by')}" if item.get("replaced_by") else ""
|
|
149
|
+
print(f"{item['id']} {item['state']:<7} {item.get('source') or '—'}{replacement}")
|
|
150
|
+
print(f"\n{payload['count']} evidence item(s)")
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
today = None
|
|
154
|
+
if args.today:
|
|
155
|
+
try:
|
|
156
|
+
today = dt.date.fromisoformat(args.today)
|
|
157
|
+
except ValueError:
|
|
158
|
+
return emit_error("invalid_argument", f"--today is not a real ISO date: {args.today}", json_mode=args.json)
|
|
159
|
+
try:
|
|
160
|
+
payload = retire_evidence(root, args.id, reason=args.reason, replaced_by=args.replaced_by, today=today)
|
|
161
|
+
except TargetNotFound as exc:
|
|
162
|
+
return emit_error("not_found", str(exc), json_mode=args.json)
|
|
163
|
+
except ValueError as exc:
|
|
164
|
+
return emit_error("operation_rejected", str(exc), json_mode=args.json)
|
|
165
|
+
except OSError as exc:
|
|
166
|
+
return emit_error("io_error", describe_os_error(exc), json_mode=args.json)
|
|
167
|
+
if args.json:
|
|
168
|
+
emit_machine(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
169
|
+
else:
|
|
170
|
+
replacement = f"; replacement {payload['replaced_by']}" if payload.get("replaced_by") else ""
|
|
171
|
+
historical = payload["references"] - payload["current_references"]
|
|
172
|
+
print(
|
|
173
|
+
f"retired {payload['id']}{replacement}; "
|
|
174
|
+
f"{payload['current_references']} current and {historical} historical reference(s)"
|
|
175
|
+
)
|
|
176
|
+
if payload["current_references"]:
|
|
177
|
+
print(f"next: `whykit impact {payload['id']}` lists what still cites it")
|
|
178
|
+
return 0
|