kingmadoc 0.2.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 (60) hide show
  1. kingmadoc/__init__.py +11 -0
  2. kingmadoc/about.py +49 -0
  3. kingmadoc/adr.py +147 -0
  4. kingmadoc/cli.py +631 -0
  5. kingmadoc/config.py +531 -0
  6. kingmadoc/d2_binary.py +164 -0
  7. kingmadoc/diagrams/__init__.py +36 -0
  8. kingmadoc/diagrams/base.py +481 -0
  9. kingmadoc/diagrams/d2.py +250 -0
  10. kingmadoc/diagrams/mermaid.py +285 -0
  11. kingmadoc/diagrams/plantuml.py +226 -0
  12. kingmadoc/documents.py +134 -0
  13. kingmadoc/exceptions.py +45 -0
  14. kingmadoc/explain.py +278 -0
  15. kingmadoc/facts/__init__.py +6 -0
  16. kingmadoc/facts/branch.py +82 -0
  17. kingmadoc/facts/collect.py +218 -0
  18. kingmadoc/facts/data_model.py +264 -0
  19. kingmadoc/facts/js_modules.py +171 -0
  20. kingmadoc/facts/projects.py +34 -0
  21. kingmadoc/facts/routes.py +252 -0
  22. kingmadoc/facts/services.py +49 -0
  23. kingmadoc/git.py +46 -0
  24. kingmadoc/naming.py +47 -0
  25. kingmadoc/plan/__init__.py +1 -0
  26. kingmadoc/plan/analyzer.py +659 -0
  27. kingmadoc/plan/dependencies.py +131 -0
  28. kingmadoc/plan/generator.py +481 -0
  29. kingmadoc/plan/models.py +197 -0
  30. kingmadoc/plandoc.py +182 -0
  31. kingmadoc/render.py +184 -0
  32. kingmadoc/skills/explaining-code/SKILL.md +214 -0
  33. kingmadoc/skills/explaining-code/reference/arc42.md +202 -0
  34. kingmadoc/skills/explaining-code/reference/c4-model.md +341 -0
  35. kingmadoc/skills/explaining-code/reference/c4.md +133 -0
  36. kingmadoc/skills/explaining-code/reference/models.md +314 -0
  37. kingmadoc/skills/explaining-code/reference/split.md +138 -0
  38. kingmadoc/skills/kingmadoc/SKILL.md +200 -0
  39. kingmadoc/skills/kingmadoc/reference/diagram-rules.md +41 -0
  40. kingmadoc/skills/kingmadoc/reference/formats.md +195 -0
  41. kingmadoc/skills.py +227 -0
  42. kingmadoc/templates/adr.md.j2 +26 -0
  43. kingmadoc/templates/domain_design.md.j2 +18 -0
  44. kingmadoc/templates/functional_design.md.j2 +86 -0
  45. kingmadoc/templates/plan_default.md.j2 +113 -0
  46. kingmadoc/templates/security_design.md.j2 +18 -0
  47. kingmadoc/templates/technical_design.md.j2 +104 -0
  48. kingmadoc/templating.py +90 -0
  49. kingmadoc/verify/__init__.py +1 -0
  50. kingmadoc/verify/changes.py +81 -0
  51. kingmadoc/verify/commands.py +143 -0
  52. kingmadoc/verify/deviations.py +115 -0
  53. kingmadoc/verify/locate.py +60 -0
  54. kingmadoc/verify/report.py +124 -0
  55. kingmadoc/vscode.py +73 -0
  56. kingmadoc-0.2.0.dist-info/METADATA +253 -0
  57. kingmadoc-0.2.0.dist-info/RECORD +60 -0
  58. kingmadoc-0.2.0.dist-info/WHEEL +4 -0
  59. kingmadoc-0.2.0.dist-info/entry_points.txt +2 -0
  60. kingmadoc-0.2.0.dist-info/licenses/LICENSE +21 -0
kingmadoc/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """KingmaDoc: feature documentation for AI coding agents."""
2
+
3
+ from importlib import metadata
4
+
5
+ try:
6
+ # Set at build time from the git tag (hatch-vcs), see pyproject.toml.
7
+ __version__ = metadata.version("kingmadoc")
8
+ except metadata.PackageNotFoundError: # a source tree that was never installed
9
+ __version__ = "0+unknown"
10
+
11
+ __all__ = ["__version__"]
kingmadoc/about.py ADDED
@@ -0,0 +1,49 @@
1
+ """Which KingmaDoc build is installed (shown by ``kingmadoc --version``).
2
+
3
+ pip records how a package was installed (PEP 610, ``direct_url.json``): for a git install
4
+ that includes the commit, so an update is visible even when the version number is the
5
+ same.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from importlib import metadata
12
+ from typing import Any
13
+
14
+ from kingmadoc import __version__
15
+
16
+
17
+ def version_text() -> str:
18
+ """Return the version, plus the git commit or ``editable`` when known.
19
+
20
+ Returns:
21
+ e.g. ``"0.2.0.dev0 (git 29244f7)"``, ``"0.2.0.dev0 (editable)"`` or ``"0.2.0.dev0"``.
22
+ """
23
+ try:
24
+ raw = _distribution().read_text("direct_url.json")
25
+ except metadata.PackageNotFoundError:
26
+ return __version__
27
+ if not raw:
28
+ return __version__
29
+ try:
30
+ info = json.loads(raw)
31
+ except json.JSONDecodeError:
32
+ return __version__
33
+ if not isinstance(info, dict):
34
+ return __version__
35
+ commit = _section(info, "vcs_info").get("commit_id")
36
+ if isinstance(commit, str) and commit:
37
+ return f"{__version__} (git {commit[:7]})"
38
+ if _section(info, "dir_info").get("editable") is True:
39
+ return f"{__version__} (editable)"
40
+ return __version__
41
+
42
+
43
+ def _section(info: dict[str, Any], key: str) -> dict[str, Any]:
44
+ value = info.get(key)
45
+ return value if isinstance(value, dict) else {}
46
+
47
+
48
+ def _distribution() -> Any:
49
+ return metadata.distribution("kingmadoc")
kingmadoc/adr.py ADDED
@@ -0,0 +1,147 @@
1
+ """Architecture Decision Records: ``kingmadoc adr "<title>"`` → ``docs/adr/<NNNN>-<slug>.md``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterable
7
+ from datetime import date
8
+ from pathlib import Path
9
+
10
+ from jinja2 import TemplateError
11
+
12
+ from kingmadoc.config import FeatureDocConfig
13
+ from kingmadoc.exceptions import AdrError
14
+ from kingmadoc.naming import slugify
15
+ from kingmadoc.templating import load_template
16
+
17
+ ADR_DIR = Path("docs/adr")
18
+ ADR_TEMPLATE = "adr.md.j2"
19
+ STATUSES: tuple[str, ...] = ("proposed", "accepted", "rejected", "superseded")
20
+
21
+ # Zero-padded to four digits (the common ADR convention); wider numbers keep sorting
22
+ # correctly up to 9999 and still parse beyond it.
23
+ NUMBER_WIDTH = 4
24
+ ADR_FILE = re.compile(r"(\d+)-.+\.md")
25
+ # Numbers in this range look like years ("2024-q3-review.md"); such files only count as
26
+ # ADRs when their first line is an ADR heading with that number ("# 2024. ...").
27
+ YEAR_LIKE = range(1900, 2101)
28
+
29
+
30
+ def next_number(existing_names: Iterable[str]) -> int:
31
+ """Return the number for a new ADR: one above the highest existing number.
32
+
33
+ Numbers are never reused, even when an ADR was deleted, so links stay stable.
34
+
35
+ Args:
36
+ existing_names: File names in the ADR directory.
37
+
38
+ Returns:
39
+ ``1`` if there are no ADRs yet.
40
+
41
+ Example:
42
+ >>> next_number(["0001-use-postgres.md", "0003-drop-redis.md", "README.md"])
43
+ 4
44
+ """
45
+ numbers = [int(m.group(1)) for n in existing_names if (m := ADR_FILE.fullmatch(n))]
46
+ return max(numbers, default=0) + 1
47
+
48
+
49
+ def adr_filename(number: int, title: str) -> str:
50
+ """Return ``<NNNN>-<slug>.md`` for an ADR.
51
+
52
+ Args:
53
+ number: ADR number.
54
+ title: ADR title (the slug is derived from it).
55
+
56
+ Returns:
57
+ The file name.
58
+
59
+ Example:
60
+ >>> adr_filename(7, "Use PostgreSQL for user data")
61
+ '0007-use-postgresql-for-user-data.md'
62
+ """
63
+ return f"{number:0{NUMBER_WIDTH}d}-{slugify(title)}.md"
64
+
65
+
66
+ def render_adr(
67
+ root: Path,
68
+ number: int,
69
+ title: str,
70
+ *,
71
+ status: str = "proposed",
72
+ today: date,
73
+ template: str = ADR_TEMPLATE,
74
+ ) -> str:
75
+ """Render an ADR in the standard format (title, date, status, context, decision,
76
+ consequences). Pure apart from reading the template; the caller passes the date.
77
+
78
+ Args:
79
+ root: Project root (for project-local templates).
80
+ number: ADR number.
81
+ title: Decision title.
82
+ status: One of :data:`STATUSES`.
83
+ today: Date of the decision record.
84
+ template: Bundled template name or explicit path (``adr.template`` in the config).
85
+
86
+ Returns:
87
+ The rendered Markdown document.
88
+
89
+ Raises:
90
+ ConfigError: If the configured template path does not exist or is not a file.
91
+ AdrError: If the title is blank, the status unknown, or the template fails.
92
+ """
93
+ title = " ".join(title.split())
94
+ if not title:
95
+ raise AdrError("The ADR title must not be empty")
96
+ if status not in STATUSES:
97
+ raise AdrError(f"Unknown ADR status {status!r}; use one of {', '.join(STATUSES)}")
98
+ try:
99
+ return load_template(template, root).render(
100
+ number=f"{number:0{NUMBER_WIDTH}d}",
101
+ title=title,
102
+ status=status,
103
+ statuses=STATUSES,
104
+ date=today.isoformat(),
105
+ )
106
+ except TemplateError as exc:
107
+ raise AdrError(f"Cannot render template {template!r}: {exc}") from exc
108
+
109
+
110
+ def adr_path(root: Path, config: FeatureDocConfig, title: str) -> tuple[int, Path]:
111
+ """Pick the number and path for a new ADR in ``<root>/docs/adr``.
112
+
113
+ Args:
114
+ root: Project root.
115
+ config: KingmaDoc configuration.
116
+ title: ADR title.
117
+
118
+ Returns:
119
+ ``(number, path)``.
120
+
121
+ Raises:
122
+ AdrError: If ADRs are not enabled in the config.
123
+ """
124
+ if not config.adr.enabled:
125
+ raise AdrError(
126
+ "ADRs are disabled. Enable them in .featuredoc.yml:\n adr:\n enabled: true"
127
+ )
128
+ directory = root / ADR_DIR
129
+ names = [p.name for p in directory.iterdir() if _is_adr(p)] if directory.is_dir() else []
130
+ number = next_number(names)
131
+ return number, directory / adr_filename(number, title)
132
+
133
+
134
+ def _is_adr(path: Path) -> bool:
135
+ """Whether ``path`` is an ADR (see :data:`YEAR_LIKE` for date-named files)."""
136
+ match = ADR_FILE.fullmatch(path.name)
137
+ if not match or not path.is_file():
138
+ return False
139
+ number = int(match.group(1))
140
+ if number not in YEAR_LIKE:
141
+ return True
142
+ try:
143
+ with path.open(encoding="utf-8", errors="ignore") as handle:
144
+ first_line = handle.readline()
145
+ except OSError:
146
+ return False
147
+ return re.match(rf"#\s*0*{number}\b", first_line) is not None